mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
preserve opensubtitles caption labels
This commit is contained in:
parent
046444f9ad
commit
ae996e7ffb
6 changed files with 628 additions and 5 deletions
|
|
@ -33,3 +33,4 @@
|
||||||
{"id":"int-f9deecdb","kind":"field_change","created_at":"2026-05-25T16:18:29.458162Z","actor":"dirtydishes","issue_id":"dreamio-urs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","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."}}
|
{"id":"int-f9deecdb","kind":"field_change","created_at":"2026-05-25T16:18:29.458162Z","actor":"dirtydishes","issue_id":"dreamio-urs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","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."}}
|
||||||
{"id":"int-569ee372","kind":"field_change","created_at":"2026-05-25T16:22:50.024736Z","actor":"dirtydishes","issue_id":"dreamio-433","extra":{"field":"status","new_value":"closed","old_value":"in_progress","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."}}
|
{"id":"int-569ee372","kind":"field_change","created_at":"2026-05-25T16:22:50.024736Z","actor":"dirtydishes","issue_id":"dreamio-433","extra":{"field":"status","new_value":"closed","old_value":"in_progress","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."}}
|
||||||
{"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-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."}}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
{"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"dreamio-2ju","title":"Show OpenSubtitles languages in caption tracks","description":"Preserve external subtitle metadata after VLC attaches OpenSubtitles tracks so the captions menu shows useful language labels instead of generic VLC track names.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T16:52:44Z","created_by":"dirtydishes","updated_at":"2026-05-25T16:54:58Z","started_at":"2026-05-25T16:52:49Z","closed_at":"2026-05-25T16:54:58Z","close_reason":"Fixed by preserving OpenSubtitles subtitle display metadata through VLC external track attachment and adding display-name tests.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-h5n","title":"Throttle VLC subtitle reapply during buffering","description":"VLC subtitle auto-selection currently reapplies the same subtitle track on every buffering state notification, producing noisy logs and unnecessary repeated player writes. Limit state-driven reapply to meaningful selection recovery or state transitions while preserving delayed retries after initial auto-selection.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T15:06:48Z","created_by":"dirtydishes","updated_at":"2026-05-25T15:09:02Z","started_at":"2026-05-25T15:06:55Z","closed_at":"2026-05-25T15:09:02Z","close_reason":"Limited VLC auto-subtitle reapply to real selection recovery while keeping bounded delayed startup confirmations.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-h5n","title":"Throttle VLC subtitle reapply during buffering","description":"VLC subtitle auto-selection currently reapplies the same subtitle track on every buffering state notification, producing noisy logs and unnecessary repeated player writes. Limit state-driven reapply to meaningful selection recovery or state transitions while preserving delayed retries after initial auto-selection.","status":"closed","priority":2,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T15:06:48Z","created_by":"dirtydishes","updated_at":"2026-05-25T15:09:02Z","started_at":"2026-05-25T15:06:55Z","closed_at":"2026-05-25T15:09:02Z","close_reason":"Limited VLC auto-subtitle reapply to real selection recovery while keeping bounded delayed startup confirmations.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-c1m","title":"Add captions selection proof logging","description":"Add DEBUG-only logs around the native captions menu and VLC subtitle selection path so subtitle tap actions prove whether the UI fires and whether VLC accepts the selected embedded track index.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:18:06Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:19:19Z","started_at":"2026-05-25T14:18:11Z","closed_at":"2026-05-25T14:19:19Z","close_reason":"Added DEBUG-only logs for captions menu actions and VLC subtitle selection results.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-c1m","title":"Add captions selection proof logging","description":"Add DEBUG-only logs around the native captions menu and VLC subtitle selection path so subtitle tap actions prove whether the UI fires and whether VLC accepts the selected embedded track index.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:18:06Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:19:19Z","started_at":"2026-05-25T14:18:11Z","closed_at":"2026-05-25T14:19:19Z","close_reason":"Added DEBUG-only logs for captions menu actions and VLC subtitle selection results.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-e9p","title":"Add native subtitle pipeline proof logging","description":"Add DEBUG-only logs across the web bridge, native player, subtitle resolution, and VLC attachment points so the next Xcode run can identify where external subtitles disappear without changing playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:03:18Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:07:14Z","started_at":"2026-05-25T14:03:22Z","closed_at":"2026-05-25T14:07:14Z","close_reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-e9p","title":"Add native subtitle pipeline proof logging","description":"Add DEBUG-only logs across the web bridge, native player, subtitle resolution, and VLC attachment points so the next Xcode run can identify where external subtitles disappear without changing playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T14:03:18Z","created_by":"dirtydishes","updated_at":"2026-05-25T14:07:14Z","started_at":"2026-05-25T14:03:22Z","closed_at":"2026-05-25T14:07:14Z","close_reason":"Added DEBUG-only subtitle pipeline proof logging and documented validation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,108 @@ struct SubtitleTrack: Equatable {
|
||||||
let name: String
|
let name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SubtitleDisplayName {
|
||||||
|
private static let genericLabels = [
|
||||||
|
"external subtitle",
|
||||||
|
"subtitle",
|
||||||
|
"unknown"
|
||||||
|
]
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if let label = meaningfulDisplayText(candidate.label) {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
||||||
|
if let languageName = languageName(for: candidate.language) {
|
||||||
|
return languageName
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackName(from: candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func name(forVLCTrackName trackName: String, preservedName: String?) -> String {
|
||||||
|
guard isGenericLabel(trackName), let preservedName else {
|
||||||
|
return trackName
|
||||||
|
}
|
||||||
|
return preservedName
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isGenericLabel(_ value: String) -> Bool {
|
||||||
|
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !normalized.isEmpty else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowercased = normalized.lowercased()
|
||||||
|
if genericLabels.contains(lowercased) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if Int(normalized) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if lowercased.range(of: #"^track\s*\d+$"#, options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func meaningfulDisplayText(_ value: String?) -> String? {
|
||||||
|
guard let value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !isGenericLabel(trimmed) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard trimmed.count <= 3 else {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
return languageName(for: trimmed) ?? trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func languageName(for value: String?) -> String? {
|
||||||
|
guard let value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty, Int(trimmed) == nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmed.count > 3 {
|
||||||
|
return trimmed.capitalized
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowercased = trimmed.lowercased()
|
||||||
|
let languageCode = languageCodeAliases[lowercased] ?? lowercased
|
||||||
|
guard let name = Locale.current.localizedString(forLanguageCode: languageCode) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return name.capitalized
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func fallbackName(from candidate: SubtitleCandidate) -> String {
|
||||||
|
let trimmedLabel = candidate.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !isGenericLabel(trimmedLabel) {
|
||||||
|
return trimmedLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileName = candidate.url.deletingPathExtension().lastPathComponent
|
||||||
|
return fileName.isEmpty ? "External Subtitle" : fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
enum SubtitleDebugFormatter {
|
enum SubtitleDebugFormatter {
|
||||||
static func candidateSummary(_ candidates: [SubtitleCandidate]) -> String {
|
static func candidateSummary(_ candidates: [SubtitleCandidate]) -> String {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
private var autoSelectedSubtitleTrackID: Int32?
|
private var autoSelectedSubtitleTrackID: Int32?
|
||||||
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
|
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
|
||||||
private var hasPendingExternalSubtitleSelection = false
|
private var hasPendingExternalSubtitleSelection = false
|
||||||
|
private var pendingExternalSubtitleDisplayNames: [String] = []
|
||||||
|
private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
@ -51,6 +53,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
autoSelectedSubtitleTrackID = nil
|
autoSelectedSubtitleTrackID = nil
|
||||||
externalSubtitleBaselineTrackIDs.removeAll()
|
externalSubtitleBaselineTrackIDs.removeAll()
|
||||||
hasPendingExternalSubtitleSelection = false
|
hasPendingExternalSubtitleSelection = false
|
||||||
|
pendingExternalSubtitleDisplayNames.removeAll()
|
||||||
|
externalSubtitleDisplayNamesByTrackID.removeAll()
|
||||||
let media = VLCMedia(url: request.playbackURL)
|
let media = VLCMedia(url: request.playbackURL)
|
||||||
let headerValue = request.headers
|
let headerValue = request.headers
|
||||||
.map { "\($0.key): \($0.value)" }
|
.map { "\($0.key): \($0.value)" }
|
||||||
|
|
@ -199,10 +203,15 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
|
|
||||||
var subtitleTracks: [SubtitleTrack] {
|
var subtitleTracks: [SubtitleTrack] {
|
||||||
#if canImport(MobileVLCKit)
|
#if canImport(MobileVLCKit)
|
||||||
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
|
reconcileExternalSubtitleDisplayNames()
|
||||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
|
return rawSubtitleTracks().map { track in
|
||||||
return zip(indexes, names).map { index, name in
|
SubtitleTrack(
|
||||||
SubtitleTrack(id: index.int32Value, name: name)
|
id: track.id,
|
||||||
|
name: SubtitleDisplayName.name(
|
||||||
|
forVLCTrackName: track.name,
|
||||||
|
preservedName: externalSubtitleDisplayNamesByTrackID[track.id]
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
[]
|
[]
|
||||||
|
|
@ -229,7 +238,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
|
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
|
||||||
var attachedCount = 0
|
var attachedCount = 0
|
||||||
var duplicateCount = 0
|
var duplicateCount = 0
|
||||||
let baselineTrackIDs = Set(subtitleTracks.filter { $0.id >= 0 }.map(\.id))
|
let baselineTrackIDs = Set(rawSubtitleTracks().filter { $0.id >= 0 }.map(\.id))
|
||||||
candidates.forEach { candidate in
|
candidates.forEach { candidate in
|
||||||
guard !attachedSubtitleURLs.contains(candidate.url) else {
|
guard !attachedSubtitleURLs.contains(candidate.url) else {
|
||||||
duplicateCount += 1
|
duplicateCount += 1
|
||||||
|
|
@ -238,6 +247,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
attachedSubtitleURLs.insert(candidate.url)
|
attachedSubtitleURLs.insert(candidate.url)
|
||||||
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
|
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
|
||||||
hasPendingExternalSubtitleSelection = true
|
hasPendingExternalSubtitleSelection = true
|
||||||
|
pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
|
||||||
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
|
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
|
||||||
attachedCount += 1
|
attachedCount += 1
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -268,6 +278,32 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
||||||
return attachedCount
|
return attachedCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func rawSubtitleTracks() -> [SubtitleTrack] {
|
||||||
|
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
|
||||||
|
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] ?? []
|
||||||
|
return zip(indexes, names).map { index, name in
|
||||||
|
SubtitleTrack(id: index.int32Value, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reconcileExternalSubtitleDisplayNames() {
|
||||||
|
guard !pendingExternalSubtitleDisplayNames.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSubtitleTracks()
|
||||||
|
.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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
externalSubtitleDisplayNamesByTrackID[track.id] = pendingExternalSubtitleDisplayNames.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private func logSubtitleTracks(reason: String) {
|
private func logSubtitleTracks(reason: String) {
|
||||||
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
|
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ struct StreamResolverTests {
|
||||||
await testSubtitleResolverRejectsNonSubtitleAPIResponse()
|
await testSubtitleResolverRejectsNonSubtitleAPIResponse()
|
||||||
testSubtitleCandidateDeduplicationPreservesLabels()
|
testSubtitleCandidateDeduplicationPreservesLabels()
|
||||||
testSubtitleCandidateDeduplicationUpgradesLabels()
|
testSubtitleCandidateDeduplicationUpgradesLabels()
|
||||||
|
testSubtitleDisplayNameNormalization()
|
||||||
|
testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks()
|
||||||
testSubtitleOptionMappingIncludesNone()
|
testSubtitleOptionMappingIncludesNone()
|
||||||
print("StreamResolverTests passed")
|
print("StreamResolverTests passed")
|
||||||
}
|
}
|
||||||
|
|
@ -437,6 +439,56 @@ struct StreamResolverTests {
|
||||||
assertEqual(options.first?.id, -1)
|
assertEqual(options.first?.id, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func testSubtitleDisplayNameNormalization() {
|
||||||
|
assertEqual(
|
||||||
|
SubtitleDisplayName.displayName(for: SubtitleCandidate(
|
||||||
|
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
|
||||||
|
label: "Track 1",
|
||||||
|
language: "eng"
|
||||||
|
)),
|
||||||
|
"English"
|
||||||
|
)
|
||||||
|
assertEqual(
|
||||||
|
SubtitleDisplayName.displayName(for: SubtitleCandidate(
|
||||||
|
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
|
||||||
|
label: "Track 2",
|
||||||
|
language: "Spanish"
|
||||||
|
)),
|
||||||
|
"Spanish"
|
||||||
|
)
|
||||||
|
assertEqual(
|
||||||
|
SubtitleDisplayName.displayName(for: SubtitleCandidate(
|
||||||
|
url: URL(string: "https://opensubtitles.example.test/download/subtitle.srt")!,
|
||||||
|
label: "English SDH",
|
||||||
|
language: "eng"
|
||||||
|
)),
|
||||||
|
"English SDH"
|
||||||
|
)
|
||||||
|
assertEqual(
|
||||||
|
SubtitleDisplayName.displayName(for: SubtitleCandidate(
|
||||||
|
url: URL(string: "https://cdn.example.test/subtitles/movie.es.srt")!,
|
||||||
|
label: "External Subtitle",
|
||||||
|
language: nil
|
||||||
|
)),
|
||||||
|
"movie.es"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() {
|
||||||
|
let options = SubtitleOptionMapper.options(from: [
|
||||||
|
SubtitleTrack(
|
||||||
|
id: 3,
|
||||||
|
name: SubtitleDisplayName.name(forVLCTrackName: "Track 1", preservedName: "English")
|
||||||
|
),
|
||||||
|
SubtitleTrack(
|
||||||
|
id: 4,
|
||||||
|
name: SubtitleDisplayName.name(forVLCTrackName: "Commentary", preservedName: "Spanish")
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
assertEqual(options.map(\.name), ["None", "English", "Commentary"])
|
||||||
|
}
|
||||||
|
|
||||||
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
|
private static func assertEqual<T: Equatable>(_ actual: T?, _ expected: T, file: StaticString = #file, line: UInt = #line) {
|
||||||
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
|
assert(actual == expected, "Expected \(String(describing: expected)), got \(String(describing: actual))", file: file, line: line)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue