From 11ed3640940eda31dbf6ba349a8ea713033edebb Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 25 May 2026 12:22:58 -0400 Subject: [PATCH] filter false opensubtitles subtitle candidates --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 1 + Dreamio/DreamioWebViewController.swift | 50 ++- Dreamio/StreamCandidate.swift | 33 +- Tests/StreamResolverTests.swift | 27 ++ ...filter-false-opensubtitles-candidates.html | 362 ++++++++++++++++++ 6 files changed, 465 insertions(+), 9 deletions(-) create mode 100644 docs/turns/2026-05-25-filter-false-opensubtitles-candidates.html diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 1417109..f943500 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -31,3 +31,4 @@ {"id":"int-6e411a6a","kind":"field_change","created_at":"2026-05-25T16:03:23.023525Z","actor":"dirtydishes","issue_id":"dreamio-656","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} {"id":"int-fe1c7364","kind":"field_change","created_at":"2026-05-25T16:04:54.482803Z","actor":"dirtydishes","issue_id":"dreamio-656","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"fixed"}} {"id":"int-f9deecdb","kind":"field_change","created_at":"2026-05-25T16:18:29.458162Z","actor":"dirtydishes","issue_id":"dreamio-urs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed by rejecting OpenSubtitles manifest.json_N identifiers as playable subtitle URLs, promoting file_id values to API download URLs, and adding parser coverage for the live log shape."}} +{"id":"int-569ee372","kind":"field_change","created_at":"2026-05-25T16:22:50.024736Z","actor":"dirtydishes","issue_id":"dreamio-433","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed by tightening OpenSubtitles subtitle URL filtering in the web bridge and Swift parser, plus adding regression coverage for logged artwork and addon endpoint false positives."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2260149..16c6888 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,5 @@ {"_type":"issue","id":"dreamio-8cz","title":"fix stremio external subtitle loading regression","description":"After adding late subtitle forwarding for native playback, Stremio external subtitle loading is failing. Investigate the injected bridge and native subtitle forwarding path, then adjust behavior so Stremio can still load external subtitles while native playback receives late candidates.","status":"closed","priority":0,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T11:05:42Z","created_by":"dirtydishes","updated_at":"2026-05-25T11:07:35Z","started_at":"2026-05-25T11:05:55Z","closed_at":"2026-05-25T11:07:35Z","close_reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"dreamio-433","title":"Filter false OpenSubtitles subtitle candidates","description":"Dreamio is treating addon artwork and OpenSubtitles addon endpoints as external subtitle candidates, which causes the native player UI to show only embedded subtitles. Tighten subtitle URL detection in the web bridge and Swift parser, and add regression coverage for the logged false positives.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T16:20:47Z","created_by":"dirtydishes","updated_at":"2026-05-25T16:22:50Z","started_at":"2026-05-25T16:20:50Z","closed_at":"2026-05-25T16:22:50Z","close_reason":"Fixed by tightening OpenSubtitles subtitle URL filtering in the web bridge and Swift parser, plus adding regression coverage for logged artwork and addon endpoint false positives.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-urs","title":"Fix OpenSubtitles manifest-style subtitle URLs","description":"OpenSubtitles subtitle candidates discovered from Stremio are being resolved as manifest.json_N URLs, producing 404s and leaving only embedded subtitles visible. Preserve and resolve real subtitle URLs so external subtitle tracks can attach in the native player.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T16:16:52Z","created_by":"dirtydishes","updated_at":"2026-05-25T16:18:29Z","started_at":"2026-05-25T16:16:57Z","closed_at":"2026-05-25T16:18:29Z","close_reason":"Fixed by rejecting OpenSubtitles manifest.json_N identifiers as playable subtitle URLs, promoting file_id values to API download URLs, and adding parser coverage for the live log shape.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-656","title":"Capture OpenSubtitles candidates from Stremio app-state messages","description":"OpenSubtitlesV3 appears loaded in Stremio before native playback launches, but Dreamio forwards zero external subtitle candidates. The likely failure is not native-player timing; it is that the injected WebKit bridge does not extract Stremio's loaded subtitle metadata/state into URL candidates before opening VLC.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T16:00:09Z","created_by":"dirtydishes","updated_at":"2026-05-25T16:04:54Z","started_at":"2026-05-25T16:00:18Z","closed_at":"2026-05-25T16:04:54Z","close_reason":"fixed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-hzj","title":"OpenSubtitles tracks missing from native captions menu","description":"OpenSubtitles subtitle candidates can be discovered or resolved inconsistently, and external VLC subtitle slaves may not become visible quickly enough to show as selectable native caption tracks. Harden discovery, resolution, attachment, diagnostics, tests, and turn documentation for the native captions path.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T15:51:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T15:53:53Z","started_at":"2026-05-25T15:51:13Z","closed_at":"2026-05-25T15:53:53Z","close_reason":"Hardened OpenSubtitles candidate discovery, nested payload resolution, VLC external subtitle visibility selection, diagnostics, tests, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index daa6544..c45e1d5 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -84,6 +84,11 @@ final class DreamioWebViewController: UIViewController { const postedSubtitleURLs = new Set(); const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig; const subtitleSignalPattern = /subtitle|subtitles|opensubtitles|vtt|srt|ass|ssa/i; + const subtitleExtensions = new Set(["srt", "vtt", "ass", "ssa", "sub"]); + const nonSubtitleExtensions = new Set([ + "aac", "avi", "bmp", "css", "gif", "heic", "ico", "jpeg", "jpg", "js", "json", + "m4a", "m4v", "mkv", "mov", "mp3", "mp4", "mpeg", "mpg", "png", "svg", "ts", "webm", "webp" + ]); const subtitleObjectKeys = [ "attributes", "files", @@ -125,14 +130,51 @@ final class DreamioWebViewController: UIViewController { } }; + const isDirectSubtitleFileURL = (url) => { + try { + const parsed = new URL(url, window.location.href); + const extension = parsed.pathname.split(".").pop().toLowerCase(); + return subtitleExtensions.has(extension) + || Array.from(subtitleExtensions).some((ext) => parsed.href.toLowerCase().includes(`.${ext}?`) || parsed.href.toLowerCase().includes(`.${ext}&`)); + } catch (_) { + return false; + } + }; + + const isProbablyNonSubtitleAssetURL = (url) => { + try { + const extension = new URL(url, window.location.href).pathname.split(".").pop().toLowerCase(); + return nonSubtitleExtensions.has(extension); + } catch (_) { + return false; + } + }; + + const isOpenSubtitlesDownloadURL = (url) => { + try { + const parsed = new URL(url, window.location.href); + const host = parsed.hostname.toLowerCase(); + const path = parsed.pathname.toLowerCase(); + if (!host.includes("opensubtitles")) { + return false; + } + if (/\/manifest\.json(?:_\d+)?$/i.test(path)) { + return false; + } + return /\/api\/v1\/download(?:\/|$)/i.test(path) + || /\/download(?:\/|$)/i.test(path) + || /\/subtitles?(?:\/|$)/i.test(path); + } catch (_) { + return false; + } + }; + const isSubtitleURL = (url) => { if (!url || isOpenSubtitlesManifestID(url)) { return false; } - subtitleURLPattern.lastIndex = 0; - const matches = subtitleURLPattern.test(url) || /api\.opensubtitles\.com\/api\/v1\/download/i.test(url); - subtitleURLPattern.lastIndex = 0; - return matches; + return !isProbablyNonSubtitleAssetURL(url) + && (isDirectSubtitleFileURL(url) || isOpenSubtitlesDownloadURL(url)); }; const findResolverURL = () => { diff --git a/Dreamio/StreamCandidate.swift b/Dreamio/StreamCandidate.swift index 99f971c..e735f88 100644 --- a/Dreamio/StreamCandidate.swift +++ b/Dreamio/StreamCandidate.swift @@ -132,6 +132,10 @@ struct StreamCandidate { enum SubtitleCandidateParser { private static let supportedExtensions = ["srt", "vtt", "ass", "ssa", "sub"] + private static let nonSubtitleExtensions = [ + "aac", "avi", "bmp", "css", "gif", "heic", "ico", "jpeg", "jpg", "js", "json", + "m4a", "m4v", "mkv", "mov", "mp3", "mp4", "mpeg", "mpg", "png", "svg", "ts", "webm", "webp" + ] private static let urlFields = ["url", "href", "src", "link", "subtitles", "subtitle", "subtitleUrl", "subtitleURL", "file", "download", "fileUrl", "fileURL"] private static let labelFields = ["label", "name", "title", "file_name", "filename", "lang", "language", "id"] private struct CandidateContext { @@ -244,14 +248,14 @@ enum SubtitleCandidateParser { return nil } - let lowercased = url.absoluteString.lowercased() if isOpenSubtitlesManifestIdentifier(url) { return nil } - guard supportedExtensions.contains(url.pathExtension.lowercased()) - || supportedExtensions.contains(where: { lowercased.contains(".\($0)?") || lowercased.contains(".\($0)&") }) - || lowercased.contains("subtitle") - || lowercased.contains("opensubtitles") + guard !nonSubtitleExtensions.contains(url.pathExtension.lowercased()) else { + return nil + } + guard isDirectSubtitleFile(url) + || isOpenSubtitlesDownloadURL(url) else { return nil } @@ -259,6 +263,25 @@ enum SubtitleCandidateParser { return url } + private static func isDirectSubtitleFile(_ url: URL) -> Bool { + let lowercased = url.absoluteString.lowercased() + return supportedExtensions.contains(url.pathExtension.lowercased()) + || supportedExtensions.contains(where: { lowercased.contains(".\($0)?") || lowercased.contains(".\($0)&") }) + } + + private static func isOpenSubtitlesDownloadURL(_ url: URL) -> Bool { + guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else { + return false + } + let path = url.path.lowercased() + guard !isOpenSubtitlesManifestIdentifier(url) else { + return false + } + return path.range(of: #"(^|/)api/v1/download(/|$)"#, options: .regularExpression) != nil + || path.range(of: #"(^|/)download(/|$)"#, options: .regularExpression) != nil + || path.range(of: #"(^|/)subtitles?(/|$)"#, options: .regularExpression) != nil + } + private static func isOpenSubtitlesManifestIdentifier(_ url: URL) -> Bool { guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else { return false diff --git a/Tests/StreamResolverTests.swift b/Tests/StreamResolverTests.swift index 330bf53..39854a8 100644 --- a/Tests/StreamResolverTests.swift +++ b/Tests/StreamResolverTests.swift @@ -12,6 +12,7 @@ struct StreamResolverTests { testOpenSubtitlesV3CandidateParsing() testOpenSubtitlesNestedAttributesFilesParsing() testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles() + testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored() testOpenSubtitlesV3DownloadResponseResolution() testOpenSubtitlesNestedDownloadResponseResolution() await testSubtitleResolverDownloadJSONReturningLink() @@ -213,6 +214,32 @@ struct StreamResolverTests { assertEqual(candidates[0].language, "eng") } + private static func testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored() { + let payload: [String: Any] = [ + "subtitles": [ + [ + "label": "External Subtitle", + "url": "http://www.strem.io/images/addons/opensubtitles-logo.png" + ], + [ + "label": "External Subtitle", + "url": "https://opensubtitles.strem.io/stremio/v1" + ], + [ + "label": "English", + "url": "https://opensubtitles.example.test/subtitles/movie.en.srt" + ] + ], + "body": "metadata https://www.strem.io/images/addons/opensubtitles-logo.png" + ] + + let candidates = SubtitleCandidateParser.candidates(in: payload) + + assertEqual(candidates.count, 1) + assertEqual(candidates[0].url.absoluteString, "https://opensubtitles.example.test/subtitles/movie.en.srt") + assertEqual(candidates[0].label, "English") + } + private static func testOpenSubtitlesV3DownloadResponseResolution() { let payload = """ { diff --git a/docs/turns/2026-05-25-filter-false-opensubtitles-candidates.html b/docs/turns/2026-05-25-filter-false-opensubtitles-candidates.html new file mode 100644 index 0000000..fcf70f5 --- /dev/null +++ b/docs/turns/2026-05-25-filter-false-opensubtitles-candidates.html @@ -0,0 +1,362 @@ + + + + + + Filter False OpenSubtitles Subtitle Candidates + + + +
+
+

Filter False OpenSubtitles Subtitle Candidates

+

Dreamio now stops treating OpenSubtitles addon artwork and base addon endpoints as native subtitle files, so VLC is no longer asked to resolve junk candidates before external subtitle tracks can appear.

+
+ +
+

Summary

+

The pasted runtime log showed two false external subtitle candidates: an opensubtitles-logo.png image and https://opensubtitles.strem.io/stremio/v1. Both were being buffered and resolved as if they were subtitles, then rejected downstream. This change tightens subtitle candidate detection at both the injected JavaScript bridge and Swift parser layers.

+
+ +
+

Changes Made

+
    +
  • Added explicit subtitle extension and non-subtitle asset filtering in the web bridge.
  • +
  • Restricted OpenSubtitles URL acceptance to direct subtitle files, OpenSubtitles download API URLs, and subtitle/download paths on OpenSubtitles hosts.
  • +
  • Mirrored the same filtering in SubtitleCandidateParser so noisy bridge payloads cannot reintroduce bad candidates.
  • +
  • Added a regression test for the logged PNG artwork URL and addon base endpoint.
  • +
+
+ +
+

Context

+

VLC was correctly detecting and selecting the embedded MKV subtitle track. The failure was earlier: Dreamio’s bridge discovered two candidates, but neither was an actual external subtitle file. The native resolver then rejected both, leaving the UI with only embedded subtitles.

+
+ +
+

Important Implementation Details

+

The previous heuristic accepted any URL containing opensubtitles or subtitle. That was too broad because addon logos, metadata endpoints, and app API routes can contain those words. The new logic keeps permissive support for real subtitle files and known OpenSubtitles download flows while rejecting common media, image, script, and manifest-style assets.

+
+ +
+

Relevant Diff Snippets

+

Dreamio/DreamioWebViewController.swift

Dreamio/DreamioWebViewController.swift
-4+46
83 unmodified lines
84
85
86
87
88
89
35 unmodified lines
125
126
127
128
129
130
131
132
133
134
135
136
137
138
83 unmodified lines
const postedSubtitleURLs = new Set();
const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig;
const subtitleSignalPattern = /subtitle|subtitles|opensubtitles|vtt|srt|ass|ssa/i;
const subtitleObjectKeys = [
"attributes",
"files",
35 unmodified lines
}
};
+
const isSubtitleURL = (url) => {
if (!url || isOpenSubtitlesManifestID(url)) {
return false;
}
subtitleURLPattern.lastIndex = 0;
const matches = subtitleURLPattern.test(url) || /api\.opensubtitles\.com\/api\/v1\/download/i.test(url);
subtitleURLPattern.lastIndex = 0;
return matches;
};
+
const findResolverURL = () => {
83 unmodified lines
84
85
86
87
88
89
90
91
92
93
94
35 unmodified lines
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
83 unmodified lines
const postedSubtitleURLs = new Set();
const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig;
const subtitleSignalPattern = /subtitle|subtitles|opensubtitles|vtt|srt|ass|ssa/i;
const subtitleExtensions = new Set(["srt", "vtt", "ass", "ssa", "sub"]);
const nonSubtitleExtensions = new Set([
"aac", "avi", "bmp", "css", "gif", "heic", "ico", "jpeg", "jpg", "js", "json",
"m4a", "m4v", "mkv", "mov", "mp3", "mp4", "mpeg", "mpg", "png", "svg", "ts", "webm", "webp"
]);
const subtitleObjectKeys = [
"attributes",
"files",
35 unmodified lines
}
};
+
const isDirectSubtitleFileURL = (url) => {
try {
const parsed = new URL(url, window.location.href);
const extension = parsed.pathname.split(".").pop().toLowerCase();
return subtitleExtensions.has(extension)
|| Array.from(subtitleExtensions).some((ext) => parsed.href.toLowerCase().includes(`.${ext}?`) || parsed.href.toLowerCase().includes(`.${ext}&`));
} catch (_) {
return false;
}
};
+
const isProbablyNonSubtitleAssetURL = (url) => {
try {
const extension = new URL(url, window.location.href).pathname.split(".").pop().toLowerCase();
return nonSubtitleExtensions.has(extension);
} catch (_) {
return false;
}
};
+
const isOpenSubtitlesDownloadURL = (url) => {
try {
const parsed = new URL(url, window.location.href);
const host = parsed.hostname.toLowerCase();
const path = parsed.pathname.toLowerCase();
if (!host.includes("opensubtitles")) {
return false;
}
if (/\/manifest\.json(?:_\d+)?$/i.test(path)) {
return false;
}
return /\/api\/v1\/download(?:\/|$)/i.test(path)
|| /\/download(?:\/|$)/i.test(path)
|| /\/subtitles?(?:\/|$)/i.test(path);
} catch (_) {
return false;
}
};
+
const isSubtitleURL = (url) => {
if (!url || isOpenSubtitlesManifestID(url)) {
return false;
}
return !isProbablyNonSubtitleAssetURL(url)
&& (isDirectSubtitleFileURL(url) || isOpenSubtitlesDownloadURL(url));
};
+
const findResolverURL = () => {
+

Dreamio/StreamCandidate.swift

Dreamio/StreamCandidate.swift
-5+28
131 unmodified lines
132
133
134
135
136
137
106 unmodified lines
244
245
246
247
248
249
250
251
252
253
254
255
256
257
1 unmodified line
259
260
261
262
263
264
131 unmodified lines
+
enum SubtitleCandidateParser {
private static let supportedExtensions = ["srt", "vtt", "ass", "ssa", "sub"]
private static let urlFields = ["url", "href", "src", "link", "subtitles", "subtitle", "subtitleUrl", "subtitleURL", "file", "download", "fileUrl", "fileURL"]
private static let labelFields = ["label", "name", "title", "file_name", "filename", "lang", "language", "id"]
private struct CandidateContext {
106 unmodified lines
return nil
}
+
let lowercased = url.absoluteString.lowercased()
if isOpenSubtitlesManifestIdentifier(url) {
return nil
}
guard supportedExtensions.contains(url.pathExtension.lowercased())
|| supportedExtensions.contains(where: { lowercased.contains(".\($0)?") || lowercased.contains(".\($0)&") })
|| lowercased.contains("subtitle")
|| lowercased.contains("opensubtitles")
else {
return nil
}
1 unmodified line
return url
}
+
private static func isOpenSubtitlesManifestIdentifier(_ url: URL) -> Bool {
guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else {
return false
131 unmodified lines
132
133
134
135
136
137
138
139
140
141
106 unmodified lines
248
249
250
251
252
253
254
255
256
257
258
259
260
261
1 unmodified line
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
131 unmodified lines
+
enum SubtitleCandidateParser {
private static let supportedExtensions = ["srt", "vtt", "ass", "ssa", "sub"]
private static let nonSubtitleExtensions = [
"aac", "avi", "bmp", "css", "gif", "heic", "ico", "jpeg", "jpg", "js", "json",
"m4a", "m4v", "mkv", "mov", "mp3", "mp4", "mpeg", "mpg", "png", "svg", "ts", "webm", "webp"
]
private static let urlFields = ["url", "href", "src", "link", "subtitles", "subtitle", "subtitleUrl", "subtitleURL", "file", "download", "fileUrl", "fileURL"]
private static let labelFields = ["label", "name", "title", "file_name", "filename", "lang", "language", "id"]
private struct CandidateContext {
106 unmodified lines
return nil
}
+
if isOpenSubtitlesManifestIdentifier(url) {
return nil
}
guard !nonSubtitleExtensions.contains(url.pathExtension.lowercased()) else {
return nil
}
guard isDirectSubtitleFile(url)
|| isOpenSubtitlesDownloadURL(url)
else {
return nil
}
1 unmodified line
return url
}
+
private static func isDirectSubtitleFile(_ url: URL) -> Bool {
let lowercased = url.absoluteString.lowercased()
return supportedExtensions.contains(url.pathExtension.lowercased())
|| supportedExtensions.contains(where: { lowercased.contains(".\($0)?") || lowercased.contains(".\($0)&") })
}
+
private static func isOpenSubtitlesDownloadURL(_ url: URL) -> Bool {
guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else {
return false
}
let path = url.path.lowercased()
guard !isOpenSubtitlesManifestIdentifier(url) else {
return false
}
return path.range(of: #"(^|/)api/v1/download(/|$)"#, options: .regularExpression) != nil
|| path.range(of: #"(^|/)download(/|$)"#, options: .regularExpression) != nil
|| path.range(of: #"(^|/)subtitles?(/|$)"#, options: .regularExpression) != nil
}
+
private static func isOpenSubtitlesManifestIdentifier(_ url: URL) -> Bool {
guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else {
return false
+

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+27
11 unmodified lines
12
13
14
15
16
17
195 unmodified lines
213
214
215
216
217
218
11 unmodified lines
testOpenSubtitlesV3CandidateParsing()
testOpenSubtitlesNestedAttributesFilesParsing()
testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles()
testOpenSubtitlesV3DownloadResponseResolution()
testOpenSubtitlesNestedDownloadResponseResolution()
await testSubtitleResolverDownloadJSONReturningLink()
195 unmodified lines
assertEqual(candidates[0].language, "eng")
}
+
private static func testOpenSubtitlesV3DownloadResponseResolution() {
let payload = """
{
11 unmodified lines
12
13
14
15
16
17
18
195 unmodified lines
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
11 unmodified lines
testOpenSubtitlesV3CandidateParsing()
testOpenSubtitlesNestedAttributesFilesParsing()
testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles()
testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored()
testOpenSubtitlesV3DownloadResponseResolution()
testOpenSubtitlesNestedDownloadResponseResolution()
await testSubtitleResolverDownloadJSONReturningLink()
195 unmodified lines
assertEqual(candidates[0].language, "eng")
}
+
private static func testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored() {
let payload: [String: Any] = [
"subtitles": [
[
"label": "External Subtitle",
"url": "http://www.strem.io/images/addons/opensubtitles-logo.png"
],
[
"label": "External Subtitle",
"url": "https://opensubtitles.strem.io/stremio/v1"
],
[
"label": "English",
"url": "https://opensubtitles.example.test/subtitles/movie.en.srt"
]
],
"body": "metadata https://www.strem.io/images/addons/opensubtitles-logo.png"
]
+
let candidates = SubtitleCandidateParser.candidates(in: payload)
+
assertEqual(candidates.count, 1)
assertEqual(candidates[0].url.absoluteString, "https://opensubtitles.example.test/subtitles/movie.en.srt")
assertEqual(candidates[0].label, "English")
}
+
private static func testOpenSubtitlesV3DownloadResponseResolution() {
let payload = """
{
+
+ +
+

Expected Impact for End-Users

+

External subtitle discovery should stop burning time on addon images and base endpoints. In the exact logged scenario, Dreamio should no longer buffer the PNG or /stremio/v1 endpoint as external subtitles. Real OpenSubtitles download candidates remain eligible for resolution and attachment.

+
+ +
+

Validation

+
    +
  • Passed: swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests
  • +
  • Passed: xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' build
  • +
  • bd dolt push was run after closing dreamio-433; Beads reported that no Dolt remote is configured, so there was nothing to push.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
+

This fix removes the false positives visible in the log, but it may not by itself surface the actual OpenSubtitles external file if Stremio is hiding it behind a different internal payload shape. The mitigation is that future logs should now be cleaner: if external subtitles are still missing, the remaining bridge messages should point at the real undiscovered payload instead of the addon logo noise.

+
+
+ +
+

Follow-up Work

+

No new Beads follow-up was filed. The next useful manual check is to replay the same OpenSubtitlesV3 stream and confirm the bridge no longer logs candidates with ext=png or ext=none for the addon base endpoint.

+
+
+ +