- UI wording: "Watch party" = classic video, "Listen party" = classic audio, "Lounge party" = static party (tab, page, create buttons, toasts/confirms, server error messages); code names are unchanged - home: "Parties en cours" with a Watch party / Listen party badge on each card - music launch buttons say "Lancer une listen party"; a failed tile launch now restores its own label instead of always "watch party" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.7 KiB
Lounge party (watch party statique)
Les trois types de party
| Nom dans l'interface | Quoi | Où |
|---|---|---|
| Watch party | party vidéo classique, éphémère | accueil, « Parties en cours » |
| Listen party | party audio classique, éphémère (barre musique) | accueil, « Parties en cours » |
| Lounge party | party statique : une série ou une playlist vidéo suivie sur plusieurs séances | onglet « Lounge parties » |
Côté code, la lounge party reste nommée « static party » (StaticPartyService,
route /static, staticLink...). Seuls les libellés visibles ont changé. Sur
l'accueil, chaque carte affiche un badge « Watch party » ou « Listen party ».
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 flagPlayedd'Emby. Un même item n'est jamais compté deux fois de suite (lastCompletedItemId). C'est la même heuristique quePlaystateService.markPlayed: « le groupe est passé à la suite » vaut « vu ». - l'hôte passe à l'item suivant (
-
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à enPlayedpour son compte (vus ailleurs). C'est la fonction purecomputeMissedIds. -
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, quePlayer.tsutilise 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_DIRvaut./datapar défaut, et/datadans l'image Docker.docker-compose.ymly 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 (
.tmppuisrename) et debouncée (2 s) party par party. Elle est flushée de façon synchrone surSIGTERM/SIGINT. - Au démarrage, un
.tmprestant (é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
.corruptet 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.jsonde 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. livePartyIdn'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 lounge party » sur
views/serieset surviews/playlist(vidéo uniquement). - Onglet dédié « Lounge parties » (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/listen parties classiques : la liste « Parties
en cours » de l'accueil exclut les séances de lounge party
(
staticPartyIdnon 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
Playedd'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.