Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/cmd/demo-gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)},
)
Expand Down
18 changes: 9 additions & 9 deletions backend/cmd/demo-gateway/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -170,24 +170,24 @@ 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."})
if id := field(out, "id"); id != "" {
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{})
Expand All @@ -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})
}
Expand Down
60 changes: 29 additions & 31 deletions backend/internal/demo/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
Expand Down
17 changes: 11 additions & 6 deletions backend/internal/demo/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
158 changes: 158 additions & 0 deletions backend/internal/demo/safe.go
Original file line number Diff line number Diff line change
@@ -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") }
Loading
Loading