From 6a29dde857cd45b83a42199ced769966f584d4d8 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 25 May 2026 07:07:59 -0400 Subject: [PATCH] keep stremio subtitle loads untouched --- .beads/interactions.jsonl | 1 + .beads/issues.jsonl | 1 + Dreamio/DreamioWebViewController.swift | 21 ++- Dreamio/StreamCandidate.swift | 23 ++- ...-forward-late-opensubtitles-subtitles.html | 158 +++++++++++++++++- 5 files changed, 196 insertions(+), 8 deletions(-) diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 9698466..fc5b74e 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -14,3 +14,4 @@ {"id":"int-9ddb7b1a","kind":"field_change","created_at":"2026-05-25T10:18:30.826897Z","actor":"dirtydishes","issue_id":"dreamio-7w6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Streamlined native player controls into a compact bottom overlay and validated the simulator build."}} {"id":"int-2a84633f","kind":"field_change","created_at":"2026-05-25T10:25:22.649574Z","actor":"dirtydishes","issue_id":"dreamio-88m","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented captions as a single-choice menu with None and selected loaded tracks, updated tests and turn documentation."}} {"id":"int-38a97132","kind":"field_change","created_at":"2026-05-25T10:43:21.805452Z","actor":"dirtydishes","issue_id":"dreamio-lw6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented late subtitle forwarding into active native playback, added VLC append path and parser tests."}} +{"id":"int-ddab585f","kind":"field_change","created_at":"2026-05-25T11:07:34.849628Z","actor":"dirtydishes","issue_id":"dreamio-8cz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Hardened subtitle bridge network observers so non-text Stremio subtitle loads are not touched, and made parser traversal deterministic for metadata preservation."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1d67a35..05a1b0e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_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-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} {"_type":"issue","id":"dreamio-poo","title":"Native player controls captions and close flow","description":"Add and validate VLC-backed native playback transport controls, subtitle track controls, external subtitle discovery, and Stremio Web close cleanup after native playback dismisses.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T09:47:56Z","created_by":"dirtydishes","updated_at":"2026-05-25T09:49:40Z","started_at":"2026-05-25T09:48:00Z","closed_at":"2026-05-25T09:49:40Z","close_reason":"Implemented and validated native player controls, subtitle handling refinements, and close-flow cleanup.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-wgk","title":"Fix native player controls tap-to-show","description":"Native player controls can be hidden by tapping, but subsequent taps on the player do not bring them back. Investigate the overlay gesture handling and restore reliable tap-to-show/tap-to-hide behavior.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T09:27:58Z","created_by":"dirtydishes","updated_at":"2026-05-25T09:51:17Z","started_at":"2026-05-25T09:28:11Z","closed_at":"2026-05-25T09:51:17Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift index bfbe113..03d20f7 100644 --- a/Dreamio/DreamioWebViewController.swift +++ b/Dreamio/DreamioWebViewController.swift @@ -194,7 +194,16 @@ final class DreamioWebViewController: UIViewController { window.fetch = async (...args) => { const response = await originalFetch(...args); try { - response.clone().text().then(inspectSubtitlePayload).catch(() => {}); + const contentType = response.headers && response.headers.get("content-type") || ""; + const url = response.url || ""; + subtitleURLPattern.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; }; @@ -203,7 +212,15 @@ final class DreamioWebViewController: UIViewController { const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(...args) { try { - this.addEventListener("load", () => inspectSubtitlePayload(this.responseText)); + this.addEventListener("load", () => { + try { + const responseType = this.responseType || ""; + if (responseType && responseType !== "text") { + return; + } + inspectSubtitlePayload(this.responseText); + } catch (_) {} + }); } catch (_) {} return originalXHRSend.apply(this, args); }; diff --git a/Dreamio/StreamCandidate.swift b/Dreamio/StreamCandidate.swift index 3371b54..d33651c 100644 --- a/Dreamio/StreamCandidate.swift +++ b/Dreamio/StreamCandidate.swift @@ -129,7 +129,7 @@ enum SubtitleCandidateParser { if let candidate = candidate(from: dictionary) { results.append(candidate) } - dictionary.values.forEach { collect(from: $0, into: &results) } + orderedNestedValues(in: dictionary).forEach { collect(from: $0, into: &results) } case let array as [Any]: array.forEach { collect(from: $0, into: &results) } case let string as String: @@ -159,6 +159,27 @@ enum SubtitleCandidateParser { ) } + private static func orderedNestedValues(in dictionary: [String: Any]) -> [Any] { + let preferredKeys = ["subtitles", "subtitle", "files", "downloads", "download"] + var visitedKeys = Set() + var values: [Any] = [] + + preferredKeys.forEach { key in + if let value = dictionary[key] { + values.append(value) + visitedKeys.insert(key) + } + } + + dictionary.keys + .filter { !visitedKeys.contains($0) && !urlFields.contains($0) } + .sorted() + .compactMap { dictionary[$0] } + .forEach { values.append($0) } + + return values + } + private static func subtitleURL(from string: String?) -> URL? { guard let string, let url = URL(string: string), diff --git a/docs/turns/2026-05-25-forward-late-opensubtitles-subtitles.html b/docs/turns/2026-05-25-forward-late-opensubtitles-subtitles.html index 3d8adee..07ff922 100644 --- a/docs/turns/2026-05-25-forward-late-opensubtitles-subtitles.html +++ b/docs/turns/2026-05-25-forward-late-opensubtitles-subtitles.html @@ -220,7 +220,7 @@
Dreamio/DreamioWebViewController.swift
-2+19
193 unmodified lines
194
195
196
197
198
199
200
2 unmodified lines
203
204
205
206
207
208
209
193 unmodified lines
window.fetch = async (...args) => {
const response = await originalFetch(...args);
try {
response.clone().text().then(inspectSubtitlePayload).catch(() => {});
} catch (_) {}
return response;
};
2 unmodified lines
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(...args) {
try {
this.addEventListener("load", () => inspectSubtitlePayload(this.responseText));
} catch (_) {}
return originalXHRSend.apply(this, args);
};
193 unmodified lines
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
2 unmodified lines
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
193 unmodified lines
window.fetch = async (...args) => {
const response = await originalFetch(...args);
try {
const contentType = response.headers && response.headers.get("content-type") || "";
const url = response.url || "";
subtitleURLPattern.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;
};
2 unmodified lines
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(...args) {
try {
this.addEventListener("load", () => {
try {
const responseType = this.responseType || "";
if (responseType && responseType !== "text") {
return;
}
inspectSubtitlePayload(this.responseText);
} catch (_) {}
});
} catch (_) {}
return originalXHRSend.apply(this, args);
};
+

Dreamio/StreamCandidate.swift

Dreamio/StreamCandidate.swift
-1+22
128 unmodified lines
129
130
131
132
133
134
135
23 unmodified lines
159
160
161
162
163
164
128 unmodified lines
if let candidate = candidate(from: dictionary) {
results.append(candidate)
}
dictionary.values.forEach { collect(from: $0, into: &results) }
case let array as [Any]:
array.forEach { collect(from: $0, into: &results) }
case let string as String:
23 unmodified lines
)
}
+
private static func subtitleURL(from string: String?) -> URL? {
guard let string,
let url = URL(string: string),
128 unmodified lines
129
130
131
132
133
134
135
23 unmodified lines
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
128 unmodified lines
if let candidate = candidate(from: dictionary) {
results.append(candidate)
}
orderedNestedValues(in: dictionary).forEach { collect(from: $0, into: &results) }
case let array as [Any]:
array.forEach { collect(from: $0, into: &results) }
case let string as String:
23 unmodified lines
)
}
+
private static func orderedNestedValues(in dictionary: [String: Any]) -> [Any] {
let preferredKeys = ["subtitles", "subtitle", "files", "downloads", "download"]
var visitedKeys = Set<String>()
var values: [Any] = []
+
preferredKeys.forEach { key in
if let value = dictionary[key] {
values.append(value)
visitedKeys.insert(key)
}
}
+
dictionary.keys
.filter { !visitedKeys.contains($0) && !urlFields.contains($0) }
.sorted()
.compactMap { dictionary[$0] }
.forEach { values.append($0) }
+
return values
}
+
private static func subtitleURL(from string: String?) -> URL? {
guard let string,
let url = URL(string: string),
+

Related issues or PRs

+

Beads issue: dreamio-8cz.

+

Validation

+
+
Passed: swiftc Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/dreamio-stream-tests && /tmp/dreamio-stream-tests
+
Passed: xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -sdk iphonesimulator -configuration Debug build CODE_SIGNING_ALLOWED=NO
+
+ +