filter false opensubtitles subtitle candidates

This commit is contained in:
dirtydishes 2026-05-25 12:22:58 -04:00
parent 6008272d0a
commit 11ed364094
6 changed files with 465 additions and 9 deletions

View file

@ -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-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-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-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."}}

View file

@ -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-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-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-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} {"_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}

View file

@ -84,6 +84,11 @@ final class DreamioWebViewController: UIViewController {
const postedSubtitleURLs = new Set(); const postedSubtitleURLs = new Set();
const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig; const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig;
const subtitleSignalPattern = /subtitle|subtitles|opensubtitles|vtt|srt|ass|ssa/i; 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 = [ const subtitleObjectKeys = [
"attributes", "attributes",
"files", "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) => { const isSubtitleURL = (url) => {
if (!url || isOpenSubtitlesManifestID(url)) { if (!url || isOpenSubtitlesManifestID(url)) {
return false; return false;
} }
subtitleURLPattern.lastIndex = 0; return !isProbablyNonSubtitleAssetURL(url)
const matches = subtitleURLPattern.test(url) || /api\.opensubtitles\.com\/api\/v1\/download/i.test(url); && (isDirectSubtitleFileURL(url) || isOpenSubtitlesDownloadURL(url));
subtitleURLPattern.lastIndex = 0;
return matches;
}; };
const findResolverURL = () => { const findResolverURL = () => {

View file

@ -132,6 +132,10 @@ struct StreamCandidate {
enum SubtitleCandidateParser { enum SubtitleCandidateParser {
private static let supportedExtensions = ["srt", "vtt", "ass", "ssa", "sub"] 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 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 static let labelFields = ["label", "name", "title", "file_name", "filename", "lang", "language", "id"]
private struct CandidateContext { private struct CandidateContext {
@ -244,14 +248,14 @@ enum SubtitleCandidateParser {
return nil return nil
} }
let lowercased = url.absoluteString.lowercased()
if isOpenSubtitlesManifestIdentifier(url) { if isOpenSubtitlesManifestIdentifier(url) {
return nil return nil
} }
guard supportedExtensions.contains(url.pathExtension.lowercased()) guard !nonSubtitleExtensions.contains(url.pathExtension.lowercased()) else {
|| supportedExtensions.contains(where: { lowercased.contains(".\($0)?") || lowercased.contains(".\($0)&") }) return nil
|| lowercased.contains("subtitle") }
|| lowercased.contains("opensubtitles") guard isDirectSubtitleFile(url)
|| isOpenSubtitlesDownloadURL(url)
else { else {
return nil return nil
} }
@ -259,6 +263,25 @@ enum SubtitleCandidateParser {
return url 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 { private static func isOpenSubtitlesManifestIdentifier(_ url: URL) -> Bool {
guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else { guard url.host?.localizedCaseInsensitiveContains("opensubtitles") == true else {
return false return false

View file

@ -12,6 +12,7 @@ struct StreamResolverTests {
testOpenSubtitlesV3CandidateParsing() testOpenSubtitlesV3CandidateParsing()
testOpenSubtitlesNestedAttributesFilesParsing() testOpenSubtitlesNestedAttributesFilesParsing()
testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles() testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles()
testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored()
testOpenSubtitlesV3DownloadResponseResolution() testOpenSubtitlesV3DownloadResponseResolution()
testOpenSubtitlesNestedDownloadResponseResolution() testOpenSubtitlesNestedDownloadResponseResolution()
await testSubtitleResolverDownloadJSONReturningLink() await testSubtitleResolverDownloadJSONReturningLink()
@ -213,6 +214,32 @@ struct StreamResolverTests {
assertEqual(candidates[0].language, "eng") 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() { private static func testOpenSubtitlesV3DownloadResponseResolution() {
let payload = """ let payload = """
{ {

File diff suppressed because one or more lines are too long