- new "Parties permanentes" tab (/static, sidebar + mobile sheet), highlighted on a party's own page too; the home page no longer lists static parties, and its live "Watch parties en cours" list leaves their sessions out - StaticPartyStore now writes data/static-parties/<id>.json - one file per party, debounced per party, so a write only touches the party that changed and a corrupt file only costs that party (moved aside to .corrupt, the rest still load); leftover .tmp files are cleaned up; deleting a party removes its file and cancels any pending save - the first release's single static-parties.json is migrated once on load, then kept as .migrated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
7.1 KiB
Markdown
140 lines
7.1 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 par watch party** :
|
|
`${DATA_DIR}/static-parties/<id>.json`, au format `{version: 1, party}`.
|
|
- `DATA_DIR` vaut `./data` par défaut, et `/data` dans l'image Docker.
|
|
`docker-compose.yml` y monte `./data`.
|
|
- Un fichier par party limite les dégâts : une écriture ne touche que la
|
|
party modifiée, et un fichier corrompu ne coûte que cette party-là.
|
|
- L'écriture est atomique (`.tmp` puis `rename`) et debouncée (2 s) party
|
|
par party. Elle est flushée de façon synchrone sur `SIGTERM`/`SIGINT`.
|
|
- Au démarrage, un `.tmp` restant (écriture interrompue) est supprimé ; le
|
|
vrai fichier, s'il existe, est la dernière version complète.
|
|
- Un fichier corrompu (ou dont le nom ne correspond pas à l'id) est
|
|
renommé en `.corrupt` et ignoré ; les autres parties se chargent
|
|
normalement. Le serveur ne crashe jamais pour ça.
|
|
- Supprimer une party supprime son fichier et annule toute sauvegarde en
|
|
attente pour elle.
|
|
- Migration : l'ancien fichier unique `${DATA_DIR}/static-parties.json` de
|
|
la première version est éclaté en un fichier par party au premier
|
|
démarrage, puis renommé en `.migrated` (conservé, pas supprimé).
|
|
- 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).
|
|
- Onglet dédié « Parties permanentes » (sidebar et menu mobile), route
|
|
`/static` (`views/staticParties/`) : une carte par watch party avec la
|
|
progression, le prochain élément, un badge « N manqués » et une pastille
|
|
« séance en cours ». L'onglet reste actif sur la page d'une party.
|
|
- Séparation avec les watch parties classiques : la liste « Watch parties en
|
|
cours » de l'accueil exclut les séances de watch party permanente
|
|
(`staticPartyId` non nul) ; elles ne s'affichent que dans leur onglet.
|
|
|
|
## 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.
|