| … | … | @@ -1,5 +1,5 @@ |
| 1 | 1 | <?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"> | |
| 3 | 3 | <requires> |
| 4 | 4 | <import addon="xbmc.python" version="3.00.0"/> |
| 5 | 5 | <import addon="script.module.requests" version="2.25.1"/> |
| … | … | @@ -86,26 +86,40 @@ class WyzieClient: |
| 86 | 86 | kodilog("Wyzie: {0} subtitle track(s) for {1}".format(len(tracks), ident)) |
| 87 | 87 | return tracks |
| 88 | 88 | |
| 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 | ||
| 89 | 104 | def _paths_for( |
| 90 | 105 | self, subtitles: List[Dict[str, Any]], title: str, folder_path: str |
| 91 | 106 | ) -> List[tuple]: |
| 92 | 107 | """(path, track) per track, with names that cannot collide or escape. |
| 93 | 108 | |
| 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. | |
| 99 | 115 | """ |
| 100 | stem = slugify_title(title) or "Subtitle" | |
| 101 | 116 | seen: Dict[str, int] = {} |
| 102 | 117 | out = [] |
| 103 | 118 | 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) | |
| 106 | 120 | 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)) | |
| 109 | 123 | return out |
| 110 | 124 | |
| 111 | 125 | def download_subtitles_batch( |
| … | … | @@ -119,13 +119,38 @@ def downloads(monkeypatch): |
| 119 | 119 | |
| 120 | 120 | |
| 121 | 121 | class 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 | """ | |
| 123 | 132 | paths = client.download_subtitles_batch( |
| 124 | 133 | TRACKS, title="No Other Choice", folder_path=str(tmp_path)) |
| 125 | 134 | 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"] | |
| 127 | 136 | assert all(os.path.getsize(p) > 0 for p in paths) |
| 128 | 137 | |
| 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 | ||
| 129 | 154 | def test_the_display_name_is_used_because_kodi_labels_by_filename( |
| 130 | 155 | self, client, downloads, tmp_path): |
| 131 | 156 | """wyzie's own label is better than anything we could map a code to: |
| … | … | @@ -140,7 +165,7 @@ class TestDownload: |
| 140 | 165 | paths = client.download_subtitles_batch( |
| 141 | 166 | tracks, title="Film", folder_path=str(tmp_path)) |
| 142 | 167 | 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"] | |
| 144 | 169 | assert len(set(names)) == 2 |
| 145 | 170 | |
| 146 | 171 | def test_two_tracks_with_the_same_label_do_not_collide( |
| … | … | @@ -171,7 +196,7 @@ class TestDownload: |
| 171 | 196 | monkeypatch.setattr("lib.clients.subtitle.wyzie.requests.get", flaky) |
| 172 | 197 | paths = client.download_subtitles_batch( |
| 173 | 198 | 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"] | |
| 175 | 200 | |
| 176 | 201 | def test_an_empty_body_is_not_written(self, client, monkeypatch, tmp_path): |
| 177 | 202 | def empty(url, timeout=None, headers=None): |