diff --git a/Model/Player/PlayerStreams.swift b/Model/Player/PlayerStreams.swift index ee36713e..abc3a861 100644 --- a/Model/Player/PlayerStreams.swift +++ b/Model/Player/PlayerStreams.swift @@ -57,41 +57,54 @@ extension PlayerModel { } func streamsWithInstance(instance _: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) { - let forbiddenAssetTestGroup = DispatchGroup() - var hasForbiddenAsset = false + // Queue for stream processing + let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue", qos: .userInitiated) + // Queue for accessing the processedStreams array + let processedStreamsQueue = DispatchQueue(label: "stream.yattee.processedStreams.Queue") + // DispatchGroup for managing multiple tasks + let streamProcessingGroup = DispatchGroup() - let (nonHLSAssets, hlsURLs) = getAssets(from: streams) + var processedStreams = [Stream]() - if let randomStream = nonHLSAssets.randomElement() { - let instance = randomStream.0 - let asset = randomStream.1 - let url = randomStream.2 - let requestRange = randomStream.3 + for stream in streams { + streamProcessingQueue.async(group: streamProcessingGroup) { + let forbiddenAssetTestGroup = DispatchGroup() + var hasForbiddenAsset = false + + let (nonHLSAssets, hlsURLs) = self.getAssets(from: [stream]) + + if let randomStream = nonHLSAssets.randomElement() { + let instance = randomStream.0 + let asset = randomStream.1 + let url = randomStream.2 + let requestRange = randomStream.3 + + // swiftlint:disable:next shorthand_optional_binding + if let asset = asset, let instance = instance, !instance.proxiesVideos { + if instance.app == .invidious { + self.testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in + hasForbiddenAsset = isForbidden + } + } else if instance.app == .piped { + self.testPipedAssets(asset: asset, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in + hasForbiddenAsset = isForbidden + } + } + } + } else if let randomHLS = hlsURLs.randomElement() { + let instance = randomHLS.0 + let asset = AVURLAsset(url: randomHLS.1) - if let asset = asset, let instance = instance, !instance.proxiesVideos { - if instance.app == .invidious { - testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in - hasForbiddenAsset = isForbidden + if instance?.app == .piped { + self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in + hasForbiddenAsset = isForbidden + } } - } else if instance.app == .piped { - testPipedAssets(asset: asset, requestRange: requestRange!, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: { isForbidden in - hasForbiddenAsset = isForbidden - }) } - } - } else if let randomHLS = hlsURLs.randomElement() { - let instance = randomHLS.0 - let asset = AVURLAsset(url: randomHLS.1) - - if instance?.app == .piped { - testPipedAssets(asset: asset, requestRange: nil, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: { isForbidden in - hasForbiddenAsset = isForbidden - }) - } - } - forbiddenAssetTestGroup.notify(queue: .main) { - let processedStreams = streams.map { stream -> Stream in + forbiddenAssetTestGroup.wait() + + // Post-processing code if let instance = stream.instance { if instance.app == .invidious { if hasForbiddenAsset || instance.proxiesVideos { @@ -104,35 +117,36 @@ extension PlayerModel { } } else if instance.app == .piped, !instance.proxiesVideos, !hasForbiddenAsset { if let hlsURL = stream.hlsURL { - forbiddenAssetTestGroup.enter() - PipedAPI.nonProxiedAsset(url: hlsURL) { nonProxiedURL in - if let nonProxiedURL = nonProxiedURL { + PipedAPI.nonProxiedAsset(url: hlsURL) { possibleNonProxiedURL in + if let nonProxiedURL = possibleNonProxiedURL { stream.hlsURL = nonProxiedURL.url } - forbiddenAssetTestGroup.leave() } } else { if let audio = stream.audioAsset { - forbiddenAssetTestGroup.enter() PipedAPI.nonProxiedAsset(asset: audio) { nonProxiedAudioAsset in stream.audioAsset = nonProxiedAudioAsset - forbiddenAssetTestGroup.leave() } } if let video = stream.videoAsset { - forbiddenAssetTestGroup.enter() PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in stream.videoAsset = nonProxiedVideoAsset - forbiddenAssetTestGroup.leave() } } } } } - return stream + + // Append to processedStreams within the processedStreamsQueue + processedStreamsQueue.sync { + processedStreams.append(stream) + } } + } - forbiddenAssetTestGroup.notify(queue: .main) { + streamProcessingGroup.notify(queue: .main) { + // Access and pass processedStreams within the processedStreamsQueue block + processedStreamsQueue.sync { completion(processedStreams) } } @@ -161,21 +175,23 @@ extension PlayerModel { } private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) { + // In case the range is nil, generate a random one. let randomEnd = Int.random(in: 200 ... 800) let requestRange = range ?? "0-\(randomEnd)" - let HTTPStatusForbidden = 403 forbiddenAssetTestGroup.enter() URLTester.testURLResponse(url: url, range: requestRange, isHLS: isHLS) { statusCode in - completion(statusCode == HTTPStatusForbidden) + completion(statusCode == HTTPStatus.Forbidden) forbiddenAssetTestGroup.leave() } } private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) { - PipedAPI.nonProxiedAsset(asset: asset) { nonProxiedAsset in - if let nonProxiedAsset = nonProxiedAsset { + PipedAPI.nonProxiedAsset(asset: asset) { possibleNonProxiedAsset in + if let nonProxiedAsset = possibleNonProxiedAsset { self.testAsset(url: nonProxiedAsset.url, range: requestRange, isHLS: isHLS, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: completion) + } else { + completion(false) } } } diff --git a/Shared/HTTPStatus.swift b/Shared/HTTPStatus.swift new file mode 100644 index 00000000..9da2aac8 --- /dev/null +++ b/Shared/HTTPStatus.swift @@ -0,0 +1,81 @@ +// HTTP response status codes + +enum HTTPStatus { + // Informational responses (100 - 199) + + static let Continue = 100 + static let SwitchingProtocols = 101 + static let Processing = 102 + static let EarlyHints = 103 + + // Successful responses (200 - 299) + + static let OK = 200 + static let Created = 201 + static let Accepted = 202 + static let NonAuthoritativeInformation = 203 + static let NoContent = 204 + static let ResetContent = 205 + static let PartialContent = 206 + static let MultiStatus = 207 + static let AlreadyReported = 208 + static let IMUsed = 226 + + // Redirection messages (300 - 399) + + static let MultipleChoices = 300 + static let MovedPermanently = 301 + static let Found = 302 + static let SeeOther = 303 + static let NotModified = 304 + static let UseProxy = 305 + static let SwitchProxy = 306 + static let TemporaryRedirect = 307 + static let PermanentRedirect = 308 + + // Client error responses (400 - 499) + + static let BadRequest = 400 + static let Unauthorized = 401 + static let PaymentRequired = 402 + static let Forbidden = 403 + static let NotFound = 404 + static let MethodNotAllowed = 405 + static let NotAcceptable = 406 + static let ProxyAuthenticationRequired = 407 + static let RequestTimeout = 408 + static let Conflict = 409 + static let Gone = 410 + static let LengthRequired = 411 + static let PreconditionFailed = 412 + static let PayloadTooLarge = 413 + static let URITooLong = 414 + static let UnsupportedMediaType = 415 + static let RangeNotSatisfiable = 416 + static let ExpectationFailed = 417 + static let IAmATeapot = 418 + static let MisdirectedRequest = 421 + static let UnprocessableEntity = 422 + static let Locked = 423 + static let FailedDependency = 424 + static let TooEarly = 425 + static let UpgradeRequired = 426 + static let PreconditionRequired = 428 + static let TooManyRequests = 429 + static let RequestHeaderFieldsTooLarge = 431 + static let UnavailableForLegalReasons = 451 + + // Server error responses (500 - 599) + + static let InternalServerError = 500 + static let NotImplemented = 501 + static let BadGateway = 502 + static let ServiceUnavailable = 503 + static let GatewayTimeout = 504 + static let HTTPVersionNotSupported = 505 + static let VariantAlsoNegotiates = 506 + static let InsufficientStorage = 507 + static let LoopDetected = 508 + static let NotExtended = 510 + static let NetworkAuthenticationRequired = 511 +} diff --git a/Shared/URLTester.swift b/Shared/URLTester.swift index c653e887..29b7e1ac 100644 --- a/Shared/URLTester.swift +++ b/Shared/URLTester.swift @@ -5,7 +5,6 @@ enum URLTester { private static let hlsMediaPrefix = "#EXT-X-MEDIA:" private static let hlsInfPrefix = "#EXTINF:" private static let uriRegex = "(?<=URI=\")(.*?)(?=\")" - private static let HTTPStatusForbidden = 403 static func testURLResponse(url: URL, range: String, isHLS: Bool, completion: @escaping (Int) -> Void) { if isHLS { @@ -24,7 +23,7 @@ enum URLTester { var dataTask: URLSessionDataTask? dataTask = URLSession.shared.dataTask(with: request) { _, response, _ in - let statusCode = (response as? HTTPURLResponse)?.statusCode ?? HTTPStatusForbidden + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? HTTPStatus.Forbidden Logger(label: "stream.yattee.httpRequest").info("URL: \(url) | Status Code: \(statusCode)") completion(statusCode, dataTask) } @@ -37,6 +36,8 @@ enum URLTester { httpRequest(url: url, range: range) { statusCode, _ in completion(statusCode) } + } else { + completion(HTTPStatus.NotFound) } } } @@ -64,10 +65,15 @@ enum URLTester { private static func parseHLSManifest(manifestUrl: URL, completion: @escaping ([URL]) -> Void) { URLSession.shared.dataTask(with: manifestUrl) { data, _, _ in - guard let data = data, - let manifest = String(data: data, encoding: .utf8), - !manifest.isEmpty - else { + // swiftlint:disable:next shorthand_optional_binding + guard let data = data else { + Logger(label: "stream.yattee.httpRequest").error("Data is nil") + completion([]) + return + } + + // swiftlint:disable:next non_optional_string_data_conversion + guard let manifest = String(data: data, encoding: .utf8), !manifest.isEmpty else { Logger(label: "stream.yattee.httpRequest").error("Cannot read or empty HLS manifest") completion([]) return diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 831680fa..ef5e7146 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -1070,6 +1070,9 @@ 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; 37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; + E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; }; + E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; }; + E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; }; E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; }; E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; }; E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; }; @@ -1542,6 +1545,7 @@ 3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = ""; }; 3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = ""; }; 3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; + E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; sourceTree = ""; }; E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2290,6 +2294,7 @@ 372915E52687E3B900F5A35B /* Defaults.swift */, 37D2E0D328B67EFC00F64D52 /* Delay.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, + E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */, 375B537828DF6CBB004C1D19 /* Localizable.strings */, 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */, @@ -3120,6 +3125,7 @@ 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37F7D82C289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */, 375EC95D289EEEE000751258 /* QualityProfile.swift in Sources */, + E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */, 3773B8102ADC076800B5FEF3 /* ScrollViewMatcher.swift in Sources */, 371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */, 37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */, @@ -3593,6 +3599,7 @@ 37D4B19826717E1500C925CA /* Video.swift in Sources */, 371B7E5D27596B8400D21217 /* Comment.swift in Sources */, 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */, + E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */, 37A7D71C2B680E66009CB1ED /* LocationsSettingsGroupExporter.swift in Sources */, 37BC50A92778A84700510953 /* HistorySettings.swift in Sources */, 374DE88128BB896C0062BBF2 /* PlayerDragGesture.swift in Sources */, @@ -3873,6 +3880,7 @@ 37130A61277657300033018A /* PersistenceController.swift in Sources */, 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, + E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */, 370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */, 376A33E62720CB35000C1D6B /* Account.swift in Sources */, 3763C98B290C7A50004D3B5F /* OpenVideosView.swift in Sources */,