mirror of
https://github.com/yattee/yattee.git
synced 2025-12-13 11:38:15 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3449b30117 | ||
|
|
d76c82eb65 | ||
|
|
6935866183 | ||
|
|
0b253a1c5c | ||
|
|
8e4b9ea440 | ||
|
|
b2593c02b9 | ||
|
|
d209602f0e | ||
|
|
04377cbc1a | ||
|
|
d4ebee2b44 | ||
|
|
8609ef7709 | ||
|
|
5b291e6e5a | ||
|
|
3d4b5fc42b | ||
|
|
06c0eaa920 | ||
|
|
06d315a1e8 | ||
|
|
cedeb29c44 | ||
|
|
150562830f | ||
|
|
e41527775a | ||
|
|
2461a33feb |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -34,6 +34,7 @@ jobs:
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
bundler-cache: true
|
||||
cache-version: 1
|
||||
- name: Replace signing certificate to AppStore
|
||||
run: |
|
||||
sed -i '' 's/match Development/match AppStore/' Yattee.xcodeproj/project.pbxproj
|
||||
@@ -58,6 +59,7 @@ jobs:
|
||||
with:
|
||||
ruby-version: '3.0'
|
||||
bundler-cache: true
|
||||
cache-version: 1
|
||||
- name: Replace signing certificate to Direct with Developer ID
|
||||
run: |
|
||||
sed -i '' 's/match AppStore/match Direct/' Yattee.xcodeproj/project.pbxproj
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
## Build 199
|
||||
## Build 201
|
||||
|
||||
## What's Changed
|
||||
* Add support for invidious companion by @lifo9 in https://github.com/yattee/yattee/pull/863
|
||||
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/851
|
||||
* MPV audio track switching and fix default audio language by @n3d1117 in https://github.com/yattee/yattee/pull/874
|
||||
* Feat: Added caption support for Piped backend by @craftycorvid in https://github.com/yattee/yattee/pull/867
|
||||
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/877
|
||||
|
||||
## Previous builds
|
||||
* Add support for invidious companion by @lifo9 in https://github.com/yattee/yattee/pull/863
|
||||
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
||||
* Added Settings Import/Export
|
||||
* Export all settings, instances and accounts
|
||||
|
||||
@@ -685,50 +685,93 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
}
|
||||
|
||||
func extractXTags(from urlString: String) -> [String: String] {
|
||||
guard let urlComponents = URLComponents(string: urlString),
|
||||
let queryItems = urlComponents.queryItems,
|
||||
let xtagsValue = queryItems.first(where: { $0.name == "xtags" })?.value else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
guard let decoded = xtagsValue.removingPercentEncoding else { return [:] }
|
||||
|
||||
// Parse key-value pairs (format: key1=value1:key2=value2)
|
||||
// Example: "acont=dubbed-auto:lang=en-US"
|
||||
let pairs = decoded.split(separator: ":")
|
||||
var result: [String: String] = [:]
|
||||
for pair in pairs {
|
||||
let parts = pair.split(separator: "=", maxSplits: 1)
|
||||
if parts.count == 2 {
|
||||
result[String(parts[0])] = String(parts[1])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func extractAdaptiveFormats(from streams: [JSON], videoId: String?) -> [Stream] {
|
||||
let audioStreams = streams
|
||||
let audioTracks = streams
|
||||
.filter { $0["type"].stringValue.starts(with: "audio/mp4") }
|
||||
.sorted {
|
||||
$0.dictionaryValue["bitrate"]?.int ?? 0 >
|
||||
$1.dictionaryValue["bitrate"]?.int ?? 0
|
||||
}
|
||||
guard let audioStream = audioStreams.first else {
|
||||
.compactMap { audioStream -> Stream.AudioTrack? in
|
||||
guard let url = audioStream["url"].url,
|
||||
let audioItag = audioStream["itag"].string
|
||||
else { return nil }
|
||||
|
||||
let finalURL: URL
|
||||
if let videoId, account.instance.invidiousCompanion {
|
||||
let audioCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(audioItag)"
|
||||
finalURL = URL(string: audioCompanionURLString) ?? url
|
||||
} else {
|
||||
finalURL = url
|
||||
}
|
||||
|
||||
let xTags = extractXTags(from: url.absoluteString)
|
||||
|
||||
return Stream.AudioTrack(
|
||||
url: finalURL,
|
||||
content: xTags["acont"],
|
||||
language: xTags["lang"]
|
||||
)
|
||||
}
|
||||
.sorted {
|
||||
/// Always prefer original audio streams over dubbed ones
|
||||
!$0.isDubbed && $1.isDubbed
|
||||
}
|
||||
|
||||
guard !audioTracks.isEmpty else {
|
||||
return .init()
|
||||
}
|
||||
|
||||
let videoStreams = streams.filter { $0["type"].stringValue.starts(with: "video/") }
|
||||
|
||||
return videoStreams.compactMap { videoStream in
|
||||
guard let audioAssetURL = audioStream["url"].url,
|
||||
let videoAssetURL = videoStream["url"].url,
|
||||
let audioItag = audioStream["itag"].string,
|
||||
guard let videoAssetURL = videoStream["url"].url,
|
||||
let videoItag = videoStream["itag"].string
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let finalAudioURL: URL
|
||||
let finalVideoURL: URL
|
||||
|
||||
if let videoId, account.instance.invidiousCompanion {
|
||||
let audioCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(audioItag)"
|
||||
let videoCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(videoItag)"
|
||||
finalAudioURL = URL(string: audioCompanionURLString) ?? audioAssetURL
|
||||
finalVideoURL = URL(string: videoCompanionURLString) ?? videoAssetURL
|
||||
} else {
|
||||
finalAudioURL = audioAssetURL
|
||||
finalVideoURL = videoAssetURL
|
||||
}
|
||||
|
||||
return Stream(
|
||||
instance: account.instance,
|
||||
audioAsset: AVURLAsset(url: finalAudioURL),
|
||||
audioAsset: AVURLAsset(url: audioTracks[0].url),
|
||||
videoAsset: AVURLAsset(url: finalVideoURL),
|
||||
resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue),
|
||||
kind: .adaptive,
|
||||
encoding: videoStream["encoding"].string,
|
||||
videoFormat: videoStream["type"].string,
|
||||
bitrate: videoStream["bitrate"].int,
|
||||
requestRange: videoStream["init"].string ?? videoStream["index"].string
|
||||
requestRange: videoStream["init"].string ?? videoStream["index"].string,
|
||||
audioTracks: audioTracks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,7 +591,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
dislikes: details["dislikes"]?.int,
|
||||
streams: extractStreams(from: content),
|
||||
related: extractRelated(from: content),
|
||||
chapters: extractChapters(from: content)
|
||||
chapters: extractChapters(from: content),
|
||||
captions: extractCaptions(from: content)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -818,6 +819,24 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
return Chapter(title: title, image: image, start: start)
|
||||
}
|
||||
}
|
||||
|
||||
private func extractCaptions(from content: JSON) -> [Captions] {
|
||||
content["subtitles"].arrayValue.compactMap { details in
|
||||
guard let url = details["url"].url,
|
||||
let code = details["code"].string,
|
||||
let label = details["name"].string,
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
else { return nil }
|
||||
|
||||
components.queryItems = components.queryItems?.map { item in
|
||||
item.name == "fmt" ? URLQueryItem(name: "fmt", value: "srt") : item
|
||||
}
|
||||
|
||||
guard let newUrl = components.url else { return nil }
|
||||
|
||||
return Captions(label: label, code: code, url: newUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private func contentItemsDictionary(from content: JSON) -> JSON {
|
||||
if let key = Self.contentItemsKeys.first(where: { content.dictionaryValue.keys.contains($0) }),
|
||||
|
||||
@@ -185,6 +185,10 @@ final class MPVBackend: PlayerBackend {
|
||||
var audioSampleRate: String {
|
||||
client?.audioSampleRate ?? "unknown"
|
||||
}
|
||||
|
||||
var availableAudioTracks: [Stream.AudioTrack] {
|
||||
stream?.audioTracks ?? []
|
||||
}
|
||||
|
||||
init() {
|
||||
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
@@ -243,6 +247,9 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
let updateCurrentStream = {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if self?.video?.id != video.id {
|
||||
self?.model.selectedAudioTrackIndex = 0
|
||||
}
|
||||
self?.stream = stream
|
||||
self?.video = video
|
||||
self?.model.stream = stream
|
||||
@@ -319,6 +326,7 @@ final class MPVBackend: PlayerBackend {
|
||||
startPlaying()
|
||||
}
|
||||
|
||||
stream.audioAsset = AVURLAsset(url: stream.audioTracks[stream.selectedAudioTrackIndex].url)
|
||||
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
|
||||
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url
|
||||
|
||||
@@ -728,4 +736,13 @@ final class MPVBackend: PlayerBackend {
|
||||
logger.info("MPV backend received unhandled property: \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
func switchAudioTrack(to index: Int) {
|
||||
guard let stream, let video else { return }
|
||||
|
||||
stream.selectedAudioTrackIndex = index
|
||||
model.saveTime { [weak self] in
|
||||
self?.playStream(stream, of: video, preservingTime: true, upgrading: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,14 @@ final class PlayerModel: ObservableObject {
|
||||
var keyPressMonitor: Any?
|
||||
#endif
|
||||
|
||||
@Published var selectedAudioTrackIndex = 0 {
|
||||
didSet {
|
||||
if oldValue != selectedAudioTrackIndex {
|
||||
handleAudioTrackChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
#if os(iOS)
|
||||
isOrientationLocked = Defaults[.isOrientationLocked]
|
||||
@@ -1467,4 +1475,12 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func handleAudioTrackChange() {
|
||||
(backend as? MPVBackend)?.switchAudioTrack(to: selectedAudioTrackIndex)
|
||||
}
|
||||
|
||||
var availableAudioTracks: [Stream.AudioTrack] {
|
||||
(backend as? MPVBackend)?.availableAudioTracks ?? []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,25 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioTrack: Hashable, Identifiable {
|
||||
let id = UUID().uuidString
|
||||
let url: URL
|
||||
let content: String?
|
||||
let language: String?
|
||||
|
||||
var displayLanguage: String {
|
||||
LanguageCodes(rawValue: language ?? "")?.description.capitalized ?? language ?? "Unknown"
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"\(displayLanguage) (\(content ?? "Unknown"))"
|
||||
}
|
||||
|
||||
var isDubbed: Bool {
|
||||
content?.lowercased().starts(with: "dubbed") ?? false
|
||||
}
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
|
||||
@@ -208,6 +227,8 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
var videoFormat: String?
|
||||
var bitrate: Int?
|
||||
var requestRange: String?
|
||||
var audioTracks: [AudioTrack] = []
|
||||
var selectedAudioTrackIndex = 0
|
||||
|
||||
init(
|
||||
instance: Instance? = nil,
|
||||
@@ -220,7 +241,8 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
encoding: String? = nil,
|
||||
videoFormat: String? = nil,
|
||||
bitrate: Int? = nil,
|
||||
requestRange: String? = nil
|
||||
requestRange: String? = nil,
|
||||
audioTracks: [AudioTrack] = []
|
||||
) {
|
||||
self.instance = instance
|
||||
self.audioAsset = audioAsset
|
||||
@@ -233,6 +255,7 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
format = .from(videoFormat ?? "")
|
||||
self.bitrate = bitrate
|
||||
self.requestRange = requestRange
|
||||
self.audioTracks = audioTracks
|
||||
}
|
||||
|
||||
var isLocal: Bool {
|
||||
|
||||
@@ -11,6 +11,7 @@ enum LanguageCodes: String, CaseIterable {
|
||||
case Greek = "el"
|
||||
case English = "en"
|
||||
case English_GB = "en-GB"
|
||||
case English_US = "en-US"
|
||||
case Spanish = "es"
|
||||
case Persian = "fa"
|
||||
case Finnish = "fi"
|
||||
@@ -76,6 +77,8 @@ enum LanguageCodes: String, CaseIterable {
|
||||
return "English"
|
||||
case .English_GB:
|
||||
return "English (United Kingdom)"
|
||||
case .English_US:
|
||||
return "English (United States)"
|
||||
case .Spanish:
|
||||
return "Spanish"
|
||||
case .Persian:
|
||||
|
||||
@@ -19,6 +19,7 @@ struct ControlsOverlay: View {
|
||||
case increaseRate
|
||||
case decreaseRate
|
||||
case captions
|
||||
case audioTrack
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
@@ -60,6 +61,15 @@ struct ControlsOverlay: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
if !player.availableAudioTracks.isEmpty {
|
||||
Section(header: controlsHeader("Audio Track".localized())) {
|
||||
audioTrackButton
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .audioTrack)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: controlsHeader("Stream & Player".localized())) {
|
||||
qualityButton
|
||||
#if os(tvOS)
|
||||
@@ -438,6 +448,46 @@ struct ControlsOverlay: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder private var audioTrackButton: some View {
|
||||
#if os(macOS)
|
||||
audioTrackPicker
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: 300)
|
||||
#elseif os(iOS)
|
||||
Menu {
|
||||
audioTrackPicker
|
||||
} label: {
|
||||
Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage)
|
||||
.frame(maxWidth: 240, alignment: .trailing)
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(maxWidth: 240, alignment: .trailing)
|
||||
.frame(height: 40)
|
||||
#else
|
||||
ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) {
|
||||
Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage)
|
||||
.frame(maxWidth: 320)
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in
|
||||
Button(track.description) { player.selectedAudioTrackIndex = index }
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var audioTrackPicker: some View {
|
||||
Picker("", selection: $player.selectedAudioTrackIndex) {
|
||||
ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in
|
||||
Text(track.description).tag(index)
|
||||
}
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
}
|
||||
}
|
||||
|
||||
struct ControlsOverlay_Previews: PreviewProvider {
|
||||
|
||||
@@ -22,6 +22,7 @@ struct PlaybackSettings: View {
|
||||
case increaseRate
|
||||
case decreaseRate
|
||||
case captions
|
||||
case audioTrack
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: Field?
|
||||
@@ -112,6 +113,17 @@ struct PlaybackSettings: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
if !player.availableAudioTracks.isEmpty {
|
||||
HStack {
|
||||
controlsHeader("Audio Track".localized())
|
||||
Spacer()
|
||||
audioTrackButton
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .audioTrack)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
controlsHeader("Backend".localized())
|
||||
Spacer()
|
||||
@@ -453,6 +465,46 @@ struct PlaybackSettings: View {
|
||||
}
|
||||
.disabled(captions.isEmpty)
|
||||
}
|
||||
|
||||
@ViewBuilder private var audioTrackButton: some View {
|
||||
#if os(macOS)
|
||||
audioTrackPicker
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: 300)
|
||||
#elseif os(iOS)
|
||||
Menu {
|
||||
audioTrackPicker
|
||||
} label: {
|
||||
Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage)
|
||||
.frame(maxWidth: 240, alignment: .trailing)
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(maxWidth: 240, alignment: .trailing)
|
||||
.frame(height: 40)
|
||||
#else
|
||||
ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) {
|
||||
Text(player.availableAudioTracks[player.selectedAudioTrackIndex].displayLanguage)
|
||||
.frame(maxWidth: 320)
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in
|
||||
Button(track.description) { player.selectedAudioTrackIndex = index }
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var audioTrackPicker: some View {
|
||||
Picker("", selection: $player.selectedAudioTrackIndex) {
|
||||
ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in
|
||||
Text(track.description).tag(index)
|
||||
}
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaybackSettings_Previews: PreviewProvider {
|
||||
|
||||
@@ -502,3 +502,18 @@
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "La llista de reproducció està buida\n\nMantén premut un vídeo i després\n\"Afegir a la llista de reproducció\"";
|
||||
"Open logs in Finder" = "Obriu els registres al Finder";
|
||||
"No locations available at the moment" = "No hi ha ubicacions disponibles en aquest moment";
|
||||
"File information" = "Informació del fitxer";
|
||||
"Account already exists" = "El compte ja existeix";
|
||||
"Your Accounts" = "Els vostres comptes";
|
||||
"Cells" = "Cel·les";
|
||||
"Lock" = "Bloca";
|
||||
"Description" = "Descripció";
|
||||
"Hide player" = "Amaga el reproductor";
|
||||
"Disable filters" = "Desactiva els filtres";
|
||||
"Show cache status" = "Mostra l'estat de la memòria cau";
|
||||
"Cache" = "Memòria cau";
|
||||
"List" = "Llista";
|
||||
"Platform" = "Plataforma";
|
||||
"Total size: %@" = "Mida total: %@";
|
||||
"Close video" = "Tanca el vídeo";
|
||||
"Podcasts" = "Pòdcasts";
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
Video duration filter in search */
|
||||
"Any" = "Cualquiera";
|
||||
"Apply to all" = "Aplicar a todo";
|
||||
"Are you sure you want to clear history of watched videos?" = "¿Estás seguro de que quieres borrar el historial de vídeos vistos?";
|
||||
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Si descubres un error o tienes ideas, puedes enviarlas a través de los issues de GitHub. ";
|
||||
"Are you sure you want to clear history of watched videos?" = "¿Confirma que quiere eliminar el historial de vídeos vistos?";
|
||||
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Si descubre un error o tiene ideas, puede enviarlas a través de los informes de GitHub. ";
|
||||
"Captions" = "Subtítulos";
|
||||
"Categories to Skip" = "Categorías a omitir";
|
||||
"Categories to Skip" = "Categorías para omitir";
|
||||
"Chapters" = "Capítulos";
|
||||
"Charging" = "Cargando";
|
||||
"Clear" = "Limpiar";
|
||||
@@ -37,23 +37,23 @@
|
||||
"Country Name or Code" = "Nombre o código del país";
|
||||
"Create Playlist" = "Crear lista de reproducción";
|
||||
"10 seconds forwards/backwards" = "10 segundos hacia detrás/hacia delante";
|
||||
"Add to Playlist" = "Añadir a playlist";
|
||||
"Add to Playlist..." = "Añadir a playlist...";
|
||||
"Add to Playlist" = "Añadir a lista";
|
||||
"Add to Playlist..." = "Añadir a lista...";
|
||||
"Advanced" = "Avanzado";
|
||||
"Always use AVPlayer for live videos" = "Siempre usar AVPlayer para vídeos en vivo";
|
||||
|
||||
/* Trending category, section containing all kinds of videos */
|
||||
"All" = "Todo";
|
||||
"Anonymous" = "Anónimo";
|
||||
"Backend" = "Backend";
|
||||
"Are you sure you want to delete playlist?" = "¿Estás seguro de que quieres eliminar esta lista de reproducción?";
|
||||
"Backend" = "Motor";
|
||||
"Are you sure you want to delete playlist?" = "¿Confirma que quiere eliminar esta lista de reproducción?";
|
||||
"Automatic" = "Automático";
|
||||
"Badge" = "Insignia";
|
||||
"Are you sure you want to clear search history?" = "¿Estás seguro de que quieres eliminar tu historial de búsqueda?";
|
||||
"Are you sure you want to restore default quality profiles?" = "¿Estás seguro de que quieres restablecer los ajustes por defecto de los perfiles de calidad?";
|
||||
"Are you sure you want to unsubscribe from %@?" = "¿Estás seguro de que quieres dejar de estar suscrito a %@?";
|
||||
"Are you sure you want to clear search history?" = "¿Confirma que quiere eliminar el historial de búsquedas?";
|
||||
"Are you sure you want to restore default quality profiles?" = "¿Confirma que quiere restablecer la configuración predeterminada de los perfiles de calidad?";
|
||||
"Are you sure you want to unsubscribe from %@?" = "¿Confirma que quiere cancelar la suscripción a %@?";
|
||||
"Cancel" = "Cancelar";
|
||||
"Autoplaying Next" = "Autoreproducir el siguiente";
|
||||
"Autoplaying Next" = "Reproducir siguiente automáticamente";
|
||||
"Based on system color scheme" = "Basado en el tema del sistema";
|
||||
"Clear Search History..." = "Limpiar el historial de búsqueda...";
|
||||
"Battery" = "Batería";
|
||||
@@ -120,7 +120,7 @@
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Solicitudes explícitas para dar me gusta, suscribirse o interactuar con ellos en una o más plataformas gratuitas o de pago (por ejemplo, hacer clic en un vídeo).";
|
||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Los formatos se seleccionarán en orden como se indica.\nHLS es un formato adaptable (no aplica la configuración de resolución).";
|
||||
"Fullscreen size" = "Tamaño de pantalla completa";
|
||||
"Badge & Decreased opacity" = "Insignia y opacidad dosminuída";
|
||||
"Badge & Decreased opacity" = "Insignia y opacidad disminuida";
|
||||
"Browsing" = "Navegando por";
|
||||
"Buffering stream..." = "Cargando flujo de datos...";
|
||||
"Cellular" = "Celular";
|
||||
@@ -161,10 +161,10 @@
|
||||
"Left" = "Izquierda";
|
||||
"Video Details" = "Datalles del Video";
|
||||
"Only for local files and URLs" = "olo para archivos locales y URLs";
|
||||
"\"%@\" will be irreversibly removed from this device." = "\"%@\" será eliminado irreversiblemente de este dispositivo.";
|
||||
"\"%@\" will be irreversibly removed from this device." = "«%@» se eliminará irreversiblemente de este dispositivo.";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Small" = "Achicar";
|
||||
"Small" = "Pequeño";
|
||||
"Now Playing" = "Reproduciendo";
|
||||
"Stream & Player" = "Transmisión y reproductor";
|
||||
"Picture in Picture" = "Imagen en imagen";
|
||||
@@ -184,15 +184,15 @@
|
||||
"Verified" = "Verificado";
|
||||
"History" = "Historial";
|
||||
"Locations Manifest" = "Manifiesto de ubicaciones";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "La lista de reproducción está vacía\n\nToca y mantén presionado un video y luego \n\"Agregar a la lista de reproducción\"";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "La lista de reproducción está vacía\n\nToque y mantenga presionado un vídeo y luego\n«Añadir a la lista de reproducción»";
|
||||
"Play Now" = "Reproducir ahora";
|
||||
"Enter link to open" = "Introduce el enlace para abrir";
|
||||
"Share..." = "Compartir...";
|
||||
"Restore default profiles..." = "Restaurar perfiles predeterminados...";
|
||||
"Round corners" = "Esquinas redondeadas";
|
||||
"SponsorBlock" = "Bloque Patrocinador";
|
||||
"SponsorBlock" = "SponsorBlock";
|
||||
"Statistics" = "Estadísticas";
|
||||
"Matrix Channel" = "Canal matriz";
|
||||
"Matrix Channel" = "Canal de Matrix";
|
||||
"More info can be found in:" = "Se puede encontrar más información en:";
|
||||
"Resolution" = "Resolución";
|
||||
"Open Videos" = "Abrir Videos";
|
||||
@@ -215,7 +215,7 @@
|
||||
"Show playback statistics" = "Mostrar estadísticas de reproducción";
|
||||
"Remove Location" = "Eliminar ubicación";
|
||||
"Inspector visibility" = "Visibilidad del inspector";
|
||||
"Open \"Playlists\" tab to create new one" = "Abra la pestaña \"Listas de reproducción\" para crear una nueva";
|
||||
"Open \"Playlists\" tab to create new one" = "Abra la pestaña «Listas» para crear una lista de reproducción";
|
||||
"Show Inspector" = "Mostrar inspector";
|
||||
|
||||
/* Video date filter in search */
|
||||
@@ -234,7 +234,7 @@
|
||||
"Play in PiP" = "Reproducir en PiP";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Sponsor" = "Sponsor";
|
||||
"Sponsor" = "Patrocinador";
|
||||
"Rate" = "Valorar";
|
||||
"Reload manifest" = "Recargar manifiesto";
|
||||
"Playback queue is empty" = "La cola de reproducción está vacía";
|
||||
@@ -242,7 +242,7 @@
|
||||
|
||||
/* Selected video was played on given date */
|
||||
"Watched %@" = "Visto %@";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "No tienes listas de reproducción\n\nToca \"Nueva lista de reproducción\" para crear una";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "No tiene listas de reproducción\n\nToque «Lista nueva» para crear una";
|
||||
"I like this app!" = "¡Me gusta esta aplicación!";
|
||||
|
||||
/* Video sort order in search */
|
||||
@@ -265,7 +265,7 @@
|
||||
"Shuffle" = "Mezclar";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Loading streams…" = "Cargando secuencias…";
|
||||
"Loading streams…" = "Cargando transmisiones…";
|
||||
"Public Locations" = "Ubicaciones públicas";
|
||||
"Yattee" = "Yattee";
|
||||
"No results" = "No hay resultados";
|
||||
@@ -310,7 +310,7 @@
|
||||
"Honor orientation lock" = "Bloqueo de orientación de honor";
|
||||
"I found a bug /" = "Encontré un error /";
|
||||
"I have a feature request" = "Tengo una solicitud de una nueva función";
|
||||
"LIVE" = "VIVO";
|
||||
"LIVE" = "EN VIVO";
|
||||
|
||||
/* Video duration filter in search */
|
||||
"Long" = "Largo";
|
||||
@@ -378,15 +378,15 @@
|
||||
"Low" = "Baja";
|
||||
"Mark as watched" = "Marcar como visto";
|
||||
"Mark video as watched after playing" = "Marcar video como visto después de reproducirlo";
|
||||
"Mark watched videos with" = "Marar videos vistos con";
|
||||
"Matrix Chat" = "Chat matriz";
|
||||
"Mark watched videos with" = "Marcar videos vistos con";
|
||||
"Matrix Chat" = "Chat de Matrix";
|
||||
"Milestones" = "Hitos";
|
||||
"No description" = "Sin descripción";
|
||||
"No Playlists" = "Sin listas de reproducción";
|
||||
"Not Playing" = "Nada reproduciendo";
|
||||
"Normal" = "Normal";
|
||||
"Nothing" = "Nada";
|
||||
"Only when signed in" = "Solo cuando estás registrado";
|
||||
"Only when signed in" = "Solo al acceder a una cuenta";
|
||||
"Open Settings" = "Abrir configuración";
|
||||
"Player" = "Reproductor";
|
||||
"Pause" = "Pausar";
|
||||
@@ -394,7 +394,7 @@
|
||||
"Play Music" = "Reproducir música";
|
||||
"Play Next" = "Reproducir siguiente";
|
||||
"Playlist" = "Lista de reproducción";
|
||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Se eliminará la lista de reproducción \"%@\".\nNo se puede revertir.";
|
||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Se eliminará la lista de reproducción «%@».\nNo se puede revertir.";
|
||||
"Playlists" = "Listas de reproducción";
|
||||
"Popular" = "Popular";
|
||||
"Proxy videos" = "Utilizar un proxy para ver los vídeos";
|
||||
@@ -419,7 +419,7 @@
|
||||
/* Player controls layout size */
|
||||
"Smaller" = "Más pequeño";
|
||||
"Source" = "Fuente";
|
||||
"SponsorBlock API Instance" = "Instancia de API del bloque Sponsor";
|
||||
"SponsorBlock API Instance" = "Instancia de API de SponsorBlock";
|
||||
"Subscribe" = "Suscribir";
|
||||
|
||||
/* Subscriptions title */
|
||||
@@ -587,12 +587,12 @@
|
||||
"Your Accounts" = "Tus cuentas";
|
||||
"Description" = "Descripción";
|
||||
"Maximum feed items" = "Número máximo de elementos en el feed";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "¿Estás seguro de que quieres eliminar %@ de favoritos?";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "¿Confirma que quiere quitar %@ de Favoritos?";
|
||||
"Limit" = "Límite";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Mantén los canales con los vídeos sin ver en la parte superior de la lista de suscripciones";
|
||||
"(shorts hidden)" = "(cortos ocultos)";
|
||||
"Disable filters" = "Desactivar los filtros";
|
||||
"Podcasts" = "Podcasts";
|
||||
"Podcasts" = "Pódcast";
|
||||
"Releases" = "Lanzamientos";
|
||||
"Show channel avatars in channels lists" = "Mostrar los avatares de los canales en las listas de los canales";
|
||||
"Play Now in MPV" = "Reproducir ahora en MPV";
|
||||
@@ -609,7 +609,7 @@
|
||||
"Other" = "Otro";
|
||||
"Other data" = "Información adicional";
|
||||
"Export..." = "Exportar…";
|
||||
"Are you sure you want to export unencrypted passwords?" = "¿Estás seguro de que quieres exportar las contraseñas sin cifrar?";
|
||||
"Are you sure you want to export unencrypted passwords?" = "¿Confirma que quiere exportar las contraseñas sin cifrar?";
|
||||
"Export" = "Exportar";
|
||||
"Build" = "Compilación";
|
||||
"Platform" = "Plataforma";
|
||||
|
||||
@@ -237,12 +237,12 @@
|
||||
/* Video date filter in search */
|
||||
"Today" = "Hoje";
|
||||
"Trending" = "Em Alta";
|
||||
"Unsubscribe" = "Desinscrever";
|
||||
"Unsubscribe" = "Desinscrever-se";
|
||||
"Upload date" = "Data de upload";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Week" = "Semana";
|
||||
"Subscribe" = "Inscrever";
|
||||
"Subscribe" = "Inscrever-se";
|
||||
"Thumbnails" = "Miniaturas";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Geralmente perto do final ou final do vídeo quando os créditos aparecem e/ou os cartões finais são exibidos.";
|
||||
"unknown" = "desconhecido";
|
||||
@@ -406,7 +406,7 @@
|
||||
"Country" = "País";
|
||||
"Clear All" = "Limpar Tudo";
|
||||
"Clear All Recents" = "Limpar Todos os Recentes";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de curtir, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).";
|
||||
"Duration" = "Duração";
|
||||
"Edit Quality Profile" = "Editar Perfil de Qualidade";
|
||||
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais.";
|
||||
@@ -537,7 +537,7 @@
|
||||
"Gesture: backwards" = "Gesto: para trás";
|
||||
"Always show controls buttons" = "Sempre mostrar botões de controle";
|
||||
"Controls button: forwards" = "Botão de controle: para frente";
|
||||
"Subscribe/Unsubscribe" = "Inscrever/Desinscrever";
|
||||
"Subscribe/Unsubscribe" = "Inscrever-se/Desinscrever-se";
|
||||
"Are you sure you want to clear cache?" = "Tem certeza que deseja limpar o cache?";
|
||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "As configurações de gesto controlam o intervalo de pulo para o clique duplo no lado direito/esquerdo do player. Mudar as configurações dos controles do sistema requer reinício.";
|
||||
"Loop one" = "Um em loop";
|
||||
|
||||
@@ -4109,7 +4109,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -4140,7 +4140,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -4171,7 +4171,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4191,7 +4191,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4355,7 +4355,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
@@ -4408,7 +4408,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -4461,7 +4461,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -4500,7 +4500,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@@ -4535,7 +4535,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4558,7 +4558,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4583,7 +4583,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4607,7 +4607,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4633,7 +4633,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -4649,7 +4649,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.5.2;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"-Wl,-no_compact_unwind",
|
||||
"-lstdc++",
|
||||
@@ -4673,7 +4673,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4690,7 +4690,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 1.5.2;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"-Wl,-no_compact_unwind",
|
||||
"-lstdc++",
|
||||
@@ -4713,7 +4713,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -4736,7 +4736,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 200;
|
||||
CURRENT_PROJECT_VERSION = 202;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
Reference in New Issue
Block a user