Conditional proxying

I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.

This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.

Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.

This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
This commit is contained in:
Toni Förster 2024-05-09 20:07:55 +02:00
parent 1fe8a32fb8
commit 6eba2a45c8
No known key found for this signature in database
GPG Key ID: 292F3E5086C83FC7
39 changed files with 434 additions and 126 deletions

View File

@ -655,7 +655,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
kind: .adaptive, kind: .adaptive,
encoding: videoStream["encoding"].string, encoding: videoStream["encoding"].string,
videoFormat: videoStream["type"].string, videoFormat: videoStream["type"].string,
bitrate: videoStream["bitrate"].int bitrate: videoStream["bitrate"].int,
requestRange: videoStream["init"].string ?? videoStream["index"].string
) )
} }
} }

View File

@ -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? { private func extractVideo(from content: JSON) -> Video? {
let details = content.dictionaryValue let details = content.dictionaryValue
@ -579,10 +608,11 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
return nil return nil
} }
return URL(string: thumbnailURL return URL(
.absoluteString string: thumbnailURL
.replacingOccurrences(of: "hqdefault", with: quality.filename) .absoluteString
.replacingOccurrences(of: "maxresdefault", with: quality.filename) .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 resolution = Stream.Resolution.from(resolution: quality, fps: fps)
let videoFormat = videoStream.dictionaryValue["format"]?.string let videoFormat = videoStream.dictionaryValue["format"]?.string
let bitrate = videoStream.dictionaryValue["bitrate"]?.int 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 { if videoOnly {
streams.append( streams.append(
@ -698,7 +741,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
resolution: resolution, resolution: resolution,
kind: .adaptive, kind: .adaptive,
videoFormat: videoFormat, videoFormat: videoFormat,
bitrate: bitrate bitrate: bitrate,
requestRange: requestRange
) )
) )
} else { } else {

View File

@ -95,7 +95,7 @@ enum VideosApp: String, CaseIterable {
} }
var allowsDisablingVidoesProxying: Bool { var allowsDisablingVidoesProxying: Bool {
self == .invidious self == .invidious || self == .piped
} }
var supportsOpeningVideosByID: Bool { var supportsOpeningVideosByID: Bool {

View File

@ -26,7 +26,7 @@ final class NetworkStateModel: ObservableObject {
} }
var bufferingStateText: String? { var bufferingStateText: String? {
guard detailsAvailable else { return nil } guard detailsAvailable && player.hasStarted else { return nil }
return String(format: "%.0f%%", bufferingState) return String(format: "%.0f%%", bufferingState)
} }

View File

@ -40,6 +40,11 @@ final class AVPlayerBackend: PlayerBackend {
var isLoadingVideo = false var isLoadingVideo = false
var hasStarted = false
var isPaused: Bool {
avPlayer.timeControlStatus == .paused
}
var isPlaying: Bool { var isPlaying: Bool {
avPlayer.timeControlStatus == .playing avPlayer.timeControlStatus == .playing
} }
@ -158,6 +163,12 @@ final class AVPlayerBackend: PlayerBackend {
} }
avPlayer.play() avPlayer.play()
// Setting hasStarted to true the first time player started
if !hasStarted {
hasStarted = true
}
model.objectWillChange.send() model.objectWillChange.send()
} }
@ -180,6 +191,7 @@ final class AVPlayerBackend: PlayerBackend {
func stop() { func stop() {
avPlayer.replaceCurrentItem(with: nil) avPlayer.replaceCurrentItem(with: nil)
hasStarted = false
} }
func cancelLoads() { func cancelLoads() {

View File

@ -44,6 +44,8 @@ final class MPVBackend: PlayerBackend {
} }
}} }}
var hasStarted = false
var isPaused = false
var isPlaying = true { didSet { var isPlaying = true { didSet {
networkStateTimer.start() networkStateTimer.start()
@ -337,7 +339,6 @@ final class MPVBackend: PlayerBackend {
} }
func play() { func play() {
isPlaying = true
startClientUpdates() startClientUpdates()
if controls.presentingControls { if controls.presentingControls {
@ -354,13 +355,22 @@ final class MPVBackend: PlayerBackend {
} }
client?.play() client?.play()
isPlaying = true
isPaused = false
// Setting hasStarted to true the first time player started
if !hasStarted {
hasStarted = true
}
} }
func pause() { func pause() {
isPlaying = false
stopClientUpdates() stopClientUpdates()
client?.pause() client?.pause()
isPaused = true
isPlaying = false
} }
func togglePlay() { func togglePlay() {
@ -377,6 +387,9 @@ final class MPVBackend: PlayerBackend {
func stop() { func stop() {
client?.stop() client?.stop()
isPlaying = false
isPaused = false
hasStarted = false
} }
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) { func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
@ -392,8 +405,8 @@ final class MPVBackend: PlayerBackend {
} }
func closeItem() { func closeItem() {
client?.pause() pause()
client?.stop() stop()
self.video = nil self.video = nil
self.stream = nil self.stream = nil
} }

View File

@ -19,6 +19,8 @@ protocol PlayerBackend {
var loadedVideo: Bool { get } var loadedVideo: Bool { get }
var isLoadingVideo: Bool { get } var isLoadingVideo: Bool { get }
var hasStarted: Bool { get }
var isPaused: Bool { get }
var isPlaying: Bool { get } var isPlaying: Bool { get }
var isSeeking: Bool { get } var isSeeking: Bool { get }
var playerItemDuration: CMTime? { get } var playerItemDuration: CMTime? { get }

View File

@ -298,6 +298,14 @@ final class PlayerModel: ObservableObject {
backend.isPlaying backend.isPlaying
} }
var isPaused: Bool {
backend.isPaused
}
var hasStarted: Bool {
backend.hasStarted
}
var playerItemDuration: CMTime? { var playerItemDuration: CMTime? {
guard !currentItem.isNil else { guard !currentItem.isNil else {
return nil return nil

View File

@ -74,7 +74,7 @@ extension PlayerModel {
preservedTime = currentItem.playbackTime preservedTime = currentItem.playbackTime
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { return } guard let self = self else { return }
guard let video = item.video else { guard let video = item.video else {
return return
} }
@ -94,7 +94,9 @@ extension PlayerModel {
} }
} else { } else {
self.videoBeingOpened = nil self.videoBeingOpened = nil
self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams) self.streamsWithInstance(instance: playerInstance, streams: video.streams) { processedStreams in
self.availableStreams = processedStreams
}
} }
} }
} }

View File

@ -1,3 +1,4 @@
import AVFoundation
import Foundation import Foundation
import Siesta import Siesta
import SwiftUI import SwiftUI
@ -41,7 +42,9 @@ extension PlayerModel {
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed") self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
return return
} }
self.availableStreams = self.streamsWithInstance(instance: instance, streams: video.streams) self.streamsWithInstance(instance: instance, streams: video.streams) { processedStreams in
self.availableStreams = processedStreams
}
} else { } else {
self.logger.critical("no streams available from \(instance.description)") self.logger.critical("no streams available from \(instance.description)")
} }
@ -53,20 +56,127 @@ extension PlayerModel {
} }
} }
func streamsWithInstance(instance: Instance, streams: [Stream]) -> [Stream] { func streamsWithInstance(instance _: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) {
streams.map { stream in let forbiddenAssetTestGroup = DispatchGroup()
stream.instance = instance var hasForbiddenAsset = false
if instance.app == .invidious, instance.proxiesVideos { let (nonHLSAssets, hlsURLs) = getAssets(from: streams)
if let audio = stream.audioAsset {
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio) if let randomStream = nonHLSAssets.randomElement() {
} let instance = randomStream.0
if let video = stream.videoAsset { let asset = randomStream.1
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video) let url = randomStream.2
let requestRange = randomStream.3
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
}
} 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)
return stream 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
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 {
forbiddenAssetTestGroup.enter()
PipedAPI.nonProxiedAsset(url: hlsURL) { nonProxiedURL in
if let nonProxiedURL = nonProxiedURL {
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
}
forbiddenAssetTestGroup.notify(queue: .main) {
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) {
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)
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 {
self.testAsset(url: nonProxiedAsset.url, range: requestRange, isHLS: isHLS, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: completion)
}
} }
} }

View File

@ -170,6 +170,7 @@ class Stream: Equatable, Hashable, Identifiable {
var encoding: String? var encoding: String?
var videoFormat: String? var videoFormat: String?
var bitrate: Int? var bitrate: Int?
var requestRange: String?
init( init(
instance: Instance? = nil, instance: Instance? = nil,
@ -181,7 +182,8 @@ class Stream: Equatable, Hashable, Identifiable {
kind: Kind = .hls, kind: Kind = .hls,
encoding: String? = nil, encoding: String? = nil,
videoFormat: String? = nil, videoFormat: String? = nil,
bitrate: Int? = nil bitrate: Int? = nil,
requestRange: String? = nil
) { ) {
self.instance = instance self.instance = instance
self.audioAsset = audioAsset self.audioAsset = audioAsset
@ -193,6 +195,7 @@ class Stream: Equatable, Hashable, Identifiable {
self.encoding = encoding self.encoding = encoding
format = .from(videoFormat ?? "") format = .from(videoFormat ?? "")
self.bitrate = bitrate self.bitrate = bitrate
self.requestRange = requestRange
} }
var isLocal: Bool { var isLocal: Bool {

View File

@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.78Z5H3M6RJ.stream.yattee.app.urlbookmarks</string> <string>group.stonerl.yattee.app.url</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -10,26 +10,28 @@ struct OpeningStream: View {
} }
var visible: Bool { 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 { var reason: String {
guard player.videoBeingOpened == nil else { guard player.videoBeingOpened == nil else {
return "Loading streams...".localized() return "Loading streams".localized()
} }
if player.musicMode { if player.musicMode {
return "Opening audio stream...".localized() return "Opening audio stream".localized()
} }
if let selection = player.streamSelection { if let selection = player.streamSelection {
if selection.isLocal { 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? { var state: String? {

View File

@ -75,16 +75,20 @@ struct PlayerControls: View {
} }
VStack { VStack {
Spacer()
ZStack { ZStack {
VStack(spacing: 0) { GeometryReader { geometry in
ZStack { VStack(spacing: 0) {
OpeningStream() ZStack {
NetworkState() OpeningStream()
NetworkState()
}
} }
.position(
Spacer() x: geometry.size.width / 2,
y: geometry.size.height / 2
)
} }
.offset(y: playerControlsLayout.osdVerticalOffset + 5)
if showControls { if showControls {
Section { Section {

View File

@ -278,10 +278,6 @@ enum PlayerControlsLayout: String, CaseIterable, Defaults.Serializable {
} }
} }
var osdVerticalOffset: Double {
buttonSize
}
var osdProgressBarHeight: Double { var osdProgressBarHeight: Double {
switch self { switch self {
case .tvRegular: case .tvRegular:

103
Shared/URLTester.swift Normal file
View File

@ -0,0 +1,103 @@
import Foundation
import Logging
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 {
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 ?? HTTPStatusForbidden
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)
}
}
}
}
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
guard let data = data,
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()
}
}

View File

@ -129,7 +129,7 @@
"LIVE" = "مباشر"; "LIVE" = "مباشر";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "تحميل بثوث..."; "Loading streams…" = "تحميل بثوث…";
"Loading..." = "تحميل..."; "Loading..." = "تحميل...";
/* Video duration filter in search */ /* Video duration filter in search */
@ -163,8 +163,8 @@
"Open Settings" = "فتح الإعدادات"; "Open Settings" = "فتح الإعدادات";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "فتح بث %@ ..."; "Opening %@ stream…" = "فتح بث %@ …";
"Opening audio stream..." = "فتح بث صوتي..."; "Opening audio stream…" = "فتح بث صوتي…";
"Orientation" = "اتجاه"; "Orientation" = "اتجاه";
"Play in PiP" = "تشغيل في الفيديو المصغر"; "Play in PiP" = "تشغيل في الفيديو المصغر";
"Play Last" = "تشغيل الأخير"; "Play Last" = "تشغيل الأخير";
@ -558,7 +558,7 @@
"Are you sure you want to clear cache?" = "هل أنت متأكد من أنك تريد مسح ذاكرة التخزين المؤقت؟"; "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 أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل."; "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" = "ملف مفتوح"; "Opened File" = "ملف مفتوح";
"Opening file..." = "فتح الملف..."; "Opening file…" = "فتح الملف…";
"Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية"; "Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية";
"Mark channel feed as watched" = "وضع علامة تمت المشاهدة على محتوى القناة"; "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." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل."; "Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";

View File

@ -277,11 +277,11 @@
"Large" = "Böyük"; "Large" = "Böyük";
/* Loading stream OSD */ /* 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"; "Only when signed in" = "Yalnız daxil olduqda";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "%@ yayımıılır..."; "Opening %@ stream…" = "%@ yayımıılır…";
"Matrix Chat" = "Matrix Söhbət"; "Matrix Chat" = "Matrix Söhbət";
/* Player controls layout size */ /* Player controls layout size */
@ -296,7 +296,7 @@
"Open Settings" = "Tənzimləmələri Aç"; "Open Settings" = "Tənzimləmələri Aç";
"Movies" = "Filmlər"; "Movies" = "Filmlər";
"No description" = "Açıqlama yoxdur"; "No description" = "Açıqlama yoxdur";
"Opening audio stream..." = "Səs yayımıılır..."; "Opening audio stream…" = "Səs yayımıılır…";
"Password" = "Şifrə"; "Password" = "Şifrə";
"Preferred Formats" = "Üstünlük Verilən Formatlar"; "Preferred Formats" = "Üstünlük Verilən Formatlar";
"Quality Profile" = "Profil Keyfiyyəti"; "Quality Profile" = "Profil Keyfiyyəti";

View File

@ -113,7 +113,7 @@
"LIVE" = "EN VIU"; "LIVE" = "EN VIU";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "S'estan carregant els fluxos..."; "Loading streams…" = "S'estan carregant els fluxos…";
"Loading..." = "Carregant..."; "Loading..." = "Carregant...";
"Locations" = "Ubicacions"; "Locations" = "Ubicacions";
"Lock portrait mode" = "Bloqueja el mode vertical"; "Lock portrait mode" = "Bloqueja el mode vertical";
@ -147,7 +147,7 @@
"Offtopic in Music Videos" = "Offtopic als vídeos musicals"; "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 \"Playlists\" tab to create new one" = "Obriu la pestanya \"Llistes de reproducció\" per crear-ne una de nova";
"Open Settings" = "Obriu Configuració"; "Open Settings" = "Obriu Configuració";
"Opening audio stream..." = "Obrint la reproducció d'àudio..."; "Opening audio stream…" = "Obrint la reproducció d'àudio…";
"Orientation" = "Orientació"; "Orientation" = "Orientació";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -454,7 +454,7 @@
"Low" = "Baix"; "Low" = "Baix";
/* Loading stream OSD */ /* 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ó"; "Only when signed in" = "Només quan s'ha iniciat la sessió";
"Password" = "Contrasenya"; "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."; "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.";

View File

@ -166,7 +166,7 @@
"Only when signed in" = "Pouze když přihlášený"; "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 \"Playlists\" tab to create new one" = "Otevřete kartu \"Playlisty\", aby jste vytvořili nový";
"Open Settings" = "Otevřete Nastavení"; "Open Settings" = "Otevřete Nastavení";
"Opening audio stream..." = "Otevírám audio stream..."; "Opening audio stream…" = "Otevírám audio stream…";
"Orientation" = "Orientace"; "Orientation" = "Orientace";
"Password" = "Heslo"; "Password" = "Heslo";
"Pause" = "Pauza"; "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."; "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 stream OSD */
"Loading streams..." = "Načítaní streamu..."; "Loading streams…" = "Načítaní streamu…";
"Matrix Chat" = "Matrix Chat"; "Matrix Chat" = "Matrix Chat";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Otevírám %@ stream..."; "Opening %@ stream…" = "Otevírám %@ stream…";
/* SponsorBlock category name */ /* SponsorBlock category name */
"Outro" = "Zakončení"; "Outro" = "Zakončení";
@ -543,7 +543,7 @@
"Available" = "Dostupné"; "Available" = "Dostupné";
"Are you sure you want to remove %@ from Favorites?" = "Opravdu chcete odstranit %@ z oblíbených položek?"; "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"; "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í"; "No videos to show" = "Žádná videa k zobrazení";
"Autoplay next" = "Automaticky přehrát další"; "Autoplay next" = "Automaticky přehrát další";
"Inspector" = "Inspektor"; "Inspector" = "Inspektor";

View File

@ -182,7 +182,7 @@
"Large" = "Groß"; "Large" = "Groß";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Lädt Streams …"; "Loading streams" = "Lädt Streams …";
/* Video duration filter in search */ /* Video duration filter in search */
"Long" = "Lang"; "Long" = "Lang";
@ -210,7 +210,7 @@
"Only when signed in" = "Nur wenn Sie eingeloggt sind"; "Only when signed in" = "Nur wenn Sie eingeloggt sind";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Öffne %@-stream …"; "Opening %@ stream…" = "Öffne %@-Stream …";
"Connection failed" = "Verbindung fehlgeschlagen"; "Connection failed" = "Verbindung fehlgeschlagen";
"Continue from %@" = "Ab %@ fortsetzen"; "Continue from %@" = "Ab %@ fortsetzen";
"Contributing" = "Beitragen"; "Contributing" = "Beitragen";
@ -227,7 +227,7 @@
"I want to ask a question" = "Ich möchte eine Frage stellen"; "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."; "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."; "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"; "Orientation" = "Ausrichtung";
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Die Wiedergabeliste \"%@\" wird gelöscht.\nDies kann nicht rückgängig gemacht werden."; "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"; "Preferred Formats" = "Bevorzugte Formate";
@ -569,7 +569,7 @@
"Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen …"; "Enter location address to connect..." = "Geben Sie die Internetadresse ein, um eine Verbindung herzustellen …";
"Opened File" = "Geöffnete Datei"; "Opened File" = "Geöffnete Datei";
"File Extension" = "Dateierweiterung"; "File Extension" = "Dateierweiterung";
"Opening file..." = "Datei öffnen …"; "Opening file" = "Datei öffnen …";
"Close video and player on end" = "Video und Player am Ende beenden"; "Close video and player on end" = "Video und Player am Ende beenden";
"Use system controls with AVPlayer" = "Systemsteuerung mit AVPlayer verwenden"; "Use system controls with AVPlayer" = "Systemsteuerung mit AVPlayer verwenden";
"Public account" = "Öffentliches Konto"; "Public account" = "Öffentliches Konto";

View File

@ -157,7 +157,7 @@
"LIVE" = "LIVE"; "LIVE" = "LIVE";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Loading streams..."; "Loading streams…" = "Loading streams…";
"Loading..." = "Loading..."; "Loading..." = "Loading...";
"Locations" = "Locations"; "Locations" = "Locations";
"Lock portrait mode" = "Lock portrait mode"; "Lock portrait mode" = "Lock portrait mode";
@ -202,8 +202,8 @@
"Open Settings" = "Open Settings"; "Open Settings" = "Open Settings";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Opening %@ stream..."; "Opening %@ stream…" = "Opening %@ stream…";
"Opening audio stream..." = "Opening audio stream..."; "Opening audio stream…" = "Opening audio stream…";
"Orientation" = "Orientation"; "Orientation" = "Orientation";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -567,7 +567,7 @@
"Seek" = "Seek"; "Seek" = "Seek";
"Opened File" = "Opened File"; "Opened File" = "Opened File";
"File Extension" = "File Extension"; "File Extension" = "File Extension";
"Opening file..." = "Opening file..."; "Opening file…" = "Opening file…";
"Public account" = "Public account"; "Public account" = "Public account";
"Your Accounts" = "Your Accounts"; "Your Accounts" = "Your Accounts";
"Browse without account" = "Browse without account"; "Browse without account" = "Browse without account";

View File

@ -138,7 +138,7 @@
/* Video date filter in search */ /* Video date filter in search */
"Today" = "Hoy"; "Today" = "Hoy";
"Opening audio stream..." = "Abriendo transmisión de audio..."; "Opening audio stream…" = "Abriendo transmisión de audio…";
"Open Video" = "Abrir Video"; "Open Video" = "Abrir Video";
"I want to ask a question" = "Quiero hacer una pregunta"; "I want to ask a question" = "Quiero hacer una pregunta";
"Save history of played videos" = "Guardar historial de videos reproducidos"; "Save history of played videos" = "Guardar historial de videos reproducidos";
@ -208,7 +208,7 @@
"No documents" = "Sin documentos"; "No documents" = "Sin documentos";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Abriendo %@ emisión..."; "Opening %@ stream…" = "Abriendo %@ emisión…";
"Documents" = "Documentos"; "Documents" = "Documentos";
"Thumbnails" = "Miniaturas"; "Thumbnails" = "Miniaturas";
"Password" = "Contraseña"; "Password" = "Contraseña";
@ -265,7 +265,7 @@
"Shuffle" = "Mezclar"; "Shuffle" = "Mezclar";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Cargando secuencias..."; "Loading streams…" = "Cargando secuencias…";
"Public Locations" = "Ubicaciones públicas"; "Public Locations" = "Ubicaciones públicas";
"Yattee" = "Yattee"; "Yattee" = "Yattee";
"No results" = "No hay resultados"; "No results" = "No hay resultados";
@ -558,7 +558,7 @@
"Available" = "Disponible"; "Available" = "Disponible";
"Loop one" = "Bucle uno"; "Loop one" = "Bucle uno";
"Use system controls with AVPlayer" = "Utilizar los controles del sistema con AVPlayer"; "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"; "No videos to show" = "No hay vídeos que mostrar";
"Autoplay next" = "Reproducir automáticamente la siguiente"; "Autoplay next" = "Reproducir automáticamente la siguiente";
"Home Settings" = "Ajustes iniciales"; "Home Settings" = "Ajustes iniciales";

View File

@ -83,7 +83,7 @@
"Profiles" = "نمایه‌ها"; "Profiles" = "نمایه‌ها";
"New Playlist" = "فهرست پخش جدید"; "New Playlist" = "فهرست پخش جدید";
"Automatic" = "خودکار"; "Automatic" = "خودکار";
"Opening file..." = "در حال باز کردن فایل…"; "Opening file" = "در حال باز کردن فایل…";
"Add Quality Profile" = "افزودن نمایهٔ کیفیت"; "Add Quality Profile" = "افزودن نمایهٔ کیفیت";
"Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند"; "Close video after playing last in the queue" = "ویدیو را پس از پخش آخرین مورد فهرست ببند";
@ -279,14 +279,14 @@
"Controls" = "کنترل‌ها"; "Controls" = "کنترل‌ها";
"This URL could not be opened" = "این نشانی باز نمی‌شود"; "This URL could not be opened" = "این نشانی باز نمی‌شود";
"Trending" = "پرطرفدار"; "Trending" = "پرطرفدار";
"Opening audio stream..." = "باز کردن استریم صوتی…"; "Opening audio stream" = "باز کردن استریم صوتی…";
"Statistics" = "آمار"; "Statistics" = "آمار";
"Pause when player is closed" = "پس از بسته شدن پخش‌کننده مکث کن"; "Pause when player is closed" = "پس از بسته شدن پخش‌کننده مکث کن";
"Play All" = "همه را پخش کن"; "Play All" = "همه را پخش کن";
"Sort: %@" = "ترتیب: %@"; "Sort: %@" = "ترتیب: %@";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "باز کردن استریم %@…"; "Opening %@ stream" = "باز کردن استریم %@…";
"Next in Queue" = "مورد بعد در صف"; "Next in Queue" = "مورد بعد در صف";
"Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر"; "Honor orientation lock" = "قفل چرخش صفحه را در نظر بگیر";
"Rate" = "امتیاز"; "Rate" = "امتیاز";
@ -405,7 +405,7 @@
"Info" = "اطلاعات"; "Info" = "اطلاعات";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "درحال دریافت استریم…"; "Loading streams" = "درحال دریافت استریم…";
"No rotation" = "بدون چرخش"; "No rotation" = "بدون چرخش";
"Codec" = "کدک (Codec)"; "Codec" = "کدک (Codec)";
"Startup section" = "بخش آغازین"; "Startup section" = "بخش آغازین";

View File

@ -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."; "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 stream OSD */
"Loading streams..." = "Chargement des flux..."; "Loading streams…" = "Chargement des flux…";
"Lock portrait mode" = "Verrouille l'orientation en mode portrait"; "Lock portrait mode" = "Verrouille l'orientation en mode portrait";
"Matrix Channel" = "Salon Matrix"; "Matrix Channel" = "Salon Matrix";
"Only when signed in" = "Uniquement lorsque vous êtes connecté"; "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"; "Orientation" = "Orientation";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -424,7 +424,7 @@
"Milestones" = "Étapes"; "Milestones" = "Étapes";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Ouverture du flux %@…"; "Opening %@ stream" = "Ouverture du flux %@…";
"Regular size" = "Taille normale"; "Regular size" = "Taille normale";
"Regular Size" = "Taille normale"; "Regular Size" = "Taille normale";
"Related" = "En relation"; "Related" = "En relation";
@ -567,7 +567,7 @@
"Seek" = "Recherche"; "Seek" = "Recherche";
"Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires"; "Show scroll to top button in comments" = "Afficher le bouton de retour en haut de la page dans les commentaires";
"Opened File" = "Fichier ouvert"; "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..."; "Enter location address to connect..." = "Entrez l'adresse de l'instance pour se connecter...";
"File Extension" = "Extension de fichier"; "File Extension" = "Extension de fichier";
"Public account" = "Compte publique"; "Public account" = "Compte publique";

View File

@ -84,8 +84,8 @@
"Open Settings" = "सेटिंग खोलें"; "Open Settings" = "सेटिंग खोलें";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "%@ स्ट्रीम खुल रहा…"; "Opening %@ stream" = "%@ स्ट्रीम खुल रहा…";
"Opening audio 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)." = "यदि आप किसी बग की रिपोर्ट कर रहे हैं, तो सभी प्रासंगिक विवरण शामिल करें (विशेषकर: ऐप संस्करण, प्रयुक्त डिवाइस और सिस्टम संस्करण, पुन: पेश करने के चरण)।"; "If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "यदि आप किसी बग की रिपोर्ट कर रहे हैं, तो सभी प्रासंगिक विवरण शामिल करें (विशेषकर: ऐप संस्करण, प्रयुक्त डिवाइस और सिस्टम संस्करण, पुन: पेश करने के चरण)।";
"Increase rate" = "दर बढ़ाएँ"; "Increase rate" = "दर बढ़ाएँ";
"Info" = "जानकारी"; "Info" = "जानकारी";
@ -104,7 +104,7 @@
"LIVE" = "लाइव"; "LIVE" = "लाइव";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "स्ट्रीम लोड हो रहें…"; "Loading streams" = "स्ट्रीम लोड हो रहें…";
"Loading..." = "लोड हो रहा…"; "Loading..." = "लोड हो रहा…";
"Locations" = "स्थान"; "Locations" = "स्थान";
"Lock portrait mode" = "पोर्ट्रेट मोड लॉक करें"; "Lock portrait mode" = "पोर्ट्रेट मोड लॉक करें";

View File

@ -158,8 +158,8 @@
"Open Settings" = "Apri Impostazioni"; "Open Settings" = "Apri Impostazioni";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Apertura stream %@..."; "Opening %@ stream…" = "Apertura stream %@…";
"Opening audio stream..." = "Apertura stream audio..."; "Opening audio stream…" = "Apertura stream audio…";
"Orientation" = "Orientamento"; "Orientation" = "Orientamento";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -170,7 +170,7 @@
"LIVE" = "DIRETTA"; "LIVE" = "DIRETTA";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Caricamento stream..."; "Loading streams…" = "Caricamento stream…";
"Loading..." = "Caricamento..."; "Loading..." = "Caricamento...";
"Locations" = "Posizioni"; "Locations" = "Posizioni";
"Low quality" = "Qualità bassa"; "Low quality" = "Qualità bassa";
@ -569,7 +569,7 @@
"Enter location address to connect..." = "Inserisci posizione per connetterti..."; "Enter location address to connect..." = "Inserisci posizione per connetterti...";
"Opened File" = "File aperto"; "Opened File" = "File aperto";
"File Extension" = "Estensione file"; "File Extension" = "Estensione file";
"Opening file..." = "Apro file..."; "Opening file…" = "Apro file…";
"Close video and player on end" = "Chiudi video e riproduttore alla fine"; "Close video and player on end" = "Chiudi video e riproduttore alla fine";
"Use system controls with AVPlayer" = "Usa controlli di sistema con AVPlayer"; "Use system controls with AVPlayer" = "Usa controlli di sistema con AVPlayer";
"Public account" = "Account pubblico"; "Public account" = "Account pubblico";

View File

@ -87,7 +87,7 @@
"Large" = "大"; "Large" = "大";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "ストリーム読込中..."; "Loading streams…" = "ストリーム読込中…";
"Lock portrait mode" = "縦モードをロック"; "Lock portrait mode" = "縦モードをロック";
"LIVE" = "ライブ"; "LIVE" = "ライブ";
"Locations" = "場所"; "Locations" = "場所";
@ -108,10 +108,10 @@
"Offtopic in Music Videos" = "音楽動画の非音楽部分"; "Offtopic in Music Videos" = "音楽動画の非音楽部分";
"Only when signed in" = "ログイン時のみ"; "Only when signed in" = "ログイン時のみ";
"Orientation" = "向き"; "Orientation" = "向き";
"Opening audio stream..." = "音声ストリーム 開始中..."; "Opening audio stream…" = "音声ストリーム 開始中…";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "%@ ストリーム 開始中..."; "Opening %@ stream…" = "%@ ストリーム 開始中…";
/* SponsorBlock category name */ /* SponsorBlock category name */
"Outro" = "終了シーン"; "Outro" = "終了シーン";
@ -566,7 +566,7 @@
"Show scroll to top button in comments" = "コメント欄に「上に戻る」表示"; "Show scroll to top button in comments" = "コメント欄に「上に戻る」表示";
"Opened File" = "開いたファイル"; "Opened File" = "開いたファイル";
"File Extension" = "ファイル拡張子"; "File Extension" = "ファイル拡張子";
"Opening file..." = "ファイルを読み込み中..."; "Opening file…" = "ファイルを読み込み中…";
"Your Accounts" = "アカウントを使用"; "Your Accounts" = "アカウントを使用";
"Public account" = "公開アカウント"; "Public account" = "公開アカウント";
"Browse without account" = "アカウントなしで閲覧"; "Browse without account" = "アカウントなしで閲覧";

View File

@ -82,7 +82,7 @@
"LIVE" = "Direkte"; "LIVE" = "Direkte";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Laster inn strømmer …"; "Loading streams" = "Laster inn strømmer …";
/* Video duration filter in search */ /* Video duration filter in search */
"Long" = "Lang"; "Long" = "Lang";
@ -223,7 +223,7 @@
"Rate" = "Takt"; "Rate" = "Takt";
"Not Playing" = "Spiller ikke"; "Not Playing" = "Spiller ikke";
"Open \"Playlists\" tab to create new one" = "Åpne «Spillelister»-fanen for å opprette ny"; "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"; "Password" = "Passord";
"Nothing" = "Ingenting"; "Nothing" = "Ingenting";
"Picture in Picture" = "Bilde-i-bilde"; "Picture in Picture" = "Bilde-i-bilde";
@ -303,7 +303,7 @@
"Offtopic in Music Videos" = "Urelaterte ting i musikkvideoer"; "Offtopic in Music Videos" = "Urelaterte ting i musikkvideoer";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Åpner %@-strøm …"; "Opening %@ stream" = "Åpner %@-strøm …";
"Play in PiP" = "Bilde-i-bilde"; "Play in PiP" = "Bilde-i-bilde";
"Pause when entering background" = "Pause ved forsendelse til bakgrunnen"; "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."; "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.";

View File

@ -157,7 +157,7 @@
"LIVE" = "LIVE"; "LIVE" = "LIVE";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Ładowanie strumieni..."; "Loading streams…" = "Ładowanie strumieni…";
"Loading..." = "Ładowanie…"; "Loading..." = "Ładowanie…";
"Locations" = "Lokalizacje"; "Locations" = "Lokalizacje";
"Lock portrait mode" = "Zablokuj tryb portretowy"; "Lock portrait mode" = "Zablokuj tryb portretowy";
@ -202,8 +202,8 @@
"Open Settings" = "Otwórz Ustawienia"; "Open Settings" = "Otwórz Ustawienia";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Otwieranie strumienia %@…"; "Opening %@ stream" = "Otwieranie strumienia %@…";
"Opening audio stream..." = "Otwieranie strumienia audio..."; "Opening audio stream…" = "Otwieranie strumienia audio…";
"Orientation" = "Orientacja"; "Orientation" = "Orientacja";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -570,7 +570,7 @@
"Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć..."; "Enter location address to connect..." = "Wprowadź adres lokalizacji, aby połączyć...";
"Opened File" = "Otwarty plik"; "Opened File" = "Otwarty plik";
"File Extension" = "Rozszerzenie pliku"; "File Extension" = "Rozszerzenie pliku";
"Opening file..." = "Otwieranie pliku..."; "Opening file…" = "Otwieranie pliku…";
"Public account" = "Konto publiczne"; "Public account" = "Konto publiczne";
"Your Accounts" = "Twoje konta"; "Your Accounts" = "Twoje konta";
"Browse without account" = "Przeglądanie bez konta"; "Browse without account" = "Przeglądanie bez konta";

View File

@ -102,7 +102,7 @@
"Just watched" = "Acabou de assistir"; "Just watched" = "Acabou de assistir";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Carregando streams…"; "Loading streams" = "Carregando streams…";
"Medium quality" = "Qualidade média"; "Medium quality" = "Qualidade média";
"No description" = "Sem descrição"; "No description" = "Sem descrição";
"No Playlists" = "Sem playlists"; "No Playlists" = "Sem playlists";
@ -114,8 +114,8 @@
"Open Settings" = "Abrir Ajustes"; "Open Settings" = "Abrir Ajustes";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Abrindo stream %@…"; "Opening %@ stream" = "Abrindo stream %@…";
"Opening audio stream..." = "Abrindo stream de áudio…"; "Opening audio stream" = "Abrindo stream de áudio…";
"Orientation" = "Orientação"; "Orientation" = "Orientação";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -569,7 +569,7 @@
"Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…"; "Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…";
"Opened File" = "Arquivo Aberto"; "Opened File" = "Arquivo Aberto";
"File Extension" = "Extensão do Arquivo"; "File Extension" = "Extensão do Arquivo";
"Opening file..." = "Abrindo arquivo…"; "Opening file" = "Abrindo arquivo…";
"Browse without account" = "Navegar sem uma conta"; "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"; "Rotate when entering fullscreen on landscape video" = "Girar quando entrar no modo tela cheia em vídeo em paisagem";
"Landscape left" = "Paisagem à esquerda"; "Landscape left" = "Paisagem à esquerda";

View File

@ -196,7 +196,7 @@
"LIVE" = "AO VIVO"; "LIVE" = "AO VIVO";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Carregando streams…"; "Loading streams" = "Carregando streams…";
"Loading..." = "Carregando…"; "Loading..." = "Carregando…";
"Locations" = "Localizações"; "Locations" = "Localizações";
"Lock portrait mode" = "Travar modo retrato"; "Lock portrait mode" = "Travar modo retrato";
@ -237,8 +237,8 @@
"Open Settings" = "Abrir Ajustes"; "Open Settings" = "Abrir Ajustes";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Abrindo stream %@…"; "Opening %@ stream" = "Abrindo stream %@…";
"Opening audio stream..." = "Abrindo stream de áudio…"; "Opening audio stream" = "Abrindo stream de áudio…";
"Orientation" = "Orientação"; "Orientation" = "Orientação";
/* SponsorBlock category name */ /* 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"; "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"; "Opened File" = "Ficheiro Aberto";
"File Extension" = "Extensão do Ficheiro"; "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"; "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"; "Use system controls with AVPlayer" = "Usar controles do sistema com o AVPlayer";
"Public account" = "Conta pública"; "Public account" = "Conta pública";

View File

@ -89,7 +89,7 @@
"LIVE" = "LIVE"; "LIVE" = "LIVE";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Se încarcă fluxurile..."; "Loading streams…" = "Se încarcă fluxurile…";
"Locations" = "Locații"; "Locations" = "Locații";
"Mark watched videos with" = "Marcați videoclipurile vizionate cu"; "Mark watched videos with" = "Marcați videoclipurile vizionate cu";
"Matrix Channel" = "Canal Matrix"; "Matrix Channel" = "Canal Matrix";
@ -100,7 +100,7 @@
"Nothing" = "Nimic"; "Nothing" = "Nimic";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Se deschide %@ flux..."; "Opening %@ stream…" = "Se deschide %@ flux…";
"Play Last" = "Reda ultimul"; "Play Last" = "Reda ultimul";
"Player" = "Player"; "Player" = "Player";
"Playlist" = "Playlist"; "Playlist" = "Playlist";
@ -243,7 +243,7 @@
/* SponsorBlock category name */ /* SponsorBlock category name */
"Outro" = "Outro"; "Outro" = "Outro";
"Orientation" = "Orientare"; "Orientation" = "Orientare";
"Opening audio stream..." = "Se deschide fluxul audio..."; "Opening audio stream…" = "Se deschide fluxul audio…";
"Password" = "Parolă"; "Password" = "Parolă";
"Pause" = "Pauză"; "Pause" = "Pauză";
"Pause when entering background" = "Pauză când intrați în fundal"; "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 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..."; "Enter location address to connect..." = "Introdu adresa locației pentru a te conecta...";
"Opened File" = "Fișier deschis"; "Opened File" = "Fișier deschis";
"Opening file..." = "Deschiderea fișierului..."; "Opening file…" = "Deschiderea fișierului…";
"File Extension" = "Extensie fișier"; "File Extension" = "Extensie fișier";
"Use system controls with AVPlayer" = "Utilizați controalele de sistem cu AVPlayer"; "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"; "Rotate when entering fullscreen on landscape video" = "Rotiți când intrați pe ecran complet în videoclipul peisaj";

View File

@ -370,7 +370,7 @@
"LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ"; "LIVE" = "ПРЯМАЯ ТРАНСЛЯЦИЯ";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Загрузка прямой трансляции..."; "Loading streams…" = "Загрузка прямой трансляции…";
"Loading..." = "Загрузка..."; "Loading..." = "Загрузка...";
"Lock portrait mode" = "Блокировка портретного режима"; "Lock portrait mode" = "Блокировка портретного режима";
"Low" = "Низкое"; "Low" = "Низкое";
@ -405,8 +405,8 @@
"Open Settings" = "Отрыть настройки"; "Open Settings" = "Отрыть настройки";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Открытие %@ прямой трансляции..."; "Opening %@ stream…" = "Открытие %@ прямой трансляции…";
"Opening audio stream..." = "Открытие прямой трансляции аудио..."; "Opening audio stream…" = "Открытие прямой трансляции аудио…";
"Orientation" = "Ориентация"; "Orientation" = "Ориентация";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -591,7 +591,7 @@
"Actions buttons" = "Кнопки действия"; "Actions buttons" = "Кнопки действия";
"Show sidebar" = "Показать боковую панель"; "Show sidebar" = "Показать боковую панель";
"Browse without account" = "Искать без аккаунта"; "Browse without account" = "Искать без аккаунта";
"Opening file..." = "Отрытие файла..."; "Opening file…" = "Отрытие файла…";
"Public account" = "Публичный аккаунт"; "Public account" = "Публичный аккаунт";
"Your Accounts" = "Ваши аккаунты"; "Your Accounts" = "Ваши аккаунты";
"Close video and player on end" = "Закрыть видео и плеер в конце"; "Close video and player on end" = "Закрыть видео и плеер в конце";

View File

@ -110,7 +110,7 @@
"I want to ask a question" = "Bir soru sormak istiyorum"; "I want to ask a question" = "Bir soru sormak istiyorum";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Akışlar yükleniyor..."; "Loading streams…" = "Akışlar yükleniyor…";
"Edit Quality Profile" = "Kalite Profilini Düzenle"; "Edit Quality Profile" = "Kalite Profilini Düzenle";
"Frontend URL" = "Ön uç URL'si"; "Frontend URL" = "Ön uç URL'si";
"Close player when starting PiP" = "Resim içinde Resim modu başlatılırken oynatıcıyı kapat"; "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"; "No description" = "Açıklama yok";
"Normal" = "Normal"; "Normal" = "Normal";
"Open \"Playlists\" tab to create new one" = "Yeni bir tane oluşturmak için \"Oynatma Listeleri\" sekmesini açın"; "Open \"Playlists\" tab to create new one" = "Yeni bir tane oluşturmak için \"Oynatma Listeleri\" sekmesini açın";
"Opening audio stream..." = "Ses akışıılıyor..."; "Opening audio stream…" = "Ses akışıılıyor…";
"Rate" = "Derecelendir"; "Rate" = "Derecelendir";
"Orientation" = "Yönlendirme"; "Orientation" = "Yönlendirme";
"No results" = "Sonuç yok"; "No results" = "Sonuç yok";
@ -396,7 +396,7 @@
"Open" = "Aç"; "Open" = "Aç";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "%@ akışıılıyor..."; "Opening %@ stream…" = "%@ akışıılıyor…";
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle"; "Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala"; "Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
"Show Inspector" = "Denetleyiciyi Göster"; "Show Inspector" = "Denetleyiciyi Göster";

View File

@ -259,7 +259,7 @@
"LIVE" = "В ЕФІРІ"; "LIVE" = "В ЕФІРІ";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "Завантаження трансляції..."; "Loading streams…" = "Завантаження трансляції…";
"Loading..." = "Завантаження..."; "Loading..." = "Завантаження...";
"Locations" = "Локації"; "Locations" = "Локації";
"Lock portrait mode" = "Заблокувати портретний режим"; "Lock portrait mode" = "Заблокувати портретний режим";
@ -298,8 +298,8 @@
"Open Settings" = "Відрити налаштування"; "Open Settings" = "Відрити налаштування";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "Запуск трансляції %@..."; "Opening %@ stream…" = "Запуск трансляції %@…";
"Opening audio stream..." = "Запуск аудіо трансляції..."; "Opening audio stream…" = "Запуск аудіо трансляції…";
"Orientation" = "Орієнтація"; "Orientation" = "Орієнтація";
/* SponsorBlock category name */ /* SponsorBlock category name */

View File

@ -152,7 +152,7 @@
"Loading..." = "加载中..."; "Loading..." = "加载中...";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "加载流中..."; "Loading streams…" = "加载流中…";
"Locations" = "地址"; "Locations" = "地址";
"Lock portrait mode" = "锁定竖屏模式"; "Lock portrait mode" = "锁定竖屏模式";
@ -191,8 +191,8 @@
"Open Settings" = "打开设置"; "Open Settings" = "打开设置";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "正在打开 %@ 的流..."; "Opening %@ stream…" = "正在打开 %@ 的流…";
"Opening audio stream..." = "正在打开音频流..."; "Opening audio stream…" = "正在打开音频流…";
"Orientation" = "方向"; "Orientation" = "方向";
/* SponsorBlock category name */ /* SponsorBlock category name */
@ -530,7 +530,7 @@
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮"; "Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
"Opened File" = "打开的文件"; "Opened File" = "打开的文件";
"File Extension" = "文件扩展"; "File Extension" = "文件扩展";
"Opening file..." = "打开文件中..."; "Opening file…" = "打开文件中…";
"Single tap gesture" = "单击手势"; "Single tap gesture" = "单击手势";
"Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单"; "Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单";
"Show unwatched feed badges" = "显示未观看的 Feed 标志"; "Show unwatched feed badges" = "显示未观看的 Feed 标志";

View File

@ -252,7 +252,7 @@
"Interface" = "介面"; "Interface" = "介面";
/* Loading stream OSD */ /* Loading stream OSD */
"Loading streams..." = "加載中..."; "Loading streams…" = "加載中…";
"Loading..." = "加載中..."; "Loading..." = "加載中...";
"Locations" = "地址"; "Locations" = "地址";
"Lock portrait mode" = "鎖定直屏"; "Lock portrait mode" = "鎖定直屏";
@ -292,8 +292,8 @@
"Open Settings" = "打開設置"; "Open Settings" = "打開設置";
/* Loading stream OSD */ /* Loading stream OSD */
"Opening %@ stream..." = "正在打開 %@ ..."; "Opening %@ stream…" = "正在打開 %@ …";
"Opening audio stream..." = "正在打開音訊..."; "Opening audio stream…" = "正在打開音訊…";
/* SponsorBlock category name */ /* SponsorBlock category name */
"Outro" = "結尾"; "Outro" = "結尾";
@ -554,7 +554,7 @@
"Queue - shuffled" = "隊列 - 隨機"; "Queue - shuffled" = "隊列 - 隨機";
"Loop one" = "單個循環"; "Loop one" = "單個循環";
"File Extension" = "副檔名"; "File Extension" = "副檔名";
"Opening file..." = "正在打開文件..."; "Opening file…" = "正在打開文件…";
"Public account" = "公共帳戶"; "Public account" = "公共帳戶";
"Enter account credentials to connect..." = "輸入帳號密碼來連接..."; "Enter account credentials to connect..." = "輸入帳號密碼來連接...";
"Enter location address to connect..." = "輸入站台地址來連接..."; "Enter location address to connect..." = "輸入站台地址來連接...";

View File

@ -1070,6 +1070,9 @@
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC441272734C3009FFD26 /* 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 */; }; 37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.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 */; }; FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -1539,6 +1542,7 @@
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; }; 3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; };
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; }; 3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; };
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; }; 3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -2293,6 +2297,7 @@
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */,
37FFC43F272734C3009FFD26 /* Throttle.swift */, 37FFC43F272734C3009FFD26 /* Throttle.swift */,
378FFBC328660172009E3FBE /* URLParser.swift */, 378FFBC328660172009E3FBE /* URLParser.swift */,
E258F3892BF61BD2005B8C28 /* URLTester.swift */,
37D4B0C22671614700C925CA /* YatteeApp.swift */, 37D4B0C22671614700C925CA /* YatteeApp.swift */,
37D4B0C42671614800C925CA /* Assets.xcassets */, 37D4B0C42671614800C925CA /* Assets.xcassets */,
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */, 37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
@ -3138,6 +3143,7 @@
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
3752069D285E910600CA655F /* ChapterView.swift in Sources */, 3752069D285E910600CA655F /* ChapterView.swift in Sources */,
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */, 375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */,
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */, 3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */, 37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */, 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
@ -3621,6 +3627,7 @@
378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */, 378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */,
370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */, 370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */,
37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */, 37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */,
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */,
370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */, 370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */,
3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
37FB28422721B22200A57617 /* ContentItem.swift in Sources */, 37FB28422721B22200A57617 /* ContentItem.swift in Sources */,
@ -3908,6 +3915,7 @@
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */, 37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */, 37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */, 37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */,
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */, 37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */, 374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */,
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */, 374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,