diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index d440b851..6d23a3f4 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -655,7 +655,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { kind: .adaptive, encoding: videoStream["encoding"].string, videoFormat: videoStream["type"].string, - bitrate: videoStream["bitrate"].int + bitrate: videoStream["bitrate"].int, + requestRange: videoStream["init"].string ?? videoStream["index"].string ) } } diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index 96df8bba..25a74b75 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -491,6 +491,35 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { ) } + static func nonProxiedAsset(asset: AVURLAsset, completion: @escaping (AVURLAsset?) -> Void) { + guard var urlComponents = URLComponents(url: asset.url, resolvingAgainstBaseURL: false) else { + completion(asset) + return + } + + guard let hostItem = urlComponents.queryItems?.first(where: { $0.name == "host" }), + let hostValue = hostItem.value + else { + completion(asset) + return + } + + urlComponents.host = hostValue + + guard let newUrl = urlComponents.url else { + completion(asset) + return + } + + completion(AVURLAsset(url: newUrl)) + } + + // Overload used for hlsURLS + static func nonProxiedAsset(url: URL, completion: @escaping (AVURLAsset?) -> Void) { + let asset = AVURLAsset(url: url) + nonProxiedAsset(asset: asset, completion: completion) + } + private func extractVideo(from content: JSON) -> Video? { let details = content.dictionaryValue @@ -579,10 +608,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { return nil } - return URL(string: thumbnailURL - .absoluteString - .replacingOccurrences(of: "hqdefault", with: quality.filename) - .replacingOccurrences(of: "maxresdefault", with: quality.filename) + return URL( + string: thumbnailURL + .absoluteString + .replacingOccurrences(of: "hqdefault", with: quality.filename) + .replacingOccurrences(of: "maxresdefault", with: quality.filename) ) } @@ -688,6 +718,19 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { let resolution = Stream.Resolution.from(resolution: quality, fps: fps) let videoFormat = videoStream.dictionaryValue["format"]?.string let bitrate = videoStream.dictionaryValue["bitrate"]?.int + var requestRange: String? + + if let initStart = videoStream.dictionaryValue["initStart"]?.int, + let initEnd = videoStream.dictionaryValue["initEnd"]?.int + { + requestRange = "\(initStart)-\(initEnd)" + } else if let indexStart = videoStream.dictionaryValue["indexStart"]?.int, + let indexEnd = videoStream.dictionaryValue["indexEnd"]?.int + { + requestRange = "\(indexStart)-\(indexEnd)" + } else { + requestRange = nil + } if videoOnly { streams.append( @@ -698,7 +741,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { resolution: resolution, kind: .adaptive, videoFormat: videoFormat, - bitrate: bitrate + bitrate: bitrate, + requestRange: requestRange ) ) } else { diff --git a/Model/Applications/VideosApp.swift b/Model/Applications/VideosApp.swift index 6da8a9f7..014414b7 100644 --- a/Model/Applications/VideosApp.swift +++ b/Model/Applications/VideosApp.swift @@ -95,7 +95,7 @@ enum VideosApp: String, CaseIterable { } var allowsDisablingVidoesProxying: Bool { - self == .invidious + self == .invidious || self == .piped } var supportsOpeningVideosByID: Bool { diff --git a/Model/NetworkStateModel.swift b/Model/NetworkStateModel.swift index 1f46fc94..25a55d72 100644 --- a/Model/NetworkStateModel.swift +++ b/Model/NetworkStateModel.swift @@ -26,7 +26,7 @@ final class NetworkStateModel: ObservableObject { } var bufferingStateText: String? { - guard detailsAvailable else { return nil } + guard detailsAvailable && player.hasStarted else { return nil } return String(format: "%.0f%%", bufferingState) } diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index f13e6188..96d38506 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -40,6 +40,11 @@ final class AVPlayerBackend: PlayerBackend { var isLoadingVideo = false + var hasStarted = false + var isPaused: Bool { + avPlayer.timeControlStatus == .paused + } + var isPlaying: Bool { avPlayer.timeControlStatus == .playing } @@ -158,6 +163,12 @@ final class AVPlayerBackend: PlayerBackend { } avPlayer.play() + + // Setting hasStarted to true the first time player started + if !hasStarted { + hasStarted = true + } + model.objectWillChange.send() } @@ -180,6 +191,7 @@ final class AVPlayerBackend: PlayerBackend { func stop() { avPlayer.replaceCurrentItem(with: nil) + hasStarted = false } func cancelLoads() { diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 4b2938cc..01a9adb7 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -44,6 +44,8 @@ final class MPVBackend: PlayerBackend { } }} + var hasStarted = false + var isPaused = false var isPlaying = true { didSet { networkStateTimer.start() @@ -337,7 +339,6 @@ final class MPVBackend: PlayerBackend { } func play() { - isPlaying = true startClientUpdates() if controls.presentingControls { @@ -354,13 +355,22 @@ final class MPVBackend: PlayerBackend { } client?.play() + + isPlaying = true + isPaused = false + + // Setting hasStarted to true the first time player started + if !hasStarted { + hasStarted = true + } } func pause() { - isPlaying = false stopClientUpdates() client?.pause() + isPaused = true + isPlaying = false } func togglePlay() { @@ -377,6 +387,9 @@ final class MPVBackend: PlayerBackend { func stop() { client?.stop() + isPlaying = false + isPaused = false + hasStarted = false } func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) { @@ -392,8 +405,8 @@ final class MPVBackend: PlayerBackend { } func closeItem() { - client?.pause() - client?.stop() + pause() + stop() self.video = nil self.stream = nil } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index d9469c09..980e9ab5 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -19,6 +19,8 @@ protocol PlayerBackend { var loadedVideo: Bool { get } var isLoadingVideo: Bool { get } + var hasStarted: Bool { get } + var isPaused: Bool { get } var isPlaying: Bool { get } var isSeeking: Bool { get } var playerItemDuration: CMTime? { get } diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 047deca0..18e36c45 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -298,6 +298,14 @@ final class PlayerModel: ObservableObject { backend.isPlaying } + var isPaused: Bool { + backend.isPaused + } + + var hasStarted: Bool { + backend.hasStarted + } + var playerItemDuration: CMTime? { guard !currentItem.isNil else { return nil diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index cbec911d..8b202d11 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -74,7 +74,7 @@ extension PlayerModel { preservedTime = currentItem.playbackTime DispatchQueue.main.async { [weak self] in - guard let self else { return } + guard let self = self else { return } guard let video = item.video else { return } @@ -94,7 +94,9 @@ extension PlayerModel { } } else { self.videoBeingOpened = nil - self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams) + self.streamsWithInstance(instance: playerInstance, streams: video.streams) { processedStreams in + self.availableStreams = processedStreams + } } } } diff --git a/Model/Player/PlayerStreams.swift b/Model/Player/PlayerStreams.swift index 72383746..abc3a861 100644 --- a/Model/Player/PlayerStreams.swift +++ b/Model/Player/PlayerStreams.swift @@ -1,3 +1,4 @@ +import AVFoundation import Foundation import Siesta import SwiftUI @@ -41,7 +42,9 @@ extension PlayerModel { self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed") return } - self.availableStreams = self.streamsWithInstance(instance: instance, streams: video.streams) + self.streamsWithInstance(instance: instance, streams: video.streams) { processedStreams in + self.availableStreams = processedStreams + } } else { self.logger.critical("no streams available from \(instance.description)") } @@ -53,20 +56,143 @@ extension PlayerModel { } } - func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] { - streams.map { stream in - stream.instance = instance + func streamsWithInstance(instance _: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) { + // 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() - if instance.app == .invidious, instance.proxiesVideos { - if let audio = stream.audioAsset { - stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio) + var processedStreams = [Stream]() + + 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 instance?.app == .piped { + self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in + hasForbiddenAsset = isForbidden + } + } } - if let video = stream.videoAsset { - stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video) + + forbiddenAssetTestGroup.wait() + + // Post-processing code + if let instance = stream.instance { + if instance.app == .invidious { + if hasForbiddenAsset || instance.proxiesVideos { + if let audio = stream.audioAsset { + stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio) + } + if let video = stream.videoAsset { + stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video) + } + } + } else if instance.app == .piped, !instance.proxiesVideos, !hasForbiddenAsset { + if let hlsURL = stream.hlsURL { + PipedAPI.nonProxiedAsset(url: hlsURL) { possibleNonProxiedURL in + if let nonProxiedURL = possibleNonProxiedURL { + stream.hlsURL = nonProxiedURL.url + } + } + } else { + if let audio = stream.audioAsset { + PipedAPI.nonProxiedAsset(asset: audio) { nonProxiedAudioAsset in + stream.audioAsset = nonProxiedAudioAsset + } + } + if let video = stream.videoAsset { + PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in + stream.videoAsset = nonProxiedVideoAsset + } + } + } + } + } + + // Append to processedStreams within the processedStreamsQueue + processedStreamsQueue.sync { + processedStreams.append(stream) } } + } - return stream + streamProcessingGroup.notify(queue: .main) { + // Access and pass processedStreams within the processedStreamsQueue block + processedStreamsQueue.sync { + completion(processedStreams) + } + } + } + + private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(Instance?, AVURLAsset?, URL, String?)], hlsURLs: [(Instance?, URL)]) { + var nonHLSAssets = [(Instance?, AVURLAsset?, URL, String?)]() + var hlsURLs = [(Instance?, URL)]() + + for stream in streams { + if stream.isHLS { + if let url = stream.hlsURL?.url { + hlsURLs.append((stream.instance, url)) + } + } else { + if let asset = stream.audioAsset { + nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange)) + } + if let asset = stream.videoAsset { + nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange)) + } + } + } + + return (nonHLSAssets, hlsURLs) + } + + 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)" + + forbiddenAssetTestGroup.enter() + URLTester.testURLResponse(url: url, range: requestRange, isHLS: isHLS) { statusCode in + 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) { 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/Model/Stream.swift b/Model/Stream.swift index 2c76c00c..8c7465a3 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -170,6 +170,7 @@ class Stream: Equatable, Hashable, Identifiable { var encoding: String? var videoFormat: String? var bitrate: Int? + var requestRange: String? init( instance: Instance? = nil, @@ -181,7 +182,8 @@ class Stream: Equatable, Hashable, Identifiable { kind: Kind = .hls, encoding: String? = nil, videoFormat: String? = nil, - bitrate: Int? = nil + bitrate: Int? = nil, + requestRange: String? = nil ) { self.instance = instance self.audioAsset = audioAsset @@ -193,6 +195,7 @@ class Stream: Equatable, Hashable, Identifiable { self.encoding = encoding format = .from(videoFormat ?? "") self.bitrate = bitrate + self.requestRange = requestRange } var isLocal: Bool { diff --git a/Open in Yattee/Open in Yattee.entitlements b/Open in Yattee/Open in Yattee.entitlements index 2618bd9b..dde0d9ac 100644 --- a/Open in Yattee/Open in Yattee.entitlements +++ b/Open in Yattee/Open in Yattee.entitlements @@ -4,7 +4,7 @@ com.apple.security.application-groups - group.78Z5H3M6RJ.stream.yattee.app.urlbookmarks + group.stonerl.yattee.app.url 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/Player/Controls/OSD/OpeningStream.swift b/Shared/Player/Controls/OSD/OpeningStream.swift index d4caa727..121d6d1e 100644 --- a/Shared/Player/Controls/OSD/OpeningStream.swift +++ b/Shared/Player/Controls/OSD/OpeningStream.swift @@ -10,26 +10,28 @@ struct OpeningStream: View { } var visible: Bool { - (!player.currentItem.isNil && !player.videoBeingOpened.isNil) || (player.isLoadingVideo && !model.pausedForCache && !player.isSeeking) + (!player.currentItem.isNil && !player.videoBeingOpened.isNil) || + (player.isLoadingVideo && !model.pausedForCache && !player.isSeeking) || + !player.hasStarted } var reason: String { guard player.videoBeingOpened == nil else { - return "Loading streams...".localized() + return "Loading streams…".localized() } if player.musicMode { - return "Opening audio stream...".localized() + return "Opening audio stream…".localized() } if let selection = player.streamSelection { if selection.isLocal { - return "Opening file...".localized() + return "Opening file…".localized() } - return String(format: "Opening %@ stream...".localized(), selection.shortQuality) + return String(format: "Opening %@ stream…".localized(), selection.shortQuality) } - return "Loading streams...".localized() + return "Loading streams…".localized() } var state: String? { diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 05d2c6e3..ea215b94 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -75,16 +75,20 @@ struct PlayerControls: View { } VStack { + Spacer() ZStack { - VStack(spacing: 0) { - ZStack { - OpeningStream() - NetworkState() + GeometryReader { geometry in + VStack(spacing: 0) { + ZStack { + OpeningStream() + NetworkState() + } } - - Spacer() + .position( + x: geometry.size.width / 2, + y: geometry.size.height / 2 + ) } - .offset(y: playerControlsLayout.osdVerticalOffset + 5) if showControls { Section { diff --git a/Shared/Player/Controls/PlayerControlsLayout.swift b/Shared/Player/Controls/PlayerControlsLayout.swift index 19391b26..9c8a20e8 100644 --- a/Shared/Player/Controls/PlayerControlsLayout.swift +++ b/Shared/Player/Controls/PlayerControlsLayout.swift @@ -278,10 +278,6 @@ enum PlayerControlsLayout: String, CaseIterable, Defaults.Serializable { } } - var osdVerticalOffset: Double { - buttonSize - } - var osdProgressBarHeight: Double { switch self { case .tvRegular: diff --git a/Shared/URLTester.swift b/Shared/URLTester.swift new file mode 100644 index 00000000..29b7e1ac --- /dev/null +++ b/Shared/URLTester.swift @@ -0,0 +1,109 @@ +import Foundation +import Logging + +enum URLTester { + private static let hlsMediaPrefix = "#EXT-X-MEDIA:" + private static let hlsInfPrefix = "#EXTINF:" + private static let uriRegex = "(?<=URI=\")(.*?)(?=\")" + + static func testURLResponse(url: URL, range: String, isHLS: Bool, completion: @escaping (Int) -> Void) { + if isHLS { + parseAndTestHLSManifest(manifestUrl: url, range: range, completion: completion) + } else { + httpRequest(url: url, range: range) { statusCode, _ in + completion(statusCode) + } + } + } + + private static func httpRequest(url: URL, range: String, completion: @escaping (Int, URLSessionDataTask?) -> Void) { + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + request.setValue("bytes=\(range)", forHTTPHeaderField: "Range") + + var dataTask: URLSessionDataTask? + dataTask = URLSession.shared.dataTask(with: request) { _, response, _ in + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? HTTPStatus.Forbidden + Logger(label: "stream.yattee.httpRequest").info("URL: \(url) | Status Code: \(statusCode)") + completion(statusCode, dataTask) + } + dataTask?.resume() + } + + static func parseAndTestHLSManifest(manifestUrl: URL, range: String, completion: @escaping (Int) -> Void) { + recursivelyParseManifest(manifestUrl: manifestUrl) { allURLs in + if let url = allURLs.randomElement() { + httpRequest(url: url, range: range) { statusCode, _ in + completion(statusCode) + } + } else { + completion(HTTPStatus.NotFound) + } + } + } + + private static func recursivelyParseManifest(manifestUrl: URL, fullyParsed: @escaping ([URL]) -> Void) { + parseHLSManifest(manifestUrl: manifestUrl) { urls in + var allURLs = [URL]() + let group = DispatchGroup() + for url in urls { + if url.pathExtension == "m3u8" { + group.enter() + recursivelyParseManifest(manifestUrl: url) { subUrls in + allURLs += subUrls + group.leave() + } + } else { + allURLs.append(url) + } + } + group.notify(queue: .main) { + fullyParsed(allURLs) + } + } + } + + private static func parseHLSManifest(manifestUrl: URL, completion: @escaping ([URL]) -> Void) { + URLSession.shared.dataTask(with: manifestUrl) { data, _, _ in + // 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 + } + + let lines = manifest.split(separator: "\n") + var mediaURLs: [URL] = [] + + for index in 0 ..< lines.count { + let lineString = String(lines[index]) + + if lineString.hasPrefix(hlsMediaPrefix), + let uriRange = lineString.range(of: uriRegex, options: .regularExpression) + { + let uri = lineString[uriRange] + if let url = URL(string: String(uri)) { + mediaURLs.append(url) + } + } else if lineString.hasPrefix(hlsInfPrefix), index < lines.count - 1 { + let possibleURL = String(lines[index + 1]) + let baseURL = manifestUrl.deletingLastPathComponent() + if let relativeURL = URL(string: possibleURL, relativeTo: baseURL), + relativeURL.scheme != nil + { + mediaURLs.append(relativeURL) + } + } + } + completion(mediaURLs) + } + .resume() + } +} diff --git a/Shared/ar.lproj/Localizable.strings b/Shared/ar.lproj/Localizable.strings index 5eb9d81b..3d010e2a 100644 --- a/Shared/ar.lproj/Localizable.strings +++ b/Shared/ar.lproj/Localizable.strings @@ -129,7 +129,7 @@ "LIVE" = "مباشر"; /* Loading stream OSD */ -"Loading streams..." = "تحميل بثوث..."; +"Loading streams…" = "تحميل بثوث…"; "Loading..." = "تحميل..."; /* Video duration filter in search */ @@ -163,8 +163,8 @@ "Open Settings" = "فتح الإعدادات"; /* Loading stream OSD */ -"Opening %@ stream..." = "فتح بث %@ ..."; -"Opening audio stream..." = "فتح بث صوتي..."; +"Opening %@ stream…" = "فتح بث %@ …"; +"Opening audio stream…" = "فتح بث صوتي…"; "Orientation" = "اتجاه"; "Play in PiP" = "تشغيل في الفيديو المصغر"; "Play Last" = "تشغيل الأخير"; @@ -558,7 +558,7 @@ "Are you sure you want to clear cache?" = "هل أنت متأكد من أنك تريد مسح ذاكرة التخزين المؤقت؟"; "Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لأزرار الأسهم عن بعد (للجيل الثاني من Siri Remote أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل."; "Opened File" = "ملف مفتوح"; -"Opening file..." = "فتح الملف..."; +"Opening file…" = "فتح الملف…"; "Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية"; "Mark channel feed as watched" = "وضع علامة تمت المشاهدة على محتوى القناة"; "Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل."; diff --git a/Shared/az.lproj/Localizable.strings b/Shared/az.lproj/Localizable.strings index 5b93dd08..b40d7312 100644 --- a/Shared/az.lproj/Localizable.strings +++ b/Shared/az.lproj/Localizable.strings @@ -277,11 +277,11 @@ "Large" = "Böyük"; /* Loading stream OSD */ -"Loading streams..." = "Yayımlar yüklənilir..."; +"Loading streams…" = "Yayımlar yüklənilir…"; "Only when signed in" = "Yalnız daxil olduqda"; /* Loading stream OSD */ -"Opening %@ stream..." = "%@ yayımı açılır..."; +"Opening %@ stream…" = "%@ yayımı açılır…"; "Matrix Chat" = "Matrix Söhbət"; /* Player controls layout size */ @@ -296,7 +296,7 @@ "Open Settings" = "Tənzimləmələri Aç"; "Movies" = "Filmlər"; "No description" = "Açıqlama yoxdur"; -"Opening audio stream..." = "Səs yayımı açılır..."; +"Opening audio stream…" = "Səs yayımı açılır…"; "Password" = "Şifrə"; "Preferred Formats" = "Üstünlük Verilən Formatlar"; "Quality Profile" = "Profil Keyfiyyəti"; diff --git a/Shared/ca.lproj/Localizable.strings b/Shared/ca.lproj/Localizable.strings index 9a631b23..eb0fc799 100644 --- a/Shared/ca.lproj/Localizable.strings +++ b/Shared/ca.lproj/Localizable.strings @@ -113,7 +113,7 @@ "LIVE" = "EN VIU"; /* Loading stream OSD */ -"Loading streams..." = "S'estan carregant els fluxos..."; +"Loading streams…" = "S'estan carregant els fluxos…"; "Loading..." = "Carregant..."; "Locations" = "Ubicacions"; "Lock portrait mode" = "Bloqueja el mode vertical"; @@ -147,7 +147,7 @@ "Offtopic in Music Videos" = "Offtopic als vídeos musicals"; "Open \"Playlists\" tab to create new one" = "Obriu la pestanya \"Llistes de reproducció\" per crear-ne una de nova"; "Open Settings" = "Obriu Configuració"; -"Opening audio stream..." = "Obrint la reproducció d'àudio..."; +"Opening audio stream…" = "Obrint la reproducció d'àudio…"; "Orientation" = "Orientació"; /* SponsorBlock category name */ @@ -454,7 +454,7 @@ "Low" = "Baix"; /* Loading stream OSD */ -"Opening %@ stream..." = "S'està obrint %@..."; +"Opening %@ stream…" = "S'està obrint %@…"; "Only when signed in" = "Només quan s'ha iniciat la sessió"; "Password" = "Contrasenya"; "Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "Part d'un vídeo que promociona un producte o servei no relacionat directament amb el creador. El creador rebrà un pagament o compensació en forma de diners o productes gratuïts."; diff --git a/Shared/cs.lproj/Localizable.strings b/Shared/cs.lproj/Localizable.strings index 67b9fbec..226081e8 100644 --- a/Shared/cs.lproj/Localizable.strings +++ b/Shared/cs.lproj/Localizable.strings @@ -166,7 +166,7 @@ "Only when signed in" = "Pouze když přihlášený"; "Open \"Playlists\" tab to create new one" = "Otevřete kartu \"Playlisty\", aby jste vytvořili nový"; "Open Settings" = "Otevřete Nastavení"; -"Opening audio stream..." = "Otevírám audio stream..."; +"Opening audio stream…" = "Otevírám audio stream…"; "Orientation" = "Orientace"; "Password" = "Heslo"; "Pause" = "Pauza"; @@ -401,11 +401,11 @@ "Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Propagace produktu nebo služby, která přímo souvisí s tvůrcem samotným. Obvykle se jedná o zboží nebo zpeněžené platformy."; /* Loading stream OSD */ -"Loading streams..." = "Načítaní streamu..."; +"Loading streams…" = "Načítaní streamu…"; "Matrix Chat" = "Matrix Chat"; /* Loading stream OSD */ -"Opening %@ stream..." = "Otevírám %@ stream..."; +"Opening %@ stream…" = "Otevírám %@ stream…"; /* SponsorBlock category name */ "Outro" = "Zakončení"; @@ -543,7 +543,7 @@ "Available" = "Dostupné"; "Are you sure you want to remove %@ from Favorites?" = "Opravdu chcete odstranit %@ z oblíbených položek?"; "Use system controls with AVPlayer" = "Použití systémových ovládacích prvků s AVPlayerem"; -"Opening file..." = "Otvírání souboru..."; +"Opening file…" = "Otvírání souboru…"; "No videos to show" = "Žádná videa k zobrazení"; "Autoplay next" = "Automaticky přehrát další"; "Inspector" = "Inspektor"; diff --git a/Shared/de.lproj/Localizable.strings b/Shared/de.lproj/Localizable.strings index b417fb2c..25eb13f6 100644 --- a/Shared/de.lproj/Localizable.strings +++ b/Shared/de.lproj/Localizable.strings @@ -182,7 +182,7 @@ "Large" = "Groß"; /* Loading stream OSD */ -"Loading streams..." = "Lädt Streams …"; +"Loading streams…" = "Lädt Streams …"; /* Video duration filter in search */ "Long" = "Lang"; @@ -210,7 +210,7 @@ "Only when signed in" = "Nur wenn Sie eingeloggt sind"; /* Loading stream OSD */ -"Opening %@ stream..." = "Öffne %@-stream …"; +"Opening %@ stream…" = "Öffne %@-Stream …"; "Connection failed" = "Verbindung fehlgeschlagen"; "Continue from %@" = "Ab %@ fortsetzen"; "Contributing" = "Beitragen"; @@ -227,7 +227,7 @@ "I want to ask a question" = "Ich möchte eine Frage stellen"; "If you are interested what's coming in future updates, you can track project Milestones." = "Wenn Sie sich für künftige Updates interessieren, können Sie die Meilensteine des Projekts verfolgen."; "Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Das große Layout ist nicht für alle Geräte geeignet und kann dazu führen, dass die Bedienelemente nicht auf den Bildschirm passen."; -"Opening audio stream..." = "Audiostream wird geöffnet …"; +"Opening audio stream…" = "Audiostream wird geöffnet …"; "Orientation" = "Ausrichtung"; "Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Die Wiedergabeliste \"%@\" wird gelöscht.\nDies kann nicht rückgängig gemacht werden."; "Preferred Formats" = "Bevorzugte Formate"; @@ -569,7 +569,7 @@ "Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen …"; "Opened File" = "Geöffnete Datei"; "File Extension" = "Dateierweiterung"; -"Opening file..." = "Datei öffnen …"; +"Opening file…" = "Datei öffnen …"; "Close video and player on end" = "Video und Player am Ende beenden"; "Use system controls with AVPlayer" = "Systemsteuerung mit AVPlayer verwenden"; "Public account" = "Öffentliches Konto"; diff --git a/Shared/en.lproj/Localizable.strings b/Shared/en.lproj/Localizable.strings index 238e7ecc..19b76835 100644 --- a/Shared/en.lproj/Localizable.strings +++ b/Shared/en.lproj/Localizable.strings @@ -157,7 +157,7 @@ "LIVE" = "LIVE"; /* Loading stream OSD */ -"Loading streams..." = "Loading streams..."; +"Loading streams…" = "Loading streams…"; "Loading..." = "Loading..."; "Locations" = "Locations"; "Lock portrait mode" = "Lock portrait mode"; @@ -202,8 +202,8 @@ "Open Settings" = "Open Settings"; /* Loading stream OSD */ -"Opening %@ stream..." = "Opening %@ stream..."; -"Opening audio stream..." = "Opening audio stream..."; +"Opening %@ stream…" = "Opening %@ stream…"; +"Opening audio stream…" = "Opening audio stream…"; "Orientation" = "Orientation"; /* SponsorBlock category name */ @@ -567,7 +567,7 @@ "Seek" = "Seek"; "Opened File" = "Opened File"; "File Extension" = "File Extension"; -"Opening file..." = "Opening file..."; +"Opening file…" = "Opening file…"; "Public account" = "Public account"; "Your Accounts" = "Your Accounts"; "Browse without account" = "Browse without account"; diff --git a/Shared/es.lproj/Localizable.strings b/Shared/es.lproj/Localizable.strings index a3b88bb7..bd17aa6f 100644 --- a/Shared/es.lproj/Localizable.strings +++ b/Shared/es.lproj/Localizable.strings @@ -138,7 +138,7 @@ /* Video date filter in search */ "Today" = "Hoy"; -"Opening audio stream..." = "Abriendo transmisión de audio..."; +"Opening audio stream…" = "Abriendo transmisión de audio…"; "Open Video" = "Abrir Video"; "I want to ask a question" = "Quiero hacer una pregunta"; "Save history of played videos" = "Guardar historial de videos reproducidos"; @@ -208,7 +208,7 @@ "No documents" = "Sin documentos"; /* Loading stream OSD */ -"Opening %@ stream..." = "Abriendo %@ emisión..."; +"Opening %@ stream…" = "Abriendo %@ emisión…"; "Documents" = "Documentos"; "Thumbnails" = "Miniaturas"; "Password" = "Contraseña"; @@ -265,7 +265,7 @@ "Shuffle" = "Mezclar"; /* Loading stream OSD */ -"Loading streams..." = "Cargando secuencias..."; +"Loading streams…" = "Cargando secuencias…"; "Public Locations" = "Ubicaciones públicas"; "Yattee" = "Yattee"; "No results" = "No hay resultados"; @@ -558,7 +558,7 @@ "Available" = "Disponible"; "Loop one" = "Bucle uno"; "Use system controls with AVPlayer" = "Utilizar los controles del sistema con AVPlayer"; -"Opening file..." = "Abriendo el archivo..."; +"Opening file…" = "Abriendo el archivo…"; "No videos to show" = "No hay vídeos que mostrar"; "Autoplay next" = "Reproducir automáticamente la siguiente"; "Home Settings" = "Ajustes iniciales"; diff --git a/Shared/fa.lproj/Localizable.strings b/Shared/fa.lproj/Localizable.strings index fe18848a..cec396b7 100644 --- a/Shared/fa.lproj/Localizable.strings +++ b/Shared/fa.lproj/Localizable.strings @@ -83,7 +83,7 @@ "Profiles" = "نمایه‌ها"; "New Playlist" = "فهرست پخش جدید"; "Automatic" = "خودکار"; -"Opening file..." = "در حال باز کردن فایل…"; +"Opening file…" = "در حال باز کردن فایل…"; "Add Quality Profile" = "افزودن نمایهٔ کیفیت"; "Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند"; @@ -279,14 +279,14 @@ "Controls" = "کنترل‌ها"; "This URL could not be opened" = "این نشانی باز نمی‌شود"; "Trending" = "پرطرفدار"; -"Opening audio stream..." = "باز کردن استریم صوتی…"; +"Opening audio stream…" = "باز کردن استریم صوتی…"; "Statistics" = "آمار"; "Pause when player is closed" = "پس از بسته شدن پخش‌کننده مکث کن"; "Play All" = "همه را پخش کن"; "Sort: %@" = "ترتیب: %@"; /* Loading stream OSD */ -"Opening %@ stream..." = "باز کردن استریم %@…"; +"Opening %@ stream…" = "باز کردن استریم %@…"; "Next in Queue" = "مورد بعد در صف"; "Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر"; "Rate" = "امتیاز"; @@ -405,7 +405,7 @@ "Info" = "اطلاعات"; /* Loading stream OSD */ -"Loading streams..." = "درحال دریافت استریم…"; +"Loading streams…" = "درحال دریافت استریم…"; "No rotation" = "بدون چرخش"; "Codec" = "کدک (Codec)"; "Startup section" = "بخش آغازین"; diff --git a/Shared/fr.lproj/Localizable.strings b/Shared/fr.lproj/Localizable.strings index 4cff3168..55600add 100644 --- a/Shared/fr.lproj/Localizable.strings +++ b/Shared/fr.lproj/Localizable.strings @@ -282,11 +282,11 @@ "Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Le grand format n'est pas adapté à tous les appareils et son utilisation peut empêcher certains contrôles de s'afficher à l'écran."; /* Loading stream OSD */ -"Loading streams..." = "Chargement des flux..."; +"Loading streams…" = "Chargement des flux…"; "Lock portrait mode" = "Verrouille l'orientation en mode portrait"; "Matrix Channel" = "Salon Matrix"; "Only when signed in" = "Uniquement lorsque vous êtes connecté"; -"Opening audio stream..." = "Ouverture du flux audio…"; +"Opening audio stream…" = "Ouverture du flux audio…"; "Orientation" = "Orientation"; /* SponsorBlock category name */ @@ -424,7 +424,7 @@ "Milestones" = "Étapes"; /* Loading stream OSD */ -"Opening %@ stream..." = "Ouverture du flux %@…"; +"Opening %@ stream…" = "Ouverture du flux %@…"; "Regular size" = "Taille normale"; "Regular Size" = "Taille normale"; "Related" = "En relation"; @@ -567,7 +567,7 @@ "Seek" = "Recherche"; "Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires"; "Opened File" = "Fichier ouvert"; -"Opening file..." = "Ouverture du fichier..."; +"Opening file…" = "Ouverture du fichier…"; "Enter location address to connect..." = "Entrez l'adresse de l'instance pour se connecter..."; "File Extension" = "Extension de fichier"; "Public account" = "Compte publique"; diff --git a/Shared/hi.lproj/Localizable.strings b/Shared/hi.lproj/Localizable.strings index 2d488651..bf4391bc 100644 --- a/Shared/hi.lproj/Localizable.strings +++ b/Shared/hi.lproj/Localizable.strings @@ -84,8 +84,8 @@ "Open Settings" = "सेटिंग खोलें"; /* Loading stream OSD */ -"Opening %@ stream..." = "%@ स्ट्रीम खुल रहा…"; -"Opening audio stream..." = "ऑडियो स्ट्रीम खुल रहा…"; +"Opening %@ stream…" = "%@ स्ट्रीम खुल रहा…"; +"Opening audio stream…" = "ऑडियो स्ट्रीम खुल रहा…"; "If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "यदि आप किसी बग की रिपोर्ट कर रहे हैं, तो सभी प्रासंगिक विवरण शामिल करें (विशेषकर: ऐप संस्करण, प्रयुक्त डिवाइस और सिस्टम संस्करण, पुन: पेश करने के चरण)।"; "Increase rate" = "दर बढ़ाएँ"; "Info" = "जानकारी"; @@ -104,7 +104,7 @@ "LIVE" = "लाइव"; /* Loading stream OSD */ -"Loading streams..." = "स्ट्रीम लोड हो रहें…"; +"Loading streams…" = "स्ट्रीम लोड हो रहें…"; "Loading..." = "लोड हो रहा…"; "Locations" = "स्थान"; "Lock portrait mode" = "पोर्ट्रेट मोड लॉक करें"; diff --git a/Shared/it.lproj/Localizable.strings b/Shared/it.lproj/Localizable.strings index d5f81d56..e6cadeb4 100644 --- a/Shared/it.lproj/Localizable.strings +++ b/Shared/it.lproj/Localizable.strings @@ -158,8 +158,8 @@ "Open Settings" = "Apri Impostazioni"; /* Loading stream OSD */ -"Opening %@ stream..." = "Apertura stream %@..."; -"Opening audio stream..." = "Apertura stream audio..."; +"Opening %@ stream…" = "Apertura stream %@…"; +"Opening audio stream…" = "Apertura stream audio…"; "Orientation" = "Orientamento"; /* SponsorBlock category name */ @@ -170,7 +170,7 @@ "LIVE" = "DIRETTA"; /* Loading stream OSD */ -"Loading streams..." = "Caricamento stream..."; +"Loading streams…" = "Caricamento stream…"; "Loading..." = "Caricamento..."; "Locations" = "Posizioni"; "Low quality" = "Qualità bassa"; @@ -569,7 +569,7 @@ "Enter location address to connect..." = "Inserisci posizione per connetterti..."; "Opened File" = "File aperto"; "File Extension" = "Estensione file"; -"Opening file..." = "Apro file..."; +"Opening file…" = "Apro file…"; "Close video and player on end" = "Chiudi video e riproduttore alla fine"; "Use system controls with AVPlayer" = "Usa controlli di sistema con AVPlayer"; "Public account" = "Account pubblico"; diff --git a/Shared/ja.lproj/Localizable.strings b/Shared/ja.lproj/Localizable.strings index 5d6c0aed..cae9e45b 100644 --- a/Shared/ja.lproj/Localizable.strings +++ b/Shared/ja.lproj/Localizable.strings @@ -87,7 +87,7 @@ "Large" = "大"; /* Loading stream OSD */ -"Loading streams..." = "ストリーム読込中..."; +"Loading streams…" = "ストリーム読込中…"; "Lock portrait mode" = "縦モードをロック"; "LIVE" = "ライブ"; "Locations" = "場所"; @@ -108,10 +108,10 @@ "Offtopic in Music Videos" = "音楽動画の非音楽部分"; "Only when signed in" = "ログイン時のみ"; "Orientation" = "向き"; -"Opening audio stream..." = "音声ストリーム 開始中..."; +"Opening audio stream…" = "音声ストリーム 開始中…"; /* Loading stream OSD */ -"Opening %@ stream..." = "%@ ストリーム 開始中..."; +"Opening %@ stream…" = "%@ ストリーム 開始中…"; /* SponsorBlock category name */ "Outro" = "終了シーン"; @@ -566,7 +566,7 @@ "Show scroll to top button in comments" = "コメント欄に「上に戻る」表示"; "Opened File" = "開いたファイル"; "File Extension" = "ファイル拡張子"; -"Opening file..." = "ファイルを読み込み中..."; +"Opening file…" = "ファイルを読み込み中…"; "Your Accounts" = "アカウントを使用"; "Public account" = "公開アカウント"; "Browse without account" = "アカウントなしで閲覧"; diff --git a/Shared/nb-NO.lproj/Localizable.strings b/Shared/nb-NO.lproj/Localizable.strings index 9bb8a2c7..36711140 100644 --- a/Shared/nb-NO.lproj/Localizable.strings +++ b/Shared/nb-NO.lproj/Localizable.strings @@ -82,7 +82,7 @@ "LIVE" = "Direkte"; /* Loading stream OSD */ -"Loading streams..." = "Laster inn strømmer …"; +"Loading streams…" = "Laster inn strømmer …"; /* Video duration filter in search */ "Long" = "Lang"; @@ -223,7 +223,7 @@ "Rate" = "Takt"; "Not Playing" = "Spiller ikke"; "Open \"Playlists\" tab to create new one" = "Åpne «Spillelister»-fanen for å opprette ny"; -"Opening audio stream..." = "Åpner lydstrøm …"; +"Opening audio stream…" = "Åpner lydstrøm …"; "Password" = "Passord"; "Nothing" = "Ingenting"; "Picture in Picture" = "Bilde-i-bilde"; @@ -303,7 +303,7 @@ "Offtopic in Music Videos" = "Urelaterte ting i musikkvideoer"; /* Loading stream OSD */ -"Opening %@ stream..." = "Åpner %@-strøm …"; +"Opening %@ stream…" = "Åpner %@-strøm …"; "Play in PiP" = "Bilde-i-bilde"; "Pause when entering background" = "Pause ved forsendelse til bakgrunnen"; "Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promoterer noe som har å gjøre med skaperen direkte. Vanligvis effekter eller betalte plattformer."; diff --git a/Shared/pl.lproj/Localizable.strings b/Shared/pl.lproj/Localizable.strings index 06b98461..31fd5614 100644 --- a/Shared/pl.lproj/Localizable.strings +++ b/Shared/pl.lproj/Localizable.strings @@ -157,7 +157,7 @@ "LIVE" = "LIVE"; /* Loading stream OSD */ -"Loading streams..." = "Ładowanie strumieni..."; +"Loading streams…" = "Ładowanie strumieni…"; "Loading..." = "Ładowanie…"; "Locations" = "Lokalizacje"; "Lock portrait mode" = "Zablokuj tryb portretowy"; @@ -202,8 +202,8 @@ "Open Settings" = "Otwórz Ustawienia"; /* Loading stream OSD */ -"Opening %@ stream..." = "Otwieranie strumienia %@…"; -"Opening audio stream..." = "Otwieranie strumienia audio..."; +"Opening %@ stream…" = "Otwieranie strumienia %@…"; +"Opening audio stream…" = "Otwieranie strumienia audio…"; "Orientation" = "Orientacja"; /* SponsorBlock category name */ @@ -570,7 +570,7 @@ "Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć..."; "Opened File" = "Otwarty plik"; "File Extension" = "Rozszerzenie pliku"; -"Opening file..." = "Otwieranie pliku..."; +"Opening file…" = "Otwieranie pliku…"; "Public account" = "Konto publiczne"; "Your Accounts" = "Twoje konta"; "Browse without account" = "Przeglądanie bez konta"; diff --git a/Shared/pt-BR.lproj/Localizable.strings b/Shared/pt-BR.lproj/Localizable.strings index e8619066..731cdfb1 100644 --- a/Shared/pt-BR.lproj/Localizable.strings +++ b/Shared/pt-BR.lproj/Localizable.strings @@ -102,7 +102,7 @@ "Just watched" = "Acabou de assistir"; /* Loading stream OSD */ -"Loading streams..." = "Carregando streams…"; +"Loading streams…" = "Carregando streams…"; "Medium quality" = "Qualidade média"; "No description" = "Sem descrição"; "No Playlists" = "Sem playlists"; @@ -114,8 +114,8 @@ "Open Settings" = "Abrir Ajustes"; /* Loading stream OSD */ -"Opening %@ stream..." = "Abrindo stream %@…"; -"Opening audio stream..." = "Abrindo stream de áudio…"; +"Opening %@ stream…" = "Abrindo stream %@…"; +"Opening audio stream…" = "Abrindo stream de áudio…"; "Orientation" = "Orientação"; /* SponsorBlock category name */ @@ -569,7 +569,7 @@ "Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…"; "Opened File" = "Arquivo Aberto"; "File Extension" = "Extensão do Arquivo"; -"Opening file..." = "Abrindo arquivo…"; +"Opening file…" = "Abrindo arquivo…"; "Browse without account" = "Navegar sem uma conta"; "Rotate when entering fullscreen on landscape video" = "Girar quando entrar no modo tela cheia em vídeo em paisagem"; "Landscape left" = "Paisagem à esquerda"; diff --git a/Shared/pt.lproj/Localizable.strings b/Shared/pt.lproj/Localizable.strings index de717ddb..dadde209 100644 --- a/Shared/pt.lproj/Localizable.strings +++ b/Shared/pt.lproj/Localizable.strings @@ -196,7 +196,7 @@ "LIVE" = "AO VIVO"; /* Loading stream OSD */ -"Loading streams..." = "Carregando streams…"; +"Loading streams…" = "Carregando streams…"; "Loading..." = "Carregando…"; "Locations" = "Localizações"; "Lock portrait mode" = "Travar modo retrato"; @@ -237,8 +237,8 @@ "Open Settings" = "Abrir Ajustes"; /* Loading stream OSD */ -"Opening %@ stream..." = "Abrindo stream %@…"; -"Opening audio stream..." = "Abrindo stream de áudio…"; +"Opening %@ stream…" = "Abrindo stream %@…"; +"Opening audio stream…" = "Abrindo stream de áudio…"; "Orientation" = "Orientação"; /* SponsorBlock category name */ @@ -557,7 +557,7 @@ "Keep channels with unwatched videos on top of subscriptions list" = "Manter canais com vídeos não vistos no topo da lista de inscrições"; "Opened File" = "Ficheiro Aberto"; "File Extension" = "Extensão do Ficheiro"; -"Opening file..." = "A abrir ficheiro…"; +"Opening file…" = "A abrir ficheiro…"; "Close video and player on end" = "Fechar vídeo e player ao final"; "Use system controls with AVPlayer" = "Usar controles do sistema com o AVPlayer"; "Public account" = "Conta pública"; diff --git a/Shared/ro.lproj/Localizable.strings b/Shared/ro.lproj/Localizable.strings index d2075517..68f53326 100644 --- a/Shared/ro.lproj/Localizable.strings +++ b/Shared/ro.lproj/Localizable.strings @@ -89,7 +89,7 @@ "LIVE" = "LIVE"; /* Loading stream OSD */ -"Loading streams..." = "Se încarcă fluxurile..."; +"Loading streams…" = "Se încarcă fluxurile…"; "Locations" = "Locații"; "Mark watched videos with" = "Marcați videoclipurile vizionate cu"; "Matrix Channel" = "Canal Matrix"; @@ -100,7 +100,7 @@ "Nothing" = "Nimic"; /* Loading stream OSD */ -"Opening %@ stream..." = "Se deschide %@ flux..."; +"Opening %@ stream…" = "Se deschide %@ flux…"; "Play Last" = "Reda ultimul"; "Player" = "Player"; "Playlist" = "Playlist"; @@ -243,7 +243,7 @@ /* SponsorBlock category name */ "Outro" = "Outro"; "Orientation" = "Orientare"; -"Opening audio stream..." = "Se deschide fluxul audio..."; +"Opening audio stream…" = "Se deschide fluxul audio…"; "Password" = "Parolă"; "Pause" = "Pauză"; "Pause when entering background" = "Pauză când intrați în fundal"; @@ -568,7 +568,7 @@ "Enter account credentials to connect..." = "Introduceți acreditările contului pentru a vă conecta..."; "Enter location address to connect..." = "Introdu adresa locației pentru a te conecta..."; "Opened File" = "Fișier deschis"; -"Opening file..." = "Deschiderea fișierului..."; +"Opening file…" = "Deschiderea fișierului…"; "File Extension" = "Extensie fișier"; "Use system controls with AVPlayer" = "Utilizați controalele de sistem cu AVPlayer"; "Rotate when entering fullscreen on landscape video" = "Rotiți când intrați pe ecran complet în videoclipul peisaj"; diff --git a/Shared/ru.lproj/Localizable.strings b/Shared/ru.lproj/Localizable.strings index cac838fc..0e988f49 100644 --- a/Shared/ru.lproj/Localizable.strings +++ b/Shared/ru.lproj/Localizable.strings @@ -370,7 +370,7 @@ "LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ"; /* Loading stream OSD */ -"Loading streams..." = "Загрузка прямой трансляции..."; +"Loading streams…" = "Загрузка прямой трансляции…"; "Loading..." = "Загрузка..."; "Lock portrait mode" = "Блокировка портретного режима"; "Low" = "Низкое"; @@ -405,8 +405,8 @@ "Open Settings" = "Отрыть настройки"; /* Loading stream OSD */ -"Opening %@ stream..." = "Открытие %@ прямой трансляции..."; -"Opening audio stream..." = "Открытие прямой трансляции аудио..."; +"Opening %@ stream…" = "Открытие %@ прямой трансляции…"; +"Opening audio stream…" = "Открытие прямой трансляции аудио…"; "Orientation" = "Ориентация"; /* SponsorBlock category name */ @@ -591,7 +591,7 @@ "Actions buttons" = "Кнопки действия"; "Show sidebar" = "Показать боковую панель"; "Browse without account" = "Искать без аккаунта"; -"Opening file..." = "Отрытие файла..."; +"Opening file…" = "Отрытие файла…"; "Public account" = "Публичный аккаунт"; "Your Accounts" = "Ваши аккаунты"; "Close video and player on end" = "Закрыть видео и плеер в конце"; diff --git a/Shared/tr.lproj/Localizable.strings b/Shared/tr.lproj/Localizable.strings index 71727197..595c7f77 100644 --- a/Shared/tr.lproj/Localizable.strings +++ b/Shared/tr.lproj/Localizable.strings @@ -110,7 +110,7 @@ "I want to ask a question" = "Bir soru sormak istiyorum"; /* Loading stream OSD */ -"Loading streams..." = "Akışlar yükleniyor..."; +"Loading streams…" = "Akışlar yükleniyor…"; "Edit Quality Profile" = "Kalite Profilini Düzenle"; "Frontend URL" = "Ön uç URL'si"; "Close player when starting PiP" = "Resim içinde Resim modu başlatılırken oynatıcıyı kapat"; @@ -227,7 +227,7 @@ "No description" = "Açıklama yok"; "Normal" = "Normal"; "Open \"Playlists\" tab to create new one" = "Yeni bir tane oluşturmak için \"Oynatma Listeleri\" sekmesini açın"; -"Opening audio stream..." = "Ses akışı açılıyor..."; +"Opening audio stream…" = "Ses akışı açılıyor…"; "Rate" = "Derecelendir"; "Orientation" = "Yönlendirme"; "No results" = "Sonuç yok"; @@ -396,7 +396,7 @@ "Open" = "Aç"; /* Loading stream OSD */ -"Opening %@ stream..." = "%@ akışı açılıyor..."; +"Opening %@ stream…" = "%@ akışı açılıyor…"; "Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle"; "Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala"; "Show Inspector" = "Denetleyiciyi Göster"; diff --git a/Shared/uk.lproj/Localizable.strings b/Shared/uk.lproj/Localizable.strings index 64dc08b2..2e45179a 100644 --- a/Shared/uk.lproj/Localizable.strings +++ b/Shared/uk.lproj/Localizable.strings @@ -259,7 +259,7 @@ "LIVE" = "В ЕФІРІ"; /* Loading stream OSD */ -"Loading streams..." = "Завантаження трансляції..."; +"Loading streams…" = "Завантаження трансляції…"; "Loading..." = "Завантаження..."; "Locations" = "Локації"; "Lock portrait mode" = "Заблокувати портретний режим"; @@ -298,8 +298,8 @@ "Open Settings" = "Відрити налаштування"; /* Loading stream OSD */ -"Opening %@ stream..." = "Запуск трансляції %@..."; -"Opening audio stream..." = "Запуск аудіо трансляції..."; +"Opening %@ stream…" = "Запуск трансляції %@…"; +"Opening audio stream…" = "Запуск аудіо трансляції…"; "Orientation" = "Орієнтація"; /* SponsorBlock category name */ diff --git a/Shared/zh-Hans.lproj/Localizable.strings b/Shared/zh-Hans.lproj/Localizable.strings index b623ab06..ff65799d 100644 --- a/Shared/zh-Hans.lproj/Localizable.strings +++ b/Shared/zh-Hans.lproj/Localizable.strings @@ -152,7 +152,7 @@ "Loading..." = "加载中..."; /* Loading stream OSD */ -"Loading streams..." = "加载流中..."; +"Loading streams…" = "加载流中…"; "Locations" = "地址"; "Lock portrait mode" = "锁定竖屏模式"; @@ -191,8 +191,8 @@ "Open Settings" = "打开设置"; /* Loading stream OSD */ -"Opening %@ stream..." = "正在打开 %@ 的流..."; -"Opening audio stream..." = "正在打开音频流..."; +"Opening %@ stream…" = "正在打开 %@ 的流…"; +"Opening audio stream…" = "正在打开音频流…"; "Orientation" = "方向"; /* SponsorBlock category name */ @@ -530,7 +530,7 @@ "Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮"; "Opened File" = "打开的文件"; "File Extension" = "文件扩展"; -"Opening file..." = "打开文件中..."; +"Opening file…" = "打开文件中…"; "Single tap gesture" = "单击手势"; "Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单"; "Show unwatched feed badges" = "显示未观看的 Feed 标志"; diff --git a/Shared/zh-Hant.lproj/Localizable.strings b/Shared/zh-Hant.lproj/Localizable.strings index 3c75cd3b..80121096 100644 --- a/Shared/zh-Hant.lproj/Localizable.strings +++ b/Shared/zh-Hant.lproj/Localizable.strings @@ -252,7 +252,7 @@ "Interface" = "介面"; /* Loading stream OSD */ -"Loading streams..." = "加載中..."; +"Loading streams…" = "加載中…"; "Loading..." = "加載中..."; "Locations" = "地址"; "Lock portrait mode" = "鎖定直屏"; @@ -292,8 +292,8 @@ "Open Settings" = "打開設置"; /* Loading stream OSD */ -"Opening %@ stream..." = "正在打開 %@ ..."; -"Opening audio stream..." = "正在打開音訊..."; +"Opening %@ stream…" = "正在打開 %@ …"; +"Opening audio stream…" = "正在打開音訊…"; /* SponsorBlock category name */ "Outro" = "結尾"; @@ -554,7 +554,7 @@ "Queue - shuffled" = "隊列 - 隨機"; "Loop one" = "單個循環"; "File Extension" = "副檔名"; -"Opening file..." = "正在打開文件..."; +"Opening file…" = "正在打開文件…"; "Public account" = "公共帳戶"; "Enter account credentials to connect..." = "輸入帳號密碼來連接..."; "Enter location address to connect..." = "輸入站台地址來連接..."; diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 5813c64d..ef5e7146 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -1070,6 +1070,12 @@ 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 */; }; FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; }; /* End PBXBuildFile section */ @@ -1539,6 +1545,8 @@ 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 */ /* Begin PBXFrameworksBuildPhase section */ @@ -2286,6 +2294,7 @@ 372915E52687E3B900F5A35B /* Defaults.swift */, 37D2E0D328B67EFC00F64D52 /* Delay.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, + E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */, 375B537828DF6CBB004C1D19 /* Localizable.strings */, 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */, @@ -2293,6 +2302,7 @@ 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, 37FFC43F272734C3009FFD26 /* Throttle.swift */, 378FFBC328660172009E3FBE /* URLParser.swift */, + E258F3892BF61BD2005B8C28 /* URLTester.swift */, 37D4B0C22671614700C925CA /* YatteeApp.swift */, 37D4B0C42671614800C925CA /* Assets.xcassets */, 37BD07C42698ADEE003EBB87 /* Yattee.entitlements */, @@ -3115,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 */, @@ -3138,6 +3149,7 @@ 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, 3752069D285E910600CA655F /* ChapterView.swift in Sources */, 375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */, + E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */, 3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */, 37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */, 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */, @@ -3587,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 */, @@ -3621,6 +3634,7 @@ 378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */, 370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */, 37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */, + E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */, 370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */, 3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 37FB28422721B22200A57617 /* ContentItem.swift in Sources */, @@ -3866,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 */, @@ -3908,6 +3923,7 @@ 37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */, 37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */, 37FB2860272225E800A57617 /* ContentItemView.swift in Sources */, + E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */, 37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */, 374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */, 374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,