trackers: media hub with webtoons, podcasts and videos; shows and anime filters
Deploy production / deploy (push) Successful in 1m22s

- /trackers/media: one card per child (reading, movies, shows, webtoons, music, podcasts,
  videos) with quick stats, a combined currently-consuming feed and a mixed watchlist
- webtoons, podcasts, videos: new owner-editable trackers on one generic media.items table,
  JSON import (owner form or scripts/media-run.sh)
- shows: /trackers/shows -> /trackers/movies?type=show; anime flag on movies.titles
  (backfilled, set from TMDB, owner toggle) and ?type=anime filter
- nav: media replaces the top-level reading, movies and music entries; their URLs are kept
This commit is contained in:
stan committed 2026-10-26 23:34:22 +00:00
1 parent 4ef75f9d25
commit 079399635d
33 files changed
+48352 -44

No files matched your search

+63 -21
View File
@@ -142,6 +142,55 @@ export const spottingTrackers: readonly Tracker[] = [
}
];
// Media: everything we read, watch and listen to, grouped under one "media" item like spotting.
// Reading, movies and music keep their own trackers and URLs; shows and anime are views of the
// movies tracker (/trackers/shows redirects to /trackers/movies?type=show); webtoons, podcasts
// and videos share media.items (#lib/media/kinds). /trackers/media is the hub.
export const mediaTrackers: readonly Tracker[] = [
{
name: 'reading',
subtitle: 'books, scientific papers and articles I am reading, have read and want to read',
href: '/trackers/books',
external: false
},
{
name: 'movies',
subtitle: 'movies we watch: Netflix, Plex and cinema, ratings and stats',
href: '/trackers/movies',
external: false
},
{
name: 'shows',
subtitle: 'TV shows and anime we watch, episode by episode',
href: '/trackers/shows',
external: false
},
{
name: 'webtoons',
subtitle: 'webtoons and manhwa we read, chapter by chapter',
href: '/trackers/webtoons',
external: false
},
{
name: 'music',
subtitle: 'everything I listen to: top artists, tracks, albums and streaks',
href: '/trackers/music',
external: false
},
{
name: 'podcasts',
subtitle: 'podcasts we follow and where we are in them',
href: '/trackers/podcasts',
external: false
},
{
name: 'videos',
subtitle: 'YouTube channels, videos and playlists worth watching',
href: '/trackers/videos',
external: false
}
];
// Games I play (plus speedcubing), grouped under "games" like spotting: every game has its
// own page at /trackers/games/<slug>, one click away from the header flyout and the chips.
// `featured` games (my main game and the ones I actively play) show first on tiles; the
@@ -334,10 +383,11 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [
private: false
},
{
name: 'reading',
subtitle: 'books, scientific papers and articles I am reading, have read and want to read',
href: '/trackers/books',
name: 'media',
subtitle: 'reading, movies, shows, webtoons, music, podcasts and videos',
href: '/trackers/media',
external: false,
children: mediaTrackers,
private: false
},
{
@@ -348,20 +398,6 @@ const allTrackers: readonly (Tracker & { private: boolean })[] = [
children: spottingTrackers,
private: false
},
{
name: 'music',
subtitle: 'everything I listen to: top artists, tracks, albums and streaks',
href: '/trackers/music',
external: false,
private: false
},
{
name: 'movies',
subtitle: 'movies and TV shows we watch: Netflix, Plex and cinema, ratings and stats',
href: '/trackers/movies',
external: false,
private: false
},
{
name: 'pc',
subtitle: 'my PC setup: every part, my devices, the next build and mouse sensitivities',
@@ -495,10 +531,16 @@ export function visibleTrackers(
hidden: readonly string[] = []
): readonly Tracker[] {
let list = isOwner ? ownerTrackers : trackers;
if (hidden.length)
list = isOwner
? list.map((t) => (hidden.includes(t.name) ? { ...t, locked: true } : t))
: list.filter((t) => !hidden.includes(t.name));
if (hidden.length) {
// Hidden trackers can sit at the top level or inside a group (movies and shows in media).
const hide = (items: readonly Tracker[]): readonly Tracker[] =>
isOwner
? items.map((t) => (hidden.includes(t.name) ? { ...t, locked: true } : t))
: items.filter((t) => !hidden.includes(t.name));
list = hide(list).map((t) =>
t.children && t.children !== gameTrackers ? { ...t, children: hide(t.children) } : t
);
}
if (!inactiveGames.length) return list;
const games = sortedGames(inactiveGames);
return list.map((t) => (t.children === gameTrackers ? { ...t, children: games } : t));