watchparty/spec/static-watch-party.md
Nicolas 1a10e5bf57
All checks were successful
Publier l'image Docker / test (pull_request) Successful in 24s
Publier l'image Docker / publish (pull_request) Has been skipped
Launch a whole series or a single season, as a watch or lounge party
- lounge parties can now be created from one season (source type
  "season", keeping seriesId/seasonNumber) as well as a whole series
- POST /api/parties accepts a Series id (every season from S1E1) and a
  Season id (that season only); series tiles get a launch button
- whole-series queues skip specials (season 0, sorted first by Emby) -
  pure seriesQueueFrom/seasonQueue in CatalogService, unit-tested, also
  used by the "from this episode" path
- series page: "Lancer toute la série" + "Créer une lounge party" in the
  header, "Lancer cette saison" + "Lounge party de cette saison" above
  the selected season's episodes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:22:57 +02:00

8.6 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 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 : une série entière, une seule saison, ou une playlist vidéo. Une playlist musicale est refusée.

    • Série entière : tous les épisodes de toutes les saisons à partir de S1E1. Les spéciaux (saison 0) sont exclus : Emby les trie en premier, et une séance commencerait par eux (CatalogService.seriesQueueFrom). Les lounge parties créées avant ce changement gardent leurs spéciaux déjà en liste.
    • Saison : uniquement les épisodes de cette saison (CatalogService.seasonQueue, par SeasonId, sinon par numéro de saison). La source garde seriesId et seasonNumber, car les épisodes sont récupérés par série puis filtrés. Nom par défaut : « Série - Saison », avec l'affiche de la série.

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, saison, é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.
  • Page série (views/series) :
    • en-tête : « Lancer toute la série » (watch party) et « Créer une lounge party » (série entière) ;
    • au-dessus des épisodes de l'onglet de saison sélectionné : « Lancer cette saison » (watch party) et « Lounge party de cette saison ».
  • « Créer une lounge party » sur views/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 (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.