diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 5441d6a..ad9b02b 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -36,3 +36,4 @@ {"id":"int-eca1f7f8","kind":"field_change","created_at":"2026-05-25T16:33:55.331041Z","actor":"dirtydishes","issue_id":"dreamio-9sp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Accepted Stremio subtitle download URLs in the bridge, parser, resolver, and regression tests."}} {"id":"int-99b3cb8b","kind":"field_change","created_at":"2026-05-25T16:54:58.390731Z","actor":"dirtydishes","issue_id":"dreamio-2ju","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed by preserving OpenSubtitles subtitle display metadata through VLC external track attachment and adding display-name tests."}} {"id":"int-697dc66d","kind":"field_change","created_at":"2026-05-25T17:01:32.697187Z","actor":"dirtydishes","issue_id":"dreamio-0lt","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"not implementing now; user asked only to move previous work to the audio-track-selection branch"}} +{"id":"int-c9b3bcd7","kind":"field_change","created_at":"2026-05-25T17:48:09.142384Z","actor":"dirtydishes","issue_id":"dreamio-ejh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed by preserving known external subtitle display names for generic VLC subtitle tracks and expanding language-code aliases."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 0ad1a46..7345a0a 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-ejh","title":"Preserve external subtitle language names in VLC captions menu","description":"VLC can surface externally attached subtitle slaves as generic Track N labels even though Dreamio already knows the OpenSubtitles language metadata. Preserve and apply that metadata when building the native captions menu so users can distinguish subtitle languages.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T17:46:38Z","created_by":"dirtydishes","updated_at":"2026-05-25T17:48:09Z","started_at":"2026-05-25T17:46:43Z","closed_at":"2026-05-25T17:48:09Z","close_reason":"Fixed by preserving known external subtitle display names for generic VLC subtitle tracks and expanding language-code aliases.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"dreamio-9sp","title":"Accept Stremio subtitle download URLs","description":"Runtime logs show Stremio external subtitle tracks using subs5.strem.io /en/download URLs. The subtitle bridge and Swift parser currently reject those URLs because they do not have a subtitle file extension and are not on an OpenSubtitles host, so native playback receives zero external subtitle candidates.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T16:32:04Z","created_by":"dirtydishes","updated_at":"2026-05-25T16:33:55Z","started_at":"2026-05-25T16:32:10Z","closed_at":"2026-05-25T16:33:55Z","close_reason":"Accepted Stremio subtitle download URLs in the bridge, parser, resolver, and regression tests.","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} diff --git a/Dreamio/StreamCandidate.swift b/Dreamio/StreamCandidate.swift index 9ff1ef2..bee9fb7 100644 --- a/Dreamio/StreamCandidate.swift +++ b/Dreamio/StreamCandidate.swift @@ -48,13 +48,48 @@ enum SubtitleDisplayName { ] private static let languageCodeAliases = [ + "ara": "ar", + "ar": "ar", + "cze": "cs", + "ces": "cs", + "cs": "cs", + "dan": "da", + "da": "da", + "de": "de", + "deu": "de", + "ell": "el", + "el": "el", "eng": "en", "en": "en", + "fin": "fi", + "fi": "fi", "spa": "es", "es": "es", + "ger": "de", + "gre": "el", + "heb": "he", + "he": "he", + "hun": "hu", + "hu": "hu", "fre": "fr", "fra": "fr", - "fr": "fr" + "fr": "fr", + "dut": "nl", + "nld": "nl", + "nl": "nl", + "per": "fa", + "fas": "fa", + "fa": "fa", + "pob": "pt", + "por": "pt", + "pt": "pt", + "ron": "ro", + "rum": "ro", + "ro": "ro", + "srp": "sr", + "sr": "sr", + "tur": "tr", + "tr": "tr" ] static func displayName(for candidate: SubtitleCandidate) -> String { diff --git a/Dreamio/VLCNativePlaybackBackend.swift b/Dreamio/VLCNativePlaybackBackend.swift index 320e2ea..c3c2318 100644 --- a/Dreamio/VLCNativePlaybackBackend.swift +++ b/Dreamio/VLCNativePlaybackBackend.swift @@ -329,6 +329,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend { .filter { $0.id >= 0 } .filter { !externalSubtitleBaselineTrackIDs.contains($0.id) } .filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil } + .filter { SubtitleDisplayName.isGenericLabel($0.name) } .sorted { $0.id < $1.id } .forEach { track in guard !pendingExternalSubtitleDisplayNames.isEmpty else { diff --git a/Tests/StreamResolverTests.swift b/Tests/StreamResolverTests.swift index c7cada5..c846579 100644 --- a/Tests/StreamResolverTests.swift +++ b/Tests/StreamResolverTests.swift @@ -472,6 +472,22 @@ struct StreamResolverTests { )), "movie.es" ) + assertEqual( + SubtitleDisplayName.displayName(for: SubtitleCandidate( + url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!, + label: "Track 3", + language: "nld" + )), + "Dutch" + ) + assertEqual( + SubtitleDisplayName.displayName(for: SubtitleCandidate( + url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!, + label: "Track 4", + language: "dan" + )), + "Danish" + ) } private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() { diff --git a/docs/turns/2026-05-25-preserve-external-subtitle-language-names.html b/docs/turns/2026-05-25-preserve-external-subtitle-language-names.html new file mode 100644 index 0000000..100d6f5 --- /dev/null +++ b/docs/turns/2026-05-25-preserve-external-subtitle-language-names.html @@ -0,0 +1,273 @@ + + + + + +Preserve External Subtitle Language Names + + + +
+
+
Dreamio Turn Document ยท May 25, 2026
+

Preserve external subtitle language names

+

Dreamio now keeps known OpenSubtitles language metadata attached to generic VLC subtitle tracks, so the native captions menu can show useful names instead of a stack of indistinguishable Track N entries.

+
+
Issuedreamio-ejh
+
ValidationTests and simulator build passed
+
ScopeNative VLC subtitles
+
+
+ +

Summary

Fixed the subtitle menu naming path for externally attached VLC subtitle tracks. Generic VLC names like Track 2 now receive Dreamio's preserved OpenSubtitles display name when available.

+ +

Changes Made

+ +

Context

The debug log showed OpenSubtitles candidates arriving with usable language metadata, then VLC eventually surfacing external subtitle tracks as Track 2, Track 3, and similar generic names. Dreamio already had a preservation queue, but it could spend a queued display name on a VLC track that already had a meaningful title, which left later generic tracks unnamed.

+ +

Important Implementation Details

The key behavioral change is deliberately narrow: reconciliation now skips non-generic VLC subtitle names. If VLC gives a real name such as English (SDH) - [English], Dreamio keeps it. If VLC gives Track 4, Dreamio can replace it with the next preserved external subtitle display name.

The alias table turns common three-letter subtitle codes into two-letter language codes before asking Locale for localized names. This makes labels such as nld and dan become Dutch and Danish instead of raw codes.

+ +

Relevant Diff Snippets

Dreamio/VLCNativePlaybackBackend.swift

Dreamio/VLCNativePlaybackBackend.swift
+1
328 unmodified lines
329
330
331
332
333
334
328 unmodified lines
.filter { $0.id >= 0 }
.filter { !externalSubtitleBaselineTrackIDs.contains($0.id) }
.filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil }
.sorted { $0.id < $1.id }
.forEach { track in
guard !pendingExternalSubtitleDisplayNames.isEmpty else {
328 unmodified lines
329
330
331
332
333
334
335
328 unmodified lines
.filter { $0.id >= 0 }
.filter { !externalSubtitleBaselineTrackIDs.contains($0.id) }
.filter { externalSubtitleDisplayNamesByTrackID[$0.id] == nil }
.filter { SubtitleDisplayName.isGenericLabel($0.name) }
.sorted { $0.id < $1.id }
.forEach { track in
guard !pendingExternalSubtitleDisplayNames.isEmpty else {
+

Dreamio/StreamCandidate.swift

Dreamio/StreamCandidate.swift
-1+36
47 unmodified lines
48
49
50
51
52
53
54
55
56
57
58
59
60
47 unmodified lines
]
+
private static let languageCodeAliases = [
"eng": "en",
"en": "en",
"spa": "es",
"es": "es",
"fre": "fr",
"fra": "fr",
"fr": "fr"
]
+
static func displayName(for candidate: SubtitleCandidate) -> String {
47 unmodified lines
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
47 unmodified lines
]
+
private static let languageCodeAliases = [
"ara": "ar",
"ar": "ar",
"cze": "cs",
"ces": "cs",
"cs": "cs",
"dan": "da",
"da": "da",
"de": "de",
"deu": "de",
"ell": "el",
"el": "el",
"eng": "en",
"en": "en",
"fin": "fi",
"fi": "fi",
"spa": "es",
"es": "es",
"ger": "de",
"gre": "el",
"heb": "he",
"he": "he",
"hun": "hu",
"hu": "hu",
"fre": "fr",
"fra": "fr",
"fr": "fr",
"dut": "nl",
"nld": "nl",
"nl": "nl",
"per": "fa",
"fas": "fa",
"fa": "fa",
"pob": "pt",
"por": "pt",
"pt": "pt",
"ron": "ro",
"rum": "ro",
"ro": "ro",
"srp": "sr",
"sr": "sr",
"tur": "tr",
"tr": "tr"
]
+
static func displayName(for candidate: SubtitleCandidate) -> String {
+

Tests/StreamResolverTests.swift

Tests/StreamResolverTests.swift
+16
471 unmodified lines
472
473
474
475
476
477
471 unmodified lines
)),
"movie.es"
)
}
+
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
471 unmodified lines
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
471 unmodified lines
)),
"movie.es"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 3",
language: "nld"
)),
"Dutch"
)
assertEqual(
SubtitleDisplayName.displayName(for: SubtitleCandidate(
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
label: "Track 4",
language: "dan"
)),
"Danish"
)
}
+
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
+ +

Expected Impact for End-Users

When subtitles load through the native VLC player, the captions menu should be easier to scan. Users should see language names for generic external subtitle tracks instead of guessing which Track N corresponds to the language they want.

+ +

Validation

+ +

Issues, Limitations, and Mitigations

This still depends on VLC eventually surfacing external subtitle tracks after addPlaybackSlave. The change improves the names once tracks appear; it does not change VLC's asynchronous attachment behavior or guarantee that every remote subtitle file becomes visible instantly.

+ +

Follow-up Work

No required follow-up remains for this fix. A useful later improvement would be deeper URL-to-track correlation if MobileVLCKit exposes enough metadata to map each external subtitle slave to an exact surfaced track.

+
+ + \ No newline at end of file