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
|
|
@ -40,6 +40,108 @@ struct SubtitleTrack: Equatable {
|
|||
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
|
||||
enum SubtitleDebugFormatter {
|
||||
static func candidateSummary(_ candidates: [SubtitleCandidate]) -> String {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
private var autoSelectedSubtitleTrackID: Int32?
|
||||
private var externalSubtitleBaselineTrackIDs = Set<Int32>()
|
||||
private var hasPendingExternalSubtitleSelection = false
|
||||
private var pendingExternalSubtitleDisplayNames: [String] = []
|
||||
private var externalSubtitleDisplayNamesByTrackID: [Int32: String] = [:]
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
|
@ -51,6 +53,8 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
autoSelectedSubtitleTrackID = nil
|
||||
externalSubtitleBaselineTrackIDs.removeAll()
|
||||
hasPendingExternalSubtitleSelection = false
|
||||
pendingExternalSubtitleDisplayNames.removeAll()
|
||||
externalSubtitleDisplayNamesByTrackID.removeAll()
|
||||
let media = VLCMedia(url: request.playbackURL)
|
||||
let headerValue = request.headers
|
||||
.map { "\($0.key): \($0.value)" }
|
||||
|
|
@ -199,10 +203,15 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
|
||||
var subtitleTracks: [SubtitleTrack] {
|
||||
#if canImport(MobileVLCKit)
|
||||
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)
|
||||
reconcileExternalSubtitleDisplayNames()
|
||||
return rawSubtitleTracks().map { track in
|
||||
SubtitleTrack(
|
||||
id: track.id,
|
||||
name: SubtitleDisplayName.name(
|
||||
forVLCTrackName: track.name,
|
||||
preservedName: externalSubtitleDisplayNamesByTrackID[track.id]
|
||||
)
|
||||
)
|
||||
}
|
||||
#else
|
||||
[]
|
||||
|
|
@ -229,7 +238,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
private func attachSubtitles(_ candidates: [SubtitleCandidate]) -> Int {
|
||||
var attachedCount = 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
|
||||
guard !attachedSubtitleURLs.contains(candidate.url) else {
|
||||
duplicateCount += 1
|
||||
|
|
@ -238,6 +247,7 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
attachedSubtitleURLs.insert(candidate.url)
|
||||
externalSubtitleBaselineTrackIDs.formUnion(baselineTrackIDs)
|
||||
hasPendingExternalSubtitleSelection = true
|
||||
pendingExternalSubtitleDisplayNames.append(SubtitleDisplayName.displayName(for: candidate))
|
||||
mediaPlayer.addPlaybackSlave(candidate.url, type: .subtitle, enforce: false)
|
||||
attachedCount += 1
|
||||
#if DEBUG
|
||||
|
|
@ -268,6 +278,32 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
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
|
||||
private func logSubtitleTracks(reason: String) {
|
||||
let names = mediaPlayer.videoSubTitlesNames as? [String] ?? []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue