mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 13:37:24 +00:00
resolve opensubtitles subtitle downloads
This commit is contained in:
parent
6a29dde857
commit
fdc4444f6a
9 changed files with 829 additions and 19 deletions
|
|
@ -30,6 +30,10 @@ protocol NativePlaybackBackend: AnyObject {
|
|||
func stop()
|
||||
}
|
||||
|
||||
protocol SubtitleResolving {
|
||||
func resolve(_ candidate: SubtitleCandidate) async -> SubtitleCandidate?
|
||||
}
|
||||
|
||||
enum NativePlaybackError: LocalizedError {
|
||||
case backendUnavailable
|
||||
case startupTimedOut
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import UIKit
|
|||
final class NativePlayerViewController: UIViewController {
|
||||
private let request: NativePlaybackRequest
|
||||
private var backend: NativePlaybackBackend
|
||||
private let subtitleResolver: SubtitleResolving
|
||||
private var startupTimer: Timer?
|
||||
private var controlsTimer: Timer?
|
||||
private var progressTimer: Timer?
|
||||
|
|
@ -92,10 +93,15 @@ final class NativePlayerViewController: UIViewController {
|
|||
return label
|
||||
}()
|
||||
|
||||
init(request: NativePlaybackRequest, backend: NativePlaybackBackend = VLCNativePlaybackBackend()) {
|
||||
init(
|
||||
request: NativePlaybackRequest,
|
||||
backend: NativePlaybackBackend = VLCNativePlaybackBackend(),
|
||||
subtitleResolver: SubtitleResolving = SubtitleResolver()
|
||||
) {
|
||||
self.request = request
|
||||
self.backend = backend
|
||||
self.attachedSubtitleURLs = Set(request.subtitleCandidates.map(\.url))
|
||||
self.subtitleResolver = subtitleResolver
|
||||
self.attachedSubtitleURLs = []
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .fullScreen
|
||||
modalTransitionStyle = .crossDissolve
|
||||
|
|
@ -126,26 +132,52 @@ final class NativePlayerViewController: UIViewController {
|
|||
configureLayout()
|
||||
startStartupTimer()
|
||||
backend.play(request: request)
|
||||
addSubtitleCandidates(request.subtitleCandidates)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addSubtitleCandidates(_ candidates: [SubtitleCandidate]) -> Int {
|
||||
let newCandidates = candidates.filter { candidate in
|
||||
guard !attachedSubtitleURLs.contains(candidate.url) else {
|
||||
return false
|
||||
}
|
||||
attachedSubtitleURLs.insert(candidate.url)
|
||||
return true
|
||||
}
|
||||
let attachedCount = backend.addSubtitleCandidates(newCandidates)
|
||||
if attachedCount > 0 {
|
||||
refreshControls()
|
||||
}
|
||||
let pendingCandidates = candidates.filter { !attachedSubtitleURLs.contains($0.url) }
|
||||
guard !pendingCandidates.isEmpty else {
|
||||
#if DEBUG
|
||||
let duplicateCount = candidates.count - newCandidates.count
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) forwarded=\(newCandidates.count) attached=\(attachedCount) duplicates=\(duplicateCount)")
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=0 duplicates=\(candidates.count)")
|
||||
#endif
|
||||
return attachedCount
|
||||
return 0
|
||||
}
|
||||
|
||||
pendingCandidates.forEach { attachedSubtitleURLs.insert($0.url) }
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let resolvedCandidates = await self.resolveSubtitleCandidates(pendingCandidates)
|
||||
await MainActor.run {
|
||||
guard !resolvedCandidates.isEmpty else {
|
||||
#if DEBUG
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=\(pendingCandidates.count) resolved=0 attached=0")
|
||||
#endif
|
||||
return
|
||||
}
|
||||
let attachableCandidates = resolvedCandidates.filter { candidate in
|
||||
guard !self.attachedSubtitleURLs.contains(candidate.url) || pendingCandidates.contains(where: { $0.url == candidate.url }) else {
|
||||
return false
|
||||
}
|
||||
self.attachedSubtitleURLs.insert(candidate.url)
|
||||
return true
|
||||
}
|
||||
let attachedCount = self.backend.addSubtitleCandidates(attachableCandidates)
|
||||
if attachedCount > 0 {
|
||||
self.refreshControls()
|
||||
}
|
||||
#if DEBUG
|
||||
let duplicateCount = candidates.count - pendingCandidates.count + resolvedCandidates.count - attachableCandidates.count
|
||||
print("[DreamioNativePlayer] subtitle candidates=\(candidates.count) pending=\(pendingCandidates.count) resolved=\(resolvedCandidates.count) attached=\(attachedCount) duplicates=\(duplicateCount)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return pendingCandidates.count
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
|
|
@ -157,6 +189,16 @@ final class NativePlayerViewController: UIViewController {
|
|||
onDismiss?()
|
||||
}
|
||||
|
||||
private func resolveSubtitleCandidates(_ candidates: [SubtitleCandidate]) async -> [SubtitleCandidate] {
|
||||
var resolved: [SubtitleCandidate] = []
|
||||
for candidate in candidates {
|
||||
if let playableCandidate = await subtitleResolver.resolve(candidate) {
|
||||
resolved.append(playableCandidate)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
private func configureBackend() {
|
||||
backend.prepare(in: self)
|
||||
backend.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ struct StreamCandidate {
|
|||
|
||||
enum SubtitleCandidateParser {
|
||||
private static let supportedExtensions = ["srt", "vtt", "ass", "ssa", "sub"]
|
||||
private static let urlFields = ["url", "href", "src", "subtitles", "subtitle", "subtitleUrl", "subtitleURL", "file", "download"]
|
||||
private static let labelFields = ["label", "name", "title", "lang", "language", "id"]
|
||||
private static let urlFields = ["url", "href", "src", "link", "subtitles", "subtitle", "subtitleUrl", "subtitleURL", "file", "download"]
|
||||
private static let labelFields = ["label", "name", "title", "file_name", "lang", "language", "id"]
|
||||
|
||||
static func candidates(in payload: Any?) -> [SubtitleCandidate] {
|
||||
var results: [SubtitleCandidate] = []
|
||||
|
|
|
|||
|
|
@ -29,6 +29,106 @@ enum StreamResolverError: LocalizedError {
|
|||
}
|
||||
}
|
||||
|
||||
final class SubtitleResolver: SubtitleResolving {
|
||||
private let session: URLSession
|
||||
|
||||
init(session: URLSession = .shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
func resolve(_ candidate: SubtitleCandidate) async -> SubtitleCandidate? {
|
||||
if Self.isDirectSubtitleFile(candidate.url) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
guard Self.shouldResolve(candidate.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var request = URLRequest(url: candidate.url)
|
||||
request.setValue("application/json, text/plain, text/vtt, application/x-subrip, */*", forHTTPHeaderField: "Accept")
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(for: request)
|
||||
if let httpResponse = response as? HTTPURLResponse,
|
||||
!(200...299).contains(httpResponse.statusCode) {
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] resolve status=\(httpResponse.statusCode) url=\(URLRedactor.redactedURLString(candidate.url.absoluteString))")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
if let finalURL = response.url, Self.isDirectSubtitleFile(finalURL) {
|
||||
return SubtitleCandidate(url: finalURL, label: candidate.label, language: candidate.language)
|
||||
}
|
||||
|
||||
return Self.bestPlayableCandidate(
|
||||
from: data,
|
||||
responseURL: response.url,
|
||||
original: candidate
|
||||
)
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("[DreamioSubtitles] resolve failure=\(URLRedactor.redactedURLString(error.localizedDescription)) url=\(URLRedactor.redactedURLString(candidate.url.absoluteString))")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func bestPlayableCandidate(
|
||||
from data: Data,
|
||||
responseURL: URL?,
|
||||
original: SubtitleCandidate
|
||||
) -> SubtitleCandidate? {
|
||||
if let responseURL, isDirectSubtitleFile(responseURL) {
|
||||
return SubtitleCandidate(url: responseURL, label: original.label, language: original.language)
|
||||
}
|
||||
|
||||
guard !data.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let payload = try? JSONSerialization.jsonObject(with: data) {
|
||||
return SubtitleCandidateParser.candidates(in: payload)
|
||||
.first(where: { isDirectSubtitleFile($0.url) })
|
||||
.map { playable in
|
||||
SubtitleCandidate(
|
||||
url: playable.url,
|
||||
label: original.label.isEmpty ? playable.label : original.label,
|
||||
language: playable.language ?? original.language
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if let text = String(data: data, encoding: .utf8) {
|
||||
return SubtitleCandidateParser.candidates(in: text)
|
||||
.first(where: { isDirectSubtitleFile($0.url) })
|
||||
.map { playable in
|
||||
SubtitleCandidate(
|
||||
url: playable.url,
|
||||
label: original.label.isEmpty ? playable.label : original.label,
|
||||
language: playable.language ?? original.language
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func isDirectSubtitleFile(_ url: URL) -> Bool {
|
||||
let lowercased = url.absoluteString.lowercased()
|
||||
return ["srt", "vtt", "ass", "ssa", "sub"].contains(url.pathExtension.lowercased())
|
||||
|| [".srt?", ".vtt?", ".ass?", ".ssa?", ".sub?", ".srt&", ".vtt&", ".ass&", ".ssa&", ".sub&"].contains(where: lowercased.contains)
|
||||
}
|
||||
|
||||
private static func shouldResolve(_ url: URL) -> Bool {
|
||||
let lowercased = url.absoluteString.lowercased()
|
||||
return lowercased.contains("opensubtitles")
|
||||
|| lowercased.contains("/subtitle")
|
||||
|| lowercased.contains("subtitle")
|
||||
}
|
||||
}
|
||||
|
||||
protocol StreamResolving {
|
||||
func resolve(request: NativePlaybackRequest) async throws -> ResolvedNativeStream
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ final class VLCNativePlaybackBackend: NSObject, NativePlaybackBackend {
|
|||
print("[DreamioVLC] opening url=\(URLRedactor.redactedURLString(request.playbackURL.absoluteString))")
|
||||
#endif
|
||||
mediaPlayer.play()
|
||||
addSubtitleCandidates(request.subtitleCandidates)
|
||||
#else
|
||||
onFailure?(NativePlaybackError.backendUnavailable)
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue