diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 6223229..0b1068f 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -19,3 +19,4 @@ {"id":"int-c7246990","kind":"field_change","created_at":"2026-05-25T14:07:13.774172Z","actor":"dirtydishes","issue_id":"dreamio-e9p","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation."}} {"id":"int-45781aa3","kind":"field_change","created_at":"2026-05-25T14:19:19.141163Z","actor":"dirtydishes","issue_id":"dreamio-c1m","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added DEBUG-only logs for captions menu actions and VLC subtitle selection results."}} {"id":"int-6343b773","kind":"field_change","created_at":"2026-05-25T14:25:59.50764Z","actor":"dirtydishes","issue_id":"dreamio-bd9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Stopped rebuilding the captions menu on every progress refresh and validated the build."}} +{"id":"int-26b872a1","kind":"field_change","created_at":"2026-05-25T14:31:46.83464Z","actor":"dirtydishes","issue_id":"dreamio-ese","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Added subtitle-shaped fetch/XHR inspection diagnostics and validated the build."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3d7b0e4..c67315b 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-ese","title":"Discover Stremio external subtitle payloads","description":"Extend and instrument the injected web subtitle discovery path so Stremio/OpenSubtitles addon responses can be captured when native playback only sees embedded VLC subtitle tracks.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:29:57Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:31:47Z","started_at":"2026-05-25T14:30:03Z","closed_at":"2026-05-25T14:31:47Z","close_reason":"Added subtitle-shaped fetch/XHR inspection diagnostics and validated the build.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-bd9","title":"Stabilize captions menu refresh","description":"Stop rebuilding the captions UIMenu on every playback progress refresh so embedded subtitle actions can remain stable long enough to fire, while keeping DEBUG logs for menu state and selection.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:24:45Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:25:59Z","started_at":"2026-05-25T14:24:50Z","closed_at":"2026-05-25T14:25:59Z","close_reason":"Stopped rebuilding the captions menu on every progress refresh and validated the build.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-h5q","title":"Resolve OpenSubtitles API subtitle URLs before VLC attachment","description":"OpenSubtitles V3 can surface API/download endpoints that are not subtitle files themselves. Dreamio should resolve those endpoints to playable subtitle file URLs before handing them to VLC so Stremio does not show failed subtitle loads after native playback opens.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T13:47:17Z","created_by":"dirtydishes","updated_at":"2026-05-25T13:50:43Z","started_at":"2026-05-25T13:47:21Z","closed_at":"2026-05-25T13:50:43Z","close_reason":"Resolved OpenSubtitles V3 API-style subtitle download URLs to direct subtitle files before VLC attachment; added parser/resolver coverage and simulator build validation.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-lw6","title":"forward late opensubtitles subtitles to native player","description":"Native playback only receives subtitle candidates discovered before the stream candidate is posted. OpenSubtitles V3 candidates can arrive later through addon/network responses, so the active native player needs an append path for newly discovered external subtitles.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T10:40:28Z","created_by":"dirtydishes","updated_at":"2026-05-25T10:43:22Z","started_at":"2026-05-25T10:40:36Z","closed_at":"2026-05-25T10:43:22Z","close_reason":"Implemented late subtitle forwarding into active native playback, added VLC append path and parser tests.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index bc1ccf0..c38ae55 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -81,6 +81,7 @@ final class DreamioWebViewController: UIViewController { const subtitleCandidates = []; 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 looksNative = (url) => { if (!url || typeof url !== "string") { @@ -126,7 +127,7 @@ final class DreamioWebViewController: UIViewController { } catch (_) {} }; - const postSubtitleCandidates = (candidates) => { + const postSubtitleCandidates = (candidates, debug = {}) => { const discoveredCount = candidates.length; const fresh = candidates.filter((candidate) => { if (postedSubtitleURLs.has(candidate.url)) { @@ -143,7 +144,8 @@ final class DreamioWebViewController: UIViewController { debug: { discovered: discoveredCount, deduped: 0, - forwarded: 0 + forwarded: 0, + ...debug } }); } catch (_) {} @@ -156,14 +158,28 @@ final class DreamioWebViewController: UIViewController { debug: { discovered: discoveredCount, deduped: fresh.length, - forwarded: fresh.length + forwarded: fresh.length, + ...debug } }); } catch (_) {} }; const addSubtitleCandidate = (entry) => { - const rawURL = typeof entry === "string" ? entry : entry && (entry.url || entry.href || entry.src || entry.file || entry.download); + const rawURL = typeof entry === "string" + ? entry + : entry && ( + entry.url || + entry.href || + entry.src || + entry.link || + entry.file || + entry.download || + entry.externalUrl || + entry.externalURL || + entry.fileUrl || + entry.fileURL + ); const url = absoluteURL(rawURL); subtitleURLPattern.lastIndex = 0; if (!url || !subtitleURLPattern.test(url)) { @@ -183,6 +199,19 @@ final class DreamioWebViewController: UIViewController { postSubtitleCandidates([candidate]); }; + const postSubtitleInspection = (source, url, beforeCount, afterCount, payloadLength) => { + if (afterCount > beforeCount) { + return; + } + postSubtitleCandidates([], { + source, + inspected: true, + url: url || "", + payloadLength: payloadLength || 0, + totalKnown: subtitleCandidates.length + }); + }; + const inspectSubtitlePayload = (payload) => { if (!payload) { return; @@ -206,6 +235,12 @@ final class DreamioWebViewController: UIViewController { } }; + const inspectSubtitleText = (source, url, text) => { + const beforeCount = subtitleCandidates.length; + inspectSubtitlePayload(text); + postSubtitleInspection(source, url, beforeCount, subtitleCandidates.length, text ? text.length : 0); + }; + const originalFetch = window.fetch; if (originalFetch) { window.fetch = async (...args) => { @@ -216,10 +251,13 @@ final class DreamioWebViewController: UIViewController { subtitleURLPattern.lastIndex = 0; const shouldInspect = !contentType || /json|text|javascript|xml|subtitle|vtt|srt/i.test(contentType) - || subtitleURLPattern.test(url); + || subtitleURLPattern.test(url) + || subtitleSignalPattern.test(url); if (shouldInspect) { subtitleURLPattern.lastIndex = 0; - response.clone().text().then(inspectSubtitlePayload).catch(() => {}); + response.clone().text().then((text) => { + inspectSubtitleText("fetch", url, text); + }).catch(() => {}); } } catch (_) {} return response; @@ -235,7 +273,13 @@ final class DreamioWebViewController: UIViewController { if (responseType && responseType !== "text") { return; } - inspectSubtitlePayload(this.responseText); + const url = this.responseURL || ""; + const text = this.responseText || ""; + if (subtitleSignalPattern.test(url) || subtitleSignalPattern.test(text)) { + inspectSubtitleText("xhr", url, text); + } else { + inspectSubtitlePayload(text); + } } catch (_) {} }); } catch (_) {} @@ -685,8 +729,13 @@ final class DreamioWebViewController: UIViewController { let discovered = debug?["discovered"] as? Int ?? parsedCandidates.count let deduped = debug?["deduped"] as? Int ?? parsedCandidates.count let posted = debug?["forwarded"] as? Int ?? parsedCandidates.count + let source = debug?["source"] as? String ?? "bridge" + let inspected = debug?["inspected"] as? Bool ?? false + let inspectedURL = (debug?["url"] as? String).map(redactedURLString) ?? "none" + let payloadLength = debug?["payloadLength"] as? Int ?? 0 + let totalKnown = debug?["totalKnown"] as? Int ?? parsedCandidates.count let pageURL = dictionary?["pageUrl"] as? String - print("[DreamioSubtitles] bridge discovered=\(discovered) deduped=\(deduped) posted=\(posted) parsed=\(parsedCandidates.count) playerActive=\(currentNativePlayer != nil) page=\(pageURL.map(redactedURLString) ?? "unknown") candidates=\(SubtitleDebugFormatter.candidateSummary(parsedCandidates))") + print("[DreamioSubtitles] bridge source=\(source) inspected=\(inspected) discovered=\(discovered) deduped=\(deduped) posted=\(posted) parsed=\(parsedCandidates.count) totalKnown=\(totalKnown) payloadLength=\(payloadLength) playerActive=\(currentNativePlayer != nil) inspectedURL=\(inspectedURL) page=\(pageURL.map(redactedURLString) ?? "unknown") candidates=\(SubtitleDebugFormatter.candidateSummary(parsedCandidates))") } #endif } diff --git a/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html b/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html index 622d885..34e226e 100644 --- a/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html +++ b/docs/turns/2026-05-25-prove-native-subtitle-pipeline.html @@ -613,6 +613,92 @@
Related Beads issue: dreamio-bd9.
Extended subtitle discovery diagnostics upstream of the native captions menu so subtitle-shaped fetch and XHR responses now log even when they produce zero parseable external candidates.
+The latest run confirmed the app only receives VLC embedded captions for this stream. The remaining unknown is whether Stremio is making subtitle addon requests that the injected parser misses, or whether no external subtitle payload is requested at all.
+80 unmodified lines81828384858639 unmodified lines12612712812913013113210 unmodified lines1431441451461471481496 unmodified lines15615715815916016116216316416516616716816913 unmodified lines18318418518618718817 unmodified lines2062072082092102114 unmodified lines2162172182192202212222232242259 unmodified lines235236237238239240241443 unmodified lines68568668768868969069169280 unmodified linesconst subtitleCandidates = [];const postedSubtitleURLs = new Set();const subtitleURLPattern = /https?:\/\/[^\s"'<>]+(?:\.srt|\.vtt|\.ass|\.ssa|\.sub|opensubtitles|subtitle)[^\s"'<>]*/ig;+const looksNative = (url) => {if (!url || typeof url !== "string") {39 unmodified lines} catch (_) {}};+const postSubtitleCandidates = (candidates) => {const discoveredCount = candidates.length;const fresh = candidates.filter((candidate) => {if (postedSubtitleURLs.has(candidate.url)) {10 unmodified linesdebug: {discovered: discoveredCount,deduped: 0,forwarded: 0}});} catch (_) {}6 unmodified linesdebug: {discovered: discoveredCount,deduped: fresh.length,forwarded: fresh.length}});} catch (_) {}};+const addSubtitleCandidate = (entry) => {const rawURL = typeof entry === "string" ? entry : entry && (entry.url || entry.href || entry.src || entry.file || entry.download);const url = absoluteURL(rawURL);subtitleURLPattern.lastIndex = 0;if (!url || !subtitleURLPattern.test(url)) {13 unmodified linespostSubtitleCandidates([candidate]);};+const inspectSubtitlePayload = (payload) => {if (!payload) {return;17 unmodified lines}};+const originalFetch = window.fetch;if (originalFetch) {window.fetch = async (...args) => {4 unmodified linessubtitleURLPattern.lastIndex = 0;const shouldInspect = !contentType|| /json|text|javascript|xml|subtitle|vtt|srt/i.test(contentType)|| subtitleURLPattern.test(url);if (shouldInspect) {subtitleURLPattern.lastIndex = 0;response.clone().text().then(inspectSubtitlePayload).catch(() => {});}} catch (_) {}return response;9 unmodified linesif (responseType && responseType !== "text") {return;}inspectSubtitlePayload(this.responseText);} catch (_) {}});} catch (_) {}443 unmodified lineslet discovered = debug?["discovered"] as? Int ?? parsedCandidates.countlet deduped = debug?["deduped"] as? Int ?? parsedCandidates.countlet posted = debug?["forwarded"] as? Int ?? parsedCandidates.countlet pageURL = dictionary?["pageUrl"] as? Stringprint("[DreamioSubtitles] bridge discovered=\(discovered) deduped=\(deduped) posted=\(posted) parsed=\(parsedCandidates.count) playerActive=\(currentNativePlayer != nil) page=\(pageURL.map(redactedURLString) ?? "unknown") candidates=\(SubtitleDebugFormatter.candidateSummary(parsedCandidates))")}#endif}80 unmodified lines8182838485868739 unmodified lines12712812913013113213310 unmodified lines1441451461471481491501516 unmodified lines15815916016116216316416516616716816917017117217317417517617717817918018118218318418513 unmodified lines19920020120220320420520620720820921021121221321421521621717 unmodified lines2352362372382392402412422432442452464 unmodified lines2512522532542552562572582592602612622639 unmodified lines273274275276277278279280281282283284285443 unmodified lines72973073173273373473573673773873974074180 unmodified linesconst subtitleCandidates = [];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 looksNative = (url) => {if (!url || typeof url !== "string") {39 unmodified lines} catch (_) {}};+const postSubtitleCandidates = (candidates, debug = {}) => {const discoveredCount = candidates.length;const fresh = candidates.filter((candidate) => {if (postedSubtitleURLs.has(candidate.url)) {10 unmodified linesdebug: {discovered: discoveredCount,deduped: 0,forwarded: 0,...debug}});} catch (_) {}6 unmodified linesdebug: {discovered: discoveredCount,deduped: fresh.length,forwarded: fresh.length,...debug}});} catch (_) {}};+const addSubtitleCandidate = (entry) => {const rawURL = typeof entry === "string"? entry: entry && (entry.url ||entry.href ||entry.src ||entry.link ||entry.file ||entry.download ||entry.externalUrl ||entry.externalURL ||entry.fileUrl ||entry.fileURL);const url = absoluteURL(rawURL);subtitleURLPattern.lastIndex = 0;if (!url || !subtitleURLPattern.test(url)) {13 unmodified linespostSubtitleCandidates([candidate]);};+const postSubtitleInspection = (source, url, beforeCount, afterCount, payloadLength) => {if (afterCount > beforeCount) {return;}postSubtitleCandidates([], {source,inspected: true,url: url || "",payloadLength: payloadLength || 0,totalKnown: subtitleCandidates.length});};+const inspectSubtitlePayload = (payload) => {if (!payload) {return;17 unmodified lines}};+const inspectSubtitleText = (source, url, text) => {const beforeCount = subtitleCandidates.length;inspectSubtitlePayload(text);postSubtitleInspection(source, url, beforeCount, subtitleCandidates.length, text ? text.length : 0);};+const originalFetch = window.fetch;if (originalFetch) {window.fetch = async (...args) => {4 unmodified linessubtitleURLPattern.lastIndex = 0;const shouldInspect = !contentType|| /json|text|javascript|xml|subtitle|vtt|srt/i.test(contentType)|| subtitleURLPattern.test(url)|| subtitleSignalPattern.test(url);if (shouldInspect) {subtitleURLPattern.lastIndex = 0;response.clone().text().then((text) => {inspectSubtitleText("fetch", url, text);}).catch(() => {});}} catch (_) {}return response;9 unmodified linesif (responseType && responseType !== "text") {return;}const url = this.responseURL || "";const text = this.responseText || "";if (subtitleSignalPattern.test(url) || subtitleSignalPattern.test(text)) {inspectSubtitleText("xhr", url, text);} else {inspectSubtitlePayload(text);}} catch (_) {}});} catch (_) {}443 unmodified lineslet discovered = debug?["discovered"] as? Int ?? parsedCandidates.countlet deduped = debug?["deduped"] as? Int ?? parsedCandidates.countlet posted = debug?["forwarded"] as? Int ?? parsedCandidates.countlet source = debug?["source"] as? String ?? "bridge"let inspected = debug?["inspected"] as? Bool ?? falselet inspectedURL = (debug?["url"] as? String).map(redactedURLString) ?? "none"let payloadLength = debug?["payloadLength"] as? Int ?? 0let totalKnown = debug?["totalKnown"] as? Int ?? parsedCandidates.countlet pageURL = dictionary?["pageUrl"] as? Stringprint("[DreamioSubtitles] bridge source=\(source) inspected=\(inspected) discovered=\(discovered) deduped=\(deduped) posted=\(posted) parsed=\(parsedCandidates.count) totalKnown=\(totalKnown) payloadLength=\(payloadLength) playerActive=\(currentNativePlayer != nil) inspectedURL=\(inspectedURL) page=\(pageURL.map(redactedURLString) ?? "unknown") candidates=\(SubtitleDebugFormatter.candidateSummary(parsedCandidates))")}#endif}
Related Beads issue: dreamio-ese.