- Architecture & Core → core/
- Connecteurs → connectors/
- Types de médias → media-types/
- Configuration utilisateur → CONFIGURATION.md
- Scaffolding projet C# plugin Emby
- Interface
IMediaServerConnectoret modèles normalisés -
EmbyConnector: auth, list libraries, list items, metadata -
StrmGenerator: génération fichiers .strm -
NfoGenerator: génération fichiers .nfo (movie + episode) -
PluginConfiguration+ConnectorConfig -
Plugin.csentry point — plugin chargé et vérifié sur serveur de test - 18 tests unitaires (xUnit + Moq)
- Page de configuration HTML dans le dashboard Emby
- Formulaire ajout / édition / suppression d'un serveur source (URL + API key)
- Bouton "Tester la connexion" →
TestConnectionAsync - Listing des bibliothèques disponibles après connexion (
ListLibrariesAsync) - Sélection des bibliothèques à synchroniser (checkboxes →
ConnectorConfig.LibraryIds) - Bouton "Synchroniser maintenant" → génération
.strm+.nfo+ scan Emby - Paramètre
ProxyBaseUrl(override de l'URL de base pour les.strm)
-
SyncService: orchestration sync par connector + bibliothèque - Génération
.strm+.nfo+ téléchargement artwork (poster, fanart, landscape, logo) - Métadonnées complètes : cast, directors, writers, tagline, trailer URL
- Skip intelligent : toujours régénérer le
.strm, ne sauter le.nfoqu'en modeRemoteSync -
MetadataModepar connecteur :RemoteSync(incrémental) /RemoteSyncFull(force) /LocalScraping -
LibraryOptionsEmby appliquées selon le mode (fetchers TMDB/TVDB/FanArt, cache, chapitres) -
LibrarySyncJob: tâche planifiée (IScheduledTask) avec intervalle configurable - Mise à jour dynamique du trigger sans redémarrage
-
QueueLibraryScan()déclenché si des items ont été créés - Compteurs d'items distants par bibliothèque (endpoint
/item-counts) - Fix
Users/Me500 :GetUserIdAsyncsans appel à/Users/Me - Progression sync par librairie dans l'UI (itération client-side)
Reste en backlog :
- Détection delta : index JSON local
{connectorId}.json(issue #12) - Gestion des suppressions (items supprimés sur la source)
- Tests intégration sync job (issue #14)
-
ProxyController: endpointGET /virtuallib/proxy/{connectorId}/{itemId} - Support
Rangeheaders (seek / scrubbing) - Forward
Content-RangeetAccept-Ranges(obligatoire pour que ffprobe détecte la taille réelle) -
[Unauthenticated]sur le DTO ProxyStreamRequest (ffprobe probe sans token) - Gestion propre des déconnexions client (broken pipe, OperationCanceledException)
- Lecture validée end-to-end : web + app, direct play + transcodage
- Redesign URL proxy :
/virtuallib/proxy/{connectorId}/{libraryId}/{itemId}(ajoutlibraryId) - Validation connecteur actif + bibliothèque activée avant de proxifier
- Contrôle d'accès token : vérification droits utilisateur sur la bibliothèque virtuelle Emby
- Blocage des requêtes navigateur sans token (filtre User-Agent :
Lavf/*et absent = interne,Mozilla/*= navigateur) - Suppression automatique des fichiers
.strm/.nfoquand une bibliothèque est décochée ou un connecteur supprimé - Fix DI :
ILogger<ProxyController>retiré du constructeur (non enregistré dans Emby SimpleInjector)
Limites connues :
- Emby ne transmet pas le token utilisateur lors des appels ffprobe internes → contrôle d'accès par token impossible côté proxy pour les requêtes server-side ; délégué à Emby (
PlaybackInfo) qui est le vrai point de contrôle
-
PlexConnector: auth par API key, API XML/library/sections, items avec métadonnées (films + séries) -
PlexTvConnector: authentification via plex.tv, sélection du serveur par machineIdentifier - Résolution automatique de la meilleure URL (locale → plex.direct → relay), exclusion des IPs LAN inaccessibles depuis Kubernetes
- Timeout 120 s sur les connexions relay (latence élevée)
- Token d'accès par serveur (
accessTokendepuis/api/v2/resources, distinct du token global plex.tv) - Support 2FA (code TOTP transmis au moment du chargement des serveurs)
- Compteur d'items distants Plex : paramètre
X-Plex-Container-Start=0requis pour obtenirtotalSize - Refonte UI : arbre collapsible par type de médiathèque (Movies, TvShows…), trié A→Z, replié par défaut
- Compteurs résumés sur chaque ligne connecteur et chaque groupe de type (X/Y libs · A/B items)
- Auto-découverte des nouvelles bibliothèques lors du sync (merge dans
KnownLibraries) - Rafraîchissement automatique de l'UI après sync (sans rechargement de page)
- Pré-remplissage du mot de passe en édition de connecteur (évite de devoir le ressaisir pour Test Connection)
- Mise à jour du compteur d'items distants pour toutes les bibliothèques (cochées et non cochées) lors du sync
Reste en backlog :
-
JellyfinConnector(API proche d'Emby) — issue #15 - Détection delta : index JSON local
{connectorId}.json— issue #12 - Gestion des suppressions (items supprimés sur la source)
-
MediaType.AudioBooketMediaType.Bookajoutés -
StrmGenerator: arborescence{livre}/{chapitre}.strmpour les livres audio -
EpubStubGenerator: téléchargement du vrai fichier epub/pdf/mobi depuis la source -
NfoGenerator.GenerateAudioBookNfo(): fichieralbum.nfoau format Music/AudioBook Emby -
LibraryProvisioner: création automatique des dossiers virtuels Emby (audiobooks,books,photos) - Support photos/homevideos dans le connector Emby
-
AudioBookNfoProvider(ILocalMetadataProvider<Audio>) : litalbum.nfopour chaque chapitre et injecteAlbum,AlbumArtists,ProductionYear -
AudioBookFolderNfoProvider(ILocalMetadataProvider<Folder>) : litalbum.nfopour le container du livre -
BookNfoProvider(ILocalMetadataProvider<Book>) : lit{filename}.nfopour les ebooks -
MediaItem.RuntimeTicks: durée en ticks 100 ns (propagée depuisEmbyItem.RunTimeTicks) -
MediaItem.AlbumArtists: auteurs propagés depuisAlbumArtist/People[Author]du serveur distant - Injection directe en DB (
ILibraryManager.UpdateItem) :RunTimeTicks,Album,AlbumArtistssur les itemsAudio— contourne le ffprobe différé d'Emby sur les.strm - Polling loop post-scan : boucle background (2 s, timeout 5 min) qui attend que le scan Emby crée les items en DB, puis injecte les métadonnées — réduit à 1 sync unique (plus besoin de 2 syncs)
- Artwork découplé de la condition NFO : images téléchargées même quand
album.nfoexiste déjà - Fallback artwork : si le container AudioBook n'a pas d'image, utilise l'artwork du chapitre
-
ArtworkTypeétendu :Banner,Disc,Art(ClearArt) en plus de Poster/Backdrop/Thumb/Logo - Images par chapitre : Primary téléchargée comme
{chapitre}.jpgà côté du.strm - Fix
AudioBookNfoProvider: injecteAlbum(titre du livre) et nonNamesur les chapitres
- Champs utilisateur sur
MediaItem:IsPlayed,IsFavorite,PlayCount,LastPlayedDate,PlaybackPositionTicks - EmbyConnector : champ
UserDataajouté dans les requêtesListItemsAsyncetGetMetadataAsync; mapping versMediaItem - PlexConnector : parsing
viewCount(lu/playCount),viewOffset(position en ms → ticks),lastViewedAtdansMapVideoToItemetMapVideoToMetadata -
SyncUserFlags(Phase 2) : après injection des métadonnées, applique les états lus/favoris/position pour tous les utilisateurs locaux viaIUserDataManager.SaveUserData(..., UserDataSaveReason.Import) -
SyncUserFlagsForFolder(Phase 1) : sync des flags show/saison directement dans le dossier (ces items ne passent pas parpendingStrms) -
LibrarySyncJob+ConfigController: injection deIUserDataManageretIUserManagerdansSyncService - Stratégie merge : les états locaux ne sont jamais réduits (seule l'augmentation est propagée — playCount, position, favori)
- Compatibilité :
IUserManager.Users(déprécié mais seul accès disponible dans Emby) protégé par#pragma warning disable CS0618 -
UserDataSaveReason: aliasEmbyUserDataSaveReason = MediaBrowser.Model.Entities.UserDataSaveReasonpour éviter l'ambiguïtéMediaType
Reste en backlog :
- Détection delta / suppressions — issue #12
-
JellyfinConnector— issue #15
- Sync 100 % parallèle : toutes les bibliothèques de tous les connecteurs synchées simultanément (
Task.WhenAll) — issues #30 et #35 - Phase 2 autonome : chaque bibliothèque enchaîne Phase 1 → Phase 2 de façon indépendante, sans attendre les autres
-
MaxParallelLibraries: limite configurable par connecteur (défaut : 4), appliquée viaSemaphoreSlimsur Phase 1 uniquement (Phase 2 = polling Emby, pas de charge réseau distante) -
SyncStateredesigné :ConcurrentDictionary<string, LibrarySyncEntry>avec champsVolatile.Read/Writepour la thread-safety, statutsPending / RunningPhase1 / RunningPhase2 / Done / Failed - Barres de progression inline : double barre (Phase 1 bleue / Phase 2 verte) sur chaque ligne de l'arbre (bibliothèque, type, connecteur), affichage via polling HTTP
/virtuallib/sync/statustoutes les 2 s - Barre globale dans le header "Remote Connectors", compteur à droite
- Barres pleine largeur : s'étirent sur tout l'espace disponible (
flex:1) — même dénominateur (p1Total) pour les deux phases, évite les sauts de progression - Épaisseur par niveau : connecteur 9 px / type 6 px / bibliothèque 4 px
- Pistes bicolores : fond clair (20 % opacité) indique le total, remplissage foncé indique l'avancement
-
LibraryOrganizationenum par connecteur :Isolated(défaut) /SharedByTypeIsolated: une médiathèque Emby dédiée par paire connecteur–bibliothèque (ConnectorName — LibraryName)SharedByType: une médiathèque Emby partagée par type de contenu (Movies, TvShows…), chaque bibliothèque distante y ajoute son propre chemin
-
SharedLibraryPrefix/SharedLibrarySuffix(paramètres globaux) : personnalisation du nom de la médiathèque partagée (ex. préfixe[VL]→[VL] Movies) - Cohérence du chemin physique : les fichiers
.strm/.nfosont toujours placés dansvirtualLibRoot/ConnectorName/LibraryName/, identique pour les deux modes — le mode d'organisation ne change que le nom de la médiathèque Emby, pas la structure sur disque - Ajout incrémental des chemins (
AddMediaPaths) : chaque bibliothèque distante ajoute son chemin individuellement à la médiathèque partagée existante - Suppression sélective : en mode SharedByType, la suppression d'une bibliothèque retire uniquement son chemin via
RemoveMediaPath(long itemId, string path); la médiathèque partagée n'est supprimée que si aucun connecteur ne l'utilise plus (NoRemainingSharedLibraries) - Bouton "Cancel Sync" : annulation d'une synchronisation en cours depuis l'UI
-
ApplyLibraryOptionspréserve lesPathInfos:UpdateLibraryOptionsréinitialise la liste des chemins si on ne les ré-injecte pas explicitement —_libraryManager.GetLibraryOptions(collectionFolder)?.PathInfosdoit être préservé avant chaque appel -
RemoveMediaPath(long, string): signature réelle Emby — premier argument = itemId (long), pas de paramètrerefreshLibrary(contrairement àAddMediaPaths/RemoveVirtualFolder)
Mettre en cache localement les flux médias proxifiés pour éviter de re-fetcher le serveur source lors des lectures répétées. Optionnel par connecteur.
-
ChunkManifest: modèle de données (chunks, TotalSize, PresentChunks) -
ICacheManager: interface complète -
CacheManager: implémentation singleton-
EnsureManifestAsync: init depuis headers upstream -
IsRangeCached: décision serve-from-cache -
ServeCachedRangeAsync: lecture disque directe -
CopyWithCacheAsync: write-through streaming (proxy + cache simultané) -
PromoteToFileAsync: concaténation chunks → fichier unique -
InitializeAsync: nettoyage au démarrage (orphelins .tmp + validation manifests) - Concurrence : locks per-chunk + per-item, écriture atomique
.tmp→ rename
-
-
PluginConfiguration: champs cache (CacheEnabled, CacheChunkSizeMb, CacheMaxSizeGb…) -
ConnectorConfig.CacheEnabled: override par connecteur -
Plugin.Cache: singleton CacheManager initialisé au démarrage -
ProxyController: intégration cache- Cache hit →
ServeCachedRangeAsync(0 appel réseau) - Cache miss → proxy direct +
CopyWithCacheAsync(write-through)
- Cache hit →
-
docs/core/CACHE.md: documentation architecture
- Fetch on-demand des chunks manquants (seeks efficaces sur contenu partiellement caché)
- Éviction LRU automatique par taille totale (
CacheMaxSizeGb) - UI dashboard : progression du cache par item
- Tests unitaires
CacheManagerTests
-
StrmGeneratoridempotent : le.strmn'est écrit que si son contenu change (URL différente ou fichier absent). Évite de modifier lemtime, ce qui déclenchait un re-scan Emby →SaveMediaStreams([])→ perte des infos codec/résolution/audio en DB lors de chaque "Scan library files" manuel entre deux syncs VirtualLib.
-
sessionKeyinclut leuserIdlocal :{connectorId}:{remoteItemId}:{localUserId}— élimine la race condition quand 2 users regardent le même item simultanément -
LocalUserIddansConnectorConfig: champ permettant de lier un connecteur à un user local spécifique ; sélecteur dans l'UI de configuration - Sessions isolées sur le distant :
playSessionIdutilisé commeDeviceIddansX-Emby-Authorization→ chaque stream apparaît comme une session distincte sur le dashboard du serveur distant - Identité dynamique :
deviceNameau formatuser@client(ex:cyril@Emby Web) propagé dans tous les appels playback (Emby + Plex) - Isolation des Progress : seul le user lié (A) envoie ses Progress au distant ; les autres users (B) maintiennent leur position localement (évite l'oscillation de
UserData.PlaybackPositionTicks) - Restauration de position au Stop(B) (
ResolveLinkedUserPosition) : au Stop d'un user non-lié, on envoie la position de A (depuis session active ouIUserDataManager.GetUserData) au lieu de 0 — préserve le resume point de A sur le serveur distant -
SyncUserFlagsciblé : sync des états vu/favori/position uniquement vers le user lié (LocalUserId), plus vers tous les users locaux
-
PlaybackEventForwarder(IServerEntryPoint) : s'abonne aux eventsISessionManager(Start / Progress / Stop) et propage les notifications vers le serveur distant via le connecteur correspondant - Détection du fichier
.strm: parse l'URL proxy{baseUrl}/virtuallib/proxy/{connectorId}/{libraryId}/{remoteItemId}pour identifier le connecteur et l'item distant - Heartbeat 30 s : maintient la session remote vivante pendant les pauses prolongées (les clients Emby cessent d'envoyer des Progress après buffer, le remote killait la session en ~60 s)
- Debounce Stop 8 s : le host Emby fire
PlaybackStoppedquand la connexion HTTP proxy se ferme (buffer client plein), même si l'utilisateur est encore en pause. On attend 8 s — si un Progress arrive avant, le Stop est annulé - Fix race condition : le debounce Stop vérifie le
PlaySessionIdcourant avant de supprimer (évite de tuer une nouvelle session démarrée entre-temps) - Réouverture transparente : un
Progresspour une session absente (_sessions) ré-envoie automatiquement Start + Progress (couvre debounce expiré, pod restart, host ne renvoyant pas dePlaybackStart) -
PositionTicksdans Stopped : la position finale est envoyée au remote pour sauvegarder l'avancement ("Continuer la lecture") - Fix
PostWithRetryAsync(EmbyConnector) : utiliseStringContentavecContent-Lengthexplicite au lieu dePostAsJsonAsync(chunked) — ServiceStack (Emby) ignore les corps chunked, causant des champs null dontPlaySessionId -
PlaySessionId: GUID généré à chaque Start, propagé dans Progress et Stopped (évitait unArgumentNullExceptiondansSessionInfo.GetOrAddPlaySessionInfo) - Reporting Plex (
PlexConnector) :GET /:/timeline?state=playing|paused|stopped&time={ms}+GET /:/progress?key={ratingKey}&time={ms}— leviewOffsetest persité en base pour "Continuer la lecture" (Plex ne persiste pas via/:/timelineseul) - Reporting Emby (
EmbyConnector) :POST Sessions/Playing,Sessions/Playing/Progress,Sessions/Playing/StoppedavecPlaySessionId,PositionTicks,UserId