Skip to content

Commit 3cf0e50

Browse files
fix(resolve): prefer the series over a same-named movie on title collision
A library can hold a movie and a TV series with the exact same title (real case: the 2017 "Baywatch" film vs the 242-episode 1989 series). resolve_title checked movie_map first and returned unconditionally, so the "Baywatch Marathon" channel resolved to the lone movie and looped one film instead of the series — visible to the user as a marathon channel "stuck on a movie." On a movie/series collision, keep whichever copy has more PLAYABLE programs — the same tie-break build_library_index already uses to stop a dead duplicate shadowing the real show. A real series beats a lone movie; an all-missing series still yields to the movie. Plain titles and non-collisions are unchanged. Fix lands on every path that resolves titles (initial deploy, live re-resolve, surgical/apply). Covered by test_resolve_title_collision.py (series-wins, movie-kept-when-dead, no-collision-unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 76627fa commit 3cf0e50

2 files changed

Lines changed: 70 additions & 6 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""test_resolve_title_collision.py — a movie and a series with the same exact title.
2+
3+
Real bug: the library held both the 2017 "Baywatch" film and the 242-episode 1989
4+
series. resolve_title checked movie_map first and returned unconditionally, so the
5+
"Baywatch Marathon" channel looped the lone movie instead of the series. The fix
6+
prefers whichever copy has more PLAYABLE programs.
7+
"""
8+
9+
import sys
10+
from pathlib import Path
11+
12+
ROOT = Path(__file__).resolve().parents[2]
13+
if str(ROOT) not in sys.path:
14+
sys.path.insert(0, str(ROOT))
15+
16+
import channel_engine
17+
18+
19+
def _movie(title, state="ok"):
20+
return {"program": {"title": title, "state": state}}
21+
22+
23+
def _show(title, n_eps, state="ok"):
24+
return {
25+
"title": title,
26+
"showId": f"uuid-{title}",
27+
"programs": [{"program": {"title": f"{title} S1E{i}", "state": state}} for i in range(n_eps)],
28+
}
29+
30+
31+
def test_series_beats_same_named_movie():
32+
movie_map = {"baywatch": _movie("Baywatch")}
33+
show_map = {"baywatch": _show("Baywatch", 242)}
34+
item = channel_engine.resolve_title("Baywatch", movie_map, show_map)
35+
assert item["type"] == "TV"
36+
assert len(item["programs"]) == 242
37+
38+
39+
def test_movie_kept_when_series_is_all_missing():
40+
movie_map = {"baywatch": _movie("Baywatch")}
41+
show_map = {"baywatch": _show("Baywatch", 5, state="missing")} # dead series, 0 playable
42+
item = channel_engine.resolve_title("Baywatch", movie_map, show_map)
43+
assert item["type"] == "Movie"
44+
45+
46+
def test_no_collision_unchanged():
47+
movie_map = {"big movie": _movie("Big Movie")}
48+
show_map = {"cheers": _show("Cheers", 10)}
49+
assert channel_engine.resolve_title("Big Movie", movie_map, show_map)["type"] == "Movie"
50+
assert channel_engine.resolve_title("Cheers", movie_map, show_map)["type"] == "TV"
51+
assert channel_engine.resolve_title("Nope", movie_map, show_map) is None

channel_engine.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,27 @@ def _playable(entry):
152152

153153
# ── Title resolution ───────────────────────────────────────────────────────────
154154

155+
def _playable_count(programs):
156+
return sum(1 for p in programs if p.get("program", {}).get("state") != "missing")
157+
158+
155159
def resolve_title(title, movie_map, show_map):
156160
key = title.lower().strip()
157-
if key in movie_map:
158-
p = movie_map[key]
159-
return {"type": "Movie", "title": title, "programs": [p]}
160-
if key in show_map:
161-
s = show_map[key]
162-
return {"type": "TV", "title": s["title"], "showId": s["showId"], "programs": s["programs"]}
161+
movie = movie_map.get(key)
162+
show = show_map.get(key)
163+
# Same exact title for a movie AND a series (e.g. the 2017 "Baywatch" film vs the
164+
# 1989 series). A plain title can't disambiguate, so prefer whichever has more
165+
# PLAYABLE programs — the same tie-break build_library_index uses for dupes. A real
166+
# series (many episodes) beats a lone movie; an all-missing series yields to it.
167+
if movie and show:
168+
if _playable_count(show["programs"]) >= _playable_count([movie]):
169+
movie = None
170+
else:
171+
show = None
172+
if movie is not None:
173+
return {"type": "Movie", "title": title, "programs": [movie]}
174+
if show is not None:
175+
return {"type": "TV", "title": show["title"], "showId": show["showId"], "programs": show["programs"]}
163176
return None
164177

165178

0 commit comments

Comments
 (0)