OverviewFilesCommitsIssuesDocsReleasesPeers

kodi-jacktook commits / 3c2b7b1

Name a subtitle after its language, since that name is the label (1.16.3)
Subtitles loaded on the Pi and the menu was unusable: all 32 entries read "in library seekable local copy". Kodi labels a subtitle by its filename, and the filename was built from `data["title"]` -- which for a stremio row is the ROW LABEL, not the film. The files were literally: 'in library seekable\nlocal copy.Albanian.srt' 'in library seekable\nlocal copy.Arabic.srt' A newline inside a filename, 32 times. slugify_title strips punctuation but keeps \s, so the row label's line break went straight through. The title is now absent from the name entirely. The folder is already per-title, so the name only has to carry what varies inside it -- the language -- and that happens to be exactly the string worth showing. `display` is still the source, because it separates 'Portuguese (BR)' from 'Portuguese' where pb/pt only do so if we know the mapping; it is remote text landing in a filename, so it is whitespace-flattened and stripped of separators first. Neither the wrong name nor the newline was reachable by the unit tests as written: they passed a sensible title, because that is what I assumed was there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3c2b7b12184a9be1e3a9925f652431d52119ec54
by plankatron <fleeky@gmail.com> · 2026-09-05T01:20:33.000Z

Changed files (3) +54 -15

plugin.video.jacktook/addon.xml +1 -1
……@@ -1,5 +1,5 @@
11<?xml version="1.0" encoding="utf-8"?>
2 <addon id="plugin.video.jacktook" name="Jacktook" version="1.16.2" provider-name="Sam-Max">
2 <addon id="plugin.video.jacktook" name="Jacktook" version="1.16.3" provider-name="Sam-Max">
33 <requires>
44 <import addon="xbmc.python" version="3.00.0"/>
55 <import addon="script.module.requests" version="2.25.1"/>
plugin.video.jacktook/lib/clients/subtitle/wyzie.py +24 -10
……@@ -86,26 +86,40 @@ class WyzieClient:
8686 kodilog("Wyzie: {0} subtitle track(s) for {1}".format(len(tracks), ident))
8787 return tracks
8888
89 def _label_for(self, track: Dict[str, Any]) -> str:
90 """The filename, which IS the label Kodi shows in its subtitle menu.
91
92 wyzie's own `display` rather than a mapped language code: it already
93 distinguishes 'Portuguese (BR)' from 'Portuguese', which pb/pt only do
94 if we happen to know that mapping.
95
96 `display` is remote text on its way into a filename, so it is flattened
97 (slugify_title keeps \s, newlines included) and stripped of any path
98 separator before use.
99 """
100 raw = str(track.get("display") or track.get("language") or "und")
101 label = slugify_title(" ".join(raw.split()))
102 return os.path.basename(label).strip(". ") or "und"
103
89104 def _paths_for(
90105 self, subtitles: List[Dict[str, Any]], title: str, folder_path: str
91106 ) -> List[tuple]:
92107 """(path, track) per track, with names that cannot collide or escape.
93108
94 The label comes from wyzie's own `display`, because Kodi labels a
95 subtitle by its filename and `display` already distinguishes
96 'Portuguese (BR)' from 'Portuguese' -- a distinction the language codes
97 (pb/pt) only carry if we happen to know that mapping. It arrives inside
98 a remote JSON body, so it goes through slugify_title like the title.
109 The title is deliberately NOT in the name. `data["title"]` for a stremio
110 row is the ROW LABEL, not the film -- measured on the Pi, all 32 tracks
111 were written as `in library seekable\nlocal copy.<Language>.srt`, which
112 is a newline inside a filename and a subtitle menu where every entry
113 read "in library seekable local copy". The folder is already per-title,
114 so the name carries the one thing that varies within it.
99115 """
100 stem = slugify_title(title) or "Subtitle"
101116 seen: Dict[str, int] = {}
102117 out = []
103118 for track in subtitles:
104 label = slugify_title(str(track.get("display") or track.get("language") or "und"))
105 label = os.path.basename(label).strip(". ") or "und"
119 label = self._label_for(track)
106120 n = seen[label] = seen.get(label, 0) + 1
107 suffix = label if n == 1 else "{0}.{1}".format(label, n)
108 out.append((os.path.join(folder_path, "{0}.{1}.srt".format(stem, suffix)), track))
121 stem = label if n == 1 else "{0}.{1}".format(label, n)
122 out.append((os.path.join(folder_path, "{0}.srt".format(stem)), track))
109123 return out
110124
111125 def download_subtitles_batch(
plugin.video.jacktook/tests/unit/test_wyzie_subtitles.py +29 -4
……@@ -119,13 +119,38 @@ def downloads(monkeypatch):
119119
120120
121121class TestDownload:
122 def test_files_land_in_the_folder_with_readable_names(self, client, downloads, tmp_path):
122 def test_the_filename_is_the_language_and_nothing_else(self, client, downloads, tmp_path):
123 """Kodi labels a subtitle by its filename, and the folder is already
124 per-title, so the title in the name buys nothing and cost a lot:
125
126 measured on the Pi, `data["title"]` for a stremio row is the ROW LABEL,
127 not the film. Every one of the 32 tracks was written as
128 `in library seekable\nlocal copy.<Language>.srt` -- a newline inside a
129 filename (slugify_title keeps \s), and a subtitle menu in which every
130 entry read "in library seekable local copy".
131 """
123132 paths = client.download_subtitles_batch(
124133 TRACKS, title="No Other Choice", folder_path=str(tmp_path))
125134 names = sorted(os.path.basename(p) for p in paths)
126 assert names == ["No Other Choice.English.srt", "No Other Choice.French.srt"]
135 assert names == ["English.srt", "French.srt"]
127136 assert all(os.path.getsize(p) > 0 for p in paths)
128137
138 def test_a_row_label_never_reaches_the_filename(self, client, downloads, tmp_path):
139 paths = client.download_subtitles_batch(
140 TRACKS, title="in library \u2014 seekable\nlocal copy",
141 folder_path=str(tmp_path))
142 for p in paths:
143 n = os.path.basename(p)
144 assert "\n" not in n and "library" not in n, repr(n)
145
146 def test_control_characters_in_a_label_are_flattened(self, client, downloads, tmp_path):
147 """`display` is remote text and lands in a filename."""
148 tracks = [{"display": "Eng\nlish\ttext", "language": "en", "url": "http://x/en"}]
149 paths = client.download_subtitles_batch(
150 tracks, title="Film", folder_path=str(tmp_path))
151 n = os.path.basename(paths[0])
152 assert "\n" not in n and "\t" not in n, repr(n)
153
129154 def test_the_display_name_is_used_because_kodi_labels_by_filename(
130155 self, client, downloads, tmp_path):
131156 """wyzie's own label is better than anything we could map a code to:
……@@ -140,7 +165,7 @@ class TestDownload:
140165 paths = client.download_subtitles_batch(
141166 tracks, title="Film", folder_path=str(tmp_path))
142167 names = sorted(os.path.basename(p) for p in paths)
143 assert names == ["Film.Portuguese BR.srt", "Film.Portuguese.srt"]
168 assert names == ["Portuguese BR.srt", "Portuguese.srt"]
144169 assert len(set(names)) == 2
145170
146171 def test_two_tracks_with_the_same_label_do_not_collide(
……@@ -171,7 +196,7 @@ class TestDownload:
171196 monkeypatch.setattr("lib.clients.subtitle.wyzie.requests.get", flaky)
172197 paths = client.download_subtitles_batch(
173198 TRACKS, title="Film", folder_path=str(tmp_path))
174 assert [os.path.basename(p) for p in paths] == ["Film.English.srt"]
199 assert [os.path.basename(p) for p in paths] == ["English.srt"]
175200
176201 def test_an_empty_body_is_not_written(self, client, monkeypatch, tmp_path):
177202 def empty(url, timeout=None, headers=None):