Skip to content

Commit c5ac3f6

Browse files
CCoupelclaude
andauthored
feat: v1.3.0 — Plex connector, collapsible tree UI, auto-discovery, password pre-fill
* feat: PlexConnector — implémentation connecteur Plex (issue #16) - Ajout de PlexConnector implémentant IMediaServerConnector - Auth via X-Plex-Token (ApiKey) ou plex.tv sign_in (UserCredentials) - Parsing XML Plex via XDocument/XElement - ListLibrariesAsync via /library/sections - ListItemsAsync via /library/sections/{id}/all (films) et ?type=4 (épisodes) - GetItemCountAsync avec X-Plex-Container-Size=0 - GetStreamUrlAsync via /library/parts/{id}/file avec token en query param - GetArtworkStreamAsync pour thumb (poster) et art (backdrop) - GetSectionTypeAsync: lookup KnownLibraries puis fallback API - Câblage dans ConnectorFactory pour ServerTypes.Plex Closes #16 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: PlexTV connector — connexion via plex.tv avec résolution auto URL (issue #32) - PlexTvConnector : wrapper autour de PlexConnector, initialise l'URL au premier appel - Auth via token (ApiKey) ou plex.tv sign_in (UserCredentials) - GET plex.tv/api/v2/resources → sélection du serveur par MachineIdentifier - Priorité connexion : local → plex.direct → relay - PlexTvApiModels : modèles JSON pour l'API plex.tv (resources + sign_in) - PlexServerInfo : DTO exposé à l'UI pour le picker de serveur - ConnectorFactory : ajout ServerTypes.PlexTV + câblage PlexTvConnector - PluginConfiguration : ajout ConnectorConfig.PlexMachineIdentifier - ConfigController : POST /virtuallib/plex/servers (liste serveurs plex.tv) + PlexMachineIdentifier propagé dans Create/Update DTOs et handlers - UI : option "Plex via plex.tv" dans le sélecteur de type + section picker serveur (Load Servers → dropdown) + validation et persistance PlexMachineIdentifier Closes #32 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: support 2FA plex.tv (TOTP) dans PlexTvConnector - GetTokenFromCredentialsAsync : accept twoFactorPin (user[two_factor_pin]) et détecte le 401 lié à 2FA avec message d'erreur explicite - ConfigController : GetPlexTvServers reçoit TwoFactorPin, retourne PlexTvServersResult { ResolvedToken, Servers } au lieu d'une liste brute - UI : champ "2FA Code" optionnel dans la section PlexTV - JS : après authentification UserCredentials + 2FA, bascule automatiquement en mode ApiKey avec le token long-lived résolu (les syncs futurs ne nécessitent plus de re-2FA) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: 2FA plex.tv — OTP concaténé au password (approche documentée) L'API plex.tv n'accepte pas de champ séparé pour le code TOTP. La méthode correcte est d'appendre le code directement au mot de passe : user[password] = password + otpCode (sans séparateur). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: PlexMachineIdentifier manquant dans TestConnectionParams - TestConnectionParams DTO : ajout champ PlexMachineIdentifier - Handler test-connection : propagation vers ConnectorConfig temporaire - JS btnTestConnection : envoi PlexMachineIdentifier + ServerUrl vide pour PlexTV Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: PlexConnector — item count, relay timeout, sync updates - Add X-Plex-Container-Start=0 to GetItemCountAsync URL so Plex includes totalSize in response (was returning 0 without this parameter) - Increase PlexConnector HTTP timeout from 30s to 120s for slow relay connections - Reuse authenticated connector in GetConnectorLibraries instead of creating a second PlexTvConnector for item count fetches - Update RemoteItemCount during sync for both checked and unchecked libraries - Save config after sync to persist updated item counts - Fix toggleLibrary to preserve PlexMachineIdentifier in PUT payload - Fix GetConnectorLibraries to only provision user-selected libraries - Add per-server accessToken support from plex.tv resources API - Improve PickBestConnection to prefer plex.direct over relay, exclude LAN IPs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: config UI — arbre collapsable par type de bibliothèque Remplace la table plate par un arbre à deux niveaux : - Niveau 1 : connecteurs (triés A→Z, collapsed par défaut) - Niveau 2 : groupes par type de bibliothèque (Movies, TV Shows, Music…) triés A→Z, collapsed par défaut - Bibliothèques triées A→Z dans chaque groupe - Cliquer sur l'en-tête collapse/expand ; boutons Edit/Delete/Sync toujours accessibles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: auto-découverte des nouvelles libraries au sync + refresh UI - SyncConnectorAsync fusionne les nouvelles libraries distantes dans KnownLibraries à chaque sync (sans écraser RemoteItemCount existant) - L'arbre de la page de config se recharge automatiquement après un sync (plus besoin de refresh manuel de la page) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: v1.3.0 — Plex connector, collapsible tree UI, auto-discovery, password pre-fill - PlexConnector + PlexTvConnector: plex.tv auth, relay support, 120s timeout, per-server accessToken - Refonte UI: arbre collapsible par type (Movies/TvShows...), trié A→Z, replié par défaut - Compteurs résumés (X/Y libs · A/B items) sur connecteur et groupe de type - Auto-découverte de nouvelles bibliothèques lors du sync (merge dans KnownLibraries) - Rafraîchissement automatique de l'UI après sync - Pré-remplissage du mot de passe en édition de connecteur - Mise à jour du compteur d'items distants pour toutes les bibliothèques lors du sync - Docs ROADMAP + CONFIGURATION mis à jour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 56eba38 commit c5ac3f6

11 files changed

Lines changed: 1542 additions & 173 deletions

File tree

docs/CONFIGURATION.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ Cliquer **"+ Add Connector"** et remplir :
5353
| Champ | Exemple | Description |
5454
|---|---|---|
5555
| Nom d'affichage | `Serveur B - Bureau` | Nom libre pour identifier la source |
56-
| Type de serveur | `Emby` | Emby / Jellyfin / Plex |
57-
| 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 |
56+
| Type de serveur | `Emby` | Emby / Jellyfin / Plex / Plex via plex.tv |
57+
| 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 (non requis pour Plex via plex.tv) |
5858
| Mode d'authentification | `User Credentials` | Voir section Authentication ci-dessous |
5959
| Mode de métadonnées | `Remote Sync` | Voir section Metadata Source ci-dessous |
6060

@@ -99,6 +99,30 @@ Les médias du serveur distant apparaissent dans l'interface d'Emby après le sc
9999

100100
---
101101

102+
## Connecteur Plex
103+
104+
### Plex (IP directe)
105+
106+
Utiliser le type **Plex (direct IP:32400)** avec :
107+
- URL : `http://192.168.1.10:32400`
108+
- Clé API : token X-Plex-Token de votre compte Plex (Settings → Account → Token)
109+
110+
### Plex via plex.tv
111+
112+
Utiliser le type **Plex via plex.tv (relay / plex.direct)** :
113+
1. Laisser le champ URL vide
114+
2. Saisir votre email/mot de passe plex.tv dans les champs d'authentification
115+
3. Si la double authentification (2FA) est activée sur votre compte, entrer le code TOTP dans le champ prévu
116+
4. Cliquer **"Load Servers from plex.tv"** — la liste de vos serveurs Plex apparaît
117+
5. Sélectionner le serveur cible dans la liste déroulante
118+
119+
Le plugin résout automatiquement la meilleure URL disponible (locale → plex.direct → relay).
120+
Le token d'accès est stocké dans la configuration du plugin et réutilisé pour les syncs suivantes.
121+
122+
**Note relay** : les connexions relay Plex peuvent être lentes (latence 30 s+). Le timeout est configuré à 120 s.
123+
124+
---
125+
102126
## Obtenir la clé API d'un serveur Emby/Jellyfin
103127

104128
### Emby

docs/ROADMAP.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,24 @@
7373

7474
---
7575

76-
## Phase 3 — Multi-connecteurs
76+
## Phase 3 — Connecteur Plex ✅ Terminé (v1.3.0)
77+
78+
- [x] `PlexConnector` : auth par API key, API XML `/library/sections`, items avec métadonnées (films + séries)
79+
- [x] `PlexTvConnector` : authentification via plex.tv, sélection du serveur par machineIdentifier
80+
- [x] Résolution automatique de la meilleure URL (locale → plex.direct → relay), exclusion des IPs LAN inaccessibles depuis Kubernetes
81+
- [x] Timeout 120 s sur les connexions relay (latence élevée)
82+
- [x] Token d'accès par serveur (`accessToken` depuis `/api/v2/resources`, distinct du token global plex.tv)
83+
- [x] Support 2FA (code TOTP transmis au moment du chargement des serveurs)
84+
- [x] Compteur d'items distants Plex : paramètre `X-Plex-Container-Start=0` requis pour obtenir `totalSize`
85+
- [x] Refonte UI : arbre collapsible par type de médiathèque (Movies, TvShows…), trié A→Z, replié par défaut
86+
- [x] Compteurs résumés sur chaque ligne connecteur et chaque groupe de type (X/Y libs · A/B items)
87+
- [x] Auto-découverte des nouvelles bibliothèques lors du sync (merge dans `KnownLibraries`)
88+
- [x] Rafraîchissement automatique de l'UI après sync (sans rechargement de page)
89+
- [x] Pré-remplissage du mot de passe en édition de connecteur (évite de devoir le ressaisir pour Test Connection)
90+
- [x] Mise à jour du compteur d'items distants pour toutes les bibliothèques (cochées et non cochées) lors du sync
7791

92+
**Reste en backlog :**
7893
- [ ] `JellyfinConnector` (API proche d'Emby)
79-
- [ ] `PlexConnector` (API XML différente)
80-
- [ ] Support N serveurs configurés simultanément
81-
- [ ] UI multi-serveurs
94+
- [ ] Détection delta : index JSON local `{connectorId}.json` (issue #12)
95+
- [ ] Gestion des suppressions (items supprimés sur la source)
96+
- [ ] Progression sync par item en temps réel (issue #20)

src/VirtualLib/Api/ConfigController.cs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using MediaBrowser.Model.Services;
66
using MediaBrowser.Model.Tasks;
77
using Microsoft.Extensions.Logging.Abstractions;
8+
using VirtualLib.Connectors;
89
using VirtualLib.Core;
910
using VirtualLib.Core.Models;
1011

@@ -25,6 +26,7 @@ public sealed class CreateConnector : IReturn<ConnectorConfig>
2526
public string DisplayName { get; set; } = string.Empty;
2627
public string ServerType { get; set; } = ServerTypes.Emby;
2728
public string ServerUrl { get; set; } = string.Empty;
29+
public string PlexMachineIdentifier { get; set; } = string.Empty;
2830
public AuthMode AuthMode { get; set; } = AuthMode.ApiKey;
2931
public string ApiKey { get; set; } = string.Empty;
3032
public string Username { get; set; } = string.Empty;
@@ -42,6 +44,7 @@ public sealed class UpdateConnector : IReturn<ConnectorConfig>
4244
public string DisplayName { get; set; } = string.Empty;
4345
public string ServerType { get; set; } = ServerTypes.Emby;
4446
public string ServerUrl { get; set; } = string.Empty;
47+
public string PlexMachineIdentifier { get; set; } = string.Empty;
4548
public AuthMode AuthMode { get; set; } = AuthMode.ApiKey;
4649
public string ApiKey { get; set; } = string.Empty;
4750
public string Username { get; set; } = string.Empty;
@@ -71,6 +74,7 @@ public sealed class TestConnectionParams : IReturn<ConnectorTestResult>
7174
{
7275
public string ServerType { get; set; } = ServerTypes.Emby;
7376
public string ServerUrl { get; set; } = string.Empty;
77+
public string PlexMachineIdentifier { get; set; } = string.Empty;
7478
public AuthMode AuthMode { get; set; } = AuthMode.ApiKey;
7579
public string ApiKey { get; set; } = string.Empty;
7680
public string Username { get; set; } = string.Empty;
@@ -124,6 +128,28 @@ public sealed class SaveSettings : IReturn<GlobalSettings>
124128
[Authenticated]
125129
public sealed class SyncAll : IReturn<List<SyncResult>> { }
126130

131+
[Route("/virtuallib/plex/servers", "POST", Summary = "List Plex servers visible on plex.tv for the given credentials")]
132+
[Authenticated]
133+
public sealed class GetPlexTvServers : IReturn<PlexTvServersResult>
134+
{
135+
public AuthMode AuthMode { get; set; } = AuthMode.ApiKey;
136+
/// <summary>Plex token (when AuthMode = ApiKey)</summary>
137+
public string ApiKey { get; set; } = string.Empty;
138+
/// <summary>plex.tv username (when AuthMode = UserCredentials)</summary>
139+
public string Username { get; set; } = string.Empty;
140+
/// <summary>plex.tv password (when AuthMode = UserCredentials)</summary>
141+
public string Password { get; set; } = string.Empty;
142+
/// <summary>6-digit TOTP code — required when the account has 2FA enabled</summary>
143+
public string TwoFactorPin { get; set; } = string.Empty;
144+
}
145+
146+
public sealed class PlexTvServersResult
147+
{
148+
/// <summary>Resolved Plex token (long-lived). Use this as ApiKey to avoid re-authenticating.</summary>
149+
public string ResolvedToken { get; set; } = string.Empty;
150+
public List<PlexServerInfo> Servers { get; set; } = new();
151+
}
152+
127153
[Route("/virtuallib/connectors/{Id}/sync", "POST", Summary = "Sync a single connector")]
128154
[Authenticated]
129155
public sealed class SyncConnector : IReturn<SyncResult>
@@ -260,6 +286,7 @@ public object Post(CreateConnector request)
260286
DisplayName = request.DisplayName,
261287
ServerType = request.ServerType,
262288
ServerUrl = request.ServerUrl,
289+
PlexMachineIdentifier = request.PlexMachineIdentifier,
263290
AuthMode = request.AuthMode,
264291
ApiKey = request.ApiKey,
265292
Username = request.Username,
@@ -294,6 +321,7 @@ public object Put(UpdateConnector request)
294321
DisplayName = request.DisplayName,
295322
ServerType = request.ServerType,
296323
ServerUrl = request.ServerUrl,
324+
PlexMachineIdentifier = request.PlexMachineIdentifier,
297325
AuthMode = request.AuthMode,
298326
ApiKey = request.ApiKey,
299327
Username = request.Username,
@@ -526,6 +554,7 @@ public object Post(TestConnectionParams request)
526554
DisplayName = "test",
527555
ServerType = request.ServerType,
528556
ServerUrl = request.ServerUrl,
557+
PlexMachineIdentifier = request.PlexMachineIdentifier,
529558
AuthMode = request.AuthMode,
530559
ApiKey = request.ApiKey,
531560
Username = request.Username,
@@ -537,6 +566,38 @@ public object Post(TestConnectionParams request)
537566
return ResultFactory.GetResult(Request, result, NoHeaders);
538567
}
539568

569+
// -----------------------------------------------------------------------
570+
// POST /virtuallib/plex/servers
571+
// -----------------------------------------------------------------------
572+
public object Post(GetPlexTvServers request)
573+
{
574+
var ct = CancellationToken.None;
575+
var factory = new DefaultHttpClientFactory();
576+
577+
string token;
578+
if (request.AuthMode == AuthMode.ApiKey)
579+
{
580+
if (string.IsNullOrEmpty(request.ApiKey))
581+
throw new ArgumentException("ApiKey (Plex token) is required.");
582+
token = request.ApiKey;
583+
}
584+
else
585+
{
586+
if (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))
587+
throw new ArgumentException("Username and Password are required for UserCredentials mode.");
588+
token = PlexTvConnector
589+
.GetTokenFromCredentialsAsync(request.Username, request.Password, factory, ct,
590+
twoFactorPin: request.TwoFactorPin)
591+
.GetAwaiter().GetResult();
592+
}
593+
594+
var servers = PlexTvConnector.ListServersAsync(token, factory, ct).GetAwaiter().GetResult();
595+
596+
return ResultFactory.GetResult(Request,
597+
new PlexTvServersResult { ResolvedToken = token, Servers = servers },
598+
NoHeaders);
599+
}
600+
540601
// -----------------------------------------------------------------------
541602
// GET /virtuallib/connectors/{Id}/libraries
542603
// -----------------------------------------------------------------------
@@ -565,12 +626,11 @@ public object Get(GetConnectorLibraries request)
565626
RemoteItemCount = existingCounts.GetValueOrDefault(l.Id, -1)
566627
}).ToList();
567628

568-
// Fetch remote item counts in parallel and update the cache
629+
// Fetch remote item counts in parallel — reuse the same connector (already authenticated)
569630
try
570631
{
571-
using var countConnector = _connectorFactory.Value.Create(connectorConfig);
572632
Task.WhenAll(connectorConfig.KnownLibraries
573-
.Select(lib => countConnector.GetItemCountAsync(lib.Id, CancellationToken.None)
633+
.Select(lib => connector.GetItemCountAsync(lib.Id, CancellationToken.None)
574634
.ContinueWith(t => { if (t.IsCompletedSuccessfully) lib.RemoteItemCount = t.Result; },
575635
TaskContinuationOptions.ExecuteSynchronously))
576636
).GetAwaiter().GetResult();
@@ -579,11 +639,11 @@ public object Get(GetConnectorLibraries request)
579639

580640
Plugin.Instance.SaveConfiguration();
581641

582-
// Provision virtual folders for all discovered libraries
642+
// Provision virtual folders only for user-selected (enabled) libraries
583643
var virtualLibRoot = config.VirtualLibraryRootPath;
584644
if (!string.IsNullOrEmpty(virtualLibRoot))
585645
{
586-
foreach (var lib in connectorConfig.KnownLibraries)
646+
foreach (var lib in connectorConfig.KnownLibraries.Where(l => connectorConfig.LibraryIds.Contains(l.Id)))
587647
_libraryProvisioner.EnsureVirtualFolder(
588648
connectorConfig.DisplayName, lib.Name, lib.Type, virtualLibRoot, connectorConfig.MetadataMode);
589649
}
@@ -616,6 +676,9 @@ public object Post(SyncAll request)
616676
results.Add(result);
617677
}
618678

679+
// Persist updated RemoteItemCount values written by SyncService
680+
Plugin.Instance.SaveConfiguration();
681+
619682
if (results.Any(r => r.Success && r.ItemsCreated > 0))
620683
_libraryManager.QueueLibraryScan();
621684

@@ -643,6 +706,9 @@ public object Post(SyncConnector request)
643706
progress: null,
644707
CancellationToken.None).GetAwaiter().GetResult();
645708

709+
// Persist the updated RemoteItemCount values written by SyncService
710+
Plugin.Instance.SaveConfiguration();
711+
646712
if (result.Success && result.ItemsCreated > 0)
647713
_libraryManager.QueueLibraryScan();
648714

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace VirtualLib.Connectors.Internal;
4+
5+
// plex.tv /api/v2/resources response
6+
internal sealed class PlexTvResource
7+
{
8+
[JsonPropertyName("name")]
9+
public string Name { get; set; } = string.Empty;
10+
11+
[JsonPropertyName("clientIdentifier")]
12+
public string ClientIdentifier { get; set; } = string.Empty;
13+
14+
[JsonPropertyName("provides")]
15+
public string Provides { get; set; } = string.Empty;
16+
17+
/// <summary>
18+
/// Per-server access token returned by plex.tv — use this to authenticate
19+
/// with the PMS directly (differs from the global auth token for shared servers).
20+
/// </summary>
21+
[JsonPropertyName("accessToken")]
22+
public string AccessToken { get; set; } = string.Empty;
23+
24+
[JsonPropertyName("connections")]
25+
public List<PlexTvConnection> Connections { get; set; } = new();
26+
}
27+
28+
internal sealed class PlexTvConnection
29+
{
30+
[JsonPropertyName("protocol")]
31+
public string Protocol { get; set; } = string.Empty;
32+
33+
[JsonPropertyName("uri")]
34+
public string Uri { get; set; } = string.Empty;
35+
36+
[JsonPropertyName("local")]
37+
public bool Local { get; set; }
38+
39+
[JsonPropertyName("relay")]
40+
public bool Relay { get; set; }
41+
42+
[JsonPropertyName("IPv6")]
43+
public bool IPv6 { get; set; }
44+
}
45+
46+
// plex.tv /users/sign_in.json response (partial)
47+
internal sealed class PlexTvSignInResponse
48+
{
49+
[JsonPropertyName("user")]
50+
public PlexTvUser? User { get; set; }
51+
}
52+
53+
internal sealed class PlexTvUser
54+
{
55+
[JsonPropertyName("authToken")]
56+
public string AuthToken { get; set; } = string.Empty;
57+
}

0 commit comments

Comments
 (0)