Skip to content

Commit 3d4d060

Browse files
CCoupelclaude
andcommitted
feat: v1.1.0 — full metadata sync, MetadataMode, RemoteSyncFull, progress UI
- GetMetadataAsync uses Users/{userId}/Items/{itemId} for compatibility - MetadataMode per connector: RemoteSync / RemoteSyncFull / LocalScraping - RemoteSyncFull: bypasses NFO skip, rewrites all metadata on every sync - NFO fallback removed: failed metadata fetches are retried on next sync - LibraryOptions applied per mode (fetchers, savers, cache, chapters) - SyncAll progress per library via client-side iteration - syncSingleConnector animated progress bar with connector name - CONFIGURATION.md updated: auth modes, metadata modes, troubleshooting - ROADMAP.md: Phase 2a marked complete Closes #11, #13 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b4bf351 commit 3d4d060

7 files changed

Lines changed: 118 additions & 66 deletions

File tree

docs/CONFIGURATION.md

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,39 @@ Ce répertoire sera créé automatiquement par le plugin. Il doit être accessib
4848

4949
### Étape 3 : Ajouter un serveur source
5050

51-
Cliquer **"Ajouter un serveur"** et remplir :
51+
Cliquer **"+ Add Connector"** et remplir :
5252

5353
| Champ | Exemple | Description |
5454
|---|---|---|
5555
| Nom d'affichage | `Serveur B - Bureau` | Nom libre pour identifier la source |
5656
| Type de serveur | `Emby` | Emby / Jellyfin / Plex |
5757
| URL du serveur | `http://192.168.1.200:8096` ou `https://media.example.com/emby` | URL complète avec port **et chemin de base** Emby |
58-
| Clé API | `abc123def456...` | Clé API du serveur source |
58+
| Mode d'authentification | `User Credentials` | Voir section Authentication ci-dessous |
59+
| Mode de métadonnées | `Remote Sync` | Voir section Metadata Source ci-dessous |
5960

6061
**Important — URL du serveur** : inclure le chemin de base si Emby est derrière un reverse proxy.
6162
Exemple : si Emby répond sur `https://media.example.com/emby/Items/...`, l'URL à configurer est `https://media.example.com/emby`.
6263

63-
Cliquer **"Tester la connexion"** pour valider avant de sauvegarder.
64+
Cliquer **"Test Connection"** pour valider avant de sauvegarder.
65+
66+
#### Authentification
67+
68+
| Mode | Description |
69+
|---|---|
70+
| **API Key** | Accès admin complet, aucun suivi d'usage sur le serveur distant |
71+
| **User Credentials** | Le serveur distant voit les sessions de cet utilisateur, applique ses restrictions et enregistre l'historique de lecture |
72+
73+
#### Metadata Source
74+
75+
| Mode | Description |
76+
|---|---|
77+
| **Remote Sync** | Le plugin télécharge métadonnées et images depuis le serveur distant. Items avec `.nfo` existant ignorés (incrémental). |
78+
| **Remote Sync Full** | Identique à Remote Sync mais réécrit tous les `.nfo` et re-télécharge les images à chaque sync. |
79+
| **Local Scraping** | Seuls les `.strm` sont créés. Emby utilise ses propres scrapers (TMDB, TVDB, FanArt) pour enrichir la bibliothèque. Nécessite un accès internet. |
6480

6581
### Étape 4 : Sélectionner les bibliothèques à synchroniser
6682

67-
Après un test de connexion réussi, la liste des bibliothèques disponibles sur le serveur source s'affiche. Cocher celles à inclure.
83+
Après un test de connexion réussi, la liste des bibliothèques disponibles s'affiche. Cocher celles à inclure — les dossiers virtuels Emby sont créés automatiquement.
6884

6985
### Étape 5 : Configurer la synchronisation automatique
7086

@@ -75,17 +91,11 @@ Timeout proxy stream : 30 secondes
7591

7692
### Étape 6 : Lancer la première synchronisation
7793

78-
Cliquer **"Synchroniser maintenant"** pour déclencher la première sync manuellement.
94+
Cliquer **"Synchronise Now"** pour déclencher la première sync manuellement. La barre de progression avance bibliothèque par bibliothèque.
7995

80-
### Étape 7 : Ajouter la bibliothèque virtuelle dans Emby
96+
Les dossiers virtuels Emby sont créés automatiquement par le plugin — aucune configuration manuelle dans le Dashboard Emby n'est nécessaire.
8197

82-
Après la sync :
83-
1. Dashboard → Bibliothèques → Ajouter une bibliothèque
84-
2. Type : Films (ou Séries selon le contenu)
85-
3. Dossier : `/media/virtual-libraries/Films_ServeurB`
86-
4. Valider
87-
88-
Les médias du serveur B apparaissent maintenant dans l'interface d'Emby A.
98+
Les médias du serveur distant apparaissent dans l'interface d'Emby après le scan de bibliothèque déclenché automatiquement en fin de sync.
8999

90100
---
91101

@@ -235,9 +245,15 @@ Normal : la terminaison TLS est faite par le reverse proxy (traefik). Le serveur
235245

236246
### La sync ne détecte pas les nouveaux ajouts
237247

238-
- La sync delta compare les IDs distants avec l'index local (`.index/{connectorId}.json`)
239-
- Si l'index est corrompu, le supprimer pour forcer une re-sync complète
240-
- Vérifier l'intervalle de sync configuré
248+
- Les items ajoutés sur le serveur distant sont détectés automatiquement à la prochaine sync (aucun `.nfo` local → créé)
249+
- Vérifier l'intervalle de sync configuré (défaut : 6h)
250+
- Déclencher une sync manuelle depuis le dashboard plugin
251+
252+
### Les métadonnées sont incomplètes ou manquantes
253+
254+
- Vérifier que le mode **Remote Sync** ou **Remote Sync Full** est sélectionné (pas Local Scraping)
255+
- Si les métadonnées ne se mettent pas à jour malgré une sync : passer en mode **Remote Sync Full** pour forcer la réécriture des `.nfo`
256+
- Les items dont la récupération de métadonnées a échoué sont comptés "failed" et retentés à la prochaine sync
241257

242258
---
243259

docs/ROADMAP.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,26 @@
2525

2626
---
2727

28-
## Phase 2a — Synchronisation des métadonnées *(en cours)*
28+
## Phase 2a — Synchronisation des métadonnées ✅ Terminé (v1.1.0)
2929

3030
- [x] `SyncService` : orchestration sync par connector + bibliothèque
31-
- [x] Génération `.strm` + `.nfo` + téléchargement artwork
32-
- [x] Skip intelligent : toujours régénérer le `.strm` (cheap), ne sauter que si `.nfo` déjà présent
33-
- [ ] `LibrarySyncJob` : tâche planifiée (`IScheduledTask`) configurable
34-
- [ ] Détection delta : index JSON local `{connectorId}.json`
31+
- [x] Génération `.strm` + `.nfo` + téléchargement artwork (poster, fanart, landscape, logo)
32+
- [x] Métadonnées complètes : cast, directors, writers, tagline, trailer URL
33+
- [x] Skip intelligent : toujours régénérer le `.strm`, ne sauter le `.nfo` qu'en mode `RemoteSync`
34+
- [x] `MetadataMode` par connecteur : `RemoteSync` (incrémental) / `RemoteSyncFull` (force) / `LocalScraping`
35+
- [x] `LibraryOptions` Emby appliquées selon le mode (fetchers TMDB/TVDB/FanArt, cache, chapitres)
36+
- [x] `LibrarySyncJob` : tâche planifiée (`IScheduledTask`) avec intervalle configurable
37+
- [x] Mise à jour dynamique du trigger sans redémarrage
38+
- [x] `QueueLibraryScan()` déclenché si des items ont été créés
39+
- [x] Compteurs d'items distants par bibliothèque (endpoint `/item-counts`)
40+
- [x] Fix `Users/Me` 500 : `GetUserIdAsync` sans appel à `/Users/Me`
41+
- [x] Progression sync par librairie dans l'UI (itération client-side)
42+
43+
**Reste en backlog :**
44+
- [ ] Détection delta : index JSON local `{connectorId}.json` (issue #12)
3545
- [ ] Gestion des suppressions (items supprimés sur la source)
36-
- [ ] Logs de sync dans le dashboard
37-
- [ ] Tests intégration sync job
46+
- [ ] Tests intégration sync job (issue #14)
47+
- [ ] Progression sync par item en temps réel (issue #20)
3848

3949
---
4050

src/VirtualLib/Connectors/EmbyConnector.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,10 @@ public async Task<MediaMetadata> GetMetadataAsync(
287287
string itemId,
288288
CancellationToken cancellationToken = default)
289289
{
290-
var url = $"Items/{itemId}?Fields=Overview,Genres,Studios,ProviderIds,People,Tags,RemoteTrailers,Taglines";
290+
var userId = await GetUserIdAsync(cancellationToken);
291+
var url = userId is not null
292+
? $"Users/{userId}/Items/{itemId}?Fields=Overview,Genres,Studios,ProviderIds,People,Tags,RemoteTrailers,Taglines"
293+
: $"Items/{itemId}?Fields=Overview,Genres,Studios,ProviderIds,People,Tags,RemoteTrailers,Taglines";
291294
using var response = await GetWithRetryAsync(url, cancellationToken);
292295
response.EnsureSuccessStatusCode();
293296

src/VirtualLib/Core/SyncService.cs

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -252,17 +252,18 @@ private async Task<LibrarySyncResult> SyncLibraryItemsAsync(
252252
continue;
253253
}
254254

255-
// RemoteSync: fetch metadata + write NFO + download artwork
255+
// RemoteSync / RemoteSyncFull: fetch metadata + write NFO + download artwork
256256
var nfoPath = Path.Combine(nfoDir, _strmGenerator.GetFileName(item) + ".nfo");
257-
if (File.Exists(nfoPath)) { libSkipped++; continue; }
257+
if (config.MetadataMode == MetadataMode.RemoteSync && File.Exists(nfoPath)) { libSkipped++; continue; }
258258

259259
MediaMetadata metadata;
260260
try { metadata = await connector.GetMetadataAsync(item.RemoteId, ct); }
261261
catch (OperationCanceledException) { throw; }
262262
catch (Exception ex)
263263
{
264-
_logger.LogWarning(ex, "Failed to get metadata for item {RemoteId}", item.RemoteId);
265-
metadata = BuildFallbackMetadata(item);
264+
_logger.LogWarning(ex, "Failed to get metadata for item {RemoteId} '{Title}' — will retry on next sync", item.RemoteId, item.Title);
265+
libFailed++;
266+
continue; // Don't write fallback NFO — let next sync retry
266267
}
267268

268269
_nfoGenerator.Generate(metadata, nfoDir);
@@ -324,24 +325,4 @@ private async Task DownloadArtworkAsync(
324325
}
325326
}
326327

327-
private static MediaMetadata BuildFallbackMetadata(MediaItem item) =>
328-
new()
329-
{
330-
RemoteId = item.RemoteId,
331-
Title = item.Title,
332-
Type = item.Type,
333-
Year = item.Year,
334-
SeriesId = item.SeriesId,
335-
SeriesName = item.SeriesName,
336-
SeasonNumber = item.SeasonNumber,
337-
EpisodeNumber = item.EpisodeNumber,
338-
ImdbId = item.ImdbId,
339-
TmdbId = item.TmdbId,
340-
TvdbId = item.TvdbId,
341-
DateAdded = item.DateAdded,
342-
AvailableArtwork = item.AvailableArtwork,
343-
Genres = Array.Empty<string>(),
344-
Studios = Array.Empty<string>(),
345-
Tags = Array.Empty<string>()
346-
};
347328
}

src/VirtualLib/PluginConfiguration.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ public enum AuthMode { ApiKey, UserCredentials }
66

77
public enum MetadataMode
88
{
9-
/// <summary>Plugin fetches metadata/images from the remote server and writes .nfo files.</summary>
9+
/// <summary>Plugin fetches metadata/images from the remote server, skips items that already have a .nfo file.</summary>
1010
RemoteSync,
1111
/// <summary>Emby's own fetchers handle metadata/images from online databases (TMDB, TVDB…).</summary>
12-
LocalScraping
12+
LocalScraping,
13+
/// <summary>Plugin fetches metadata/images from the remote server for all items, overwriting existing .nfo files.</summary>
14+
RemoteSyncFull
1315
}
1416

1517
public sealed class PluginConfiguration : BasePluginConfiguration

src/VirtualLib/Web_Pages/config.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,16 @@ <h2 class="sectionTitle">VirtualLib</h2>
147147
<div class="inputContainer" style="margin-top:1em">
148148
<label class="inputLabel inputLabelUnfocused" for="connectorMetadataMode">Metadata Source</label>
149149
<select id="connectorMetadataMode" is="emby-select" class="emby-select-withcolor emby-input">
150-
<option value="RemoteSync">Remote Sync — plugin fetches metadata &amp; images from the source server</option>
150+
<option value="RemoteSync">Remote Sync — plugin fetches metadata &amp; images (incremental)</option>
151+
<option value="RemoteSyncFull">Remote Sync Full — re-fetches all metadata &amp; images on every sync</option>
151152
<option value="LocalScraping">Local Scraping — Emby fetches from TMDB/TVDB, writes NFO files locally</option>
152153
</select>
153154
<div class="fieldDescription">
154-
<strong>Remote Sync</strong>: actors, directors, posters and backdrops are downloaded from the remote server during sync.
155-
No internet access required from this server.<br/>
155+
<strong>Remote Sync</strong>: actors, directors, posters and backdrops are downloaded from the remote server.
156+
Items already having a .nfo are skipped (incremental).<br/>
157+
<strong>Remote Sync Full</strong>: same as Remote Sync but overwrites existing .nfo and re-downloads artwork on every sync.<br/>
156158
<strong>Local Scraping</strong>: only .strm files are created; Emby uses its own scrapers (TMDB, TVDB, FanArt) to enrich
157-
the library. Requires internet access. Chapter images, video thumbnails and image pre-caching are enabled in both modes.
159+
the library. Requires internet access. Chapter images, video thumbnails and image pre-caching are enabled in all modes.
158160
</div>
159161
</div>
160162

src/VirtualLib/Web_Pages/configjs.js

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ define([], function () {
117117
btnSync.setAttribute('is', 'emby-button');
118118
btnSync.className = 'emby-button';
119119
btnSync.textContent = 'Sync All';
120-
btnSync.addEventListener('click', (function (id) { return function () { syncSingleConnector(id); }; }(c.Id)));
120+
btnSync.addEventListener('click', (function (id, name) { return function () { syncSingleConnector(id, name); }; }(c.Id, c.DisplayName)));
121121

122122
actionCell.appendChild(btnEdit);
123123
actionCell.appendChild(btnDelete);
@@ -307,15 +307,28 @@ define([], function () {
307307
q('syncProgressBar').style.background = 'var(--theme-error-color, #e53935)';
308308
}
309309

310-
function syncSingleConnector(id) {
310+
function syncSingleConnector(id, displayName) {
311311
startSyncUI();
312-
q('syncProgressBar').style.width = '30%';
312+
var bar = q('syncProgressBar');
313+
var label = q('syncProgressLabel');
314+
bar.style.width = '10%';
315+
label.textContent = (displayName ? displayName + ' \u2014 ' : '') + 'Connecting\u2026';
316+
// Animate bar while waiting (10% → 80% over ~2s)
317+
var pct = 10;
318+
var timer = setInterval(function () {
319+
pct = Math.min(pct + 5, 80);
320+
bar.style.width = pct + '%';
321+
}, 400);
313322
apiPost('/virtuallib/connectors/' + encodeURIComponent(id) + '/sync')
314323
.then(function (result) {
324+
clearInterval(timer);
315325
finishSyncUI([result], 'Done.');
316326
loadLibraryStats(id);
317327
})
318-
.catch(function (e) { errorSyncUI(e.message); });
328+
.catch(function (e) {
329+
clearInterval(timer);
330+
errorSyncUI(e.message);
331+
});
319332
}
320333

321334
function refreshRemoteCounts(connectorId) {
@@ -578,14 +591,39 @@ define([], function () {
578591

579592
q('btnSyncAll').addEventListener('click', function () {
580593
startSyncUI();
581-
apiPost('/virtuallib/sync')
582-
.then(function (results) {
583-
finishSyncUI(results || [], 'Synchronisation complete.');
584-
apiGet('/virtuallib/connectors').then(function (connectors) {
585-
connectors.forEach(function (c) { loadLibraryStats(c.Id); });
586-
}).catch(function () {});
587-
})
588-
.catch(function (e) { errorSyncUI(e.message); });
594+
apiGet('/virtuallib/connectors').then(function (connectors) {
595+
var enabled = (connectors || []).filter(function (c) { return c.Enabled; });
596+
if (!enabled.length) {
597+
finishSyncUI([], 'No enabled connectors.');
598+
return;
599+
}
600+
var results = [];
601+
var bar = q('syncProgressBar');
602+
var label = q('syncProgressLabel');
603+
604+
function syncNext(i) {
605+
if (i >= enabled.length) {
606+
bar.style.width = '100%';
607+
finishSyncUI(results, 'Synchronisation complete.');
608+
enabled.forEach(function (c) { loadLibraryStats(c.Id); });
609+
return;
610+
}
611+
var c = enabled[i];
612+
var pct = Math.round((i / enabled.length) * 100);
613+
bar.style.width = pct + '%';
614+
label.textContent = '(' + (i + 1) + '/' + enabled.length + ') ' + c.DisplayName + '\u2026';
615+
apiPost('/virtuallib/connectors/' + encodeURIComponent(c.Id) + '/sync')
616+
.then(function (result) {
617+
results.push(result);
618+
syncNext(i + 1);
619+
})
620+
.catch(function (e) {
621+
results.push({ ConnectorName: c.DisplayName, Success: false, ErrorMessage: e.message });
622+
syncNext(i + 1);
623+
});
624+
}
625+
syncNext(0);
626+
}).catch(function (e) { errorSyncUI(e.message); });
589627
});
590628
});
591629
};

0 commit comments

Comments
 (0)