# Watch party statique (permanente) ## Objectif Suivre une série entière, ou une playlist vidéo, en groupe sur plusieurs soirées. L'app mémorise : - **l'avancement du groupe** : les éléments terminés ensemble, et où la dernière séance s'est arrêtée ; - **l'avancement de chaque personne** : ce qu'elle a vu. Une personne qui revient ou qui rejoint en cours de route voit la liste de ce qu'elle a **manqué** et peut le rattraper. Contrairement à une watch party classique, qui vit en mémoire et disparaît avec son dernier membre, une watch party statique est **persistée sur disque** et survit aux redémarrages. ## Modèle | Entité | Où | Rôle | |---|---|---| | `StaticParty` | `server/src/types.ts`, persistée | la « saison » du groupe : items, progression du groupe, membres | | `StaticPartyMember` | dans `StaticParty.members` | `watchedItemIds` = ce que la personne a vu dans l'app | | Séance | une `Party` live ordinaire + `party.staticLink` | la lecture elle-même : lecteur, sync, flux partagé et lobby réutilisés tels quels | `party.staticLink.mode` : - `group` : la séance du groupe. Un item terminé fait avancer le groupe **et** est crédité à chaque participant connecté. - `catchUp` : un rattrapage. L'item est crédité aux participants connectés uniquement, le groupe ne bouge pas. `party.hiddenFromLobby` : un rattrapage en solo n'apparaît pas dans la liste des watch parties en cours. ## Règles - **Item terminé** : - l'hôte passe à l'item suivant (`next`), - ou le dernier item de la file se termine (`itemEnded`, nouvel event socket réservé à l'hôte). Les deux passent par `PartyService.completeCurrentItem`, qui gère aussi le flag `Played` d'Emby. Un même item n'est jamais compté deux fois de suite (`lastCompletedItemId`). C'est la même heuristique que `PlaystateService.markPlayed` : « le groupe est passé à la suite » vaut « vu ». - **Seuls les membres connectés** au moment où l'item se termine sont crédités. Une personne partie en cours d'épisode l'a donc manqué ; elle peut le marquer comme vu. - **Manqués d'une personne** : les items terminés par le groupe, dans l'ordre de la liste, moins ceux qu'elle a vus dans l'app (`watchedItemIds`), moins ceux qu'Emby a déjà en `Played` pour son compte (vus ailleurs). C'est la fonction pure `computeMissedIds`. - **Reprise** : le sweep existant des 20 s enregistre la position de l'hôte (`group.resumePositionTicks`). La séance suivante démarre avec cette position comme position de l'hôte, que `Player.ts` utilise déjà pour placer la première image. - **Nouveaux épisodes** : chaque lecture de la page, et chaque lancement de séance, recharge la source. Les nouveaux items sont ajoutés à la fin ; les items retirés de la source sont conservés dans l'historique (`mergeItemLists`). - **Accès** : ouverte à tous les comptes Emby. - Rejoindre = devenir membre. Rejoindre la séance du groupe depuis le lobby rend aussi membre de la watch party statique. - N'importe quel membre peut lancer la séance ; celui qui la lance en devient l'hôte. - Quitter efface sa propre progression. - Seul le créateur peut supprimer la watch party. - **Contenus** : séries et playlists vidéo uniquement. Une playlist musicale est refusée. ## API (`server/src/index.ts`) | Méthode | Route | Rôle | |---|---|---| | GET | `/api/static-parties` | liste, avec le nombre de manqués de l'utilisateur et la séance en cours | | POST | `/api/static-parties` | `{sourceId, name?}` : série, épisode (sa série) ou playlist | | GET | `/api/static-parties/:id` | détail : items + état groupe / utilisateur, membres, `missedItemIds` | | POST | `/:id/join`, `/:id/leave` | devenir membre / quitter | | DELETE | `/:id` | supprimer (créateur uniquement) | | POST | `/:id/session` | lancer la séance du groupe, ou rejoindre celle en cours, puis renvoyer `{partyId}` | | POST | `/:id/items/:itemId/watched` | marquer comme vu | | POST | `/:id/catch-up` | `{itemIds, mode: 'solo'\|'party'}`, renvoie `{partyId}` | ## Persistance - `StaticPartyStore` écrit un fichier JSON unique : `${DATA_DIR}/static-parties.json`. - `DATA_DIR` vaut `./data` par défaut, et `/data` dans l'image Docker. `docker-compose.yml` y monte `./data`. - L'écriture est atomique (`.tmp` puis `rename`) et debouncée (2 s). Elle est flushée de façon synchrone sur `SIGTERM`/`SIGINT`. - Un fichier corrompu est renommé en `.corrupt` et le serveur démarre à vide ; il ne crashe jamais pour ça. - Pas de SQLite : l'image tourne en Node 20 (pas de `node:sqlite`), une dépendance native serait une première dans l'image, et les données sont minuscules. - `livePartyId` n'est jamais persisté : un redémarrage termine toutes les séances, et la reprise se fait depuis la position enregistrée. ## Client - `views/staticParty/` (route `/static/:id`) contient : - l'en-tête, avec la progression du groupe et le bouton lancer / rejoindre la séance ; - « Tu as manqué », avec pour chaque élément cases à cocher, *Regarder en solo* et *Marquer comme vu*, plus deux actions groupées : rattraper en solo, ou lancer une watch party de rattrapage ; - les membres ; - la liste complète des items. - Bouton « Créer une watch party permanente » sur `views/series` et sur `views/playlist` (vidéo uniquement). - Accueil : - une section « Watch parties permanentes », avec la progression, un badge « N manqués » et une pastille « séance en cours » ; - les séances live affichent un lien `↻ nom` vers leur watch party statique. ## Limites connues - « Vu » reste une heuristique : passer à la suite compte comme vu, comme pour le flag `Played` d'Emby. Il n'y a pas de seuil de pourcentage. - La liste et le détail relisent la source Emby à chaque affichage (une requête par watch party pour la liste). C'est acceptable à l'échelle d'un serveur perso ; à mettre en cache si la liste grossit. - Plusieurs watch parties statiques peuvent exister sur la même série (des groupes différents) : c'est voulu, sans déduplication.