import Foundation @main struct StreamResolverTests { static func main() async { testClassifierPrefersObservedDirectFile() testResolverSelectsUnsupportedDirectURLAndHeaders() testResolverRejectsHLSOnlyResponse() testRedactorHandlesPercentEncodedPath() testPlaybackTimeFormatting() testSubtitleCandidateParsing() testOpenSubtitlesV3CandidateParsing() testOpenSubtitlesNestedAttributesFilesParsing() testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles() testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored() testStremioSubtitleDownloadURLParsing() testOpenSubtitlesV3DownloadResponseResolution() testOpenSubtitlesNestedDownloadResponseResolution() await testSubtitleResolverCachesStremioDownloadBody() await testSubtitleResolverCachesPlainStremioDownloadBody() await testSubtitleResolverDownloadJSONReturningLink() await testSubtitleResolverRedirectToDirectSubtitle() await testSubtitleResolverRejectsNonSubtitleAPIResponse() testSubtitleCandidateDeduplicationPreservesLabels() testSubtitleCandidateDeduplicationUpgradesLabels() testSubtitleDisplayNameNormalization() testSubtitleDisplayNameUsesPreservedNamesForGenericVLCTracks() testSubtitleOptionMappingIncludesNone() testExternalSubtitleParserHandlesCRLFSRT() testExternalSubtitleCueLookupBoundaries() testExternalSubtitleParserCleansMultilineCueText() testExternalSubtitleParserHandlesSouthParkFirstCueTiming() testContentRangeParsing() testSparseRangeStoreMergesOverlaps() testSparseRangeStoreHitPartialHitAndMiss() testSparseRangeStoreEvictsOutsideWindow() testSparseRangeStoreTrimsOverlappingWindow() testSparseRangeStoreEvictsByBudgetWhilePreservingUsefulRanges() testRangeCacheSessionCapsResponseRange() testRangeCachePrefetchPrioritizesSeekOffset() testRangeCacheSeekPrimingIncludesObservedVLCStart() testRangeCachePrefetchUsesGlobalChunkBoundaries() testRangeCacheForegroundMissFetchesAlignedChunks() await testRangeCacheForegroundMissReprioritizesPrefetch() await testRangeCacheHitFollowsActualPostSeekReadArea() await testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges() await testRangeProbeFallsBackWhenServerIgnoresRange() await testRangeFetcherPreservesHeaders() print("StreamResolverTests passed") } private static func testClassifierPrefersObservedDirectFile() { let body: [String: Any] = [ "url": "https://cdn.example.test/movie.mkv?token=secret", "resolverUrl": "https://addon.debridio.com/play/example" ] let candidate = StreamCandidate(messageBody: body)! let request = StreamClassifier.playbackRequest(from: candidate, userAgent: "DreamioTest/1")! assertEqual(request.playbackURL.absoluteString, "https://cdn.example.test/movie.mkv?token=secret") assertEqual(request.headers["Referer"], "https://web.stremio.com/") assertEqual(request.headers["User-Agent"], "DreamioTest/1") } private static func testResolverSelectsUnsupportedDirectURLAndHeaders() { let payload: [String: Any] = [ "streams": [ [ "url": "https://cdn.example.test/trailer.mp4" ], [ "externalUrl": "https://cdn.example.test/movie.mkv?signature=secret", "behaviorHints": [ "proxyHeaders": [ "request": [ "Referer": "https://resolver.example.test/", "User-Agent": "ResolverAgent/1" ] ] ] ] ] ] let stream = StremioStreamResolver.bestPlayableStream( in: payload, fallbackHeaders: ["Referer": "https://web.stremio.com/"] )! assertEqual(stream.playbackURL.absoluteString, "https://cdn.example.test/movie.mkv?signature=secret") assertEqual(stream.headers["Referer"], "https://resolver.example.test/") assertEqual(stream.headers["User-Agent"], "ResolverAgent/1") } private static func testResolverRejectsHLSOnlyResponse() { let payload: [String: Any] = [ "streams": [ ["url": "https://cdn.example.test/live.m3u8"] ] ] let stream = StremioStreamResolver.bestPlayableStream( in: payload, fallbackHeaders: ["Referer": "https://web.stremio.com/"] ) assert(stream == nil, "Expected HLS-only resolver response to stay out of native playback") } private static func testRedactorHandlesPercentEncodedPath() { let original = "https://cdn.example.test/video/abcdefghijklmnopqrstuvwxyz012345/%E2%9C%93.mp4?token=secret#fragment" let redacted = URLRedactor.redactedURLString(original) assertEqual(redacted, "https://cdn.example.test/video/%5Bredacted%5D/%E2%9C%93.mp4") } private static func testPlaybackTimeFormatting() { assertEqual(PlaybackTimeFormatter.label(for: 0), "0:00") assertEqual(PlaybackTimeFormatter.label(for: 65), "1:05") assertEqual(PlaybackTimeFormatter.label(for: 3_725), "1:02:05") } private static func testSubtitleCandidateParsing() { let payload: [String: Any] = [ "subtitles": [ [ "lang": "eng", "url": "https://opensubtitles.example.test/download/subtitle.srt?token=secret" ], [ "language": "Spanish", "file": "https://cdn.example.test/movie.es.vtt" ], "https://cdn.example.test/ignored.txt" ], "nested": [ "body": "metadata https://cdn.example.test/movie.fr.ass?download=1" ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 3) assertEqual(candidates[0].language, "eng") assertEqual(candidates[1].label, "Spanish") assertEqual(candidates[2].url.absoluteString, "https://cdn.example.test/movie.fr.ass?download=1") } private static func testOpenSubtitlesV3CandidateParsing() { let payload: [String: Any] = [ "subtitles": [ [ "language": "English", "download": "https://api.opensubtitles.com/api/v1/download/subtitle-file", "nested": [ [ "file": "https://dl.opensubtitles.org/en/subtitle.vtt?download=1" ] ] ], [ "lang": "spa", "url": "https://opensubtitles.example.test/download/episode.srt" ] ], "body": "alternate https://cdn.example.test/from-string.ass?source=opensubtitles", "ignored": [ "https://cdn.example.test/poster.jpg", ["file": "https://cdn.example.test/video.mkv"] ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 4) assertEqual(candidates[0].label, "English") assertEqual(candidates[0].language, "English") assertEqual(candidates[1].url.absoluteString, "https://dl.opensubtitles.org/en/subtitle.vtt?download=1") assertEqual(candidates[1].label, "English") assertEqual(candidates[1].language, "English") assertEqual(candidates[2].label, "spa") assertEqual(candidates[2].language, "spa") assertEqual(candidates[3].url.absoluteString, "https://cdn.example.test/from-string.ass?source=opensubtitles") } private static func testOpenSubtitlesNestedAttributesFilesParsing() { let payload: [String: Any] = [ "data": [ [ "attributes": [ "language": "English", "file_name": "episode.en.srt", "files": [ [ "file_id": 12345, "file_name": "nested.en.srt" ], [ "link": "https://dl.opensubtitles.org/en/download/nested.vtt?token=secret", "language": "eng" ] ] ] ] ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 2) assertEqual(candidates[0].url.absoluteString, "https://api.opensubtitles.com/api/v1/download/12345") assertEqual(candidates[0].label, "nested.en.srt") assertEqual(candidates[0].language, "English") assertEqual(candidates[1].url.absoluteString, "https://dl.opensubtitles.org/en/download/nested.vtt?token=secret") assertEqual(candidates[1].label, "eng") assertEqual(candidates[1].language, "eng") } private static func testOpenSubtitlesManifestIDsAreNotResolvedAsSubtitles() { let payload: [String: Any] = [ "subtitles": [ [ "url": "https://opensubtitles-v3.strem.io/manifest.json_14", "file_id": 98765, "lang": "eng" ], [ "url": "https://opensubtitles-v3.strem.io/manifest.json_15", "lang": "spa" ], "https://opensubtitles-v3.strem.io/manifest.json_16" ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 1) assertEqual(candidates[0].url.absoluteString, "https://api.opensubtitles.com/api/v1/download/98765") assertEqual(candidates[0].language, "eng") } private static func testOpenSubtitlesArtworkAndAddonEndpointsAreIgnored() { let payload: [String: Any] = [ "subtitles": [ [ "label": "External Subtitle", "url": "http://www.strem.io/images/addons/opensubtitles-logo.png" ], [ "label": "External Subtitle", "url": "https://opensubtitles.strem.io/stremio/v1" ], [ "label": "English", "url": "https://opensubtitles.example.test/subtitles/movie.en.srt" ] ], "body": "metadata https://www.strem.io/images/addons/opensubtitles-logo.png" ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 1) assertEqual(candidates[0].url.absoluteString, "https://opensubtitles.example.test/subtitles/movie.en.srt") assertEqual(candidates[0].label, "English") } private static func testStremioSubtitleDownloadURLParsing() { let payload: [String: Any] = [ "subtitles": [ [ "label": "English", "lang": "eng", "url": "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941" ], [ "label": "Not a subtitle", "url": "https://www.strem.io/images/addons/opensubtitles-logo.png" ] ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 1) assertEqual(candidates[0].url.absoluteString, "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941") assertEqual(candidates[0].label, "English") assertEqual(candidates[0].language, "eng") assert(!SubtitleResolver.isDirectSubtitleFile(candidates[0].url), "Expected Stremio subtitle downloads to be resolved before VLC attachment") } private static func testContentRangeParsing() { let parsed = HTTPContentRange.parse("bytes 10-19/100") assertEqual(parsed?.range.start, 10) assertEqual(parsed?.range.end, 19) assertEqual(parsed?.totalLength, 100) assert(HTTPContentRange.parse("items 10-19/100") == nil, "Expected non-byte content range to be rejected") assert(HTTPContentRange.parse("bytes 20-10/100") == nil, "Expected invalid content range to be rejected") } private static func testSparseRangeStoreMergesOverlaps() { let store = SparseHTTPByteRangeStore() store.insert(data: Data([0, 1, 2, 3]), at: 0) store.insert(data: Data([3, 4, 5]), at: 3) assertEqual(store.cachedRanges, [HTTPByteRange(start: 0, end: 5)]) assertEqual(Array(store.data(for: HTTPByteRange(start: 0, end: 5)) ?? Data()), [0, 1, 2, 3, 4, 5]) } private static func testSparseRangeStoreHitPartialHitAndMiss() { let store = SparseHTTPByteRangeStore() store.insert(data: Data([10, 11, 12, 13]), at: 10) assertEqual(Array(store.data(for: HTTPByteRange(start: 10, end: 13)) ?? Data()), [10, 11, 12, 13]) assert(store.data(for: HTTPByteRange(start: 11, end: 14)) == nil, "Expected partial hit to miss") assert(store.data(for: HTTPByteRange(start: 20, end: 21)) == nil, "Expected uncached range to miss") } private static func testSparseRangeStoreEvictsOutsideWindow() { let store = SparseHTTPByteRangeStore() store.insert(data: Data([0, 1, 2]), at: 0) store.insert(data: Data([10, 11, 12]), at: 10) store.evict(keeping: HTTPByteRange(start: 9, end: 12)) assertEqual(store.cachedRanges, [HTTPByteRange(start: 10, end: 12)]) } private static func testSparseRangeStoreTrimsOverlappingWindow() { let store = SparseHTTPByteRangeStore() store.insert(data: Data([0, 1, 2, 3, 4, 5]), at: 0) store.evict(keeping: HTTPByteRange(start: 2, end: 4)) assertEqual(store.cachedRanges, [HTTPByteRange(start: 2, end: 4)]) assertEqual(Array(store.data(for: HTTPByteRange(start: 2, end: 4)) ?? Data()), [2, 3, 4]) assert(store.data(for: HTTPByteRange(start: 0, end: 5)) == nil, "Expected trimmed bytes outside the window to be evicted") } private static func testSparseRangeStoreEvictsByBudgetWhilePreservingUsefulRanges() { let store = SparseHTTPByteRangeStore() store.insert(data: Data(repeating: 1, count: 4), at: 0) store.insert(data: Data(repeating: 2, count: 4), at: 100) store.insert(data: Data(repeating: 3, count: 4), at: 200) let evicted = store.evict( toByteBudget: 8, preserving: [ HTTPByteRange(start: 0, end: 3), HTTPByteRange(start: 190, end: 210) ] ) assertEqual(evicted, [HTTPByteRange(start: 100, end: 103)]) assertEqual(store.cachedRanges, [ HTTPByteRange(start: 0, end: 3), HTTPByteRange(start: 200, end: 203) ]) } private static func testRangeCacheSessionCapsResponseRange() { let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]), contentLength: 711_080_522, durationProvider: { 0 } ) let responseRange = session.responseRange(for: HTTPByteRange(start: 0, end: 711_080_521)) assertEqual(responseRange, HTTPByteRange(start: 0, end: 1_048_575)) } private static func testRangeCachePrefetchPrioritizesSeekOffset() { let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]), contentLength: 20_000_000, durationProvider: { 0 } ) let chunks = session.prefetchChunks( in: HTTPByteRange(start: 0, end: 4_194_303), preferredOffset: 2_200_000 ) assertEqual(chunks.prefix(2).map { $0 }, [ HTTPByteRange(start: 2_097_152, end: 3_145_727), HTTPByteRange(start: 3_145_728, end: 4_194_303) ]) assertEqual(chunks.suffix(2).map { $0 }, [ HTTPByteRange(start: 0, end: 1_048_575), HTTPByteRange(start: 1_048_576, end: 2_097_151) ]) } private static func testRangeCacheSeekPrimingIncludesObservedVLCStart() { let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]), contentLength: 711_080_522, durationProvider: { 0 } ) let estimatedOffset: Int64 = 213_615_760 let firstVLCRequest = HTTPByteRange(start: 212_942_432, end: 213_991_007) let window = session.seekPrimeWindow(aroundByteOffset: estimatedOffset) let chunks = session.prefetchChunks( in: window, preferredOffset: estimatedOffset, startsAtWindowStart: true ) let chunkContainingVLCStart = chunks.firstIndex { $0.contains(firstVLCRequest.start) } let chunkContainingEstimatedOffset = chunks.firstIndex { $0.contains(estimatedOffset) } assert(chunkContainingVLCStart != nil, "Expected seek priming to include VLC's first request start") assert(chunkContainingEstimatedOffset != nil, "Expected seek priming to include the estimated offset") assert( chunkContainingVLCStart! <= chunkContainingEstimatedOffset!, "Expected bytes behind the seek target to be primed before ahead chunks" ) assertEqual(chunks[chunkContainingVLCStart!], HTTPByteRange(start: 212_860_928, end: 213_909_503)) } private static func testRangeCachePrefetchUsesGlobalChunkBoundaries() { let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]), contentLength: 711_080_522, durationProvider: { 0 } ) let chunks = session.prefetchChunks( in: HTTPByteRange(start: 213_278_260, end: 216_000_000), preferredOffset: 213_615_760 ) assert(chunks.allSatisfy { $0.start % 1_048_576 == 0 }, "Expected prefetch chunk starts to use stable global 1 MB boundaries: \(chunks)") assertEqual(chunks[0], HTTPByteRange(start: 212_860_928, end: 213_909_503)) } private static func testRangeCacheForegroundMissFetchesAlignedChunks() { let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher(url: URL(string: "https://example.test/video.mkv")!, headers: [:]), contentLength: 711_080_522, durationProvider: { 0 } ) let chunks = session.alignedChunks(covering: HTTPByteRange(start: 48_234_649, end: 49_185_907)) assertEqual(chunks, [HTTPByteRange(start: 48_234_496, end: 49_283_071)]) } private static func testRangeCacheForegroundMissReprioritizesPrefetch() async { let queue = DispatchQueue(label: "dreamio.range-cache-test") var requestedRanges: [String] = [] MockURLProtocol.handler = { request in let range = request.value(forHTTPHeaderField: "Range") ?? "" queue.sync { requestedRanges.append(range) } let byteRange = byteRange(fromHeader: range, contentLength: 80_000_000) let response = HTTPURLResponse( url: request.url!, statusCode: 206, httpVersion: nil, headerFields: ["Content-Range": "bytes \(byteRange.start)-\(byteRange.end)/80000000"] )! return (Data(repeating: 1, count: Int(byteRange.length)), response) } let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher( url: URL(string: "https://cdn.example.test/movie.mp4")!, headers: [:], session: mockSession() ), contentLength: 80_000_000, durationProvider: { 100 } ) defer { session.cancelPrefetch() } session.prefetch(aroundByteOffset: 28_242_716) _ = try? await session.data(for: HTTPByteRange(start: 51_818_977, end: 52_867_552)) try? await Task.sleep(nanoseconds: 50_000_000) let ranges = queue.sync { requestedRanges } assert(ranges.contains("bytes=51380224-52428799"), "Expected foreground VLC miss to fetch aligned cache chunks") assert(ranges.contains { range in range.hasPrefix("bytes=52428800-") }, "Expected prefetch to restart on a global chunk boundary near VLC's foreground range, got \(ranges)") session.cancelPrefetch() MockURLProtocol.handler = nil try? await Task.sleep(nanoseconds: 50_000_000) } private static func testRangeCacheHitFollowsActualPostSeekReadArea() async { let queue = DispatchQueue(label: "dreamio.range-cache-hit-follow-test") var requestedRanges: [String] = [] MockURLProtocol.handler = { request in let range = request.value(forHTTPHeaderField: "Range") ?? "" queue.sync { requestedRanges.append(range) } let byteRange = byteRange(fromHeader: range, contentLength: 80_000_000) let response = HTTPURLResponse( url: request.url!, statusCode: 206, httpVersion: nil, headerFields: ["Content-Range": "bytes \(byteRange.start)-\(byteRange.end)/80000000"] )! return (Data(repeating: 1, count: Int(byteRange.length)), response) } let session = ProgressiveHTTPRangeCacheSession( fetcher: HTTPRangeRemoteFetcher( url: URL(string: "https://cdn.example.test/movie.mp4")!, headers: [:], session: mockSession() ), contentLength: 80_000_000, durationProvider: { 100 } ) defer { session.cancelPrefetch() } session.store.insert(data: Data(repeating: 7, count: 1_048_576), at: 27_165_812) session.prefetchForSeek(aroundByteOffset: 15_936_567) _ = try? await session.data(for: HTTPByteRange(start: 27_165_812, end: 28_214_387)) try? await Task.sleep(nanoseconds: 100_000_000) let ranges = queue.sync { requestedRanges } assert(ranges.contains { range in range.hasPrefix("bytes=27262976-") }, "Expected a cache hit far from the seek estimate to restart prefetch near VLC's real read area, got \(ranges)") MockURLProtocol.handler = nil try? await Task.sleep(nanoseconds: 50_000_000) } private static func testRangeProbeAllowsRangeCacheForMKVWhenServerSupportsRanges() async { var requestCount = 0 MockURLProtocol.handler = { request in requestCount += 1 assertEqual(request.httpMethod, "HEAD") let response = HTTPURLResponse( url: request.url!, statusCode: 200, httpVersion: nil, headerFields: [ "Accept-Ranges": "bytes", "Content-Length": "20" ] )! return (Data(), response) } let fetcher = HTTPRangeRemoteFetcher( url: URL(string: "https://cdn.example.test/show.mkv?token=secret")!, headers: [:], session: mockSession() ) let probe = await fetcher.probe() assertEqual(probe.isCacheable, true) assertEqual(probe.contentLength, 20) assertEqual(probe.fallbackReason, nil) assertEqual(requestCount, 1) MockURLProtocol.handler = nil } private static func byteRange(fromHeader header: String, contentLength: Int64) -> HTTPByteRange { let value = header.replacingOccurrences(of: "bytes=", with: "") let pieces = value.split(separator: "-", maxSplits: 1).map(String.init) guard pieces.count == 2, let start = Int64(pieces[0]) else { return HTTPByteRange(start: 0, end: 0) } let end = pieces[1].isEmpty ? contentLength - 1 : (Int64(pieces[1]) ?? contentLength - 1) return HTTPByteRange(start: start, end: min(end, contentLength - 1)) } private static func testRangeProbeFallsBackWhenServerIgnoresRange() async { MockURLProtocol.handler = { request in if request.httpMethod == "HEAD" { let response = HTTPURLResponse( url: request.url!, statusCode: 200, httpVersion: nil, headerFields: ["Content-Length": "4"] )! return (Data(), response) } assertEqual(request.value(forHTTPHeaderField: "Range"), "bytes=0-0") let response = HTTPURLResponse( url: request.url!, statusCode: 200, httpVersion: nil, headerFields: ["Content-Length": "4"] )! return (Data([1, 2, 3, 4]), response) } let fetcher = HTTPRangeRemoteFetcher( url: URL(string: "https://cdn.example.test/movie.mp4")!, headers: [:], session: mockSession() ) let probe = await fetcher.probe() assertEqual(probe.isCacheable, false) assertEqual(probe.fallbackReason, "range-probe-status-200") } private static func testRangeFetcherPreservesHeaders() async { MockURLProtocol.handler = { request in assertEqual(request.value(forHTTPHeaderField: "User-Agent"), "DreamioTest/1") assertEqual(request.value(forHTTPHeaderField: "Referer"), "https://web.stremio.com/") assertEqual(request.value(forHTTPHeaderField: "Cookie"), "session=abc") assertEqual(request.value(forHTTPHeaderField: "Range"), "bytes=5-7") let response = HTTPURLResponse( url: request.url!, statusCode: 206, httpVersion: nil, headerFields: ["Content-Range": "bytes 5-7/20"] )! return (Data([5, 6, 7]), response) } let fetcher = HTTPRangeRemoteFetcher( url: URL(string: "https://cdn.example.test/movie.mp4")!, headers: [ "User-Agent": "DreamioTest/1", "Referer": "https://web.stremio.com/", "Cookie": "session=abc" ], session: mockSession() ) let data = try? await fetcher.fetch(range: HTTPByteRange(start: 5, end: 7)) assertEqual(Array(data ?? Data()), [5, 6, 7]) } private static func testOpenSubtitlesV3DownloadResponseResolution() { let payload = """ { "link": "https://dl.opensubtitles.org/en/download/subtitle.srt?token=secret", "file_name": "episode.srt", "requests": 1 } """.data(using: .utf8)! let original = SubtitleCandidate( url: URL(string: "https://api.opensubtitles.com/api/v1/download")!, label: "English", language: "eng" ) let candidate = SubtitleResolver.bestPlayableCandidate( from: payload, responseURL: original.url, original: original ) assertEqual(candidate?.url.absoluteString, "https://dl.opensubtitles.org/en/download/subtitle.srt?token=secret") assertEqual(candidate?.label, "English") assertEqual(candidate?.language, "eng") } private static func testOpenSubtitlesNestedDownloadResponseResolution() { let payload = """ { "data": { "attributes": { "files": [ { "file_name": "ignored.txt", "link": "https://cdn.example.test/ignored.txt" }, { "file_name": "episode.en.ass", "download": { "link": "https://dl.opensubtitles.org/en/download/episode.en.ass?token=secret" } } ] } } } """.data(using: .utf8)! let original = SubtitleCandidate( url: URL(string: "https://api.opensubtitles.com/api/v1/download/987")!, label: "English SDH", language: "eng" ) let candidate = SubtitleResolver.bestPlayableCandidate( from: payload, responseURL: original.url, original: original ) assertEqual(candidate?.url.absoluteString, "https://dl.opensubtitles.org/en/download/episode.en.ass?token=secret") assertEqual(candidate?.label, "English SDH") assertEqual(candidate?.language, "eng") } private static func testSubtitleResolverCachesStremioDownloadBody() async { let sourceURL = "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341941" let subtitleBody = """ 1 00:00:01,000 --> 00:00:02,000 Hello from Stremio """ MockURLProtocol.handler = nil MockURLProtocol.handlers = [ sourceURL: ( 200, URL(string: sourceURL)!, subtitleBody.data(using: .utf8)! ) ] let cacheDirectory = FileManager.default.temporaryDirectory .appendingPathComponent("DreamioSubtitleResolverTests-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: cacheDirectory) } let resolver = SubtitleResolver(session: mockSession(), cacheDirectory: cacheDirectory) let candidate = await resolver.resolve(SubtitleCandidate( url: URL(string: sourceURL)!, label: "English", language: "eng" )) assertEqual(candidate?.url.isFileURL, true) assertEqual(candidate?.url.pathExtension, "srt") assertEqual(candidate?.label, "English") assertEqual(candidate?.language, "eng") let cachedBody = try? String(contentsOf: candidate!.url, encoding: .utf8) assertEqual(cachedBody, subtitleBody) } private static func testSubtitleResolverCachesPlainStremioDownloadBody() async { let sourceURL = "https://subs5.strem.io/en/download/subencoding-stremio-utf8/src-api/file/1952341942" let subtitleBody = """ 00:01.000 --> 00:02.000 Plain cue text without an index """ MockURLProtocol.handler = nil MockURLProtocol.handlers = [ sourceURL: ( 200, URL(string: sourceURL)!, subtitleBody.data(using: .utf8)! ) ] let cacheDirectory = FileManager.default.temporaryDirectory .appendingPathComponent("DreamioSubtitleResolverTests-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: cacheDirectory) } let resolver = SubtitleResolver(session: mockSession(), cacheDirectory: cacheDirectory) let candidate = await resolver.resolve(SubtitleCandidate( url: URL(string: sourceURL)!, label: "English", language: "eng" )) assertEqual(candidate?.url.isFileURL, true) assertEqual(candidate?.url.pathExtension, "srt") let cachedBody = try? String(contentsOf: candidate!.url, encoding: .utf8) assertEqual(cachedBody, subtitleBody) } private static func testSubtitleResolverDownloadJSONReturningLink() async { MockURLProtocol.handler = nil MockURLProtocol.handlers = [ "https://api.opensubtitles.com/api/v1/download/123": ( 200, URL(string: "https://api.opensubtitles.com/api/v1/download/123")!, #"{"link":"https://dl.opensubtitles.org/en/download/movie.srt?token=secret"}"#.data(using: .utf8)! ) ] let resolver = SubtitleResolver(session: mockSession()) let candidate = await resolver.resolve(SubtitleCandidate( url: URL(string: "https://api.opensubtitles.com/api/v1/download/123")!, label: "English", language: "eng" )) assertEqual(candidate?.url.absoluteString, "https://dl.opensubtitles.org/en/download/movie.srt?token=secret") assertEqual(candidate?.label, "English") assertEqual(candidate?.language, "eng") } private static func testSubtitleResolverRedirectToDirectSubtitle() async { MockURLProtocol.handler = nil MockURLProtocol.handlers = [ "https://api.opensubtitles.com/api/v1/download/redirect": ( 200, URL(string: "https://dl.opensubtitles.org/en/redirected.vtt?download=1")!, Data() ) ] let resolver = SubtitleResolver(session: mockSession()) let candidate = await resolver.resolve(SubtitleCandidate( url: URL(string: "https://api.opensubtitles.com/api/v1/download/redirect")!, label: "English", language: "eng" )) assertEqual(candidate?.url.absoluteString, "https://dl.opensubtitles.org/en/redirected.vtt?download=1") } private static func testSubtitleResolverRejectsNonSubtitleAPIResponse() async { MockURLProtocol.handler = nil MockURLProtocol.handlers = [ "https://api.opensubtitles.com/api/v1/download/not-found": ( 200, URL(string: "https://api.opensubtitles.com/api/v1/download/not-found")!, #"{"message":"not found"}"#.data(using: .utf8)! ) ] let resolver = SubtitleResolver(session: mockSession()) let candidate = await resolver.resolve(SubtitleCandidate( url: URL(string: "https://api.opensubtitles.com/api/v1/download/not-found")!, label: "English", language: "eng" )) assert(candidate == nil, "Expected non-subtitle API response to be rejected") } private static func testSubtitleCandidateDeduplicationPreservesLabels() { let payload: [String: Any] = [ "subtitles": [ [ "label": "English SDH", "lang": "eng", "url": "https://opensubtitles.example.test/download/duplicate.srt" ], [ "label": "Duplicate", "language": "English", "download": "https://opensubtitles.example.test/download/duplicate.srt" ], "https://opensubtitles.example.test/download/duplicate.srt" ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 1) assertEqual(candidates[0].label, "English SDH") assertEqual(candidates[0].language, "eng") } private static func testSubtitleCandidateDeduplicationUpgradesLabels() { let payload: [String: Any] = [ "subtitles": [ "https://opensubtitles.example.test/download/duplicate.srt", [ "label": "English SDH", "lang": "eng", "url": "https://opensubtitles.example.test/download/duplicate.srt" ] ] ] let candidates = SubtitleCandidateParser.candidates(in: payload) assertEqual(candidates.count, 1) assertEqual(candidates[0].label, "English SDH") assertEqual(candidates[0].language, "eng") } private static func testSubtitleOptionMappingIncludesNone() { let options = SubtitleOptionMapper.options(from: [ SubtitleTrack(id: 2, name: "English"), SubtitleTrack(id: 5, name: "Spanish") ]) assertEqual(options.map(\.name), ["None", "English", "Spanish"]) assertEqual(options.first?.id, -1) } private static func testExternalSubtitleParserHandlesCRLFSRT() { let body = "1\r\n00:00:01,000 --> 00:00:02,500\r\nHello from CRLF\r\n\r\n" let cues = ExternalSubtitleTrackParser.parseCues(from: body) assertEqual(cues.count, 1) assertEqual(cues[0].start, 1) assertEqual(cues[0].end, 2.5) assertEqual(cues[0].text, "Hello from CRLF") } private static func testExternalSubtitleCueLookupBoundaries() { let track = ExternalSubtitleTrack( id: 1, name: "English", cues: [ ExternalSubtitleCue(start: 7.101, end: 9.25, text: "First cue") ] ) assert(track.cue(at: 7.100) == nil, "Expected time before first cue to hide overlay") assertEqual(track.cue(at: 7.101)?.text, "First cue") assertEqual(track.cue(at: 8.0)?.text, "First cue") assert(track.cue(at: 9.25) == nil, "Expected cue end boundary to hide overlay") assert(track.cue(at: 9.251) == nil, "Expected time after cue end to hide overlay") } private static func testExternalSubtitleParserCleansMultilineCueText() { let body = """ 1 00:00:03,000 --> 00:00:05,000 Hello {\\an8}there """ let cues = ExternalSubtitleTrackParser.parseCues(from: body) assertEqual(cues.count, 1) assertEqual(cues[0].text, "Hello\nthere") } private static func testExternalSubtitleParserHandlesSouthParkFirstCueTiming() { let body = """ 1 00:00:07,101 --> 00:00:09,103 I'm going down to South Park """ let cues = ExternalSubtitleTrackParser.parseCues(from: body) let track = ExternalSubtitleTrack(id: 1, name: "English", cues: cues) assertEqual(cues.count, 1) assertEqual(cues[0].start, 7.101) assert(track.cue(at: 7.100) == nil, "Expected no text before the South Park-style first cue") assertEqual(track.cue(at: 7.101)?.text, "I'm going down to South Park") } 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" ) 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() { 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(_ 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) } private static func mockSession() -> URLSession { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] return URLSession(configuration: configuration) } } private final class MockURLProtocol: URLProtocol { static var handler: ((URLRequest) throws -> (Data, HTTPURLResponse))? static var handlers: [String: (status: Int, url: URL, data: Data)] = [:] override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { if let handler = Self.handler { do { let (data, response) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } return } guard let url = request.url, let handler = Self.handlers[url.absoluteString], let response = HTTPURLResponse( url: handler.url, statusCode: handler.status, httpVersion: "HTTP/1.1", headerFields: nil ) else { client?.urlProtocol(self, didFailWithError: URLError(.badURL)) return } client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: handler.data) client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} }