From 9f3f16f1fb9e489363d9757d54606623d877630a Mon Sep 17 00:00:00 2001 From: Rombond Date: Thu, 12 Nov 2026 11:30:58 -0500 Subject: [PATCH] demo: keep mature titles and artwork out of the public demo Replace the seeded library, watch history, requests, issues and watchlists with family-friendly titles, and filter TMDB answers served by the demo proxy: adult entries, horror (genre 27) and a denylist of titles with gory or explicit artwork (True Blood, The Boys, Evil Dead, Locke & Key...) are removed from every list. The cache keeps raw answers; only the demo gateway applies the filter. Seed version bumped to 3. --- backend/cmd/demo-gateway/main.go | 2 +- backend/cmd/demo-gateway/seed.go | 18 ++-- backend/internal/demo/catalog.go | 60 ++++++----- backend/internal/demo/proxy.go | 17 ++-- backend/internal/demo/safe.go | 158 +++++++++++++++++++++++++++++ backend/internal/demo/safe_test.go | 99 ++++++++++++++++++ 6 files changed, 307 insertions(+), 47 deletions(-) create mode 100644 backend/internal/demo/safe.go create mode 100644 backend/internal/demo/safe_test.go diff --git a/backend/cmd/demo-gateway/main.go b/backend/cmd/demo-gateway/main.go index bbab56b..ba1c838 100644 --- a/backend/cmd/demo-gateway/main.go +++ b/backend/cmd/demo-gateway/main.go @@ -115,7 +115,7 @@ func run(seedOnly bool) error { ttl := envDur("DEMO_TMDB_TTL", 24*time.Hour) tmdbLimit := demo.NewLimiter(tmdbRate, max(1, int(tmdbRate*2))) proxy := demo.NewProxy(filepath.Join(data, "cache"), - demo.Upstream{Prefix: "/tmdb/", Base: env("DEMO_TMDB_UPSTREAM", "https://api.themoviedb.org"), TTL: ttl, Limiter: tmdbLimit, TMDBKey: key}, + demo.Upstream{Prefix: "/tmdb/", Base: env("DEMO_TMDB_UPSTREAM", "https://api.themoviedb.org"), TTL: ttl, Limiter: tmdbLimit, TMDBKey: key, Filter: demo.SafeTMDB}, demo.Upstream{Prefix: "/tmdbimg/", Base: env("DEMO_TMDB_IMAGE_UPSTREAM", "https://image.tmdb.org"), TTL: 30 * 24 * time.Hour, Limiter: demo.NewLimiter(20, 40)}, demo.Upstream{Prefix: "/boxoffice/", Base: env("DEMO_BOXOFFICE_UPSTREAM", "https://www.boxofficemojo.com"), TTL: 12 * time.Hour, Limiter: demo.NewLimiter(0.5, 2)}, ) diff --git a/backend/cmd/demo-gateway/seed.go b/backend/cmd/demo-gateway/seed.go index 06cb840..e20f125 100644 --- a/backend/cmd/demo-gateway/seed.go +++ b/backend/cmd/demo-gateway/seed.go @@ -16,7 +16,7 @@ import ( ) // The template is rebuilt when this changes (or the internal address, which is saved in it). -const seedVersion = "2" +const seedVersion = "3" type seedClient struct { base string @@ -170,16 +170,16 @@ func seed(addr, internal string) { _, out := u.do("POST", "/requests", b) return field(out, "id") } - ask(bob, "movie", 693134) // Dune: Part Two, pending - ask(bob, "tv", 76479, 1, 2) // The Boys, pending - if id := ask(bob, "movie", 335984); id != "" { // Blade Runner 2049, approved (dry-run) + ask(bob, "movie", 693134) // Dune: Part Two, pending + ask(bob, "tv", 1421, 1, 2) // Modern Family, pending + if id := ask(bob, "movie", 1022789); id != "" { // Inside Out 2, approved (dry-run) admin.do("POST", "/requests/"+id+"/approve", obj{}) } if id := ask(bob, "movie", 299534); id != "" { // Avengers: Endgame, declined carol.do("POST", "/requests/"+id+"/decline", obj{"reason": "Nous avons déjà la version 4K."}) } - ask(carol, "movie", 324857) // a request manager: approved at once - ask(dave, "movie", 545611) + ask(carol, "movie", 324857) // Spider-Man: Into the Spider-Verse; a request manager: approved at once + ask(dave, "movie", 508442) // Soul ask(admin, "tv", 1668, 1) _, out := bob.do("POST", "/issues", obj{"type": "movie", "tmdbId": 27205, "kind": "subtitles", "message": "Les sous-titres français sont décalés après 40 minutes."}) @@ -187,7 +187,7 @@ func seed(addr, internal string) { admin.do("POST", "/issues/"+id+"/comments", obj{"message": "Merci, je regarde le fichier de sous-titres."}) bob.do("POST", "/issues/"+id+"/comments", obj{"message": "Ça commence vers 41:30."}) } - _, out = carol.do("POST", "/issues", obj{"type": "tv", "tmdbId": 1396, "kind": "video", "season": 1, "episode": 3, "message": "L'image se fige au début de l'épisode."}) + _, out = carol.do("POST", "/issues", obj{"type": "tv", "tmdbId": 2316, "kind": "video", "season": 1, "episode": 3, "message": "L'image se fige au début de l'épisode."}) if id := field(out, "id"); id != "" { admin.do("POST", "/issues/"+id+"/comments", obj{"message": "Fichier remplacé, ça devrait aller."}) admin.do("POST", "/issues/"+id+"/resolve", obj{}) @@ -199,8 +199,8 @@ func seed(addr, internal string) { id int list string }{ - {bob, "movie", 274, "watchlist"}, {bob, "movie", 807, "watchlist"}, {bob, "tv", 87108, "watchlist"}, {bob, "movie", 597, "blocklist"}, - {admin, "movie", 496243, "watchlist"}, {carol, "tv", 100088, "watchlist"}, + {bob, "movie", 425, "watchlist"}, {bob, "movie", 920, "watchlist"}, {bob, "tv", 97546, "watchlist"}, {bob, "movie", 597, "blocklist"}, + {admin, "movie", 315162, "watchlist"}, {carol, "tv", 82856, "watchlist"}, } { w.u.do("POST", "/"+w.list, obj{"type": w.typ, "tmdbId": w.id}) } diff --git a/backend/internal/demo/catalog.go b/backend/internal/demo/catalog.go index cb2aa85..b873c87 100644 --- a/backend/internal/demo/catalog.go +++ b/backend/internal/demo/catalog.go @@ -19,45 +19,43 @@ type libEntry struct { } var demoMovies = []libEntry{ - {550, "Fight Club", 1999, 139, "Drama", 8.4, nil}, - {603, "The Matrix", 1999, 136, "Action,Science Fiction", 8.2, nil}, - {155, "The Dark Knight", 2008, 152, "Drama,Action,Crime", 8.5, nil}, - {27205, "Inception", 2010, 148, "Action,Science Fiction,Adventure", 8.4, nil}, - {157336, "Interstellar", 2014, 169, "Adventure,Drama,Science Fiction", 8.4, nil}, - {13, "Forrest Gump", 1994, 142, "Comedy,Drama,Romance", 8.5, nil}, - {680, "Pulp Fiction", 1994, 154, "Thriller,Crime", 8.5, nil}, - {278, "The Shawshank Redemption", 1994, 142, "Drama,Crime", 8.7, nil}, - {238, "The Godfather", 1972, 175, "Drama,Crime", 8.7, nil}, + {862, "Toy Story", 1995, 81, "Animation,Adventure,Family,Comedy", 8.0, nil}, + {863, "Toy Story 2", 1999, 92, "Animation,Comedy,Family", 7.6, nil}, + {10193, "Toy Story 3", 2010, 103, "Animation,Family,Comedy", 7.8, nil}, + {129, "Spirited Away", 2001, 125, "Animation,Family,Fantasy", 8.5, nil}, + {8392, "My Neighbor Totoro", 1988, 86, "Fantasy,Animation,Family", 8.1, nil}, + {14160, "Up", 2009, 96, "Animation,Comedy,Family,Adventure", 7.9, nil}, + {8587, "The Lion King", 1994, 89, "Family,Animation,Drama", 8.3, nil}, + {12, "Finding Nemo", 2003, 100, "Animation,Family", 7.8, nil}, + {585, "Monsters, Inc.", 2001, 92, "Animation,Comedy,Family", 7.8, nil}, + {9806, "The Incredibles", 2004, 115, "Action,Adventure,Animation,Family", 7.7, nil}, + {10681, "WALL·E", 2008, 98, "Animation,Family,Science Fiction", 8.1, nil}, + {2062, "Ratatouille", 2007, 111, "Animation,Comedy,Family,Fantasy", 7.8, nil}, + {150540, "Inside Out", 2015, 95, "Animation,Family,Adventure,Drama,Comedy", 7.9, nil}, + {354912, "Coco", 2017, 105, "Family,Animation,Fantasy,Music,Comedy,Adventure", 8.2, nil}, + {109445, "Frozen", 2013, 102, "Animation,Family,Adventure,Fantasy", 7.2, nil}, + {502356, "The Super Mario Bros. Movie", 2023, 92, "Family,Comedy,Adventure,Fantasy,Animation", 7.6, nil}, + {671, "Harry Potter and the Philosopher's Stone", 2001, 152, "Adventure,Fantasy", 7.9, nil}, {120, "The Lord of the Rings: The Fellowship of the Ring", 2001, 179, "Adventure,Fantasy,Action", 8.4, nil}, {121, "The Lord of the Rings: The Two Towers", 2002, 179, "Adventure,Fantasy,Action", 8.4, nil}, {122, "The Lord of the Rings: The Return of the King", 2003, 201, "Adventure,Fantasy,Action", 8.5, nil}, {11, "Star Wars", 1977, 121, "Adventure,Action,Science Fiction", 8.2, nil}, {105, "Back to the Future", 1985, 116, "Adventure,Comedy,Science Fiction", 8.3, nil}, {329, "Jurassic Park", 1993, 127, "Adventure,Science Fiction", 7.9, nil}, - {862, "Toy Story", 1995, 81, "Animation,Adventure,Family,Comedy", 8.0, nil}, - {129, "Spirited Away", 2001, 125, "Animation,Family,Fantasy", 8.5, nil}, - {497, "The Green Mile", 1999, 189, "Fantasy,Drama,Crime", 8.5, nil}, - {98, "Gladiator", 2000, 155, "Action,Drama,Adventure", 8.2, nil}, - {78, "Blade Runner", 1982, 117, "Science Fiction,Drama,Thriller", 7.9, nil}, + {27205, "Inception", 2010, 148, "Action,Science Fiction,Adventure", 8.4, nil}, + {157336, "Interstellar", 2014, 169, "Adventure,Drama,Science Fiction", 8.4, nil}, {438631, "Dune", 2021, 155, "Science Fiction,Adventure", 7.8, nil}, - {872585, "Oppenheimer", 2023, 180, "Drama,History", 8.1, nil}, - {496243, "Parasite", 2019, 133, "Comedy,Thriller,Drama", 8.5, nil}, - {76341, "Mad Max: Fury Road", 2015, 121, "Action,Adventure,Science Fiction", 7.6, nil}, - {14160, "Up", 2009, 96, "Animation,Comedy,Family,Adventure", 7.9, nil}, + {13, "Forrest Gump", 1994, 142, "Comedy,Drama,Romance", 8.5, nil}, {194, "Amélie", 2001, 122, "Comedy,Romance", 7.9, nil}, - {8587, "The Lion King", 1994, 89, "Family,Animation,Drama", 8.3, nil}, - {1124, "The Prestige", 2006, 130, "Drama,Mystery,Science Fiction", 8.2, nil}, } var demoShows = []libEntry{ - {1396, "Breaking Bad", 2008, 47, "Drama,Crime", 8.9, []int{7, 13, 13, 13, 16}}, - {1399, "Game of Thrones", 2011, 60, "Sci-Fi & Fantasy,Drama,Action & Adventure", 8.5, []int{10, 10, 10, 10, 10, 10}}, - {66732, "Stranger Things", 2016, 50, "Drama,Sci-Fi & Fantasy,Mystery", 8.6, []int{8, 9, 8}}, - {94605, "Arcane", 2021, 40, "Animation,Drama,Sci-Fi & Fantasy", 8.7, []int{9}}, - {100088, "The Last of Us", 2023, 55, "Drama", 8.6, []int{9}}, + {2316, "The Office", 2005, 22, "Comedy", 8.6, []int{6, 22, 25, 19, 28, 26, 26, 24, 25}}, + {246, "Avatar: The Last Airbender", 2005, 24, "Animation,Action & Adventure,Sci-Fi & Fantasy", 8.7, []int{20, 20, 21}}, + {97546, "Ted Lasso", 2020, 35, "Comedy,Drama", 8.4, []int{10, 12, 12}}, + {82856, "The Mandalorian", 2019, 38, "Sci-Fi & Fantasy,Action & Adventure,Drama", 8.4, []int{8, 8, 8}}, {95396, "Severance", 2022, 50, "Drama,Mystery,Sci-Fi & Fantasy", 8.4, []int{9}}, - {87108, "Chernobyl", 2019, 65, "Drama", 8.7, []int{5}}, - {60059, "Better Call Saul", 2015, 46, "Crime,Drama", 8.7, []int{10, 10, 10}}, + {456, "The Simpsons", 1989, 22, "Family,Animation,Comedy", 8.0, []int{13, 22}}, } // DemoUsers are the Jellyfin accounts of the demo (AdminUser is the Jellyfin and Tipsarr admin). @@ -75,10 +73,10 @@ const JellyfinAPIKey = "demo-jellyfin-key" func Catalog(now time.Time) fakejf.Catalog { c := fakejf.Catalog{ServerName: "Tipsarr demo Jellyfin", APIKey: JellyfinAPIKey, Password: Password, Users: DemoUsers, Now: now, History: map[string]fakejf.Watched{ - AdminUser: {Movies: []int{550, 603, 155, 27205, 157336, 680, 278, 120, 121, 122, 438631, 1124}, Shows: []int{1396, 1399, 94605}}, - PlainUser: {Movies: []int{13, 862, 129, 14160, 8587, 105, 329, 11}, Shows: []int{66732, 94605}}, - "carol": {Movies: []int{238, 497, 98, 496243, 194}, Shows: []int{60059}}, - "dave": {Movies: []int{78, 76341, 872585}}, + AdminUser: {Movies: []int{27205, 157336, 438631, 120, 121, 122, 11, 329, 671, 10681}, Shows: []int{2316, 82856, 95396}}, + PlainUser: {Movies: []int{13, 862, 129, 14160, 8587, 105, 12, 585}, Shows: []int{246, 456}}, + "carol": {Movies: []int{354912, 150540, 2062, 194, 8392}, Shows: []int{97546}}, + "dave": {Movies: []int{9806, 502356, 109445}}, }, Art: func(t fakejf.Title) (string, string, float64) { return t.Name, "", float64(t.TMDB%20) / 20 diff --git a/backend/internal/demo/proxy.go b/backend/internal/demo/proxy.go index c5ca35f..4a56ff7 100644 --- a/backend/internal/demo/proxy.go +++ b/backend/internal/demo/proxy.go @@ -20,11 +20,12 @@ import ( // Upstream is one service the caching proxy forwards to. type Upstream struct { - Prefix string // e.g. "/tmdb/" - Base string // e.g. "https://api.themoviedb.org" - TTL time.Duration // how long an answer is reused - Limiter *Limiter // shared outbound rate limit (nil = none) - TMDBKey string // when set, the key is injected (any api_key from the caller is dropped) + Prefix string // e.g. "/tmdb/" + Base string // e.g. "https://api.themoviedb.org" + TTL time.Duration // how long an answer is reused + Limiter *Limiter // shared outbound rate limit (nil = none) + TMDBKey string // when set, the key is injected (any api_key from the caller is dropped) + Filter func([]byte) []byte // when set, rewrites successful JSON answers as they are served (the cache keeps them raw) } // Proxy is a caching, rate-limited reverse proxy for GET calls to TMDB (API and images) and the box office. Every @@ -92,8 +93,12 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if ent.ContentType != "" { w.Header().Set("Content-Type", ent.ContentType) } + body := ent.body + if up.Filter != nil && ent.Status == http.StatusOK && isTMDBJSON(ent.ContentType) { + body = up.Filter(body) + } w.WriteHeader(ent.Status) - _, _ = w.Write(ent.body) + _, _ = w.Write(body) return } http.NotFound(w, r) diff --git a/backend/internal/demo/safe.go b/backend/internal/demo/safe.go new file mode 100644 index 0000000..6ec7bee --- /dev/null +++ b/backend/internal/demo/safe.go @@ -0,0 +1,158 @@ +package demo + +import ( + "bytes" + "encoding/json" + "strings" +) + +// The public demo is used for store screenshots and app review (13+ age rating), so the TMDB answers it serves are +// cleaned of mature titles: adult entries, horror, and a denylist of titles with gory or explicit artwork that +// TMDB does not flag. Only the demo proxy applies this; a real Tipsarr talks to TMDB directly. + +// unsafeGenres are TMDB genre ids never shown in the demo (27 = Horror). +var unsafeGenres = map[int]bool{27: true} + +// unsafeMovies and unsafeShows are TMDB ids of titles kept out of the demo. +var ( + unsafeMovies = map[int]bool{ + 764: true, 765: true, 766: true, 109428: true, 713704: true, // Evil Dead films + 550: true, 680: true, 807: true, 274: true, 24: true, 393: true, 68718: true, // Fight Club, Pulp Fiction, Se7en, Silence of the Lambs, Kill Bill, Django + 293660: true, 383498: true, 533535: true, // Deadpool films + 475557: true, 889737: true, // Joker films + 1034541: true, 663712: true, 423108: true, // Terrifier 3, Terrifier 2, The Conjuring 3 + 933260: true, 1100782: true, // The Substance, Smile 2 + 76341: true, 98: true, 1124: true, 496243: true, // Mad Max: Fury Road, Gladiator, The Prestige, Parasite (old demo library) + } + unsafeShows = map[int]bool{ + 10545: true, // True Blood + 76479: true, 205715: true, // The Boys, Gen V + 86423: true, // Locke & Key + 62264: true, // Ash vs Evil Dead + 1402: true, 62286: true, 94305: true, // The Walking Dead and spin-offs + 1413: true, 40008: true, 1405: true, 90462: true, 72844: true, 1622: true, // AHS, Hannibal, Dexter, Chucky, Hill House, Supernatural + 1399: true, 94997: true, 93405: true, 85552: true, 95557: true, 46296: true, 63247: true, // GoT, HotD, Squid Game, Euphoria, Invincible, Spartacus, Westworld + 100088: true, 42009: true, 105248: true, 1396: true, 60059: true, 87108: true, 71912: true, // TLOU, Black Mirror, Edgerunners, BB, BCS, Chernobyl, The Witcher + } +) + +// SafeTMDB removes mature titles from a TMDB JSON answer (any list of movies or series at any depth: results, +// recommendations, similar, credits, known_for...). It returns the body unchanged when nothing is removed or the +// body is not JSON. +func SafeTMDB(body []byte) []byte { + t := bytes.TrimSpace(body) + if len(t) == 0 || (t[0] != '{' && t[0] != '[') { + return body + } + d := json.NewDecoder(bytes.NewReader(body)) + d.UseNumber() + var v any + if d.Decode(&v) != nil { + return body + } + v, changed := clean(v, "") + if !changed { + return body + } + out, err := json.Marshal(v) + if err != nil { + return body + } + return out +} + +// clean walks v; kind is the media type implied by the enclosing key ("movie", "tv" or ""). +func clean(v any, kind string) (any, bool) { + changed := false + switch x := v.(type) { + case map[string]any: + for k, c := range x { + ck := kind + switch { + case k == "movie_results": + ck = "movie" + case k == "tv_results": + ck = "tv" + } + n, ch := clean(c, ck) + if ch { + x[k] = n + changed = true + } + } + return x, changed + case []any: + out := x[:0:0] + for _, c := range x { + if m, ok := c.(map[string]any); ok && unsafeTitle(m, kind) { + changed = true + continue + } + n, ch := clean(c, kind) + changed = changed || ch + out = append(out, n) + } + if !changed { + return x, false + } + return out, true + } + return v, false +} + +func num(v any) (int, bool) { + n, ok := v.(json.Number) + if !ok { + return 0, false + } + i, err := n.Int64() + return int(i), err == nil +} + +// unsafeTitle reports whether m is a movie or series entry of a list that must not be shown. +func unsafeTitle(m map[string]any, kind string) bool { + id, ok := num(m["id"]) + if !ok { + return false + } + mt, _ := m["media_type"].(string) + if mt == "" { + mt = kind + } + if mt == "" { + if _, person := m["known_for_department"]; person { + return false + } + _, title := m["title"] + _, first := m["first_air_date"] + _, orig := m["original_name"] + switch { + case title: + mt = "movie" + case first || orig: + mt = "tv" + default: + return false // not a title (a person, a genre, a video...) + } + } + if mt != "movie" && mt != "tv" { + return false + } + if a, _ := m["adult"].(bool); a { + return true + } + if gs, ok := m["genre_ids"].([]any); ok { + for _, g := range gs { + if n, ok := num(g); ok && unsafeGenres[n] { + return true + } + } + } + if mt == "movie" { + return unsafeMovies[id] + } + return unsafeShows[id] +} + +// isTMDBJSON reports whether a proxied answer is a TMDB API JSON body. +func isTMDBJSON(contentType string) bool { return strings.Contains(contentType, "json") } diff --git a/backend/internal/demo/safe_test.go b/backend/internal/demo/safe_test.go new file mode 100644 index 0000000..701e6f5 --- /dev/null +++ b/backend/internal/demo/safe_test.go @@ -0,0 +1,99 @@ +package demo + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestSafeTMDBFiltersMatureTitles(t *testing.T) { + in := `{"page":1,"results":[ + {"id":10545,"name":"True Blood","first_air_date":"2008-09-07","genre_ids":[18]}, + {"id":76479,"media_type":"tv","name":"The Boys"}, + {"id":86423,"name":"Locke & Key","original_name":"Locke & Key"}, + {"id":713704,"media_type":"movie","title":"Evil Dead Rise"}, + {"id":1,"title":"Some horror","genre_ids":[35,27]}, + {"id":2,"title":"Adult","adult":true}, + {"id":862,"title":"Toy Story","genre_ids":[16,10751]}, + {"id":10545,"media_type":"movie","title":"Same id, a movie"}, + {"id":7,"media_type":"person","name":"Someone","known_for":[{"id":1399,"media_type":"tv","name":"Game of Thrones"},{"id":11,"media_type":"movie","title":"Star Wars"}]} + ],"total_results":9}` + out := string(SafeTMDB([]byte(in))) + for _, bad := range []string{"True Blood", "The Boys", "Locke", "Evil Dead", "Some horror", "Adult", "Game of Thrones"} { + if strings.Contains(out, bad) { + t.Errorf("%q kept: %s", bad, out) + } + } + for _, good := range []string{"Toy Story", "Same id, a movie", "Someone", "Star Wars", `"total_results":9`} { + if !strings.Contains(out, good) { + t.Errorf("%q dropped: %s", good, out) + } + } +} + +func TestSafeTMDBNested(t *testing.T) { + in := `{"id":1399,"name":"x","recommendations":{"results":[{"id":10545,"name":"True Blood","first_air_date":"x"},{"id":246,"name":"Avatar","first_air_date":"x"}]}, + "credits":{"cast":[{"id":1399,"name":"Actor","original_name":"Actor","known_for_department":"Acting"}]}}` + out := string(SafeTMDB([]byte(in))) + if strings.Contains(out, "True Blood") || !strings.Contains(out, "Avatar") || !strings.Contains(out, "Actor") { + t.Fatal(out) + } + if !strings.Contains(out, `"name":"x"`) { // the detail itself is not a list entry + t.Fatal(out) + } +} + +func TestSafeTMDBUnchanged(t *testing.T) { + for _, in := range []string{`{"results":[{"id":862,"title":"Toy Story"}]}`, "not json", "", ``} { + if got := string(SafeTMDB([]byte(in))); got != in { + t.Errorf("%q rewritten to %q", in, got) + } + } +} + +func TestProxyAppliesFilter(t *testing.T) { + up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"results":[{"id":10545,"name":"True Blood","first_air_date":"x"},{"id":246,"name":"Avatar","first_air_date":"x"}]}`)) + })) + defer up.Close() + for _, filter := range []bool{true, false} { + u := Upstream{Prefix: "/tmdb/", Base: up.URL, TTL: time.Hour} + if filter { + u.Filter = SafeTMDB + } + p := NewProxy(t.TempDir(), u) + rec := httptest.NewRecorder() + p.ServeHTTP(rec, httptest.NewRequest("GET", "/tmdb/3/trending/all/week", nil)) + if got := strings.Contains(rec.Body.String(), "True Blood"); got == filter { + t.Errorf("filter=%v: %s", filter, rec.Body.String()) + } + } +} + +func TestCatalogHasNoDeniedTitles(t *testing.T) { + for _, m := range demoMovies { + if unsafeMovies[m.tmdb] { + t.Errorf("library movie %s is denied", m.name) + } + } + for _, s := range demoShows { + if unsafeShows[s.tmdb] { + t.Errorf("library show %s is denied", s.name) + } + } + c := Catalog(time.Now()) + known := map[int]bool{} + for _, ti := range c.Titles { + known[ti.TMDB] = true + } + for u, h := range c.History { + for _, id := range append(append([]int{}, h.Movies...), h.Shows...) { + if !known[id] { + t.Errorf("%s history has %d, not in the library", u, id) + } + } + } +}