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

@@ -0,0 +1,63 @@
CREATE SCHEMA "media";
--> statement-breakpoint
CREATE TABLE "media"."items" (
"id" serial PRIMARY KEY,
"kind" text NOT NULL,
"match_key" text NOT NULL,
"title" text NOT NULL,
"creator" text,
"platform" text,
"genre" text,
"status" text NOT NULL,
"progress" integer,
"total" integer,
"series_status" text,
"form" text,
"url" text,
"cover_url" text,
"rating" real,
"who" text,
"started_on" date,
"finished_on" date,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "items_kind_check" CHECK ("kind" IN ('webtoon','podcast','video')),
CONSTRAINT "items_status_check" CHECK (("kind" = 'webtoon' AND "status" IN ('reading','completed','on_hold','dropped','plan'))
OR ("kind" = 'podcast' AND "status" IN ('listening','caught_up','paused','dropped','plan'))
OR ("kind" = 'video' AND "status" IN ('watching','following','watched','dropped','plan'))),
CONSTRAINT "items_title_check" CHECK (length(btrim("title")) > 0),
CONSTRAINT "items_series_check" CHECK ("series_status" IS NULL OR "series_status" IN ('ongoing','completed')),
CONSTRAINT "items_form_check" CHECK ("form" IS NULL OR "form" IN ('channel','video','playlist')),
CONSTRAINT "items_who_check" CHECK ("who" IS NULL OR "who" IN ('stan','juhee','both')),
CONSTRAINT "items_rating_check" CHECK ("rating" IS NULL OR ("rating" >= 0.5 AND "rating" <= 5 AND "rating" * 2 = round("rating" * 2))),
CONSTRAINT "items_progress_check" CHECK (("progress" IS NULL OR "progress" >= 0) AND ("total" IS NULL OR "total" >= 0)),
CONSTRAINT "items_url_check" CHECK ("url" IS NULL OR "url" ~ '^https?://'),
CONSTRAINT "items_cover_check" CHECK ("cover_url" IS NULL OR "cover_url" ~ '^https://')
);
--> statement-breakpoint
ALTER TABLE "movies"."titles" ADD COLUMN "anime" boolean DEFAULT false NOT NULL;--> statement-breakpoint
CREATE UNIQUE INDEX "items_match_key" ON "media"."items" ("match_key");--> statement-breakpoint
CREATE INDEX "items_kind_status_idx" ON "media"."items" ("kind","status");--> statement-breakpoint
-- Anime is a filter inside Shows: Japanese animation (kana/kanji original title) plus the two
-- with Latin original titles. New TMDB matches set it from original_language = 'ja'.
UPDATE "movies"."titles" SET "anime" = true
WHERE "type" = 'show' AND 'Animation' = ANY("genres")
AND ("original_title" ~ '[ぁ-ゟ゠-ヿ一-鿿]' OR "title" IN ('Bleach', 'Hunter x Hunter'));
--> statement-breakpoint
-- Grants: app role read/write only (like movies; no personal_ro). Prod only, where migrations
-- run as `personal`.
REVOKE ALL ON SCHEMA media FROM PUBLIC;
--> statement-breakpoint
DO $$
BEGIN
IF current_user = 'personal' AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN
GRANT USAGE ON SCHEMA media TO personal_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA media TO personal_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA media TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA media
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA media
GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
END IF;
END $$;
File diff suppressed because it is too large. Load diff