# 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 lines84858687888935 unmodified lines12512612712812913013113213313413513613713883 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 lines848586878889909192939435 unmodified lines13013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918083 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 lines132133134135136137106 unmodified lines2442452462472482492502512522532542552562571 unmodified line259260261262263264131 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 false131 unmodified lines132133134135136137138139140141106 unmodified lines2482492502512522532542552562572582592602611 unmodified line263264265266267268269270271272273274275276277278279280281282283284285286287131 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 lines121314151617195 unmodified lines21321421521621721811 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 lines12131415161718195 unmodified lines21421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424511 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.