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.
+
+
Issue dreamio-ejh
+
Validation Tests and simulator build passed
+
Scope Native 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 Changed VLC subtitle display-name reconciliation so saved external subtitle names are only consumed by generic VLC track labels. Expanded subtitle language-code aliases for the ISO-639-2 codes observed in Stremio/OpenSubtitles payloads, including Dutch, Danish, Romanian, Serbian, Persian, Portuguese, Finnish, Hebrew, Hungarian, Turkish, Czech, Arabic, Greek, French, German, Spanish, and English. Added regression coverage for Dutch and Danish three-letter subtitle codes.
+
+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
. 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 {
. 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
48
49
50
51
52
53
54
55
56
57
58
59
60
]
+
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 {
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
]
+
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
)),
"movie.es"
)
}
+
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks () {
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
)),
"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 Ran swiftc -parse-as-library Dreamio/StreamCandidate.swift Dreamio/StreamResolver.swift Tests/StreamResolverTests.swift -o /tmp/StreamResolverTests && /tmp/StreamResolverTests: passed. Ran DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -workspace Dreamio.xcworkspace -scheme Dreamio -configuration Debug -sdk iphonesimulator build: passed.
+
+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