A watch party built from a whole series or video playlist that remembers the group's progress and each member's, so someone coming back or joining late sees the items they missed and can catch up (solo, as a catch-up watch party, or by marking items watched). - StaticPartyService + StaticPartyStore: first on-disk state (JSON file under DATA_DIR, atomic debounced writes, flushed on SIGTERM) - each session is an ordinary Party linked via party.staticLink; group position is saved by the 20s sweep and resumed on the next session - PartyService.completeCurrentItem centralizes "item done" (Emby Played + static progress); new host-only "itemEnded" socket event covers the last queued item, which was never marked Played before - missed = group-completed minus watched in-app minus Emby Played - /static/:id view, create buttons on series/playlist pages, home section - Docker volume for /data, spec/static-watch-party.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
6 KiB
Markdown
128 lines
6 KiB
Markdown
# 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.
|