diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 11168aea..a945da58 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -23,4 +23,4 @@ jobs: stale-pr-label: "stale" ascending: true # Never mark feature requests/enhancements as stale - exempt-issue-labels: "feature-request,enhancement" + exempt-issue-labels: "feature-request,enhancement,exempt-stale" diff --git a/.gitignore b/.gitignore index 1779a73d..7a26e1a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/doc/ +/docs/ /dev/ /lib/ /bin/ diff --git a/README.md b/README.md index 8d668a29..602ad2e2 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,13 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player. - [PeerTubeify](https://gitlab.com/Cha_de_L/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists. - [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube. -- [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favorites. +- [HoloPlay](https://github.com/stephane-r/holoplay-wa): Progressive Web App connecting on Invidious API's with search, playlists and favorites. - [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch. - [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV. - [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. - [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API) +- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV +- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android ## Liability diff --git a/assets/css/default.css b/assets/css/default.css index 80bf6a20..88ec6ef1 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -145,6 +145,24 @@ img.thumbnail { object-fit: cover; } +div.watched-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255,255,255,.4); +} + +div.watched-indicator { + position: absolute; + left: 0; + bottom: 0; + height: 4px; + width: 100%; + background-color: red; +} + .length { z-index: 100; position: absolute; @@ -490,13 +508,17 @@ hr { } /* Description Expansion Styling*/ -#descexpansionbutton { - display: none +#descexpansionbutton, +#music-desc-expansion { + display: none; } #descexpansionbutton ~ div { overflow: hidden; - height: 8.3em; +} + +#descexpansionbutton:not(:checked) ~ div { + max-height: 8.3em; } #descexpansionbutton:checked ~ div { @@ -509,7 +531,8 @@ hr { margin-top: 20px; } -label[for="descexpansionbutton"]:hover { +label[for="descexpansionbutton"]:hover, +label[for="music-desc-expansion"]:hover { cursor: pointer; } @@ -521,14 +544,38 @@ h4, h5, p, #descriptionWrapper, -#description-box { - unicode-bidi: plaintext; - text-align: start; +#description-box, +#music-description-box { + unicode-bidi: plaintext; + text-align: start; } #descriptionWrapper { - max-width: 600px; - white-space: pre-wrap; + max-width: 600px; + white-space: pre-wrap; +} + +#music-description-box { + display: none; +} + +#music-desc-expansion:checked ~ #music-description-box { + display: block; +} + +#music-desc-expansion ~ label > h3 > .ion-ios-arrow-up, +#music-desc-expansion:checked ~ label > h3 > .ion-ios-arrow-down { + display: none; +} + +#music-desc-expansion:checked ~ label > h3 > .ion-ios-arrow-up, +#music-desc-expansion ~ label > h3 > .ion-ios-arrow-down { + display: inline; +} + +/* Select all the music items except the first one */ +.music-item + .music-item { + border-top: 1px solid #ffffff; } /* Center the "invidious" logo on the search page */ @@ -539,3 +586,7 @@ p, /* Wider settings name to less word wrap */ .pure-form-aligned .pure-control-group label { width: 19em; } + +.channel-emoji { + margin: 0 2px; +} diff --git a/assets/js/themes.js b/assets/js/themes.js index 76767d5f..84a9f6d9 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -22,9 +22,11 @@ function setTheme(theme) { if (theme === THEME_DARK) { toggle_theme.children[0].className = 'icon ion-ios-sunny'; document.body.className = 'dark-theme'; - } else { + } else if (theme === THEME_LIGHT) { toggle_theme.children[0].className = 'icon ion-ios-moon'; document.body.className = 'light-theme'; + } else { + document.body.className = 'no-theme'; } } diff --git a/assets/js/watched_indicator.js b/assets/js/watched_indicator.js new file mode 100644 index 00000000..e971cd80 --- /dev/null +++ b/assets/js/watched_indicator.js @@ -0,0 +1,24 @@ +'use strict'; +var save_player_pos_key = 'save_player_pos'; + +function get_all_video_times() { + return helpers.storage.get(save_player_pos_key) || {}; +} + +document.querySelectorAll('.watched-indicator').forEach(function (indicator) { + var watched_part = get_all_video_times()[indicator.dataset.id]; + var total = parseInt(indicator.dataset.length, 10); + if (watched_part === undefined) { + watched_part = total; + } + var percentage = Math.round((watched_part / total) * 100); + + if (percentage < 5) { + percentage = 5; + } + if (percentage > 90) { + percentage = 100; + } + + indicator.style.width = percentage + '%'; +}); diff --git a/docker/Dockerfile b/docker/Dockerfile index 34549df1..57864883 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,7 +43,7 @@ RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ FROM alpine:3.16 -RUN apk add --no-cache librsvg ttf-opensans +RUN apk add --no-cache librsvg ttf-opensans tini WORKDIR /invidious RUN addgroup -g 1000 -S invidious && \ adduser -u 1000 -S invidious -G invidious @@ -58,4 +58,5 @@ RUN chmod o+rX -R ./assets ./config ./locales EXPOSE 3000 USER invidious +ENTRYPOINT ["/sbin/tini", "--"] CMD [ "/invidious/invidious" ] diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index ef3284b1..10135efb 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -42,7 +42,7 @@ RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ fi FROM alpine:3.16 -RUN apk add --no-cache librsvg ttf-opensans +RUN apk add --no-cache librsvg ttf-opensans tini WORKDIR /invidious RUN addgroup -g 1000 -S invidious && \ adduser -u 1000 -S invidious -G invidious @@ -57,4 +57,5 @@ RUN chmod o+rX -R ./assets ./config ./locales EXPOSE 3000 USER invidious +ENTRYPOINT ["/sbin/tini", "--"] CMD [ "/invidious/invidious" ] diff --git a/kubernetes/Chart.lock b/kubernetes/Chart.lock index 37fcdbbd..cc76e920 100644 --- a/kubernetes/Chart.lock +++ b/kubernetes/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: postgresql repository: https://charts.bitnami.com/bitnami/ - version: 11.1.3 -digest: sha256:79061645472b6fb342d45e8e5b3aacd018ef5067193e46a060bccdc99fe7f6e1 -generated: "2022-03-02T05:57:20.081432389+13:00" + version: 12.1.9 +digest: sha256:71ff342a6c0a98bece3d7fe199983afb2113f8db65a3e3819de875af2c45add7 +generated: "2023-01-20T20:42:32.757707004Z" diff --git a/locales/af.json b/locales/af.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/af.json @@ -0,0 +1 @@ +{} diff --git a/locales/ar.json b/locales/ar.json index 2a746e5d..3ce34c2d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "`x` أعجب بهذا", "Audio mode": "الوضع الصوتي", "Video mode": "وضع الفيديو", - "Videos": "الفيديوهات", + "channel_tab_videos_label": "الفيديوهات", "Playlists": "قوائم التشغيل", - "Community": "المجتمع", + "channel_tab_community_label": "المجتمع", "search_filters_sort_option_relevance": "ملائمة", "search_filters_sort_option_rating": "تقييم", "search_filters_sort_option_date": "التاريخ", @@ -536,5 +536,14 @@ "generic_count_seconds_3": "{{count}} ثوانٍ", "generic_count_seconds_4": "{{count}} ثانية", "generic_count_seconds_5": "{{count}} ثانية", - "error_video_not_in_playlist": "الفيديو المطلوب غير موجود في قائمة التشغيل هذه. انقر هنا للحصول على الصفحة الرئيسية لقائمة التشغيل. " + "error_video_not_in_playlist": "الفيديو المطلوب غير موجود في قائمة التشغيل هذه. انقر هنا للحصول على الصفحة الرئيسية لقائمة التشغيل. ", + "channel_tab_shorts_label": "الفيديوهات القصيرة", + "channel_tab_streams_label": "البث المباشر", + "channel_tab_playlists_label": "قوائم التشغيل", + "channel_tab_channels_label": "القنوات", + "Music in this video": "الموسيقى في هذا الفيديو", + "Album: ": "الألبوم: ", + "Artist: ": "الفنان: ", + "Song: ": "أغنية: ", + "Channel Sponsor": "راعي القناة" } diff --git a/locales/ca.json b/locales/ca.json index 741414d2..54a0b177 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -51,7 +51,7 @@ "Movies": "Películes", "Download": "Descarrega", "Download as: ": "Descarrega com: ", - "Videos": "Vídeos", + "channel_tab_videos_label": "Vídeos", "search_filters_type_label": "Tipus", "search_filters_duration_label": "Duració", "search_filters_sort_label": "Ordena per", @@ -75,7 +75,7 @@ "Title": "Títol", "Belarusian": "Bielorús", "Enable web notifications": "Activa notificacions web", - "search": "busca", + "search": "cerca", "Catalan": "Català", "Croatian": "Croat", "preferences_category_admin": "Preferències d'administrador", @@ -99,5 +99,387 @@ "Music": "Música", "search_filters_sort_option_relevance": "Rellevància", "search_filters_date_option_hour": "Última hora", - "search_filters_date_option_today": "Avui" + "search_filters_date_option_today": "Avui", + "preferences_volume_label": "Volum del reproductor: ", + "invidious": "Invidious", + "preferences_quality_dash_option_144p": "144p", + "Turkish (auto-generated)": "Turc (generat automàticament)", + "Urdu": "Urdú", + "Vietnamese (auto-generated)": "Vietnamita (generat automàticament)", + "Welsh": "Gal·lès", + "Yoruba": "Ioruba", + "YouTube comment permalink": "Enllaç permanent de comentari de YouTube", + "Channel Sponsor": "Patrocinador del canal", + "Audio mode": "Mode d'àudio", + "search_filters_date_option_none": "Qualsevol data", + "search_filters_type_option_playlist": "Llista de reproducció", + "search_filters_type_option_movie": "Pel·lícula", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_subtitles": "Subtítols/CC", + "search_filters_features_option_live": "Directe", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_hdr": "HDR", + "search_filters_features_option_location": "Ubicació", + "search_filters_apply_button": "Aplica els filtres seleccionats", + "videoinfo_started_streaming_x_ago": "Ha començat el directe fa `x`", + "next_steps_error_message_go_to_youtube": "Anar a YouTube", + "footer_donate_page": "Donar", + "footer_original_source_code": "Codi font original", + "videoinfo_watch_on_youTube": "Veure a YouTube", + "user_saved_playlists": "`x` llistes de reproducció guardades", + "adminprefs_modified_source_code_url_label": "URL al repositori de codi font modificat", + "none": "cap", + "footer_modfied_source_code": "Codi font modificat", + "videoinfo_invidious_embed_link": "Incrusta l'enllaç", + "download_subtitles": "Subtítols - `x` (.vtt)", + "user_created_playlists": "`x`llistes de reproducció creades", + "Video unavailable": "Vídeo no disponible", + "channel_tab_channels_label": "Canals", + "channel_tab_playlists_label": "Llistes de reproducció", + "channel_tab_community_label": "Comunitat", + "Invalid TFA code": "Codi TFA no vàlid", + "Czech": "Txec", + "Default": "Per defecte", + "Amharic": "Amàric", + "preferences_automatic_instance_redirect_label": "Redirecció automàtica d'instàncies (retorna a redirect.invidious.io): ", + "Login enabled: ": "Activa inici de sessió: ", + "Registration enabled: ": "Activa registre: ", + "Whitelisted regions: ": "Regions a la llista blanca: ", + "Chinese (Simplified)": "Xinès (Simplificat)", + "Corsican": "Cors", + "Estonian": "Estonià", + "Japanese (auto-generated)": "Japonès (generat automàticament)", + "English (United States)": "Anglès (Estats Units)", + "English (auto-generated)": "Anglès (generat automàticament)", + "Cebuano": "Cebuà", + "Esperanto": "Esperanto", + "Scottish Gaelic": "Gaèlic escocès", + "Playlists": "Llistes de reproducció", + "search_filters_title": "Filtres", + "search_filters_type_option_all": "Qualsevol tipus", + "search_filters_duration_option_none": "Qualsevol duració", + "next_steps_error_message": "Després d'això, hauríeu d'intentar: ", + "next_steps_error_message_refresh": "Recarregar la pàgina", + "crash_page_refresh": "ha intentat actualitzar la pàgina", + "crash_page_report_issue": "Si cap de les anteriors no ha ajudat, obre un nou issue a GitHub (preferiblement en anglès) i inclou el text següent al missatge (NO tradueixis aquest text):", + "generic_subscriptions_count": "{{count}} subscripció", + "generic_subscriptions_count_plural": "{{count}} subscripcions", + "error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. Fes clic aquí per a la pàgina d'inici de la llista de reproducció.", + "comments_points_count": "{{count}} punt", + "comments_points_count_plural": "{{count}} punts", + "%A %B %-d, %Y": "%A %B %-d, %Y", + "Create playlist": "Crear llista de reproducció", + "Text CAPTCHA": "Text CAPTCHA", + "Next page": "Pàgina següent", + "preferences_category_visual": "Preferències visuals", + "preferences_unseen_only_label": "Mostra només no vistos: ", + "preferences_listen_label": "Escolta per defecte: ", + "Import": "Importar", + "Token": "Senyal", + "Wilson score: ": "Puntuació de Wilson: ", + "search_filters_date_label": "Data de càrrega", + "search_filters_features_option_three_sixty": "360°", + "source": "font", + "preferences_default_home_label": "Pàgina d'inici per defecte: ", + "preferences_comments_label": "Comentaris per defecte: ", + "`x` uploaded a video": "`x` ha penjat un vídeo", + "Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.", + "Token manager": "Gestor de tokens", + "Watch history": "Historial de reproduccions", + "Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google", + "Authorize token?": "Autoritzar senyal?", + "Source available here.": "Font disponible aquí.", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)", + "Log in": "Inicia sessió", + "search_filters_sort_option_date": "Data de càrrega", + "Unlisted": "No llistat", + "View privacy policy.": "Veure política de privadesa.", + "Public": "Públic", + "View all playlists": "Veure totes les llistes de reproducció", + "reddit": "Reddit", + "Manage tokens": "Gestiona senyals", + "Not a playlist.": "No és una llista de reproducció.", + "preferences_local_label": "Vídeos de Proxy: ", + "View channel on YouTube": "Veure canal a Youtube", + "preferences_quality_dash_option_1080p": "1080p", + "Top enabled: ": "Activa top: ", + "Delete playlist `x`?": "Eliminar llista de reproducció `x`?", + "View JavaScript license information.": "Consulta la informació de la llicència de JavaScript.", + "Playlist privacy": "Privacitat de la llista de reproducció", + "search_message_no_results": "No s'han trobat resultats.", + "search_message_use_another_instance": " També es pot buscar en una altra instància.", + "Genre: ": "Gènere: ", + "Hidden field \"challenge\" is a required field": "El camp ocult \"repte\" és un camp obligatori", + "Burmese": "Birmà", + "View as playlist": "Mostra com a llista de reproducció", + "preferences_category_subscription": "Preferències de subscripció", + "Music in this video": "Música en aquest vídeo", + "Artist: ": "Artista: ", + "Album: ": "Àlbum: ", + "Shared `x`": "Compartit `x`", + "Premieres `x`": "Estrena `x`", + "View more comments on Reddit": "Veure més comentaris a Reddit", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Veure `x` comentari", + "": "Veure `x` comentaris" + }, + "View Reddit comments": "Veure comentaris de Reddit", + "Incorrect password": "Contrasenya incorrecta", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No es pot iniciar la sessió, assegureu-vos que l'autenticació de dos factors (Autenticador o SMS) estigui activada.", + "Erroneous CAPTCHA": "CAPTCHA erroni", + "CAPTCHA is a required field": "El CAPTCHA és un camp obligatori", + "Korean (auto-generated)": "Coreà (generat automàticament)", + "Kyrgyz": "Kirguís", + "Latin": "Llatí", + "Malagasy": "Malgaix", + "Maori": "Maori", + "Marathi": "Marathi", + "Norwegian Bokmål": "Bokmål Noruec", + "Nyanja": "Nyanja", + "Portuguese (Brazil)": "Portuguès (Brazil)", + "Punjabi": "Panjabi", + "Russian (auto-generated)": "Rus (generat automàticament)", + "Samoan": "Samoà", + "Somali": "Somali", + "Southern Sotho": "Sesotho", + "Spanish (Mexico)": "Espanyol (Mèxic)", + "Spanish (Spain)": "Espanyol (Espanya)", + "Sundanese": "Sondanès", + "Swahili": "Suahili", + "Tamil": "Tàmil", + "Telugu": "Telugu", + "Zulu": "Zulu", + "generic_count_months": "{{count}} mes", + "generic_count_months_plural": "{{count}} mesos", + "generic_count_weeks": "{{count}} setmana", + "generic_count_weeks_plural": "{{count}} setmanes", + "About": "Sobre", + "`x` marked it with a ❤": "`x`marca'l amb un ❤", + "Video mode": "Mode de vídeo", + "search_filters_features_label": "Característiques", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_vr180": "VR180", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_purchased": "Comprat", + "Chinese (Hong Kong)": "Xinès (Hong Kong)", + "Chinese (Taiwan)": "Xinès (Taiwan)", + "Hmong": "Hmong", + "Kazakh": "Kazakh", + "Igbo": "Igbo", + "Javanese": "Javanès", + "Indonesian (auto-generated)": "Indonesi (generat automàticament)", + "Interlingue": "Interlingüe", + "Khmer": "Khmer", + "This channel does not exist.": "Aquest canal no existeix.", + "Song: ": "Cançó: ", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Error a l'iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.", + "channel:`x`": "canal: `x`", + "Deleted or invalid channel": "Canal suprimit o no vàlid", + "Could not get channel info.": "No s'ha pogut obtenir la informació del canal.", + "Could not pull trending pages.": "No s'han pogut extreure les pàgines de tendència.", + "comments_view_x_replies": "Veure {{count}} resposta", + "comments_view_x_replies_plural": "Veure {{count}} respostes", + "Subscriptions": "Subscripcions", + "generic_count_seconds": "{{count}} segon", + "generic_count_seconds_plural": "{{count}} segons", + "channel_tab_shorts_label": "Vídeos curts", + "preferences_save_player_pos_label": "Desa la posició de reproducció: ", + "crash_page_before_reporting": "Abans d'informar d'un error, assegureu-vos que teniu:", + "crash_page_switch_instance": "ha intentat utilitzar una altra instància", + "crash_page_read_the_faq": "heu llegit les Preguntes més freqüents (FAQ)", + "crash_page_search_issue": "ha cercat problemes existents a GitHub", + "User ID is a required field": "L'identificador d'usuari és un camp obligatori", + "Password is a required field": "La contrasenya és un camp obligatori", + "Wrong username or password": "Nom d'usuari o contrasenya incorrectes", + "Please sign in using 'Log in with Google'": "Si us plau, inicieu la sessió amb 'Inicieu sessió amb Google'", + "Password cannot be longer than 55 characters": "La contrasenya no pot tenir més de 55 caràcters", + "Invidious Private Feed for `x`": "Feed privat Invidious per a `x`", + "generic_views_count": "{{count}} visualització", + "generic_views_count_plural": "{{count}} visualitzacions", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "Token is expired, please try again": "La senyal ha caducat, torna-ho a provar", + "English": "Anglès", + "Kannada": "Kanarès", + "Erroneous token": "Senyal errònia", + "`x` ago": "fa `x`", + "Empty playlist": "Llista de reproducció buida", + "Playlist does not exist.": "La llista de reproducció no existeix.", + "No such user": "No hi ha tal usuari", + "Afrikaans": "Afrikàans", + "Azerbaijani": "Azerbaidjana", + "Cantonese (Hong Kong)": "Cantonès (Hong Kong)", + "Chinese": "Xinès", + "Chinese (China)": "Xinès (Xina)", + "Chinese (Traditional)": "Xinès (Tradicional)", + "Dutch": "Holandès", + "Dutch (auto-generated)": "Holandès (generat automàticament)", + "French (auto-generated)": "Francès (generat automàticament)", + "Georgian": "Georgià", + "German (auto-generated)": "Alemany (generat automàticament)", + "Gujarati": "Gujarati", + "Hawaiian": "Hawaià", + "generic_count_years": "{{count}} any", + "generic_count_years_plural": "{{count}} anys", + "Popular": "Popular", + "Rating: ": "Valoració: ", + "permalink": "enllaç permanent", + "preferences_quality_dash_option_worst": "Pitjor", + "Yiddish": "Ídix", + "preferences_quality_dash_option_auto": "Automàtic", + "Western Frisian": "Frisó occidental", + "Swedish": "Suec", + "Only show latest unwatched video from channel: ": "Mostra només l'últim vídeo no vist del canal: ", + "preferences_continue_label": "Reprodueix el següent per defecte: ", + "Import YouTube subscriptions": "Importar subscripcions de YouTube", + "search_filters_sort_option_rating": "Valoració", + "preferences_thin_mode_label": "Mode prim: ", + "preferences_quality_option_small": "Petit", + "CAPTCHA enabled: ": "activa CAPTCHA: ", + "Import and Export Data": "Importar i exportar dades", + "preferences_quality_dash_option_360p": "360p", + "Popular enabled: ": "Activa popular: ", + "Password": "Contrasenya", + "Blacklisted regions: ": "Regions a la llista negra: ", + "Register": "Registra't", + "Shared `x` ago": "Compartit fa `x`", + "search_filters_sort_option_views": "Recompte de visualitzacions", + "Import Invidious data": "Importa dades JSON d'Invidious", + "preferences_related_videos_label": "Mostra vídeos relacionats: ", + "preferences_show_nick_label": "Mostra l'àlies a la part superior: ", + "Time (h:mm:ss):": "Temps (h:mm:ss):", + "Could not fetch comments": "No s'han pogut obtenir els comentaris", + "New password": "Nova contrasenya", + "preferences_notifications_only_label": "Mostra només notificacions (si n'hi ha): ", + "preferences_annotations_label": "Mostra anotacions per defecte: ", + "Import FreeTube subscriptions (.db)": "Importar subscripcions de FreeTube (.db)", + "Fallback captions: ": "Subtítols alternatius: ", + "Log out": "Tancar sessió", + "preferences_quality_dash_option_2160p": "2160p", + "Unsubscribe": "Cancel·la la subscripció", + "Log in/register": "Inicia sessió/registra't", + "Nepali": "Nepalí", + "Xhosa": "Xosa", + "preferences_captions_label": "Subtítols per defecte: ", + "preferences_autoplay_label": "Reproducció automàtica: ", + "`x` is live": "`x` està en directe", + "Uzbek": "Uzbek", + "Hausa": "Haussa", + "Bosnian": "Bosnià", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hola! Sembla que tens JavaScript desactivat. Feu clic aquí per veure els comentaris, tingueu en compte que poden trigar una mica més a carregar-se.", + "Password cannot be empty": "La contrasenya no pot estar buida", + "preferences_video_loop_label": "Sempre en bucle: ", + "preferences_quality_option_dash": "DASH (qualitat adaptativa)", + "Change password": "Canvia la contrasenya", + "Export data as JSON": "Exporta dades d'Invidious com a JSON", + "Wrong answer": "Resposta incorrecta", + "Clear watch history": "Neteja l'historial de reproduccions", + "Mongolian": "Mongol", + "preferences_quality_dash_option_best": "Millor", + "Authorize token for `x`?": "Autoritzar senyal per a `x`?", + "Report statistics: ": "Estadístiques de l'informe: ", + "Switch Invidious Instance": "Canvia la instància d'Invidious", + "History": "Historial", + "Portuguese (auto-generated)": "Portuguès (generat automàticament)", + "footer_source_code": "Codi font", + "videoinfo_youTube_embed_link": "Insereix", + "generic_count_minutes": "{{count}} minut", + "generic_count_minutes_plural": "{{count}} minuts", + "preferences_category_player": "Preferències del reproductor", + "Sign In": "Inicia Sessió", + "preferences_continue_autoplay_label": "Reprodueix automàticament el següent vídeo: ", + "generic_playlists_count": "{{count}} llista de reproducció", + "generic_playlists_count_plural": "{{count}} llistes de reproducció", + "Delete account?": "Esborrar compte?", + "Please log in": "Si us plau inicieu sessió", + "Import NewPipe data (.zip)": "Importar dades de NewPipe (.zip)", + "Image CAPTCHA": "Imatge CAPTCHA", + "channel_tab_streams_label": "Transmissions en directe", + "preferences_category_misc": "Preferències diverses", + "preferences_annotations_subscribed_label": "Mostra les anotacions per defecte dels canals subscrits? ", + "Tajik": "Tadjik", + "preferences_player_style_label": "Estil del reproductor: ", + "Load more": "Carrega més", + "preferences_vr_mode_label": "Vídeos interactius de 360 graus (requereix WebGL): ", + "Manage subscriptions": "Gestionar les subscripcions", + "preferences_quality_option_medium": "Mitjà", + "Editing playlist `x`": "Editant la llista de reproducció `x`", + "search_filters_duration_option_medium": "Mitjà (4 - 20 minuts)", + "E-mail": "Correu electrònic", + "Spanish (auto-generated)": "Castellà (generat automàticament)", + "Export": "Exportar", + "preferences_quality_dash_option_4320p": "4320p", + "JavaScript license information": "Informació de la llicència de JavaScript", + "Hidden field \"token\" is a required field": "El camp ocult \"senyal\" és un camp obligatori", + "Shona": "Xona", + "Family friendly? ": "Apte per a tots els públics? ", + "preferences_quality_dash_label": "Qualitat de vídeo DASH preferida: ", + "Hindi": "Hindi", + "An alternative front-end to YouTube": "Una interfície alternativa a YouTube", + "Export subscriptions as OPML": "Exporta subscripcions com a OPML", + "Watch on YouTube": "Veure a YouTube", + "Lao": "Laosià", + "search_message_change_filters_or_query": "Proveu d'ampliar la vostra consulta de cerca i/o canviar els filtres.", + "View YouTube comments": "Veure comentaris de YouTube", + "New passwords must match": "Les contrasenyes noves han de coincidir", + "Subscription manager": "Gestor de subscripcions", + "Premieres in `x`": "Estrena en `x`", + "youtube": "YouTube", + "Latvian": "Letó", + "LIVE": "EN VIU", + "Could not create mix.": "No s'ha pogut crear la barreja.", + "preferences_speed_label": "Velocitat per defecte: ", + "preferences_extend_desc_label": "Amplieu automàticament la descripció del vídeo: ", + "popular": "popular", + "Erroneous challenge": "Repte erroni", + "last": "darrer", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_dash_option_480p": "480p", + "Log in with Google": "Inicia sessió amb Google", + "preferences_quality_dash_option_1440p": "1440p", + "Previous page": "Pàgina anterior", + "Only show latest video from channel: ": "Mostra només l'últim vídeo del canal: ", + "unsubscribe": "cancel·la la subscripció", + "View playlist on YouTube": "Veure llista de reproducció a YouTube", + "Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)", + "crash_page_you_found_a_bug": "Sembla que has trobat un error a Invidious!", + "Subscribe": "Subscriu-me", + "Quota exceeded, try again in a few hours": "S'ha superat la quota, torna-ho a provar d'aquí a unes hores", + "generic_count_days": "{{count}} dia", + "generic_count_days_plural": "{{count}} dies", + "Trending": "Tendència", + "Updated `x` ago": "Actualitzat fa `x`", + "Haitian Creole": "Crioll Haitià", + "preferences_watch_history_label": "Habilita historial de reproduccions: ", + "generic_count_hours": "{{count}} hora", + "generic_count_hours_plural": "{{count}} hores", + "Malayalam": "Maialàiam", + "Clear watch history?": "Neteja historial de reproduccions?", + "Import/export data": "Importa/exporta dades", + "Sinhala": "Singalès", + "Delete playlist": "Eliminar llista de reproducció", + "Bangla": "Bengalí", + "Italian (auto-generated)": "Italià (generat automàticament)", + "License: ": "Llicència: ", + "(edited)": "(editat)", + "Pashto": "Paixtu", + "preferences_dark_mode_label": "Tema: ", + "revoke": "revocar", + "English (United Kingdom)": "Anglès (Regne Unit)", + "preferences_quality_option_hd720": "HD720", + "tokens_count": "{{count}} senyal", + "tokens_count_plural": "{{count}} senyals", + "subscriptions_unseen_notifs_count": "{{count}} notificació no vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificacions no vistes", + "generic_subscribers_count": "{{count}} subscriptor", + "generic_subscribers_count_plural": "{{count}} subscriptors", + "Sindhi": "Sindhi", + "Slovenian": "Eslovè", + "preferences_feed_menu_label": "Menú del feed: ", + "Fallback comments: ": "Comentaris alternatius: ", + "Top": "Millors", + "preferences_max_results_label": "Nombre de vídeos mostrats al feed: ", + "Engagement: ": "Atracció: ", + "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: " } diff --git a/locales/cs.json b/locales/cs.json index 7538365a..4611c4fd 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -63,7 +63,7 @@ "reddit": "Reddit", "preferences_captions_label": "Výchozí titulky: ", "Fallback captions: ": "Záložní titulky: ", - "preferences_related_videos_label": "Zobrazit podobné videa: ", + "preferences_related_videos_label": "Zobrazit podobná videa: ", "preferences_annotations_label": "Zobrazovat poznámky ve výchozím nastavení: ", "preferences_extend_desc_label": "Rozšířit automaticky popis u videa: ", "preferences_category_visual": "Nastavení vzhledu", @@ -260,8 +260,8 @@ "`x` marked it with a ❤": "`x` to označil(a) se ❤", "Audio mode": "Audiový režim", "Video mode": "Videový režim", - "Videos": "Videa", - "Community": "Komunita", + "channel_tab_videos_label": "Videa", + "channel_tab_community_label": "Komunita", "search_filters_sort_option_rating": "Hodnocení", "search_filters_sort_option_date": "Datum nahrání", "search_filters_sort_option_views": "Počet zhlédnutí", @@ -488,5 +488,14 @@ "search_filters_sort_option_relevance": "Relevantnost", "search_filters_apply_button": "Použít vybrané filtry", "Popular enabled: ": "Populární povoleno: ", - "error_video_not_in_playlist": "Požadované video v tomto playlistu neexistuje. Klikněte sem pro navštívení domovské stránky playlistu." + "error_video_not_in_playlist": "Požadované video v tomto playlistu neexistuje. Klikněte sem pro navštívení domovské stránky playlistu.", + "channel_tab_shorts_label": "Shorts", + "channel_tab_playlists_label": "Playlisty", + "channel_tab_channels_label": "Kanály", + "channel_tab_streams_label": "Živé přenosy", + "Music in this video": "Hudba v tomto videu", + "Artist: ": "Umělec: ", + "Album: ": "Album: ", + "Channel Sponsor": "Sponzor kanálu", + "Song: ": "Skladba: " } diff --git a/locales/da.json b/locales/da.json index 4816c2c9..2bee6c80 100644 --- a/locales/da.json +++ b/locales/da.json @@ -187,7 +187,7 @@ "Esperanto": "Esperanto", "Czech": "Tjekkisk", "Danish": "Dansk", - "Community": "Samfund", + "channel_tab_community_label": "Samfund", "Afrikaans": "Afrikansk", "Portuguese": "Portugisisk", "Ukrainian": "Ukrainsk", @@ -267,7 +267,7 @@ "search_filters_sort_option_rating": "Bedømmelse", "Yoruba": "Yoruba", "Erroneous token": "Fejlagtig token", - "Videos": "Videoer", + "channel_tab_videos_label": "Videoer", "search_filters_type_option_show": "Vis", "Luxembourgish": "Luxemboursk", "Vietnamese": "Vietnamesisk", diff --git a/locales/de.json b/locales/de.json index a2070cf5..c2941d6d 100644 --- a/locales/de.json +++ b/locales/de.json @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "`x` markierte es mit einem ❤", "Audio mode": "Audiomodus", "Video mode": "Videomodus", - "Videos": "Videos", + "channel_tab_videos_label": "Videos", "Playlists": "Wiedergabelisten", - "Community": "Gemeinschaft", + "channel_tab_community_label": "Gemeinschaft", "search_filters_sort_option_relevance": "Relevanz", "search_filters_sort_option_rating": "Bewertung", "search_filters_sort_option_date": "Datum", @@ -472,5 +472,12 @@ "search_filters_duration_option_none": "Beliebige Länge", "search_filters_date_label": "Upload-Datum", "search_filters_date_option_none": "Beliebiges Datum", - "error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen." + "error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen.", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Livestreams", + "Music in this video": "Musik in diesem Video", + "Artist: ": "Künstler: ", + "Album: ": "Album: ", + "channel_tab_playlists_label": "Wiedergabelisten", + "channel_tab_channels_label": "Kanäle" } diff --git a/locales/el.json b/locales/el.json index d91d64fc..8d0c84dd 100644 --- a/locales/el.json +++ b/locales/el.json @@ -315,9 +315,9 @@ "`x` marked it with a ❤": "Ο χρηστης `x` έβαλε ❤", "Audio mode": "Λειτουργία ήχου", "Video mode": "Λειτουργία βίντεο", - "Videos": "Βίντεο", + "channel_tab_videos_label": "Βίντεο", "Playlists": "Λίστες Αναπαραγωγής", - "Community": "Κοινότητα", + "channel_tab_community_label": "Κοινότητα", "Current version: ": "Τρέχουσα έκδοση: ", "generic_playlists_count": "{{count}} λίστα αναπαραγωγής", "generic_playlists_count_plural": "{{count}} λίστες αναπαραγωγής", @@ -366,7 +366,7 @@ "preferences_quality_option_hd720": "HD720", "preferences_quality_option_medium": "Μεσαία", "preferences_quality_option_small": "Μικρό", - "preferences_quality_option_dash": "DASH (προσαρμοστική ποιότητα)", + "preferences_quality_option_dash": "DASH (προσαρμόσιμη ποιότητα)", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_720p": "720p", "invidious": "Invidious", @@ -450,5 +450,5 @@ "search_filters_type_option_show": "Μπάρα προόδου διαβάσματος", "preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ", "search_filters_title": "Φίλτρο", - "search_message_no_results": "Δεν" + "search_message_no_results": "Δε βρέθηκαν αποτελέσματα." } diff --git a/locales/en-US.json b/locales/en-US.json index 8f1ec58d..96b6799b 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -184,11 +184,16 @@ "Show annotations": "Show annotations", "Genre: ": "Genre: ", "License: ": "License: ", + "Standard YouTube license": "Standard YouTube license", "Family friendly? ": "Family friendly? ", "Wilson score: ": "Wilson score: ", "Engagement: ": "Engagement: ", "Whitelisted regions: ": "Whitelisted regions: ", "Blacklisted regions: ": "Blacklisted regions: ", + "Music in this video": "Music in this video", + "Artist: ": "Artist: ", + "Song: ": "Song: ", + "Album: ": "Album: ", "Shared `x`": "Shared `x`", "Premieres in `x`": "Premieres in `x`", "Premieres `x`": "Premieres `x`", @@ -398,11 +403,13 @@ "Movies": "Movies", "Download": "Download", "Download as: ": "Download as: ", + "Download is disabled": "Download is disabled", "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(edited)", "YouTube comment permalink": "YouTube comment permalink", "permalink": "permalink", "`x` marked it with a ❤": "`x` marked it with a ❤", + "Channel Sponsor": "Channel Sponsor", "Audio mode": "Audio mode", "Video mode": "Video mode", "Playlists": "Playlists", @@ -452,7 +459,7 @@ "footer_documentation": "Documentation", "footer_source_code": "Source code", "footer_original_source_code": "Original source code", - "footer_modfied_source_code": "Modified Source code", + "footer_modfied_source_code": "Modified source code", "adminprefs_modified_source_code_url_label": "URL to modified source code repository", "none": "none", "videoinfo_started_streaming_x_ago": "Started streaming `x` ago", diff --git a/locales/eo.json b/locales/eo.json index 5aa2bbc6..9f37c7cb 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "`x` markis ĝin per ❤", "Audio mode": "Aŭda reĝimo", "Video mode": "Videa reĝimo", - "Videos": "Filmetoj", + "channel_tab_videos_label": "Videoj", "Playlists": "Ludlistoj", - "Community": "Komunumo", + "channel_tab_community_label": "Komunumo", "search_filters_sort_option_relevance": "rilateco", "search_filters_sort_option_rating": "takso", "search_filters_sort_option_date": "dato", @@ -472,5 +472,12 @@ "generic_subscribers_count_plural": "{{count}} abonantoj", "generic_count_months": "{{count}} monato", "generic_count_months_plural": "{{count}} monatoj", - "preferences_save_player_pos_label": "Konservi ludadan pozicion: " + "preferences_save_player_pos_label": "Konservi ludadan pozicion: ", + "channel_tab_streams_label": "Tujelsendoj", + "channel_tab_playlists_label": "Ludlistoj", + "channel_tab_channels_label": "Kanaloj", + "channel_tab_shorts_label": "Mallongaj", + "Music in this video": "Muziko en ĉi tiu video", + "Artist: ": "Artisto: ", + "Album: ": "Albumo: " } diff --git a/locales/es.json b/locales/es.json index 8603e9fe..bb082c06 100644 --- a/locales/es.json +++ b/locales/es.json @@ -52,21 +52,21 @@ "preferences_video_loop_label": "Repetir siempre: ", "preferences_autoplay_label": "Reproducción automática: ", "preferences_continue_label": "Reproducir siguiente por defecto: ", - "preferences_continue_autoplay_label": "Reproducir automáticamente el vídeo siguiente: ", + "preferences_continue_autoplay_label": "Reproducir automáticamente el video siguiente: ", "preferences_listen_label": "Activar el sonido por defecto: ", - "preferences_local_label": "¿Usar un proxy para los vídeos? ", + "preferences_local_label": "¿Usar un proxy para los videos? ", "preferences_speed_label": "Velocidad por defecto: ", - "preferences_quality_label": "Calidad de vídeo preferida: ", + "preferences_quality_label": "Calidad de video preferida: ", "preferences_volume_label": "Volumen del reproductor: ", "preferences_comments_label": "Comentarios por defecto: ", "youtube": "YouTube", "reddit": "Reddit", "preferences_captions_label": "Subtítulos por defecto: ", "Fallback captions: ": "Subtítulos alternativos: ", - "preferences_related_videos_label": "¿Mostrar vídeos relacionados? ", + "preferences_related_videos_label": "¿Mostrar videos relacionados? ", "preferences_annotations_label": "¿Mostrar anotaciones por defecto? ", - "preferences_extend_desc_label": "Extender automáticamente la descripción del vídeo: ", - "preferences_vr_mode_label": "Vídeos interactivos de 360 grados (necesita WebGL): ", + "preferences_extend_desc_label": "Extender automáticamente la descripción del video: ", + "preferences_vr_mode_label": "Videos interactivos de 360 grados (necesita WebGL): ", "preferences_category_visual": "Preferencias visuales", "preferences_player_style_label": "Estilo de reproductor: ", "Dark mode: ": "Modo oscuro: ", @@ -79,16 +79,16 @@ "preferences_category_subscription": "Preferencias de la suscripción", "preferences_annotations_subscribed_label": "¿Mostrar anotaciones por defecto para los canales suscritos? ", "Redirect homepage to feed: ": "Redirigir la página de inicio a la fuente: ", - "preferences_max_results_label": "Número de vídeos mostrados en la fuente: ", - "preferences_sort_label": "Ordenar los vídeos por: ", + "preferences_max_results_label": "Número de videos mostrados en la fuente: ", + "preferences_sort_label": "Ordenar los videos por: ", "published": "fecha de publicación", "published - reverse": "fecha de publicación: orden inverso", "alphabetically": "alfabéticamente", "alphabetically - reverse": "alfabéticamente: orden inverso", "channel name": "nombre del canal", "channel name - reverse": "nombre del canal: orden inverso", - "Only show latest video from channel: ": "Mostrar solo el último vídeo del canal: ", - "Only show latest unwatched video from channel: ": "Mostrar solo el último vídeo sin ver del canal: ", + "Only show latest video from channel: ": "Mostrar solo el último video del canal: ", + "Only show latest unwatched video from channel: ": "Mostrar solo el último video sin ver del canal: ", "preferences_unseen_only_label": "Mostrar solo los no vistos: ", "preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ", "Enable web notifications": "Habilitar notificaciones web", @@ -139,7 +139,7 @@ "Editing playlist `x`": "Editando la lista de reproducción 'x'", "Show more": "Mostrar más", "Show less": "Mostrar menos", - "Watch on YouTube": "Ver el vídeo en YouTube", + "Watch on YouTube": "Ver en YouTube", "Switch Invidious Instance": "Cambiar Instancia de Invidious", "Hide annotations": "Ocultar anotaciones", "Show annotations": "Mostrar anotaciones", @@ -153,7 +153,7 @@ "Shared `x`": "Compartido `x`", "Premieres in `x`": "Se estrena en `x`", "Premieres `x`": "Estrenos `x`", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tiene JavaScript desactivado. Haga clic aquí para ver los comentarios, pero tenga en cuenta que pueden tardar un poco más en cargarse.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.", "View YouTube comments": "Ver los comentarios de YouTube", "View more comments on Reddit": "Ver más comentarios en Reddit", "View `x` comments": { @@ -164,7 +164,7 @@ "Hide replies": "Ocultar las respuestas", "Show replies": "Mostrar las respuestas", "Incorrect password": "Contraseña incorrecta", - "Quota exceeded, try again in a few hours": "Cuota excedida, pruebe otra vez en unas horas", + "Quota exceeded, try again in a few hours": "Cuota excedida, prueba otra vez en unas horas", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No se puede iniciar sesión, asegúrese de que la autentificación de dos factores (autentificador o SMS) esté habilitada.", "Invalid TFA code": "Código TFA no válido", "Login failed. This may be because two-factor authentication is not turned on for your account.": "Error de inicio de sesion. Puede deberse a que la autentificación de dos factores no está habilitada en su cuenta.", @@ -176,7 +176,7 @@ "Wrong username or password": "Nombre o contraseña incorrecto", "Please sign in using 'Log in with Google'": "Inicie sesión con «Iniciar sesión con Google»", "Password cannot be empty": "La contraseña no puede estar en blanco", - "Password cannot be longer than 55 characters": "La contraseña no puede tener más de 55 caracteres", + "Password cannot be longer than 55 characters": "La contraseña no debe tener más de 55 caracteres", "Please log in": "Inicie sesión, por favor", "Invidious Private Feed for `x`": "Fuente privada de Invidious para `x`", "channel:`x`": "canal: `x`", @@ -198,7 +198,7 @@ "No such user": "Usuario no existe", "Token is expired, please try again": "El símbolo ha caducado, inténtelo de nuevo", "English": "Inglés", - "English (auto-generated)": "Inglés (generados automáticamente)", + "English (auto-generated)": "Inglés (generado automáticamente)", "Afrikaans": "Afrikáans", "Albanian": "Albanés", "Amharic": "Amárico", @@ -324,50 +324,51 @@ "permalink": "enlace permanente", "`x` marked it with a ❤": "`x` lo ha marcado con un ❤", "Audio mode": "Modo de audio", - "Video mode": "Modo de vídeo", - "Videos": "Vídeos", + "Video mode": "Modo de video", + "channel_tab_videos_label": "Videos", "Playlists": "Listas de reproducción", - "Community": "Comunidad", - "search_filters_sort_option_relevance": "relevancia", - "search_filters_sort_option_rating": "valoración", - "search_filters_sort_option_date": "fecha", - "search_filters_sort_option_views": "visualizaciones", - "search_filters_type_label": "content_type", + "channel_tab_community_label": "Comunidad", + "search_filters_sort_option_relevance": "Relevancia", + "search_filters_sort_option_rating": "Valoración", + "search_filters_sort_option_date": "Fecha de subida", + "search_filters_sort_option_views": "Visualizaciones", + "search_filters_type_label": "tipo de contenido", "search_filters_duration_label": "duración", "search_filters_features_label": "funcionalidades", "search_filters_sort_label": "ordenar", - "search_filters_date_option_hour": "hora", - "search_filters_date_option_today": "hoy", - "search_filters_date_option_week": "semana", - "search_filters_date_option_month": "mes", - "search_filters_date_option_year": "año", - "search_filters_type_option_video": "vídeo", - "search_filters_type_option_channel": "canal", - "search_filters_type_option_playlist": "lista de reproducción", - "search_filters_type_option_movie": "película", - "search_filters_type_option_show": "programa", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "subtítulos", - "search_filters_features_option_c_commons": "creative_commons", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "directo", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "ubicación", - "search_filters_features_option_hdr": "hdr", + "search_filters_date_option_hour": "Última hora", + "search_filters_date_option_today": "Hoy", + "search_filters_date_option_week": "Esta semana", + "search_filters_date_option_month": "Este mes", + "search_filters_date_option_year": "Este año", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Canal", + "search_filters_type_option_playlist": "Lista de reproducción", + "search_filters_type_option_movie": "Película", + "search_filters_type_option_show": "Programa", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Subtítulos", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "En directo", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Ubicación", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Versión actual: ", "next_steps_error_message": "Después de lo cual debes intentar: ", "next_steps_error_message_refresh": "Recargar la página", "next_steps_error_message_go_to_youtube": "Ir a YouTube", - "search_filters_duration_option_short": "Corto (< 4 minutos)", - "search_filters_duration_option_long": "Largo (> 20 minutos)", + "search_filters_duration_option_short": "Menos de 4 minutos", + "search_filters_duration_option_medium": "De 4 a 20 minutos", + "search_filters_duration_option_long": "Más de 20 minutos", "footer_documentation": "Documentación", "footer_original_source_code": "Código fuente original", - "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado", + "adminprefs_modified_source_code_url_label": "Enlace al repositorio de código fuente modificado", "footer_source_code": "Código fuente", "footer_modfied_source_code": "Código fuente modificado", "footer_donate_page": "Donar", "preferences_region_label": "País del contenido: ", - "preferences_quality_dash_label": "Calidad de vídeo DASH preferida: ", + "preferences_quality_dash_label": "Calidad de video DASH preferida: ", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_medium": "Intermedia", "preferences_quality_dash_option_auto": "Automática", @@ -376,7 +377,7 @@ "download_subtitles": "Subtítulos- `x` (.vtt)", "user_created_playlists": "`x` listas de reproducción creadas", "user_saved_playlists": "`x` listas de reproducción guardadas", - "Video unavailable": "Vídeo no disponible", + "Video unavailable": "Video no disponible", "videoinfo_youTube_embed_link": "Insertar", "preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_4320p": "4320p", @@ -413,8 +414,9 @@ "generic_count_weeks_plural": "{{count}} semanas", "generic_playlists_count": "{{count}} lista de reproducción", "generic_playlists_count_plural": "{{count}} listas de reproducción", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} videos", + "generic_videos_count_2": "{{count}} videos", "generic_count_months": "{{count}} mes", "generic_count_months_plural": "{{count}} meses", "comments_points_count": "{{count}} punto", @@ -433,7 +435,7 @@ "crash_page_search_issue": "buscado problemas existentes en GitHub", "crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!", "crash_page_refresh": "probado a recargar la página", - "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, abre una nueva incidencia en GitHub (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):", + "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, abre una nueva incidencia en GitHub (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:", "English (United States)": "Inglés (Estados Unidos)", "Cantonese (Hong Kong)": "Cantonés (Hong Kong)", "Dutch (auto-generated)": "Neerlandés (generados automáticamente)", @@ -461,16 +463,24 @@ "search_message_no_results": "No se han encontrado resultados.", "search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.", "search_filters_title": "Filtros", - "search_filters_date_label": "Fecha de subida", + "search_filters_date_label": "fecha de subida", "search_filters_date_option_none": "Cualquier fecha", "search_filters_type_option_all": "Cualquier tipo", "search_filters_duration_option_none": "Cualquier duración", "search_filters_features_option_vr180": "VR180", - "search_filters_apply_button": "Aplicar filtros seleccionados", + "search_filters_apply_button": "Aplicar filtros", "tokens_count": "{{count}} ficha", "tokens_count_plural": "{{count}} fichas", "search_message_use_another_instance": " También puede buscar en otra instancia.", - "search_filters_duration_option_medium": "Medio (4 - 20 minutes)", "Popular enabled: ": "¿Habilitar la sección popular? ", - "error_video_not_in_playlist": "El vídeo solicitado no existe en esta lista de reproducción. Haga clic aquí para acceder a la página de inicio de la lista de reproducción." + "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. Haz clic aquí para acceder a la página de inicio de la lista de reproducción.", + "channel_tab_streams_label": "Directos", + "channel_tab_channels_label": "Canales", + "channel_tab_shorts_label": "Cortos", + "channel_tab_playlists_label": "Listas de reproducción", + "Music in this video": "Música en este video", + "Artist: ": "Artista: ", + "Album: ": "Álbum: ", + "Song: ": "Canción: ", + "Channel Sponsor": "Patrocinador del canal" } diff --git a/locales/et.json b/locales/et.json index 7beb1749..74338aba 100644 --- a/locales/et.json +++ b/locales/et.json @@ -296,8 +296,8 @@ "Corsican": "Korsika", "Javanese": "Jaava", "Lithuanian": "Leedu", - "Videos": "Videod", - "Community": "Kogukond", + "channel_tab_videos_label": "Videod", + "channel_tab_community_label": "Kogukond", "CAPTCHA is a required field": "CAPTCHA on kohustuslik väli", "comments_points_count": "{{count}} punkt", "comments_points_count_plural": "{{count}} punkti", diff --git a/locales/fa.json b/locales/fa.json index 3a8f547f..56685f64 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -26,15 +26,15 @@ "No": "خیر", "Import and Export Data": "درون‌برد و برون‌برد داده", "Import": "درون‌برد", - "Import Invidious data": "درون‌برد داده اینویدیوس", - "Import YouTube subscriptions": "درون‌برد اشتراک‌های یوتیوب", + "Import Invidious data": "وارد کردن داده JSON اینویدیوس", + "Import YouTube subscriptions": "وارد کردن اشتراک OPML/ یوتیوب", "Import FreeTube subscriptions (.db)": "درون‌برد اشتراک‌های فری‌تیوب (.db)", "Import NewPipe subscriptions (.json)": "درون‌برد اشتراک‌های نیوپایپ (.json)", "Import NewPipe data (.zip)": "درون‌برد داده نیوپایپ (.zip)", "Export": "برون‌برد", "Export subscriptions as OPML": "برون‌برد اشتراک‌ها در قالب OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "برون‌برد اشتراک‌ها در قالب OPML (برای نیوپایپ و فری‌تیوب)", - "Export data as JSON": "برون‌برد داده در قالب JSON", + "Export data as JSON": "گرفتن(خارج کردن) اطلاعات اینویدیوس با فرمت JSON", "Delete account?": "حذف حساب کاربری؟", "History": "تاریخچه", "An alternative front-end to YouTube": "یک پیشانه جایگزین برای یوتیوب", @@ -71,7 +71,7 @@ "preferences_related_videos_label": "نمایش ویدیو های مرتبط: ", "preferences_annotations_label": "نمایش حاشیه نویسی ها به طور پیشفرض: ", "preferences_extend_desc_label": "گسترش خودکار توضیحات ویدئو: ", - "preferences_vr_mode_label": "ویدئوها ۳۶۰ درجه تعاملی: ", + "preferences_vr_mode_label": "ویدئوها ۳۶۰ درجه تعاملی(نیازمند WebGL): ", "preferences_category_visual": "ترجیحات بصری", "preferences_player_style_label": "حالت پخش کننده: ", "Dark mode: ": "حالت تاریک: ", @@ -80,7 +80,7 @@ "light": "روشن", "preferences_thin_mode_label": "حالت نازک: ", "preferences_category_misc": "ترجیحات متفرقه", - "preferences_automatic_instance_redirect_label": "هدایت خودکار نمونه (به طور پیش‌فرض به redirect.invidious.io): ", + "preferences_automatic_instance_redirect_label": "هدایت خودکار نمونه (انتقال به redirect.invidious.io): ", "preferences_category_subscription": "ترجیحات اشتراک", "preferences_annotations_subscribed_label": "نمایش حاشیه نویسی ها به طور پیشفرض برای کانال های مشترک شده: ", "Redirect homepage to feed: ": "تغییر مسیر صفحه خانه به خوراک: ", @@ -157,7 +157,7 @@ "Engagement: ": "نامزدی: ", "Whitelisted regions: ": "مناطق لیست سفید: ", "Blacklisted regions: ": "مناطق لیست سیاه: ", - "Shared `x`": "به اشتراک گذاشته شده `x`", + "Shared `x`": "`x` به اشتراک گذاشته شد", "Premieres in `x`": "برای اولین بار در `x`", "Premieres `x`": "برای اولین بار `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "سلام! مثل اینکه تو جاوا اسکریپت رو خاموش کرده ای. اینجا کلیک کن تا نظرات را ببینی، این رو یادت باشه که ممکنه بارگذاری اونها کمی طول بکشه.", @@ -341,9 +341,9 @@ "`x` marked it with a ❤": "`x` نشان گذاری شده با یک ❤", "Audio mode": "حالت صدا", "Video mode": "حالت ویدیو", - "Videos": "ویدیو ها", + "channel_tab_videos_label": "ویدیو ها", "Playlists": "سیاهه‌های پخش", - "Community": "اجتماع", + "channel_tab_community_label": "اجتماع", "search_filters_sort_option_relevance": "مرتبط بودن", "search_filters_sort_option_rating": "امتیاز", "search_filters_sort_option_date": "تاریخ بارگذاری", @@ -375,7 +375,7 @@ "next_steps_error_message_refresh": "تازه‌سازی", "next_steps_error_message_go_to_youtube": "رفتن به یوتیوب", "preferences_quality_option_hd720": "HD720", - "preferences_quality_option_dash": "DASH (کیفیت قابل تطبیق)", + "preferences_quality_option_dash": "DASH (کیفیت تطبیفی)", "preferences_quality_option_medium": "میانه", "preferences_quality_option_small": "پایین", "preferences_quality_dash_option_auto": "خودکار", @@ -408,9 +408,9 @@ "preferences_region_label": "کشور محتوا: ", "footer_documentation": "مستندات", "footer_original_source_code": "کد منبع اصلی", - "search_filters_duration_option_long": "بلند (> 20 دقیقه)", + "search_filters_duration_option_long": "بلند (> ۲۰ دقیقه)", "adminprefs_modified_source_code_url_label": "URL مخزن کد منبع ویریش شده", - "search_filters_duration_option_short": "کوتاه (< 4 دقیقه)", + "search_filters_duration_option_short": "کوتاه (< ۴ دقیقه)", "search_filters_title": "پالایه", "Chinese (Hong Kong)": "چینی (هنگ‌کنگ)", "Dutch (auto-generated)": "هلندی (تولید خودکار)", @@ -424,5 +424,31 @@ "search_message_no_results": "نتیجه‌ای یافت نشد.", "search_message_change_filters_or_query": "سعی کنید جست‌و‌جوی خود را وسیع‌تر کنید و/یا فیلترها را تغییر دهید.", "Chinese (China)": "چینی (چین)", - "German (auto-generated)": "آلمانی (تولید خودکار)" + "German (auto-generated)": "آلمانی (تولید خودکار)", + "Japanese (auto-generated)": "ژاپنی (تولید خودکار)", + "Korean (auto-generated)": "کره‌ای (تولید خودکار)", + "Portuguese (Brazil)": "پرتغالی (برزیل)", + "search_filters_apply_button": "اعمال فیلترهای انتخاب شده", + "Italian (auto-generated)": "ایتالیایی (تولید خودکار)", + "Vietnamese (auto-generated)": "ویتنامی (تولید خودکار)", + "search_filters_type_option_all": "هر نوعی", + "search_filters_duration_option_none": "هر مدت زمانی", + "search_filters_date_label": "تاریخ بارگذاری", + "search_filters_date_option_none": "هر تاریخی", + "user_created_playlists": "`x` فهرست پخش ایجاد شد", + "Interlingue": "سرخپوستی", + "Russian (auto-generated)": "روسی (تولید خودکار)", + "Spanish (auto-generated)": "اسپانیایی (تولید خودکار)", + "search_filters_duration_option_medium": "متوسط (۴ تا ۲۰ دقیقه)", + "Portuguese (auto-generated)": "پرتغالی (تولید خودکار)", + "Cantonese (Hong Kong)": "کانتونی (هنگ کنگ)", + "Spanish (Spain)": "اسپانیایی (اسپانیا)", + "Turkish (auto-generated)": "ترکی (تولید خودکار)", + "search_filters_features_option_vr180": "VR180", + "Spanish (Mexico)": "اسپانیایی (مکزیک)", + "Popular enabled: ": "محبوب ها فعال شد: ", + "Music in this video": "آهنگ در این ویدیو", + "Artist: ": "هنرمند: ", + "Album: ": "آلبوم: ", + "Song: ": "آهنگ: " } diff --git a/locales/fi.json b/locales/fi.json index bef9027f..366a2739 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -324,9 +324,9 @@ "`x` marked it with a ❤": "`x` merkkasi ❤:llä", "Audio mode": "Äänitila", "Video mode": "Videotila", - "Videos": "Videot", + "channel_tab_videos_label": "Videot", "Playlists": "Soittolistat", - "Community": "Yhteisö", + "channel_tab_community_label": "Yhteisö", "search_filters_sort_option_relevance": "Osuvuus", "search_filters_sort_option_rating": "Arvostelu", "search_filters_sort_option_date": "Latauspäivämäärä", diff --git a/locales/fr.json b/locales/fr.json index 2f384eb1..9d3e117f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -358,9 +358,9 @@ "`x` marked it with a ❤": "`x` l'a marqué d'un ❤", "Audio mode": "Mode audio", "Video mode": "Mode vidéo", - "Videos": "Vidéos", + "channel_tab_videos_label": "Vidéos", "Playlists": "Listes de lecture", - "Community": "Communauté", + "channel_tab_community_label": "Communauté", "search_filters_sort_option_relevance": "Pertinence", "search_filters_sort_option_rating": "Notation", "search_filters_sort_option_date": "Date d'ajout", @@ -472,5 +472,9 @@ "search_filters_date_label": "Date d'ajout", "search_filters_features_option_vr180": "VR180", "search_filters_duration_option_none": "Toutes les durées", - "error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. Cliquez ici pour retourner à la liste de lecture." + "error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. Cliquez ici pour retourner à la liste de lecture.", + "channel_tab_shorts_label": "Clips", + "channel_tab_streams_label": "En direct", + "channel_tab_playlists_label": "Listes de lecture", + "channel_tab_channels_label": "Chaînes" } diff --git a/locales/he.json b/locales/he.json index 384b2657..ab42313b 100644 --- a/locales/he.json +++ b/locales/he.json @@ -271,9 +271,9 @@ "`x` marked it with a ❤": "סומנה ב־❤ על ידי `x`", "Audio mode": "Audio mode", "Video mode": "Video mode", - "Videos": "סרטונים", + "channel_tab_videos_label": "סרטונים", "Playlists": "פלייליסטים", - "Community": "קהילה", + "channel_tab_community_label": "קהילה", "search_filters_sort_option_relevance": "רלוונטיות", "search_filters_sort_option_rating": "דירוג", "search_filters_sort_option_date": "תאריך העלאה", diff --git a/locales/hi.json b/locales/hi.json index 32ae7823..41335266 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -401,12 +401,12 @@ "(edited)": "(संपादित)", "YouTube comment permalink": "YouTube पर टिप्पणी की स्थायी कड़ी", "permalink": "स्थायी कड़ी", - "Videos": "वीडियो", + "channel_tab_videos_label": "वीडियो", "`x` marked it with a ❤": "`x` ने इसे एक ❤ से चिह्नित किया", "Audio mode": "ऑडियो मोड", "Playlists": "प्लेलिस्ट्स", "Video mode": "वीडियो मोड", - "Community": "समुदाय", + "channel_tab_community_label": "समुदाय", "search_filters_title": "फ़िल्टर", "search_filters_date_label": "अपलोड करने का समय", "search_filters_date_option_none": "कोई भी समय", @@ -470,5 +470,14 @@ "crash_page_switch_instance": "किसी दूसरे उदाहरण का इस्तेमाल करें", "crash_page_read_the_faq": "अक्सर पूछे जाने वाले प्रश्न (FAQ) पढ़ें", "crash_page_refresh": "पृष्ठ को एक बार साफ़ करें", - "crash_page_search_issue": "GitHub पर मौजूदा मुद्दे ढूँढ़ें" + "crash_page_search_issue": "GitHub पर मौजूदा मुद्दे ढूँढ़ें", + "Popular enabled: ": "लोकप्रिय सक्षम: ", + "Artist: ": "कलाकार: ", + "Music in this video": "इस वीडियो में संगीत", + "Album: ": "एल्बम: ", + "error_video_not_in_playlist": "अनुरोधित वीडियो इस प्लेलिस्ट में मौजूद नहीं है। प्लेलिस्ट के मुखपृष्ठ पर जाने के लिए यहाँ क्लिक करें।", + "channel_tab_shorts_label": "शॉर्ट्स", + "channel_tab_streams_label": "लाइवस्ट्रीम्स", + "channel_tab_playlists_label": "प्लेलिस्ट्स", + "channel_tab_channels_label": "चैनल्स" } diff --git a/locales/hr.json b/locales/hr.json index e42cc4f5..ade732ad 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -7,8 +7,8 @@ "View playlist on YouTube": "Prikaži zbirku na YouTubeu", "newest": "najnovije", "oldest": "najstarije", - "popular": "popularni", - "last": "zadnji", + "popular": "popularne", + "last": "zadnje", "Next page": "Sljedeća stranica", "Previous page": "Prethodna stranica", "Clear watch history?": "Izbrisati povijest gledanja?", @@ -43,9 +43,9 @@ "Time (h:mm:ss):": "Vrijeme (h:mm:ss):", "Text CAPTCHA": "Tekstualni CAPTCHA", "Image CAPTCHA": "Slikovni CAPTCHA", - "Sign In": "Prijava", + "Sign In": "Prijavi se", "Register": "Registriraj se", - "E-mail": "E-mail", + "E-mail": "E-mail adresa", "Google verification code": "Googleov potvrdni kod", "Preferences": "Postavke", "preferences_category_player": "Postavke playera", @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "Označeno sa ❤ od `x`", "Audio mode": "Audio modus", "Video mode": "Videomodus", - "Videos": "Videa", + "channel_tab_videos_label": "Videa", "Playlists": "Zbirke", - "Community": "Zajednica", + "channel_tab_community_label": "Zajednica", "search_filters_sort_option_relevance": "Značaj", "search_filters_sort_option_rating": "Ocjena", "search_filters_sort_option_date": "Datum prijenosa", @@ -359,13 +359,13 @@ "next_steps_error_message_refresh": "Aktualiziraj stranicu", "next_steps_error_message_go_to_youtube": "Idi na YouTube", "footer_donate_page": "Doniraj", - "adminprefs_modified_source_code_url_label": "URL do repozitorija izmijenjenog izvornog koda", + "adminprefs_modified_source_code_url_label": "URL do repozitorija prilagođenog izvornog koda", "search_filters_duration_option_short": "Kratko (< 4 minute)", "search_filters_duration_option_long": "Dugo (> 20 minute)", "footer_source_code": "Izvorni kod", - "footer_modfied_source_code": "Izmijenjeni izvorni kod", + "footer_modfied_source_code": "Prilagođen izvorni kod", "footer_documentation": "Dokumentacija", - "footer_original_source_code": "Izvoran izvorni kod", + "footer_original_source_code": "Prvobitan izvorni kod", "preferences_region_label": "Zemlja sadržaja: ", "preferences_quality_dash_label": "Preferirana DASH videokvaliteta: ", "preferences_quality_option_dash": "DASH (adaptativna kvaliteta)", @@ -488,5 +488,14 @@ "search_filters_apply_button": "Primijeni odabrane filtre", "search_filters_type_option_all": "Bilo koja vrsta", "Popular enabled: ": "Popularni aktivirani: ", - "error_video_not_in_playlist": "Traženi video ne postoji u ovoj zbirci. Pritisni ovdje za početnu stranicu zbirke." + "error_video_not_in_playlist": "Traženi video ne postoji u ovoj zbirci. Pritisni ovdje za početnu stranicu zbirke.", + "channel_tab_streams_label": "Prijenosi uživo", + "channel_tab_playlists_label": "Zbirke", + "channel_tab_channels_label": "Kanali", + "channel_tab_shorts_label": "Kratka videa", + "Music in this video": "Glazba u ovom videu", + "Album: ": "Album: ", + "Artist: ": "Izvođač: ", + "Channel Sponsor": "Sponzor kanala", + "Song: ": "Pjesma: " } diff --git a/locales/hu-HU.json b/locales/hu-HU.json index 19ada1d8..f93930e0 100644 --- a/locales/hu-HU.json +++ b/locales/hu-HU.json @@ -348,9 +348,9 @@ "`x` marked it with a ❤": "`x` ❤ jelet adott a hozzászóláshoz", "Audio mode": "Csak hanggal", "Video mode": "Hanggal és képpel", - "Videos": "Videói", + "channel_tab_videos_label": "Videói", "Playlists": "Lejátszási listái", - "Community": "Közösség", + "channel_tab_community_label": "Közösség", "Current version: ": "Jelenlegi verzió: ", "preferences_quality_option_medium": "Közepes", "preferences_quality_dash_option_auto": "Automatikus", diff --git a/locales/id.json b/locales/id.json index a30f0ad4..51d6d55c 100644 --- a/locales/id.json +++ b/locales/id.json @@ -341,9 +341,9 @@ "`x` marked it with a ❤": "`x` telah ditandai dengan ❤", "Audio mode": "Mode audio", "Video mode": "Mode video", - "Videos": "Video", + "channel_tab_videos_label": "Video", "Playlists": "Daftar putar", - "Community": "Komunitas", + "channel_tab_community_label": "Komunitas", "search_filters_sort_option_relevance": "Relevansi", "search_filters_sort_option_rating": "Penilaian", "search_filters_sort_option_date": "Tanggal Unggah", diff --git a/locales/is.json b/locales/is.json index 99bd6574..3282eb50 100644 --- a/locales/is.json +++ b/locales/is.json @@ -315,9 +315,9 @@ "`x` marked it with a ❤": "`x` merkti það með ❤", "Audio mode": "Hljóð ham", "Video mode": "Myndband ham", - "Videos": "Myndbönd", + "channel_tab_videos_label": "Myndbönd", "Playlists": "Spilunarlistar", - "Community": "Samfélag", + "channel_tab_community_label": "Samfélag", "Current version: ": "Núverandi útgáfa: ", "preferences_watch_history_label": "Virkja áhorfssögu: " } diff --git a/locales/it.json b/locales/it.json index c195f3b9..c60f760b 100644 --- a/locales/it.json +++ b/locales/it.json @@ -344,9 +344,8 @@ "`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤", "Audio mode": "Modalità audio", "Video mode": "Modalità video", - "Videos": "Video", + "channel_tab_videos_label": "Video", "Playlists": "Playlist", - "Community": "Comunità", "search_filters_sort_option_relevance": "Pertinenza", "search_filters_sort_option_rating": "Valutazione", "search_filters_sort_option_date": "Data di caricamento", @@ -472,5 +471,13 @@ "search_filters_features_option_vr180": "VR180", "search_filters_apply_button": "Applica filtri selezionati", "crash_page_refresh": "provato a ricaricare la pagina", - "error_video_not_in_playlist": "Il video richiesto non esiste in questa playlist. Fai clic qui per la pagina iniziale della playlist." + "error_video_not_in_playlist": "Il video richiesto non esiste in questa playlist. Fai clic qui per la pagina iniziale della playlist.", + "channel_tab_shorts_label": "Short", + "channel_tab_playlists_label": "Playlist", + "channel_tab_channels_label": "Canali", + "channel_tab_streams_label": "Livestream", + "channel_tab_community_label": "Comunità", + "Music in this video": "Musica in questo video", + "Artist: ": "Artista: ", + "Album: ": "Album: " } diff --git a/locales/ja.json b/locales/ja.json index 4971c472..8a4537d4 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -5,7 +5,7 @@ "generic_subscribers_count_0": "{{count}} 人の登録者", "generic_subscriptions_count_0": "{{count}} 個の登録チャンネル", "LIVE": "ライブ", - "Shared `x` ago": "`x`前に共有", + "Shared `x` ago": "`x`前に公開", "Unsubscribe": "登録解除", "Subscribe": "登録", "View channel on YouTube": "YouTube でチャンネルを見る", @@ -53,34 +53,34 @@ "E-mail": "メールアドレス", "Google verification code": "Google 認証コード", "Preferences": "設定", - "preferences_category_player": "プレイヤー設定", + "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", "preferences_autoplay_label": "自動再生: ", - "preferences_continue_label": "デフォルトで次を再生: ", + "preferences_continue_label": "次の動画を再生: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", - "preferences_listen_label": "デフォルトでオーディオモードを使用: ", - "preferences_local_label": "動画をプロキシーに通す: ", - "preferences_speed_label": "デフォルトの再生速度: ", + "preferences_listen_label": "デフォルトで音声モードを使用: ", + "preferences_local_label": "動画視聴にプロキシーを経由: ", + "preferences_speed_label": "標準の再生速度: ", "preferences_quality_label": "優先する画質: ", "preferences_volume_label": "プレイヤーの音量: ", "preferences_comments_label": "デフォルトのコメント: ", "youtube": "YouTube", "reddit": "Reddit", - "preferences_captions_label": "デフォルトの字幕: ", + "preferences_captions_label": "優先する字幕: ", "Fallback captions: ": "フォールバック時の字幕: ", "preferences_related_videos_label": "関連動画を表示: ", "preferences_annotations_label": "デフォルトでアノテーションを表示: ", "preferences_extend_desc_label": "動画の説明文を自動的に拡張: ", "preferences_vr_mode_label": "対話的な360°動画 (WebGL が必要): ", "preferences_category_visual": "外観設定", - "preferences_player_style_label": "プレイヤースタイル: ", + "preferences_player_style_label": "プレイヤーのスタイル: ", "Dark mode: ": "ダークモード: ", "preferences_dark_mode_label": "テーマ: ", "dark": "ダーク", "light": "ライト", "preferences_thin_mode_label": "最小モード: ", - "preferences_category_misc": "雑設定", - "preferences_automatic_instance_redirect_label": "自動的なインスタンスの移転(redirect.invidious.ioにフォールバック): ", + "preferences_category_misc": "ほかの設定", + "preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.ioにフォールバック): ", "preferences_category_subscription": "登録チャンネル設定", "preferences_annotations_subscribed_label": "デフォルトで登録チャンネルのアノテーションを表示しますか? ", "Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ", @@ -108,7 +108,7 @@ "Watch history": "再生履歴", "Delete account": "アカウントを削除", "preferences_category_admin": "管理者設定", - "preferences_default_home_label": "デフォルトのホーム: ", + "preferences_default_home_label": "ホームに表示するページ: ", "preferences_feed_menu_label": "フィードメニュー: ", "preferences_show_nick_label": "ニックネームを一番上に表示する: ", "Top enabled: ": "トップページを有効化: ", @@ -117,8 +117,8 @@ "Registration enabled: ": "登録を有効化: ", "Report statistics: ": "統計を報告: ", "Save preferences": "設定を保存", - "Subscription manager": "登録チャンネルマネージャー", - "Token manager": "トークンマネージャー", + "Subscription manager": "登録チャンネルの管理", + "Token manager": "トークンの管理", "Token": "トークン", "tokens_count_0": "{{count}} 個のトークン", "Import/export": "インポート/エクスポート", @@ -128,7 +128,7 @@ "subscriptions_unseen_notifs_count_0": "{{count}} 個の未読通知", "search": "検索", "Log out": "ログアウト", - "Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開されています。", + "Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開", "Source available here.": "ソースはここで閲覧可能です。", "View JavaScript license information.": "JavaScript ライセンス情報", "View privacy policy.": "プライバシーポリシー", @@ -136,28 +136,28 @@ "Public": "公開", "Unlisted": "限定公開", "Private": "非公開", - "View all playlists": "再生リストをすべて見る", + "View all playlists": "すべての再生リストを表示", "Updated `x` ago": "`x`前に更新", "Delete playlist `x`?": "再生リスト `x` を削除しますか?", "Delete playlist": "再生リストを削除", "Create playlist": "再生リストを作成", "Title": "タイトル", - "Playlist privacy": "再生リストのプライバシー", + "Playlist privacy": "再生リストの公開設定", "Editing playlist `x`": "再生リスト `x` を編集中", - "Show more": "表示を増やす", - "Show less": "表示を減らす", + "Show more": "もっと見る", + "Show less": "表示を少なく", "Watch on YouTube": "YouTube で視聴", - "Switch Invidious Instance": "Invidiousインスタンスの変更", + "Switch Invidious Instance": "Invidious インスタンスの変更", "Hide annotations": "アノテーションを隠す", "Show annotations": "アノテーションを表示", "Genre: ": "ジャンル: ", "License: ": "ライセンス: ", "Family friendly? ": "家族向け: ", - "Wilson score: ": "ウィルソンスコア: ", + "Wilson score: ": "ウィルソン得点区間: ", "Engagement: ": "エンゲージメント: ", "Whitelisted regions: ": "ホワイトリストの地域: ", "Blacklisted regions: ": "ブラックリストの地域: ", - "Shared `x`": "`x`に共有", + "Shared `x`": "公開日 `x`", "Premieres in `x`": "`x`後にプレミア公開", "Premieres `x`": "`x`にプレミア公開", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "やあ!君は JavaScript を無効にしているのかな?ここをクリックしてコメントを見れるけど、読み込みには少し時間がかかることがあるのを覚えておいてね。", @@ -181,31 +181,31 @@ "User ID is a required field": "ユーザー ID は必須項目です", "Password is a required field": "パスワードは必須項目です", "Wrong username or password": "ユーザー名またはパスワードが間違っています", - "Please sign in using 'Log in with Google'": "'Google でログイン' を使用してログインしてください", - "Password cannot be empty": "パスワードを空にすることはできません", + "Please sign in using 'Log in with Google'": "「Google でログイン」を使用してログインしてください", + "Password cannot be empty": "パスワードは空にできません", "Password cannot be longer than 55 characters": "パスワードは55文字より長くできません", - "Please log in": "ログインをしてください", - "Invidious Private Feed for `x`": "`x` の Invidious プライベートフィード", + "Please log in": "ログインしてください", + "Invidious Private Feed for `x`": "`x` 個人の Invidious によるフィード", "channel:`x`": "チャンネル:`x`", "Deleted or invalid channel": "削除済みまたは無効なチャンネルです", "This channel does not exist.": "このチャンネルは存在しません。", "Could not get channel info.": "チャンネル情報を取得できませんでした。", "Could not fetch comments": "コメントを取得できませんでした", - "comments_view_x_replies_0": "{{count}} 件の返信を見る", + "comments_view_x_replies_0": "{{count}}件の返信を表示", "`x` ago": "`x`前", - "Load more": "もっと読み込む", - "comments_points_count_0": "{{count}} ポイント", + "Load more": "もっと見る", + "comments_points_count_0": "{{count}}点", "Could not create mix.": "ミックスを作成できませんでした。", "Empty playlist": "空の再生リスト", "Not a playlist.": "再生リストではありません。", "Playlist does not exist.": "再生リストが存在しません。", "Could not pull trending pages.": "急上昇ページを取得できませんでした。", - "Hidden field \"challenge\" is a required field": "非表示項目 \"challenge\" は必須項目です", - "Hidden field \"token\" is a required field": "非表示項目 \"token\" は必須項目です", + "Hidden field \"challenge\" is a required field": "非表示項目 challenge は必須項目です", + "Hidden field \"token\" is a required field": "非表示項目 token は必須項目です", "Erroneous challenge": "チャレンジが間違っています", "Erroneous token": "トークンが間違っています", "No such user": "ユーザーが存在しません", - "Token is expired, please try again": "トークンが期限切れです。再度試してください", + "Token is expired, please try again": "トークンが期限切れです。再度お試しください", "English": "英語", "English (auto-generated)": "英語 (自動生成)", "Afrikaans": "アフリカーンス語", @@ -313,7 +313,7 @@ "Yoruba": "ヨルバ語", "Zulu": "ズール語", "generic_count_years_0": "{{count}}年", - "generic_count_months_0": "{{count}}ヶ月", + "generic_count_months_0": "{{count}}か月", "generic_count_weeks_0": "{{count}}週", "generic_count_days_0": "{{count}}日", "generic_count_hours_0": "{{count}}時間", @@ -338,21 +338,21 @@ "(edited)": "(編集済み)", "YouTube comment permalink": "YouTube コメントのパーマリンク", "permalink": "パーマリンク", - "`x` marked it with a ❤": "`x` が❤を込めてマークしました", - "Audio mode": "オーディオモード", - "Video mode": "ビデオモード", - "Videos": "動画", - "Playlists": "プレイリスト", - "Community": "コミュニティ", - "search_filters_sort_option_relevance": "関連", + "`x` marked it with a ❤": "`x` が❤を送りました", + "Audio mode": "音声モード", + "Video mode": "動画モード", + "channel_tab_videos_label": "動画", + "Playlists": "再生リスト", + "channel_tab_community_label": "コミュニティ", + "search_filters_sort_option_relevance": "関連度", "search_filters_sort_option_rating": "評価", - "search_filters_sort_option_date": "時刻", + "search_filters_sort_option_date": "アップロード日", "search_filters_sort_option_views": "再生回数", - "search_filters_type_label": "コンテンツの種類", + "search_filters_type_label": "種類", "search_filters_duration_label": "再生時間", - "search_filters_features_label": "機能", + "search_filters_features_label": "特徴", "search_filters_sort_label": "順番", - "search_filters_date_option_hour": "1時間前", + "search_filters_date_option_hour": "1時間以内", "search_filters_date_option_today": "今日", "search_filters_date_option_week": "今週", "search_filters_date_option_month": "今月", @@ -366,7 +366,7 @@ "search_filters_features_option_subtitles": "字幕", "search_filters_features_option_c_commons": "クリエイティブ・コモンズ", "search_filters_features_option_three_d": "3D", - "search_filters_features_option_live": "生配信", + "search_filters_features_option_live": "ライブ", "search_filters_features_option_four_k": "4K", "search_filters_features_option_location": "場所", "search_filters_features_option_hdr": "HDR", @@ -377,9 +377,9 @@ "search_filters_duration_option_short": "4 分未満", "footer_documentation": "文書", "footer_source_code": "ソースコード", - "footer_original_source_code": "ソースコード(元)", - "footer_modfied_source_code": "ソースコード(編集)", - "adminprefs_modified_source_code_url_label": "編集したソースコードのレポジトリーURL", + "footer_original_source_code": "元のソースコード", + "footer_modfied_source_code": "改変して使用", + "adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL", "search_filters_duration_option_long": "20 分以上", "preferences_region_label": "地域: ", "footer_donate_page": "寄付する", @@ -406,10 +406,10 @@ "preferences_quality_option_dash": "DASH (適応品質)", "preferences_quality_dash_option_worst": "最悪", "preferences_quality_dash_option_best": "最高", - "videoinfo_started_streaming_x_ago": "`x`分前に配信を開始", + "videoinfo_started_streaming_x_ago": "`x`前に配信を開始", "videoinfo_watch_on_youTube": "YouTube上で見る", - "user_created_playlists": "`x`が作成したプレイリスト", - "Video unavailable": "ビデオは利用できません", + "user_created_playlists": "`x`個の作成した再生リスト", + "Video unavailable": "動画は利用できません", "Chinese": "中国語", "Chinese (Taiwan)": "中国語 (台湾)", "Korean (auto-generated)": "韓国語 (自動生成)", @@ -434,24 +434,36 @@ "Vietnamese (auto-generated)": "ベトナム語 (自動生成)", "search_filters_title": "フィルタ", "search_filters_features_option_three_sixty": "360°", - "search_message_change_filters_or_query": "別のキーワードを試してみるか、検索フィルタを削除してください", - "search_message_no_results": "一致する検索結果はありませんでした", + "search_message_change_filters_or_query": "別の検索語句を試したり、検索フィルタを変更してください。", + "search_message_no_results": "一致する検索結果はありません。", "English (United States)": "英語 (アメリカ)", "search_filters_date_label": "アップロード日", "search_filters_features_option_vr180": "VR180", - "crash_page_switch_instance": "別のインスタンスを使用しようとしました", + "crash_page_switch_instance": "別のインスタンスを使用を試す", "crash_page_read_the_faq": "よくある質問 (FAQ) を読む", "Popular enabled: ": "人気動画を有効化 ", - "search_message_use_another_instance": " 別のインスタンスで検索することもできます。", + "search_message_use_another_instance": " 別のインスタンス上でも検索できます。", "search_filters_apply_button": "選択したフィルターを適用", - "user_saved_playlists": "`x` 個の保存済みプレイリスト", - "crash_page_you_found_a_bug": "Invidious でバグを見つけたようです。", - "crash_page_refresh": "ページを更新しようとしました", - "preferences_watch_history_label": "視聴履歴を有効化 ", - "search_filters_date_option_none": "任意の日付", - "search_filters_type_option_all": "いかなるタイプ", - "search_filters_duration_option_none": "任意の期間", - "search_filters_duration_option_medium": "ミディアム (4 ~ 20 分)", + "user_saved_playlists": "`x` 個の保存した再生リスト", + "crash_page_you_found_a_bug": "Invidious のバグのようです!", + "crash_page_refresh": "ページを更新を試す", + "preferences_watch_history_label": "再生履歴を有効化 ", + "search_filters_date_option_none": "すべて", + "search_filters_type_option_all": "すべての種類", + "search_filters_duration_option_none": "すべての長さ", + "search_filters_duration_option_medium": "4 ~ 20 分", "preferences_save_player_pos_label": "再生位置を保存: ", - "crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。" + "crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。", + "crash_page_report_issue": "上記が助けにならないなら、GitHub に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてください(テキストは翻訳しない)。", + "crash_page_search_issue": "GitHub の既存の問題 (issue) を検索", + "channel_tab_streams_label": "ライブ", + "channel_tab_playlists_label": "再生リスト", + "error_video_not_in_playlist": "要求された動画はこの再生リスト内に存在しません。再生リストのホームへ。", + "channel_tab_shorts_label": "ショート", + "channel_tab_channels_label": "チャンネル", + "Music in this video": "この動画の音楽", + "Artist: ": "アーティスト: ", + "Album: ": "アルバム: ", + "Song: ": "曲: ", + "Channel Sponsor": "チャンネルのスポンサー" } diff --git a/locales/ko.json b/locales/ko.json index 28b518a2..d4f3a711 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -2,7 +2,7 @@ "preferences_sort_label": "동영상 정렬 기준: ", "preferences_max_results_label": "피드에 표시된 동영상 수: ", "Redirect homepage to feed: ": "피드로 홈페이지 리디렉션: ", - "preferences_annotations_subscribed_label": "구독한 채널에 기본적으로 특수효과를 표시하시겠습니까? ", + "preferences_annotations_subscribed_label": "구독한 채널에 기본으로 주석 표시: ", "preferences_category_subscription": "구독 설정", "preferences_automatic_instance_redirect_label": "자동 인스턴스 리디렉션 (redirect.invidious.io로 대체): ", "preferences_thin_mode_label": "단순 모드: ", @@ -11,7 +11,7 @@ "preferences_dark_mode_label": "테마: ", "Dark mode: ": "다크 모드: ", "preferences_player_style_label": "플레이어 스타일: ", - "preferences_category_visual": "시각 설정", + "preferences_category_visual": "환경 설정", "preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ", "preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ", "preferences_annotations_label": "기본으로 주석 표시: ", @@ -26,7 +26,7 @@ "preferences_speed_label": "기본 속도: ", "preferences_local_label": "비디오를 프록시: ", "preferences_listen_label": "라디오 모드: ", - "preferences_continue_autoplay_label": "다음 동영상 자동재생 ", + "preferences_continue_autoplay_label": "다음 동영상 자동재생: ", "preferences_continue_label": "다음 동영상으로 이동: ", "preferences_autoplay_label": "자동재생: ", "preferences_video_loop_label": "항상 반복: ", @@ -37,8 +37,8 @@ "Register": "회원가입", "Sign In": "로그인", "preferences_category_misc": "기타 설정", - "Image CAPTCHA": "이미지 CAPTCHA", - "Text CAPTCHA": "텍스트 CAPTCHA", + "Image CAPTCHA": "이미지 캡차", + "Text CAPTCHA": "텍스트 캡차", "Time (h:mm:ss):": "시각 (h:mm:ss):", "Password": "비밀번호", "User ID": "사용자 ID", @@ -50,15 +50,15 @@ "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", "History": "역사", "Delete account?": "계정을 삭제 하시겠습니까?", - "Export data as JSON": "데이터를 JSON으로 내보내기", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "구독을 OPML로 내보내기 (NewPipe 및 FreeTube 용)", - "Export subscriptions as OPML": "구독을 OPML로 내보내기", + "Export data as JSON": "JSON으로 데이터 내보내기", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)", + "Export subscriptions as OPML": "OPML로 구독 내보내기", "Export": "내보내기", - "Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)", - "Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)", - "Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)", + "Import NewPipe data (.zip)": "뉴파이프 데이터 가져오기 (.zip)", + "Import NewPipe subscriptions (.json)": "뉴파이프 구독 가져오기 (.json)", + "Import FreeTube subscriptions (.db)": "프리튜브 구독 가져오기 (.db)", "Import YouTube subscriptions": "유튜브 구독 가져오기", - "Import Invidious data": "인비디어스 JSON 데이터 가져오기", + "Import Invidious data": "인비디어스 데이터 가져오기 (.json)", "Import": "가져오기", "Import and Export Data": "데이터 가져오기 및 내보내기", "No": "아니요", @@ -150,9 +150,9 @@ "Subscription manager": "구독 관리자", "Save preferences": "설정 저장", "Report statistics: ": "통계 보고: ", - "Registration enabled: ": "등록 활성화: ", + "Registration enabled: ": "회원가입 활성화: ", "Login enabled: ": "로그인 활성화: ", - "CAPTCHA enabled: ": "CAPTCHA 활성화: ", + "CAPTCHA enabled: ": "캡차 활성화: ", "Top enabled: ": "Top 활성화: ", "preferences_show_nick_label": "상단에 닉네임 표시: ", "preferences_feed_menu_label": "피드 메뉴: ", @@ -187,8 +187,8 @@ "Polish": "폴란드어", "Persian": "페르시아어", "Pashto": "파슈토어", - "Nyanja": "체와어", - "Norwegian Bokmål": "보크몰", + "Nyanja": "냔자어", + "Norwegian Bokmål": "노르웨이 부크몰어", "Nepali": "네팔어", "Mongolian": "몽골어", "Marathi": "마라티어", @@ -284,10 +284,10 @@ "Password cannot be empty": "비밀번호는 비워둘 수 없습니다", "Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요", "Wrong username or password": "잘못된 사용자 이름 또는 비밀번호", - "Password is a required field": "비밀번호는 필수 필드입니다", - "User ID is a required field": "사용자 ID는 필수 필드입니다", - "CAPTCHA is a required field": "CAPTCHA는 필수 필드입니다", - "Erroneous CAPTCHA": "잘못된 CAPTCHA", + "Password is a required field": "비밀번호는 필수 입력란입니다", + "User ID is a required field": "사용자 ID는 필수 입력란입니다", + "CAPTCHA is a required field": "캡차는 필수 입력란입니다", + "Erroneous CAPTCHA": "잘못된 캡차", "Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.", "Blacklisted regions: ": "차단된 지역: ", "Playlists": "재생목록", @@ -297,7 +297,7 @@ "Empty playlist": "재생목록 비어 있음", "Show annotations": "주석 보이기", "Hide annotations": "주석 숨기기", - "Switch Invidious Instance": "Invidious 인스턴스 변경", + "Switch Invidious Instance": "인비디어스 인스턴스 변경", "Spanish": "스페인어", "Southern Sotho": "소토어", "Somali": "소말리어", @@ -347,8 +347,8 @@ "search_filters_sort_option_date": "업로드 날짜", "search_filters_sort_option_rating": "평점", "search_filters_sort_option_relevance": "관련성", - "Community": "커뮤니티", - "Videos": "동영상", + "channel_tab_community_label": "커뮤니티", + "channel_tab_videos_label": "동영상", "Video mode": "비디오 모드", "Audio mode": "오디오 모드", "permalink": "퍼머링크", @@ -383,7 +383,7 @@ "adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL", "search_filters_title": "필터", "preferences_quality_dash_option_4320p": "4320p", - "Popular enabled: ": "인기 급상승 활성화: ", + "Popular enabled: ": "인기 활성화: ", "Dutch (auto-generated)": "네덜란드어 (자동 생성됨)", "Chinese (Hong Kong)": "중국어 (홍콩)", "Chinese (Taiwan)": "중국어 (대만)", @@ -442,7 +442,7 @@ "preferences_save_player_pos_label": "이어서 보기: ", "none": "없음", "videoinfo_started_streaming_x_ago": "`x` 전에 스트리밍을 시작했습니다", - "crash_page_you_found_a_bug": "Invidious에서 버그를 찾은 것 같습니다!", + "crash_page_you_found_a_bug": "인비디어스에서 버그를 찾은 것 같습니다!", "download_subtitles": "자막 - `x`(.vtt)", "user_saved_playlists": "`x`개의 저장된 재생목록", "crash_page_before_reporting": "버그를 보고하기 전에 다음 사항이 있는지 확인합니다:", @@ -456,5 +456,9 @@ "crash_page_report_issue": "위의 방법 중 어느 것도 도움이 되지 않았다면, 깃허브에서 새 이슈를 열고(가능하면 영어로) 메시지에 다음 텍스트를 포함하세요(해당 텍스트를 번역하지 마십시오):", "videoinfo_youTube_embed_link": "임베드", "videoinfo_invidious_embed_link": "임베드 링크", - "error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. 재생목록 목록을 보려면 여기를 클릭하십시오." + "error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. 재생목록 목록을 보려면 여기를 클릭하십시오.", + "channel_tab_shorts_label": "쇼츠", + "channel_tab_streams_label": "실시간 스트리밍", + "channel_tab_channels_label": "채널", + "channel_tab_playlists_label": "재생목록" } diff --git a/locales/lt.json b/locales/lt.json index 35ababee..9bfcfdba 100644 --- a/locales/lt.json +++ b/locales/lt.json @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "`x` pažymėjo tai su ❤", "Audio mode": "Garso rėžimas", "Video mode": "Vaizdo rėžimas", - "Videos": "Vaizdo įrašai", + "channel_tab_videos_label": "Vaizdo įrašai", "Playlists": "Grojaraiščiai", - "Community": "Bendruomenė", + "channel_tab_community_label": "Bendruomenė", "search_filters_sort_option_relevance": "Aktualumas", "search_filters_sort_option_rating": "Reitingas", "search_filters_sort_option_date": "Įkėlimo data", diff --git a/locales/nb-NO.json b/locales/nb-NO.json index f4c2021b..d29cca43 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "`x` levnet et ❤", "Audio mode": "Lydmodus", "Video mode": "Video-modus", - "Videos": "Videoer", + "channel_tab_videos_label": "Videoer", "Playlists": "Spillelister", - "Community": "Gemenskap", + "channel_tab_community_label": "Gemenskap", "search_filters_sort_option_relevance": "relevans", "search_filters_sort_option_rating": "vurdering", "search_filters_sort_option_date": "dato", diff --git a/locales/nl.json b/locales/nl.json index 17057553..dfc68671 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -320,9 +320,9 @@ "`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤", "Audio mode": "Audiomodus", "Video mode": "Videomodus", - "Videos": "Video's", + "channel_tab_videos_label": "Video's", "Playlists": "Afspeellijsten", - "Community": "Gemeenschap", + "channel_tab_community_label": "Gemeenschap", "search_filters_sort_option_relevance": "relevantie", "search_filters_sort_option_rating": "beoordeling", "search_filters_sort_option_date": "datum", diff --git a/locales/pl.json b/locales/pl.json index f1a07490..3c713e70 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -67,7 +67,7 @@ "preferences_annotations_label": "Domyślnie pokazuj adnotacje: ", "preferences_extend_desc_label": "Automatycznie rozwijaj opisy filmów: ", "preferences_vr_mode_label": "Interaktywne filmy 360 stopni (wymaga WebGL): ", - "preferences_category_visual": "Preferencje Wizualne", + "preferences_category_visual": "Preferencje wizualne", "preferences_player_style_label": "Styl odtwarzacza: ", "Dark mode: ": "Ciemny motyw: ", "preferences_dark_mode_label": "Motyw: ", @@ -317,6 +317,7 @@ "Movies": "Filmy", "Download": "Pobierz", "Download as: ": "Pobierz jako: ", + "Download is disabled": "Pobieranie jest wyłączone", "%A %B %-d, %Y": "%A, %-d %B %Y", "(edited)": "(edytowany)", "YouTube comment permalink": "Odnośnik bezpośredni do komentarza na YouTube", @@ -324,9 +325,9 @@ "`x` marked it with a ❤": "`x` oznaczonych ❤", "Audio mode": "Tryb audio", "Video mode": "Tryb wideo", - "Videos": "Filmy", + "channel_tab_videos_label": "Wideo", "Playlists": "Playlisty", - "Community": "Społeczność", + "channel_tab_community_label": "Społeczność", "search_filters_sort_option_relevance": "Trafność", "search_filters_sort_option_rating": "Ocena", "search_filters_sort_option_date": "Data przesłania", @@ -443,7 +444,7 @@ "user_saved_playlists": "`x` zapisanych playlist", "Video unavailable": "Film niedostępny", "preferences_save_player_pos_label": "Zapisz pozycję odtwarzania: ", - "preferences_region_label": "Region zawartości: ", + "preferences_region_label": "Kraj treści: ", "Released under the AGPLv3 on Github.": "Wydany na licencji AGPLv3 na GitHub.", "search_filters_duration_option_short": "Krótka (< 4 minut)", "search_filters_duration_option_long": "Długa (> 20 minut)", @@ -481,12 +482,21 @@ "search_message_no_results": "Nie znaleziono wyników.", "preferences_watch_history_label": "Włącz historię oglądania: ", "search_filters_apply_button": "Zastosuj wybrane filtry", - "search_message_change_filters_or_query": "Spróbuj poszerzyć zapytanie i/lub zmienić filtry.", + "search_message_change_filters_or_query": "Spróbuj poszerzyć zapytanie wyszukiwania i/lub zmienić filtry.", "search_filters_date_label": "Data przesłania", "search_filters_features_option_vr180": "VR180", "search_filters_date_option_none": "Dowolna data", "search_message_use_another_instance": " Możesz także wyszukać w innej instancji.", "search_filters_type_option_all": "Dowolny typ", "search_filters_duration_option_none": "Dowolna długość", - "search_filters_duration_option_medium": "Średnia (4-20 minut)" + "search_filters_duration_option_medium": "Średnia (4-20 minut)", + "channel_tab_streams_label": "Na żywo", + "channel_tab_channels_label": "Kanały", + "channel_tab_playlists_label": "Playlisty", + "channel_tab_shorts_label": "Shorts", + "Music in this video": "Muzyka w tym filmie", + "Artist: ": "Wykonawca: ", + "Album: ": "Album: ", + "Song: ": "Piosenka: ", + "Channel Sponsor": "Sponsor kanału" } diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 41b457bb..ec00d46e 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -341,9 +341,9 @@ "`x` marked it with a ❤": "`x` foi marcado como ❤", "Audio mode": "Modo de áudio", "Video mode": "Modo de vídeo", - "Videos": "Vídeos", + "channel_tab_videos_label": "Vídeos", "Playlists": "Listas de reprodução", - "Community": "Comunidade", + "channel_tab_community_label": "Comunidade", "search_filters_sort_option_relevance": "relevância", "search_filters_sort_option_rating": "avaliação", "search_filters_sort_option_date": "data", @@ -381,7 +381,7 @@ "footer_documentation": "Documentação", "footer_source_code": "Código fonte", "footer_original_source_code": "Código fonte original", - "footer_modfied_source_code": "Código Fonte Modificado", + "footer_modfied_source_code": "Código-fonte modificado", "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", @@ -472,5 +472,12 @@ "search_filters_duration_option_medium": "Médio (4 - 20 minutos)", "search_filters_features_option_vr180": "VR180", "Popular enabled: ": "Popular habilitado: ", - "error_video_not_in_playlist": "O vídeo solicitado não existe nesta playlist. Clique aqui para acessar a página inicial da playlist." + "error_video_not_in_playlist": "O vídeo solicitado não existe nesta playlist. Clique aqui para acessar a página inicial da playlist.", + "channel_tab_channels_label": "Canais", + "channel_tab_playlists_label": "Listas de reprodução", + "channel_tab_shorts_label": "Curtos", + "channel_tab_streams_label": "Ao Vivo", + "Music in this video": "Música neste vídeo", + "Artist: ": "Artista: ", + "Album: ": "Álbum: " } diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 1bee2807..43834d70 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -341,9 +341,9 @@ "`x` marked it with a ❤": "`x` foi marcado como ❤", "Audio mode": "Modo de áudio", "Video mode": "Modo de vídeo", - "Videos": "Vídeos", + "channel_tab_videos_label": "Vídeos", "Playlists": "Listas de reprodução", - "Community": "Comunidade", + "channel_tab_community_label": "Comunidade", "search_filters_sort_option_relevance": "Relevância", "search_filters_sort_option_rating": "Avaliação", "search_filters_sort_option_date": "Data de envio", @@ -472,5 +472,12 @@ "search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", - "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução." + "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução.", + "Artist: ": "Artista: ", + "Album: ": "Álbum: ", + "channel_tab_streams_label": "Diretos", + "channel_tab_playlists_label": "Listas de reprodução", + "channel_tab_channels_label": "Canais", + "Music in this video": "Música neste vídeo", + "channel_tab_shorts_label": "Curtos" } diff --git a/locales/pt.json b/locales/pt.json index b550bc87..310381ae 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -267,9 +267,9 @@ "Next page": "Próxima página", "last": "últimos", "Current version: ": "Versão atual: ", - "Community": "Comunidade", + "channel_tab_community_label": "Comunidade", "Playlists": "Listas de reprodução", - "Videos": "Vídeos", + "channel_tab_videos_label": "Vídeos", "Video mode": "Modo de vídeo", "Audio mode": "Modo de áudio", "`x` marked it with a ❤": "`x` foi marcado como ❤", @@ -472,5 +472,14 @@ "search_filters_type_option_all": "Qualquer tipo", "search_filters_duration_option_none": "Qualquer duração", "Popular enabled: ": "Página \"popular\" ativada: ", - "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução." + "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução.", + "channel_tab_playlists_label": "Listas de reprodução", + "channel_tab_channels_label": "Canais", + "channel_tab_shorts_label": "Curtos", + "channel_tab_streams_label": "Diretos", + "Music in this video": "Música neste vídeo", + "Artist: ": "Artista: ", + "Album: ": "Álbum: ", + "Song: ": "Canção: ", + "Channel Sponsor": "Patrocinador do canal" } diff --git a/locales/ro.json b/locales/ro.json index 342f5f37..0f6407d6 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -315,9 +315,9 @@ "`x` marked it with a ❤": "`x` l-a marcat cu o ❤", "Audio mode": "Mod audio", "Video mode": "Mod video", - "Videos": "Videoclipuri", + "channel_tab_videos_label": "Videoclipuri", "Playlists": "Liste de redare", - "Community": "Comunitate", + "channel_tab_community_label": "Comunitate", "Current version: ": "Versiunea actuală: ", "crash_page_read_the_faq": "citit lista Întrebărilor Frecvente (FAQ)", "generic_count_days_0": "{{count}} zi", diff --git a/locales/ru.json b/locales/ru.json index 93c9cbec..7ca5cf1f 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -69,11 +69,11 @@ "preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ", "preferences_category_visual": "Настройки сайта", "preferences_player_style_label": "Стиль проигрывателя: ", - "Dark mode: ": "Тёмное оформление: ", + "Dark mode: ": "Темное оформление: ", "preferences_dark_mode_label": "Тема: ", - "dark": "тёмная", + "dark": "темная", "light": "светлая", - "preferences_thin_mode_label": "Облегчённое оформление: ", + "preferences_thin_mode_label": "Облегченное оформление: ", "preferences_category_misc": "Прочие настройки", "preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ", "preferences_category_subscription": "Настройки подписок", @@ -88,7 +88,7 @@ "channel name": "по названию канала", "channel name - reverse": "по названию канала в обратном порядке", "Only show latest video from channel: ": "Показывать только последние видео с каналов: ", - "Only show latest unwatched video from channel: ": "Показывать только непросмотренные видео с каналов: ", + "Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ", "preferences_unseen_only_label": "Показывать только непросмотренные видео: ", "preferences_notifications_only_label": "Показывать только оповещения, если они есть: ", "Enable web notifications": "Включить уведомления в браузере", @@ -147,13 +147,13 @@ "License: ": "Лицензия: ", "Family friendly? ": "Семейный просмотр: ", "Wilson score: ": "Оценка Уилсона: ", - "Engagement: ": "Вовлечённость: ", + "Engagement: ": "Вовлеченность: ", "Whitelisted regions: ": "Доступно в регионах: ", "Blacklisted regions: ": "Недоступно в регионах: ", "Shared `x`": "Опубликовано `x`", "Premieres in `x`": "Премьера через `x`", "Premieres `x`": "Премьера `x`", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", "View YouTube comments": "Показать комментарии с YouTube", "View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View `x` comments": { @@ -180,23 +180,23 @@ "Please log in": "Пожалуйста, войдите", "Invidious Private Feed for `x`": "Приватная лента Invidious для `x`", "channel:`x`": "канал: `x`", - "Deleted or invalid channel": "Канал удалён или не найден", + "Deleted or invalid channel": "Канал удален или не найден", "This channel does not exist.": "Такого канала не существует.", - "Could not get channel info.": "Не удаётся получить информацию об этом канале.", - "Could not fetch comments": "Не удаётся загрузить комментарии", + "Could not get channel info.": "Не удается получить информацию об этом канале.", + "Could not fetch comments": "Не удается загрузить комментарии", "`x` ago": "`x` назад", - "Load more": "Загрузить ещё", + "Load more": "Загрузить еще", "Could not create mix.": "Не удалось создать микс.", "Empty playlist": "Плейлист пуст", - "Not a playlist.": "Некорректный плейлист.", + "Not a playlist.": "Это не плейлист.", "Playlist does not exist.": "Плейлист не существует.", - "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».", + "Could not pull trending pages.": "Не удается загрузить страницы «в тренде».", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", "Erroneous challenge": "Неправильный ответ в «challenge»", "Erroneous token": "Неправильный токен", "No such user": "Пользователь не найден", - "Token is expired, please try again": "Срок действия токена истёк, попробуйте позже", + "Token is expired, please try again": "Срок действия токена истек, попробуйте позже", "English": "Английский", "English (auto-generated)": "Английский (созданы автоматически)", "Afrikaans": "Африкаанс", @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "❤ от автора канала \"`x`\"", "Audio mode": "Аудио режим", "Video mode": "Видео режим", - "Videos": "Видео", + "channel_tab_videos_label": "Видео", "Playlists": "Плейлисты", - "Community": "Сообщество", + "channel_tab_community_label": "Сообщество", "search_filters_sort_option_relevance": "по актуальности", "search_filters_sort_option_rating": "по рейтингу", "search_filters_sort_option_date": "по дате загрузки", @@ -379,7 +379,7 @@ "Turkish (auto-generated)": "Турецкий (созданы автоматически)", "Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)", "footer_documentation": "Документация", - "adminprefs_modified_source_code_url_label": "Ссылка на нашу ветку репозитория", + "adminprefs_modified_source_code_url_label": "URL-адрес репозитория измененного исходного кода", "none": "ничего", "videoinfo_watch_on_youTube": "Смотреть на YouTube", "videoinfo_youTube_embed_link": "Версия для встраивания", @@ -453,8 +453,8 @@ "Portuguese (Brazil)": "Португальский (Бразилия)", "footer_source_code": "Исходный код", "footer_original_source_code": "Оригинальный исходный код", - "footer_modfied_source_code": "Изменённый исходный код", - "user_saved_playlists": "`x` сохранённых плейлистов", + "footer_modfied_source_code": "Измененный исходный код", + "user_saved_playlists": "`x` сохраненных плейлистов", "crash_page_search_issue": "поискали похожую проблему на GitHub", "comments_points_count_0": "{{count}} плюс", "comments_points_count_1": "{{count}} плюса", @@ -488,5 +488,12 @@ "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры", "Popular enabled: ": "Популярное включено: ", - "error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. Нажмите тут, чтобы вернуться к странице плейлиста." + "error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. Нажмите тут, чтобы вернуться к странице плейлиста.", + "channel_tab_playlists_label": "Плейлисты", + "channel_tab_channels_label": "Каналы", + "channel_tab_streams_label": "Живое вещание", + "channel_tab_shorts_label": "Shorts", + "Music in this video": "Музыка в этом видео", + "Artist: ": "Исполнитель: ", + "Album: ": "Альбом: " } diff --git a/locales/sl.json b/locales/sl.json index 5994ca1a..47f295e0 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -206,7 +206,7 @@ "generic_count_years_2": "{{count}} leti", "generic_count_years_3": "{{count}} leti", "generic_count_days_0": "{{count}} dnevom", - "generic_count_days_1": "{{count}} dnevi", + "generic_count_days_1": "{{count}} dnevoma", "generic_count_days_2": "{{count}} dnevi", "generic_count_days_3": "{{count}} dnevi", "generic_count_hours_0": "{{count}} uro", @@ -222,7 +222,7 @@ "About": "O aplikaciji", "%A %B %-d, %Y": "%A %-d %B %Y", "Audio mode": "Avdio način", - "Videos": "Videoposnetki", + "channel_tab_videos_label": "Videoposnetki", "search_filters_date_label": "Datum nalaganja", "search_filters_date_option_today": "Danes", "search_filters_date_option_week": "Ta teden", @@ -246,10 +246,10 @@ "generic_videos_count_1": "{{count}} videa", "generic_videos_count_2": "{{count}} videi", "generic_videos_count_3": "{{count}} videov", - "generic_views_count_0": "{{count}} ogled", - "generic_views_count_1": "{{count}} ogleda", - "generic_views_count_2": "{{count}} ogledi", - "generic_views_count_3": "{{count}} ogledov", + "generic_views_count_0": "Ogledov: {{count}}", + "generic_views_count_1": "Ogledov: {{count}}", + "generic_views_count_2": "Ogledov: {{count}}", + "generic_views_count_3": "Ogledov: {{count}}", "generic_playlists_count_0": "{{count}} seznam predvajanja", "generic_playlists_count_1": "{{count}} seznama predvajanja", "generic_playlists_count_2": "{{count}} seznami predvajanja", @@ -455,7 +455,7 @@ "Download": "Prenesi", "permalink": "stalna povezava", "`x` marked it with a ❤": "`x` ga je označil/a z ❤", - "Community": "Skupnost", + "channel_tab_community_label": "Skupnost", "search_filters_features_option_three_sixty": "360°", "Video mode": "Video način", "search_filters_features_option_c_commons": "Creative Commons", @@ -495,7 +495,7 @@ "footer_modfied_source_code": "Spremenjena izvorna koda", "user_created_playlists": "`x` ustvarjenih seznamov predvajanja", "adminprefs_modified_source_code_url_label": "URL do shrambe spremenjene izvorne kode", - "videoinfo_youTube_embed_link": "Vdelati", + "videoinfo_youTube_embed_link": "Vdelaj", "videoinfo_invidious_embed_link": "Povezava za vdelavo", "crash_page_switch_instance": "poskušal/a uporabiti drugo instanco", "download_subtitles": "Podnapisi - `x` (.vtt)", @@ -504,5 +504,12 @@ "crash_page_search_issue": "preiskal/a obstoječe težave na GitHubu", "crash_page_report_issue": "Če nič od navedenega ni pomagalo, prosim odpri novo težavo v GitHubu (po možnosti v angleščini) in v svoje sporočilo vključi naslednje besedilo (tega besedila NE prevajaj):", "Popular enabled: ": "Priljubljeni omogočeni: ", - "error_video_not_in_playlist": "Zahtevani videoposnetek ne obstaja na tem seznamu predvajanja. Klikni tukaj za domačo stran seznama predvajanja." + "error_video_not_in_playlist": "Zahtevani videoposnetek ne obstaja na tem seznamu predvajanja. Klikni tukaj za domačo stran seznama predvajanja.", + "channel_tab_playlists_label": "Seznami predvajanja", + "channel_tab_shorts_label": "Kratki videoposnetki", + "channel_tab_channels_label": "Kanali", + "channel_tab_streams_label": "Prenosi v živo", + "Artist: ": "Umetnik/ca: ", + "Music in this video": "Glasba v tem videoposnetku", + "Album: ": "Album: " } diff --git a/locales/sq.json b/locales/sq.json index 76dfd1b7..7f29a035 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -259,10 +259,10 @@ "YouTube comment permalink": "Permalidhje komenti YouTube", "Audio mode": "Mënyrë për audion", "Playlists": "Luajlista", - "Community": "Bashkësi", + "channel_tab_community_label": "Bashkësi", "search_filters_sort_option_relevance": "Rëndësi", "Video mode": "Mënyrë video", - "Videos": "Video", + "channel_tab_videos_label": "Video", "search_filters_sort_option_rating": "Vlerësim", "search_filters_sort_option_date": "Datë ngarkimi", "search_filters_sort_option_views": "Numër parjesh", @@ -286,7 +286,7 @@ "search_filters_type_option_show": "Shfaqe", "search_filters_duration_option_short": "E shkurtër (< 4 minuta)", "search_filters_features_option_purchased": "Të blera", - "footer_modfied_source_code": "Kod Burim i ndryshuar", + "footer_modfied_source_code": "Kod burim i ndryshuar", "adminprefs_modified_source_code_url_label": "URL e depos së ndryshuar të kodit burim", "none": "asnjë", "videoinfo_started_streaming_x_ago": "Filloi transmetimin `x` më parë", @@ -463,5 +463,12 @@ "search_filters_duration_option_none": "Çfarëdo kohëzgjatjeje", "search_filters_duration_option_medium": "Mesatare (4 - 20 minuta)", "search_filters_features_option_vr180": "VR180", - "search_filters_apply_button": "Apliko filtrat e përzgjedhur" + "search_filters_apply_button": "Apliko filtrat e përzgjedhur", + "channel_tab_playlists_label": "Luajlista", + "Artist: ": "Artist: ", + "Album: ": "Album: ", + "channel_tab_channels_label": "Kanale", + "Music in this video": "Muzikë në këtë video", + "channel_tab_shorts_label": "Të shkurtra", + "channel_tab_streams_label": "Transmetime të drejtpërdrejta" } diff --git a/locales/sr.json b/locales/sr.json index d2f990ae..fd19c493 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -257,7 +257,7 @@ "preferences_volume_label": "Jačina zvuka: ", "preferences_locale_label": "Jezik: ", "adminprefs_modified_source_code_url_label": "URL veza do skladišta sa Izmenjenom Izvornom Kodom", - "Community": "Zajednica", + "channel_tab_community_label": "Zajednica", "Video mode": "Video mod", "Fallback captions: ": "Titl u slučaju da glavni nije dostupan: ", "Private": "Privatno", @@ -289,7 +289,7 @@ "Erroneous token": "Pogrešan žeton", "Czech": "Češki", "Latin": "Latinski", - "Videos": "Video klipovi", + "channel_tab_videos_label": "Video klipovi", "search_filters_features_option_four_k": "4К", "footer_donate_page": "Doniraj", "English": "Engleski", diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index c0f1224f..bef9915d 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -245,7 +245,7 @@ "(edited)": "(измењено)", "`x` marked it with a ❤": "`x` је означио/ла ово са ❤", "Audio mode": "Аудио мод", - "Videos": "Видео клипови", + "channel_tab_videos_label": "Видео клипови", "search_filters_sort_option_views": "Број прегледа", "search_filters_features_label": "Карактеристике", "search_filters_date_option_today": "Данас", @@ -298,7 +298,7 @@ "Ukrainian": "Украјински", "permalink": "трајна веза", "Pashto": "Паштунски", - "Community": "Заједница", + "channel_tab_community_label": "Заједница", "Sindhi": "Синди", "Could not fetch comments": "Узимање коментара није успело", "Bangla": "Бангла/Бенгалски", diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 777899d0..39e94fd3 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -323,9 +323,9 @@ "`x` marked it with a ❤": "`x` lämnade ett ❤", "Audio mode": "Ljudläge", "Video mode": "Videoläge", - "Videos": "Videor", + "channel_tab_videos_label": "Videor", "Playlists": "Spellistor", - "Community": "Gemenskap", + "channel_tab_community_label": "Gemenskap", "search_filters_sort_option_relevance": "Relevans", "search_filters_sort_option_rating": "Rankning", "search_filters_sort_option_date": "Datum", diff --git a/locales/tr.json b/locales/tr.json index 17db1cf1..6e0bc175 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -325,9 +325,9 @@ "`x` marked it with a ❤": "`x` ❤ İle İşaretledi", "Audio mode": "Ses Modu", "Video mode": "Video Modu", - "Videos": "Videolar", + "channel_tab_videos_label": "Videolar", "Playlists": "Oynatma Listeleri", - "Community": "Topluluk", + "channel_tab_community_label": "Topluluk", "search_filters_sort_option_relevance": "İlgi", "search_filters_sort_option_rating": "Değerlendirme", "search_filters_sort_option_date": "Yükleme Tarihi", @@ -363,7 +363,7 @@ "footer_documentation": "Belgelendirme", "footer_source_code": "Kaynak Kodları", "footer_original_source_code": "Orijinal Kaynak Kodları", - "footer_modfied_source_code": "Değiştirilmiş Kaynak Kodları", + "footer_modfied_source_code": "Değiştirilmiş kaynak kodları", "adminprefs_modified_source_code_url_label": "Değiştirilmiş Kaynak Kodları Deposunun URL'si", "footer_donate_page": "Bağış Yap", "preferences_region_label": "İçerik Ülkesi: ", @@ -397,8 +397,8 @@ "videoinfo_watch_on_youTube": "YouTube'da İzle", "download_subtitles": "Alt Yazılar - `x` (.vtt)", "preferences_save_player_pos_label": "Oynatma Konumunu Kaydet: ", - "generic_views_count": "{{count}} Görüntüleme", - "generic_views_count_plural": "{{count}} Görüntüleme", + "generic_views_count": "{{count}} Görüntülenme", + "generic_views_count_plural": "{{count}} Görüntülenme", "generic_subscribers_count": "{{count}} Abone", "generic_subscribers_count_plural": "{{count}} Abone", "generic_subscriptions_count": "{{count}} Abonelik", @@ -472,5 +472,14 @@ "search_filters_title": "Filtreler", "search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin.", "Popular enabled: ": "Popüler Etkin: ", - "error_video_not_in_playlist": "İstenen video bu oynatma listesinde yok. Oynatma listesi ana sayfası için buraya tıklayın." + "error_video_not_in_playlist": "İstenen video bu oynatma listesinde yok. Oynatma listesi ana sayfası için buraya tıklayın.", + "channel_tab_channels_label": "Kanallar", + "channel_tab_shorts_label": "Kısa Çekimler", + "channel_tab_streams_label": "Canlı Yayınlar", + "channel_tab_playlists_label": "Oynatma Listeleri", + "Album: ": "Albüm: ", + "Music in this video": "Bu videodaki müzik", + "Artist: ": "Sanatçı: ", + "Channel Sponsor": "Kanal Sponsoru", + "Song: ": "Şarkı: " } diff --git a/locales/uk.json b/locales/uk.json index b6994c56..4d748e7f 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -54,7 +54,7 @@ "preferences_continue_label": "Завжди вмикати наступне відео: ", "preferences_continue_autoplay_label": "Автовідтворення наступного відео: ", "preferences_listen_label": "Режим «тільки звук» як усталений: ", - "preferences_local_label": "Програвати відео через проксі? ", + "preferences_local_label": "Відтворення відео через проксі: ", "preferences_speed_label": "Усталена швидкість відео: ", "preferences_quality_label": "Пріорітетна якість відео: ", "preferences_volume_label": "Гучність відео: ", @@ -63,13 +63,13 @@ "reddit": "Reddit", "preferences_captions_label": "Основна мова субтитрів: ", "Fallback captions: ": "Запасна мова субтитрів: ", - "preferences_related_videos_label": "Показувати схожі відео? ", - "preferences_annotations_label": "Завжди показувати анотації? ", + "preferences_related_videos_label": "Показувати схожі відео: ", + "preferences_annotations_label": "Завжди показувати анотації: ", "preferences_category_visual": "Налаштування сайту", "preferences_player_style_label": "Стиль програвача: ", - "Dark mode: ": "Темне оформлення: ", + "Dark mode: ": "Темний режим: ", "preferences_dark_mode_label": "Тема: ", - "dark": "темна", + "dark": "Темна", "light": "Світла", "preferences_thin_mode_label": "Полегшене оформлення: ", "preferences_category_subscription": "Налаштування підписок", @@ -101,11 +101,11 @@ "preferences_category_admin": "Адміністраторські налаштування", "preferences_default_home_label": "Усталена домашня сторінка: ", "preferences_feed_menu_label": "Меню потоку з відео: ", - "Top enabled: ": "Увімкнути топ відео? ", - "CAPTCHA enabled: ": "Увімкнути капчу? ", - "Login enabled: ": "Увімкнути авторизацію? ", - "Registration enabled: ": "Увімкнути реєстрацію? ", - "Report statistics: ": "Повідомляти статистику? ", + "Top enabled: ": "Увімкнути топ відео: ", + "CAPTCHA enabled: ": "Увімкнути CAPTCHA: ", + "Login enabled: ": "Увімкнути вхід: ", + "Registration enabled: ": "Увімкнути реєстрацію: ", + "Report statistics: ": "Повідомляти статистику: ", "Save preferences": "Зберегти налаштування", "Subscription manager": "Менеджер підписок", "Token manager": "Менеджер токенів", @@ -125,7 +125,7 @@ "Private": "Особистий", "View all playlists": "Переглянути всі списки відтворення", "Updated `x` ago": "Оновлено `x` тому", - "Delete playlist `x`?": "Видалити список відтворення \"x\"?", + "Delete playlist `x`?": "Видалити список відтворення `x`?", "Delete playlist": "Видалити список відтворення", "Create playlist": "Створити список відтворення", "Title": "Заголовок", @@ -315,9 +315,9 @@ "`x` marked it with a ❤": "❤ цьому від каналу `x`", "Audio mode": "Аудіорежим", "Video mode": "Відеорежим", - "Videos": "Відео", + "channel_tab_videos_label": "Відео", "Playlists": "Плейлисти", - "Community": "Спільнота", + "channel_tab_community_label": "Спільнота", "Current version: ": "Поточна версія: ", "generic_views_count_0": "{{count}} перегляд", "generic_views_count_1": "{{count}} перегляди", @@ -386,12 +386,12 @@ "Spanish (Mexico)": "Іспанська (Мексика)", "Spanish (Spain)": "Іспанська (Іспанія)", "next_steps_error_message_go_to_youtube": "Перейти до YouTube", - "footer_donate_page": "Пожертвувати", + "footer_donate_page": "Підтримати", "footer_documentation": "Документація", - "footer_source_code": "Вихідний код", - "footer_original_source_code": "Оригінал вихідного коду", - "footer_modfied_source_code": "Змінений вихідний код", - "adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого вихідного коду", + "footer_source_code": "Джерельний код", + "footer_original_source_code": "Оригінал джерельного коду", + "footer_modfied_source_code": "Змінений джерельний код", + "adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого джерельного коду", "none": "нема", "videoinfo_started_streaming_x_ago": "Трансляцію розпочато `x` тому", "crash_page_you_found_a_bug": "Схоже, ви знайшли ваду в Invidious!", @@ -408,7 +408,7 @@ "next_steps_error_message": "Після чого спробуйте: ", "next_steps_error_message_refresh": "Оновити сторінку", "Search": "Пошук", - "preferences_extend_desc_label": "Автоматично розширювати опис відео: ", + "preferences_extend_desc_label": "Автоматично розгортати опис відео: ", "preferences_category_misc": "Різноманітні параметри", "Show less": "Коротше", "preferences_quality_option_small": "Низька", @@ -488,5 +488,14 @@ "search_filters_sort_option_rating": "Рейтингові", "search_filters_sort_option_views": "Популярні", "Popular enabled: ": "Популярне ввімкнено: ", - "error_video_not_in_playlist": "Запитуваного відео в цьому списку відтворення не існує. Клацніть тут, щоб переглянути домашню сторінку списку відтворення." + "error_video_not_in_playlist": "Запитуваного відео в цьому списку відтворення не існує. Клацніть тут, щоб переглянути домашню сторінку списку відтворення.", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Прямі трансляції", + "channel_tab_playlists_label": "Добірки", + "channel_tab_channels_label": "Канали", + "Music in this video": "Музика в цьому відео", + "Artist: ": "Виконавець: ", + "Album: ": "Альбом: ", + "Song: ": "Пісня: ", + "Channel Sponsor": "Спонсор каналу" } diff --git a/locales/vi.json b/locales/vi.json index 07fcf52f..3f7125c4 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -311,9 +311,9 @@ "`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤", "Audio mode": "Chế độ âm thanh", "Video mode": "Chế độ quay", - "Videos": "Video", + "channel_tab_videos_label": "Video", "Playlists": "Danh sách phát", - "Community": "Cộng đồng", + "channel_tab_community_label": "Cộng đồng", "search_filters_sort_option_relevance": "liên quan", "search_filters_sort_option_rating": "Xếp hạng", "search_filters_sort_option_date": "ngày", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 7e749dc9..f202cf88 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -341,9 +341,9 @@ "`x` marked it with a ❤": "`x` 为此加 ❤", "Audio mode": "音频模式", "Video mode": "视频模式", - "Videos": "视频", + "channel_tab_videos_label": "视频", "Playlists": "播放列表", - "Community": "社区", + "channel_tab_community_label": "社区", "search_filters_sort_option_relevance": "相关度", "search_filters_sort_option_rating": "评分", "search_filters_sort_option_date": "上传日期", @@ -456,5 +456,14 @@ "search_filters_type_option_all": "任意类型", "search_filters_features_option_vr180": "VR180", "Popular enabled: ": "已启用流行度: ", - "error_video_not_in_playlist": "此播放列表中不存在请求的视频。 单击析出查看播放列表主页。" + "error_video_not_in_playlist": "此播放列表中不存在请求的视频。 单击析出查看播放列表主页。", + "Music in this video": "此视频中的音乐", + "channel_tab_playlists_label": "播放列表", + "Artist: ": "艺术家: ", + "channel_tab_streams_label": "直播", + "Album: ": "专辑: ", + "channel_tab_shorts_label": "短视频", + "channel_tab_channels_label": "频道", + "Song: ": "歌曲: ", + "Channel Sponsor": "频道赞助者" } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 54933701..54090d3d 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -341,9 +341,9 @@ "`x` marked it with a ❤": "`x` 為此標記 ❤", "Audio mode": "音訊模式", "Video mode": "視訊模式", - "Videos": "影片", + "channel_tab_videos_label": "影片", "Playlists": "播放清單", - "Community": "社群", + "channel_tab_community_label": "社群", "search_filters_sort_option_relevance": "關聯", "search_filters_sort_option_rating": "評分", "search_filters_sort_option_date": "日期", @@ -456,5 +456,14 @@ "search_filters_type_option_all": "任何類型", "search_filters_date_option_none": "任何日期", "Popular enabled: ": "已啟用人氣: ", - "error_video_not_in_playlist": "此播放清單不存在請求的影片。點擊此處檢視播放清單首頁。" + "error_video_not_in_playlist": "此播放清單不存在請求的影片。點擊此處檢視播放清單首頁。", + "channel_tab_shorts_label": "短片", + "channel_tab_playlists_label": "播放清單", + "channel_tab_channels_label": "頻道", + "channel_tab_streams_label": "直播", + "Artist: ": "藝術家: ", + "Album: ": "專輯: ", + "Music in this video": "此影片中的音樂", + "Channel Sponsor": "頻道贊助者", + "Song: ": "歌曲: " } diff --git a/mocks b/mocks index dfd53ea6..cb16e034 160000 --- a/mocks +++ b/mocks @@ -1 +1 @@ -Subproject commit dfd53ea6ceb3cbcbbce6004f6ce60b330ad0f9b1 +Subproject commit cb16e0343c8f94182615610bfe3c503db89717a7 diff --git a/scripts/deploy-database.sh b/scripts/deploy-database.sh old mode 100644 new mode 100755 diff --git a/scripts/fetch-player-dependencies.cr b/scripts/fetch-player-dependencies.cr old mode 100644 new mode 100755 index ed658b51..813e4ce4 --- a/scripts/fetch-player-dependencies.cr +++ b/scripts/fetch-player-dependencies.cr @@ -129,7 +129,7 @@ dependencies_to_install.each do |dep| dep = "videojs.markers" if dep == "videojs-markers" if File.exists?("#{download_path}/package/dist/#{dep}.css") - if minified && File.exists?("#{tmp_dir_path}/#{dep}/package/dist/#{dep}.min.css") + if minified && File.exists?("#{download_path}/package/dist/#{dep}.min.css") `mv #{download_path}/package/dist/#{dep}.min.css #{dest_path}/#{dep}.css` else `mv #{download_path}/package/dist/#{dep}.css #{dest_path}/#{dep}.css` diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh old mode 100644 new mode 100755 diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index 132b37a3..cbe80010 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island") - expect(info["views"].as_i).to eq(32_846_329) - expect(info["likes"].as_i).to eq(2_611_650) + expect(info["views"].as_i).to eq(115_784_415) + expect(info["likes"].as_i).to eq(4_932_790) # For some reason the video length from VideoDetails and the # one from microformat differs by 1s... @@ -46,14 +46,14 @@ Spectator.describe "parse_video_info" do # Related videos - expect(info["relatedVideos"].as_a.size).to eq(19) + expect(info["relatedVideos"].as_a.size).to eq(20) - expect(info["relatedVideos"][0]["id"]).to eq("tVWWp1PqDus") - expect(info["relatedVideos"][0]["title"]).to eq("100 Girls Vs 100 Boys For $500,000") + expect(info["relatedVideos"][0]["id"]).to eq("iogcY_4xGjo") + expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $1,000,000 Hotel Room!") expect(info["relatedVideos"][0]["author"]).to eq("MrBeast") expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") - expect(info["relatedVideos"][0]["view_count"]).to eq("49702799") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("49M") + expect(info["relatedVideos"][0]["view_count"]).to eq("172972109") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("172M") expect(info["relatedVideos"][0]["author_verified"]).to eq("true") # Description @@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") expect(info["authorThumbnail"].as_s).to eq( - "https://yt3.ggpht.com/ytc/AMLnZu84dsnlYtuUFBMC8imQs0IUcTKA9khWAmUOgQZltw=s48-c-k-c0x00ffffff-no-rj" + "https://yt3.ggpht.com/ytc/AL5GRJUfhQdJS6n-YJtsAf-ouS2myDavDOq_zXBfebal3Q=s48-c-k-c0x00ffffff-no-rj" ) expect(info["authorVerified"].as_bool).to be_true - expect(info["subCountText"].as_s).to eq("101M") + expect(info["subCountText"].as_s).to eq("135M") end it "parses a regular video with no descrition/comments" do @@ -99,7 +99,7 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("Chris Rea - Auberge") - expect(info["views"].as_i).to eq(10_356_197) + expect(info["views"].as_i).to eq(10_698_554) expect(info["likes"].as_i).to eq(0) expect(info["lengthSeconds"].as_i).to eq(283_i64) expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z") @@ -132,16 +132,14 @@ Spectator.describe "parse_video_info" do # Related videos - expect(info["relatedVideos"].as_a.size).to eq(19) + expect(info["relatedVideos"].as_a.size).to eq(18) - expect(info["relatedVideos"][0]["id"]).to eq("0bkrY_V0yZg") - expect(info["relatedVideos"][0]["title"]).to eq( - "Chris Rea Best Songs Collection - Chris Rea Greatest Hits Full Album 2022" - ) - expect(info["relatedVideos"][0]["author"]).to eq("Rock Ultimate") - expect(info["relatedVideos"][0]["ucid"]).to eq("UCekSc2A19di9koUIpj8gxlQ") - expect(info["relatedVideos"][0]["view_count"]).to eq("1992412") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("1.9M") + expect(info["relatedVideos"][0]["id"]).to eq("rfyZrJUmzxU") + expect(info["relatedVideos"][0]["title"]).to eq("cheb mami - bekatni") + expect(info["relatedVideos"][0]["author"]).to eq("pelitovic") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCsp6vFyJeGoLxgn-AsHp1tw") + expect(info["relatedVideos"][0]["view_count"]).to eq("13863619") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("13M") expect(info["relatedVideos"][0]["author_verified"]).to eq("false") # Description diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr index ff5aacd5..9dd22b97 100644 --- a/spec/invidious/videos/scheduled_live_extract_spec.cr +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -1,114 +1,13 @@ require "../../parsers_helper.cr" Spectator.describe "parse_video_info" do - it "parses scheduled livestreams data (test 1)" do - # Enable mock - _player = load_mock("video/scheduled_live_nintendo.player") - _next = load_mock("video/scheduled_live_nintendo.next") - - raw_data = _player.merge!(_next) - info = parse_video_info("QMGibBzTu0g", raw_data) - - # Some basic verifications - expect(typeof(info)).to eq(Hash(String, JSON::Any)) - - expect(info["videoType"].as_s).to eq("Scheduled") - - # Basic video infos - - expect(info["title"].as_s).to eq("Xenoblade Chronicles 3 Nintendo Direct") - expect(info["views"].as_i).to eq(160) - expect(info["likes"].as_i).to eq(2_283) - expect(info["lengthSeconds"].as_i).to eq(0_i64) - expect(info["published"].as_s).to eq("2022-06-22T14:00:00Z") # Unix 1655906400 - - # Extra video infos - - expect(info["allowedRegions"].as_a).to_not be_empty - expect(info["allowedRegions"].as_a.size).to eq(249) - - expect(info["allowedRegions"].as_a).to contain( - "AD", "BA", "BB", "BW", "BY", "EG", "GG", "HN", "NP", "NR", "TR", - "TT", "TV", "TW", "TZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", - "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW" - ) - - expect(info["keywords"].as_a).to_not be_empty - expect(info["keywords"].as_a.size).to eq(11) - - expect(info["keywords"].as_a).to contain_exactly( - "nintendo", - "game", - "gameplay", - "fun", - "video game", - "action", - "adventure", - "rpg", - "play", - "switch", - "nintendo switch" - ).in_any_order - - expect(info["allowRatings"].as_bool).to be_true - expect(info["isFamilyFriendly"].as_bool).to be_true - expect(info["isListed"].as_bool).to be_true - expect(info["isUpcoming"].as_bool).to be_true - - # Related videos - - expect(info["relatedVideos"].as_a.size).to eq(20) - - # related video #1 - expect(info["relatedVideos"][3]["id"].as_s).to eq("a-SN3lLIUEo") - expect(info["relatedVideos"][3]["author"].as_s).to eq("Nintendo") - expect(info["relatedVideos"][3]["ucid"].as_s).to eq("UCGIY_O-8vW4rfX98KlMkvRg") - expect(info["relatedVideos"][3]["view_count"].as_s).to eq("147796") - expect(info["relatedVideos"][3]["short_view_count"].as_s).to eq("147K") - expect(info["relatedVideos"][3]["author_verified"].as_s).to eq("true") - - # Related video #2 - expect(info["relatedVideos"][16]["id"].as_s).to eq("l_uC1jFK0lo") - expect(info["relatedVideos"][16]["author"].as_s).to eq("Nintendo") - expect(info["relatedVideos"][16]["ucid"].as_s).to eq("UCGIY_O-8vW4rfX98KlMkvRg") - expect(info["relatedVideos"][16]["view_count"].as_s).to eq("53510") - expect(info["relatedVideos"][16]["short_view_count"].as_s).to eq("53K") - expect(info["relatedVideos"][16]["author_verified"].as_s).to eq("true") - - # Description - - description = "Tune in on 6/22 at 7 a.m. PT for a livestreamed Xenoblade Chronicles 3 Direct presentation featuring roughly 20 minutes of information about the upcoming RPG adventure for Nintendo Switch." - - expect(info["description"].as_s).to eq(description) - expect(info["shortDescription"].as_s).to eq(description) - expect(info["descriptionHtml"].as_s).to eq(description) - - # Video metadata - - expect(info["genre"].as_s).to eq("Gaming") - expect(info["genreUcid"].as_s).to be_empty - expect(info["license"].as_s).to be_empty - - # Author infos - - expect(info["author"].as_s).to eq("Nintendo") - expect(info["ucid"].as_s).to eq("UCGIY_O-8vW4rfX98KlMkvRg") - - expect(info["authorThumbnail"].as_s).to eq( - "https://yt3.ggpht.com/ytc/AKedOLTt4vtjREUUNdHlyu9c4gtJjG90M9jQheRlLKy44A=s48-c-k-c0x00ffffff-no-rj" - ) - - expect(info["authorVerified"].as_bool).to be_true - expect(info["subCountText"].as_s).to eq("8.5M") - end - - it "parses scheduled livestreams data (test 2)" do + it "parses scheduled livestreams data" do # Enable mock _player = load_mock("video/scheduled_live_PBD-Podcast.player") _next = load_mock("video/scheduled_live_PBD-Podcast.next") raw_data = _player.merge!(_next) - info = parse_video_info("RG0cjYbXxME", raw_data) + info = parse_video_info("N-yVic7BbY0", raw_data) # Some basic verifications expect(typeof(info)).to eq(Hash(String, JSON::Any)) @@ -117,11 +16,11 @@ Spectator.describe "parse_video_info" do # Basic video infos - expect(info["title"].as_s).to eq("The Truth About Greenpeace w/ Dr. Patrick Moore | PBD Podcast | Ep. 171") - expect(info["views"].as_i).to eq(24) - expect(info["likes"].as_i).to eq(22) + expect(info["title"].as_s).to eq("Home Team | PBD Podcast | Ep. 241") + expect(info["views"].as_i).to eq(6) + expect(info["likes"].as_i).to eq(7) expect(info["lengthSeconds"].as_i).to eq(0_i64) - expect(info["published"].as_s).to eq("2022-07-14T13:00:00Z") # Unix 1657803600 + expect(info["published"].as_s).to eq("2023-02-28T14:00:00Z") # Unix 1677592800 # Extra video infos @@ -173,39 +72,22 @@ Spectator.describe "parse_video_info" do expect(info["relatedVideos"].as_a.size).to eq(20) - # related video #1 - expect(info["relatedVideos"][2]["id"]).to eq("La9oLLoI5Rc") - expect(info["relatedVideos"][2]["author"]).to eq("Tom Bilyeu") - expect(info["relatedVideos"][2]["ucid"]).to eq("UCnYMOamNKLGVlJgRUbamveA") - expect(info["relatedVideos"][2]["view_count"]).to eq("13329149") - expect(info["relatedVideos"][2]["short_view_count"]).to eq("13M") - expect(info["relatedVideos"][2]["author_verified"]).to eq("true") - - # Related video #2 - expect(info["relatedVideos"][9]["id"]).to eq("IQ_4fvpzYuA") - expect(info["relatedVideos"][9]["author"]).to eq("Business Today") - expect(info["relatedVideos"][9]["ucid"]).to eq("UCaPHWiExfUWaKsUtENLCv5w") - expect(info["relatedVideos"][9]["view_count"]).to eq("26432") - expect(info["relatedVideos"][9]["short_view_count"]).to eq("26K") - expect(info["relatedVideos"][9]["author_verified"]).to eq("true") + expect(info["relatedVideos"][0]["id"]).to eq("j7jPzzjbVuk") + expect(info["relatedVideos"][0]["author"]).to eq("Democracy Now!") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCzuqE7-t13O4NIDYJfakrhw") + expect(info["relatedVideos"][0]["view_count"]).to eq("7576") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("7.5K") + expect(info["relatedVideos"][0]["author_verified"]).to eq("true") # Description - description_start_text = <<-TXT - PBD Podcast Episode 171. In this episode, Patrick Bet-David is joined by Dr. Patrick Moore and Adam Sosnick. - - Join the channel to get exclusive access to perks: https://bit.ly/3Q9rSQL - TXT + description_start_text = "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - https://aura.com/pbd" expect(info["description"].as_s).to start_with(description_start_text) expect(info["shortDescription"].as_s).to start_with(description_start_text) expect(info["descriptionHtml"].as_s).to start_with( - <<-TXT - PBD Podcast Episode 171. In this episode, Patrick Bet-David is joined by Dr. Patrick Moore and Adam Sosnick. - - Join the channel to get exclusive access to perks: bit.ly/3Q9rSQL - TXT + "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - aura.com/pbd" ) # Video metadata @@ -223,6 +105,6 @@ Spectator.describe "parse_video_info" do "https://yt3.ggpht.com/61ArDiQshJrvSXcGLhpFfIO3hlMabe2fksitcf6oGob0Mdr5gztdkXxRljICUodL4iuTSrtxW4A=s48-c-k-c0x00ffffff-no-rj" ) expect(info["authorVerified"].as_bool).to be_false - expect(info["subCountText"].as_s).to eq("227K") + expect(info["subCountText"].as_s).to eq("594K") end end diff --git a/src/invidious.cr b/src/invidious.cr index 5064f0b8..d4f8e0fb 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -34,6 +34,7 @@ require "protodec/utils" require "./invidious/database/*" require "./invidious/database/migrations/*" +require "./invidious/http_server/*" require "./invidious/helpers/*" require "./invidious/yt_backend/*" require "./invidious/frontend/*" diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 8e300288..ce34ff82 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -1,3 +1,5 @@ +private IMAGE_QUALITIES = {320, 560, 640, 1280, 2000} + # TODO: Add "sort_by" def fetch_channel_community(ucid, continuation, locale, format, thin_mode) response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") @@ -69,7 +71,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) next if !post content_html = post["contentText"]?.try { |t| parse_content(t) } || "" - author = post["authorText"]?.try &.["simpleText"]? || "" + author = post["authorText"]["runs"]?.try &.[0]?.try &.["text"]? || "" json.object do json.field "author", author @@ -108,6 +110,8 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) like_count = post["actionButtons"]["commentActionButtonsRenderer"]["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"] .try &.as_s.gsub(/\D/, "").to_i? || 0 + reply_count = short_text_to_number(post.dig?("actionButtons", "commentActionButtonsRenderer", "replyButton", "buttonRenderer", "text", "simpleText").try &.as_s || "0") + json.field "content", html_to_content(content_html) json.field "contentHtml", content_html @@ -115,6 +119,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) json.field "likeCount", like_count + json.field "replyCount", reply_count json.field "commentId", post["postId"]? || post["commentId"]? || "" json.field "authorIsChannelOwner", post["authorEndpoint"]["browseEndpoint"]["browseId"] == ucid @@ -174,9 +179,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) aspect_ratio = (width.to_f / height.to_f) url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") - qualities = {320, 560, 640, 1280, 2000} - - qualities.each do |quality| + IMAGE_QUALITIES.each do |quality| json.object do json.field "url", url.gsub(/=s\d+/, "=s#{quality}") json.field "width", quality @@ -185,10 +188,63 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - # TODO - # when .has_key?("pollRenderer") - # attachment = attachment["pollRenderer"] - # json.field "type", "poll" + when .has_key?("pollRenderer") + attachment = attachment["pollRenderer"] + json.field "type", "poll" + json.field "totalVotes", short_text_to_number(attachment["totalVotes"]["simpleText"].as_s.split(" ")[0]) + json.field "choices" do + json.array do + attachment["choices"].as_a.each do |choice| + json.object do + json.field "text", choice.dig("text", "runs", 0, "text").as_s + # A choice can have an image associated with it. + # Ex post: https://www.youtube.com/post/UgkxD4XavXUD4NQiddJXXdohbwOwcVqrH9Re + if choice["image"]? + thumbnail = choice["image"]["thumbnails"][0].as_h + width = thumbnail["width"].as_i + height = thumbnail["height"].as_i + aspect_ratio = (width.to_f / height.to_f) + url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") + json.field "image" do + json.array do + IMAGE_QUALITIES.each do |quality| + json.object do + json.field "url", url.gsub(/=s\d+/, "=s#{quality}") + json.field "width", quality + json.field "height", (quality / aspect_ratio).ceil.to_i + end + end + end + end + end + end + end + end + end + when .has_key?("postMultiImageRenderer") + attachment = attachment["postMultiImageRenderer"] + json.field "type", "multiImage" + json.field "images" do + json.array do + attachment["images"].as_a.each do |image| + json.array do + thumbnail = image["backstageImageRenderer"]["image"]["thumbnails"][0].as_h + width = thumbnail["width"].as_i + height = thumbnail["height"].as_i + aspect_ratio = (width.to_f / height.to_f) + url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") + + IMAGE_QUALITIES.each do |quality| + json.object do + json.field "url", url.gsub(/=s\d+/, "=s#{quality}") + json.field "width", quality + json.field "height", (quality / aspect_ratio).ceil.to_i + end + end + end + end + end + end else json.field "type", "unknown" json.field "error", "Unrecognized attachment type." diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index befec03d..3d53f2ab 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -30,7 +30,9 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so "15:embedded" => { "1:embedded" => { "1:string" => object_inner_2_encoded, - "2:string" => "00000000-0000-0000-0000-000000000000", + }, + "2:embedded" => { + "1:string" => "00000000-0000-0000-0000-000000000000", }, "3:varint" => sort_by_numerical, }, diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index d691ca36..b15d63d4 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -181,6 +181,12 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b json.field "content", html_to_content(content_html) json.field "contentHtml", content_html + json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) + json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil) + if node_comment["sponsorCommentBadge"]? + # Sponsor icon thumbnails always have one object and there's only ever the url property in it + json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s + end json.field "published", published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) @@ -322,11 +328,21 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) end author_name = HTML.escape(child["author"].as_s) + sponsor_icon = "" if child["verified"]?.try &.as_bool && child["authorIsChannelOwner"]?.try &.as_bool author_name += " " elsif child["verified"]?.try &.as_bool author_name += " " end + + if child["isSponsor"]?.try &.as_bool + sponsor_icon = String.build do |str| + str << %() + end + end html << <<-END_HTML
@@ -337,6 +353,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) #{author_name} + #{sponsor_icon}

#{child["contentHtml"]}

END_HTML @@ -670,8 +687,30 @@ def content_to_comment_html(content, video_id : String? = "") end text = "#{text}" if run["bold"]? + text = "#{text}" if run["strikethrough"]? text = "#{text}" if run["italics"]? + # check for custom emojis + if run["emoji"]? + if run["emoji"]["isCustomEmoji"]?.try &.as_bool + if emojiImage = run.dig?("emoji", "image") + emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text + emojiThumb = emojiImage["thumbnails"][0] + text = String.build do |str| + str << %() << emojiAlt << ) + end + else + # Hide deleted channel emoji + text = "" + end + end + end + text end diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index a9b00860..e3214469 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -20,7 +20,7 @@ module Invidious::Frontend::WatchPage def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String if CONFIG.disabled?("downloads") - return "

#{translate(locale, "Download is disabled.")}

" + return "

#{translate(locale, "Download is disabled")}

" end return String.build(4000) do |str| diff --git a/src/invidious/helpers/json_filter.cr b/src/invidious/helpers/json_filter.cr index b8e8f96d..3f4080ba 100644 --- a/src/invidious/helpers/json_filter.cr +++ b/src/invidious/helpers/json_filter.cr @@ -20,7 +20,7 @@ module JSONFilter /^\(|\(\(|\/\(/ end - def self.parse_fields(fields_text : String) : Nil + def self.parse_fields(fields_text : String, &) : Nil if fields_text.empty? raise FieldsParser::ParseError.new "Fields is empty" end @@ -42,7 +42,7 @@ module JSONFilter parse_nest_groups(fields_text) { |nest_list| yield nest_list } end - def self.parse_single_nests(fields_text : String) : Nil + def self.parse_single_nests(fields_text : String, &) : Nil single_nests = remove_nest_groups(fields_text) if !single_nests.empty? @@ -60,7 +60,7 @@ module JSONFilter end end - def self.parse_nest_groups(fields_text : String) : Nil + def self.parse_nest_groups(fields_text : String, &) : Nil nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64) bracket_pairs = get_bracket_pairs(fields_text, true) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 635f0984..c1874780 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -74,6 +74,7 @@ struct SearchVideo json.field "author", self.author json.field "authorId", self.ucid json.field "authorUrl", "/channel/#{self.ucid}" + json.field "authorVerified", self.author_verified json.field "videoThumbnails" do Invidious::JSONify::APIv1.thumbnails(json, self.id) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index ed0cca38..500a2582 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -162,7 +162,7 @@ def number_with_separator(number) end def short_text_to_number(short_text : String) : Int64 - matches = /(?\d+(\.\d+)?)\s?(?[mMkKbB])?/.match(short_text) + matches = /(?\d+(\.\d+)?)\s?(?[mMkKbB]?)/.match(short_text) number = matches.try &.["number"].to_f || 0.0 case matches.try &.["suffix"].downcase @@ -259,7 +259,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.request_target - referer = "/" + referer.gsub(/[^\/?@&%=\-_.0-9a-zA-Z]/, "").lstrip("/\\") + referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\") if referer == env.request.path referer = fallback diff --git a/src/invidious/http_server/utils.cr b/src/invidious/http_server/utils.cr new file mode 100644 index 00000000..e3f1fa0f --- /dev/null +++ b/src/invidious/http_server/utils.cr @@ -0,0 +1,20 @@ +module Invidious::HttpServer + module Utils + extend self + + def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false) + url = URI.parse(raw_url) + + # Add some URL parameters + params = url.query_params + params["host"] = url.host.not_nil! # Should never be nil, in theory + params["region"] = region if !region.nil? + + if absolute + return "#{HOST_URL}#{url.request_target}?#{params}" + else + return "#{url.request_target}?#{params}" + end + end + end +end diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 642789aa..fe4b5223 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -3,7 +3,7 @@ require "json" module Invidious::JSONify::APIv1 extend self - def video(video : Video, json : JSON::Builder, *, locale : String?) + def video(video : Video, json : JSON::Builder, *, locale : String?, proxy : Bool = false) json.object do json.field "type", video.video_type @@ -89,7 +89,14 @@ module Invidious::JSONify::APIv1 # Not available on MPEG-4 Timed Text (`text/mp4`) streams (livestreams only) json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]? - json.field "url", fmt["url"] + if proxy + json.field "url", Invidious::HttpServer::Utils.proxy_video_url( + fmt["url"].to_s, absolute: true + ) + else + json.field "url", fmt["url"] + end + json.field "itag", fmt["itag"].as_i.to_s json.field "type", fmt["mimeType"] json.field "clen", fmt["contentLength"]? || "-1" @@ -190,6 +197,21 @@ module Invidious::JSONify::APIv1 end end + if !video.music.empty? + json.field "musicTracks" do + json.array do + video.music.each do |music| + json.object do + json.field "song", music.song + json.field "artist", music.artist + json.field "album", music.album + json.field "license", music.license + end + end + end + end + end + json.field "recommendedVideos" do json.array do video.related_videos.each do |rv| diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 9bb73136..5aa4452c 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -203,7 +203,7 @@ module Invidious::Routes::Account referer = get_referer(env) if !user - return env.redirect referer + return env.redirect "/login?referer=#{URI.encode_path_segment(env.request.resource)}" end user = user.as(User) @@ -262,6 +262,7 @@ module Invidious::Routes::Account end query["token"] = access_token + query["username"] = URI.encode_path_segment(user.email) url.query = query.to_s env.redirect url.to_s diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr index ae65f10d..662d1002 100644 --- a/src/invidious/routes/api/manifest.cr +++ b/src/invidious/routes/api/manifest.cr @@ -29,7 +29,7 @@ module Invidious::Routes::API::Manifest if local uri = URI.parse(url) - url = "#{uri.request_target}host/#{uri.host}/" + url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/" end "#{url}" @@ -42,7 +42,7 @@ module Invidious::Routes::API::Manifest if local adaptive_fmts.each do |fmt| - fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) + fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}") end end diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index 421355bb..ce2ee812 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -31,6 +31,88 @@ module Invidious::Routes::API::V1::Authenticated env.response.status_code = 204 end + def self.export_invidious(env) + env.response.content_type = "application/json" + user = env.get("user").as(User) + + return Invidious::User::Export.to_invidious(user) + end + + def self.import_invidious(env) + user = env.get("user").as(User) + + begin + if body = env.request.body + body = env.request.body.not_nil!.gets_to_end + else + body = "{}" + end + Invidious::User::Import.from_invidious(user, body) + rescue + end + + env.response.status_code = 204 + end + + def self.get_history(env) + env.response.content_type = "application/json" + user = env.get("user").as(User) + + page = env.params.query["page"]?.try &.to_i?.try &.clamp(0, Int32::MAX) + page ||= 1 + + max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) + max_results ||= user.preferences.max_results + max_results ||= CONFIG.default_user_preferences.max_results + + start_index = (page - 1) * max_results + if user.watched[start_index]? + watched = user.watched.reverse[start_index, max_results] + end + watched ||= [] of String + + return watched.to_json + end + + def self.mark_watched(env) + user = env.get("user").as(User) + + if !user.preferences.watch_history + return error_json(409, "Watch history is disabled in preferences.") + end + + id = env.params.url["id"] + if !id.match(/^[a-zA-Z0-9_-]{11}$/) + return error_json(400, "Invalid video id.") + end + + Invidious::Database::Users.mark_watched(user, id) + env.response.status_code = 204 + end + + def self.mark_unwatched(env) + user = env.get("user").as(User) + + if !user.preferences.watch_history + return error_json(409, "Watch history is disabled in preferences.") + end + + id = env.params.url["id"] + if !id.match(/^[a-zA-Z0-9_-]{11}$/) + return error_json(400, "Invalid video id.") + end + + Invidious::Database::Users.mark_unwatched(user, id) + env.response.status_code = 204 + end + + def self.clear_history(env) + user = env.get("user").as(User) + + Invidious::Database::Users.clear_watch_history(user) + env.response.status_code = 204 + end + def self.feed(env) env.response.content_type = "application/json" diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index ca2b2734..bcb4db2c 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -89,6 +89,8 @@ module Invidious::Routes::API::V1::Channels json.field "descriptionHtml", channel.description_html json.field "allowedRegions", channel.allowed_regions + json.field "tabs", channel.tabs + json.field "authorVerified", channel.verified json.field "latestVideos" do json.array do diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 43d360e6..e499f4d6 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -150,4 +150,31 @@ module Invidious::Routes::API::V1::Misc response end + + # resolve channel and clip urls, return the UCID + def self.resolve_url(env) + env.response.content_type = "application/json" + url = env.params.query["url"]? + + return error_json(400, "Missing URL to resolve") if !url + + begin + resolved_url = YoutubeAPI.resolve_url(url.as(String)) + endpoint = resolved_url["endpoint"] + pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" + if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId") + elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId") + elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" + return error_json(400, "Unknown url") + end + rescue ex + return error_json(500, ex) + end + JSON.build do |json| + json.object do + json.field "ucid", resolved_ucid.try &.as_s || "" + json.field "pageType", pageType + end + end + end end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index a6b2eb4e..f312211e 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -6,6 +6,7 @@ module Invidious::Routes::API::V1::Videos id = env.params.url["id"] region = env.params.query["region"]? + proxy = {"1", "true"}.any? &.== env.params.query["local"]? begin video = get_video(id, region: region) @@ -15,7 +16,9 @@ module Invidious::Routes::API::V1::Videos return error_json(500, ex) end - video.to_json(locale, nil) + return JSON.build do |json| + Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy) + end end def self.captions(env) @@ -90,45 +93,50 @@ module Invidious::Routes::API::V1::Videos # as well as some other markup that makes it cumbersome, so we try to fix that here if caption.name.includes? "auto-generated" caption_xml = YT_POOL.client &.get(url).body - caption_xml = XML.parse(caption_xml) - webvtt = String.build do |str| - str << <<-END_VTT - WEBVTT - Kind: captions - Language: #{tlang || caption.language_code} + if caption_xml.starts_with?(" i + 1 - end_time = caption_nodes[i + 1]["start"].to_f.seconds - else - end_time = start_time + duration + if caption_nodes.size > i + 1 + end_time = caption_nodes[i + 1]["start"].to_f.seconds + else + end_time = start_time + duration + end + + start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" + end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" + + text = HTML.unescape(node.content) + text = text.gsub(//, "") + text = text.gsub(/<\/font>/, "") + if md = text.match(/(?.*) : (?.*)/) + text = "#{md["text"]}" + end + + str << <<-END_CUE + #{start_time} --> #{end_time} + #{text} + + + END_CUE end - - start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" - end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" - - text = HTML.unescape(node.content) - text = text.gsub(//, "") - text = text.gsub(/<\/font>/, "") - if md = text.match(/(?.*) : (?.*)/) - text = "#{md["text"]}" - end - - str << <<-END_CUE - #{start_time} --> #{end_time} - #{text} - - - END_CUE end end else @@ -138,7 +146,12 @@ module Invidious::Routes::API::V1::Videos # # See: https://github.com/iv-org/invidious/issues/2391 webvtt = YT_POOL.client &.get("#{url}&format=vtt").body - .gsub(/([0-9:.]{12} --> [0-9:.]{12}).+/, "\\1") + if webvtt.starts_with?(" [0-9:.]{12}).+/, "\\1") + end end if title = env.params.query["title"]? diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 99fc13a2..6454131a 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -6,14 +6,14 @@ module Invidious::Routes::Login user = env.get? "user" - return env.redirect "/feed/subscriptions" if user + referer = get_referer(env, "/feed/subscriptions") + + return env.redirect referer if user if !CONFIG.login_enabled return error_template(400, "Login has been disabled by administrator.") end - referer = get_referer(env, "/feed/subscriptions") - email = nil password = nil captcha = nil diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr index 7b1fa876..0704c05e 100644 --- a/src/invidious/routes/subscriptions.cr +++ b/src/invidious/routes/subscriptions.cr @@ -104,33 +104,8 @@ module Invidious::Routes::Subscriptions if format == "json" env.response.content_type = "application/json" env.response.headers["content-disposition"] = "attachment" - playlists = Invidious::Database::Playlists.select_like_iv(user.email) - return JSON.build do |json| - json.object do - json.field "subscriptions", user.subscriptions - json.field "watch_history", user.watched - json.field "preferences", user.preferences - json.field "playlists" do - json.array do - playlists.each do |playlist| - json.object do - json.field "title", playlist.title - json.field "description", html_to_content(playlist.description_html) - json.field "privacy", playlist.privacy.to_s - json.field "videos" do - json.array do - Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| - json.string video_id - end - end - end - end - end - end - end - end - end + return Invidious::User::Export.to_invidious(user) else env.response.content_type = "application/xml" env.response.headers["content-disposition"] = "attachment" diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 560f9c19..9641e01a 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -35,6 +35,13 @@ module Invidious::Routes::VideoPlayback end end + # See: https://github.com/iv-org/invidious/issues/3302 + range_header = env.request.headers["Range"]? + if range_header.nil? + range_for_head = query_params["range"]? || "0-640" + headers["Range"] = "bytes=#{range_for_head}" + end + client = make_client(URI.parse(host), region) response = HTTP::Client::Response.new(500) error = "" @@ -70,6 +77,9 @@ module Invidious::Routes::VideoPlayback end end + # Remove the Range header added previously. + headers.delete("Range") if range_header.nil? + if response.status_code >= 400 env.response.content_type = "text/plain" haltf env, response.status_code @@ -91,14 +101,8 @@ module Invidious::Routes::VideoPlayback env.response.headers["Access-Control-Allow-Origin"] = "*" if location = resp.headers["Location"]? - location = URI.parse(location) - location = "#{location.request_target}&host=#{location.host}" - - if region - location += "®ion=#{region}" - end - - return env.redirect location + url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region) + return env.redirect url end IO.copy(resp.body_io, env.response) @@ -252,7 +256,7 @@ module Invidious::Routes::VideoPlayback return error_template(400, "Invalid video ID") end - if itag.nil? || itag <= 0 || itag >= 1000 + if !itag.nil? && (itag <= 0 || itag >= 1000) return error_template(400, "Invalid itag") end @@ -273,7 +277,11 @@ module Invidious::Routes::VideoPlayback return error_template(500, ex) end - fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } + if itag.nil? + fmt = video.fmt_stream[-1]? + else + fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } + end url = fmt.try &.["url"]?.try &.as_s if !url diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 491022a5..9e2ade3d 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -132,6 +132,8 @@ module Invidious::Routing get "/c/:user#{path}", Routes::Channels, :brand_redirect # /user/linustechtips | Not always the same as /c/ get "/user/:user#{path}", Routes::Channels, :brand_redirect + # /@LinusTechTips | Handle + get "/@:user#{path}", Routes::Channels, :brand_redirect # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow get "/attribution_link#{path}", Routes::Channels, :brand_redirect # /profile?user=linustechtips @@ -252,6 +254,14 @@ module Invidious::Routing get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences + get "/api/v1/auth/export/invidious", {{namespace}}::Authenticated, :export_invidious + post "/api/v1/auth/import/invidious", {{namespace}}::Authenticated, :import_invidious + + get "/api/v1/auth/history", {{namespace}}::Authenticated, :get_history + post "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_watched + delete "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_unwatched + delete "/api/v1/auth/history", {{namespace}}::Authenticated, :clear_history + get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions @@ -279,6 +289,7 @@ module Invidious::Routing get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes + get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url {% end %} end end diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 1f957081..134eb437 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -4,11 +4,12 @@ def fetch_trending(trending_type, region, locale) plid = nil - if trending_type == "Music" + case trending_type.try &.downcase + when "music" params = "4gINGgt5dG1hX2NoYXJ0cw%3D%3D" - elsif trending_type == "Gaming" + when "gaming" params = "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D" - elsif trending_type == "Movies" + when "movies" params = "4gIKGgh0cmFpbGVycw%3D%3D" else # Default params = "" diff --git a/src/invidious/user/exports.cr b/src/invidious/user/exports.cr new file mode 100644 index 00000000..b52503c9 --- /dev/null +++ b/src/invidious/user/exports.cr @@ -0,0 +1,35 @@ +struct Invidious::User + module Export + extend self + + def to_invidious(user : User) + playlists = Invidious::Database::Playlists.select_like_iv(user.email) + + return JSON.build do |json| + json.object do + json.field "subscriptions", user.subscriptions + json.field "watch_history", user.watched + json.field "preferences", user.preferences + json.field "playlists" do + json.array do + playlists.each do |playlist| + json.object do + json.field "title", playlist.title + json.field "description", html_to_content(playlist.description_html) + json.field "privacy", playlist.privacy.to_s + json.field "videos" do + json.array do + Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: CONFIG.playlist_length_limit).each do |video_id| + json.string video_id + end + end + end + end + end + end + end + end + end + end + end # module +end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index d626c7d1..0038a97a 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -247,6 +247,17 @@ struct Video info["reason"]?.try &.as_s end + def music : Array(VideoMusic) + info["music"].as_a.map { |music_json| + VideoMusic.new( + music_json["song"].as_s, + music_json["album"].as_s, + music_json["artist"].as_s, + music_json["license"].as_s + ) + } + end + # Macros defining getters/setters for various types of data private macro getset_string(name) diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 4642c1a7..13f81a31 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -31,6 +31,72 @@ module Invidious::Videos return captions_list end + def timedtext_to_vtt(timedtext : String, tlang = nil) : String + # In the future, we could just directly work with the url. This is more of a POC + cues = [] of XML::Node + tree = XML.parse(timedtext) + tree = tree.children.first + + tree.children.each do |item| + if item.name == "body" + item.children.each do |cue| + if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n") + cues << cue + end + end + break + end + end + result = String.build do |result| + result << <<-END_VTT + WEBVTT + Kind: captions + Language: #{tlang || @language_code} + + + END_VTT + + result << "\n\n" + + cues.each_with_index do |node, i| + start_time = node["t"].to_f.milliseconds + + duration = node["d"]?.try &.to_f.milliseconds + + duration ||= start_time + + if cues.size > i + 1 + end_time = cues[i + 1]["t"].to_f.milliseconds + else + end_time = start_time + duration + end + + # start_time + result << start_time.hours.to_s.rjust(2, '0') + result << ':' << start_time.minutes.to_s.rjust(2, '0') + result << ':' << start_time.seconds.to_s.rjust(2, '0') + result << '.' << start_time.milliseconds.to_s.rjust(3, '0') + + result << " --> " + + # end_time + result << end_time.hours.to_s.rjust(2, '0') + result << ':' << end_time.minutes.to_s.rjust(2, '0') + result << ':' << end_time.seconds.to_s.rjust(2, '0') + result << '.' << end_time.milliseconds.to_s.rjust(3, '0') + + result << "\n" + + node.children.each do |s| + result << s.content + end + result << "\n" + result << "\n" + end + end + return result + end + # List of all caption languages available on Youtube. LANGUAGES = { "", diff --git a/src/invidious/videos/music.cr b/src/invidious/videos/music.cr new file mode 100644 index 00000000..08d88a3e --- /dev/null +++ b/src/invidious/videos/music.cr @@ -0,0 +1,13 @@ +require "json" + +struct VideoMusic + include JSON::Serializable + + property song : String + property album : String + property artist : String + property license : String + + def initialize(@song : String, @album : String, @artist : String, @license : String) + end +end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 5df49286..13ee5f65 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -66,8 +66,10 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") reason ||= player_response.dig("playabilityStatus", "reason").as_s - # Stop here if video is not a scheduled livestream - if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) + # Stop here if video is not a scheduled livestream or + # for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help + if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) || + playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails") return { "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "reason" => JSON::Any.new(reason), @@ -183,10 +185,12 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # We have to try to extract viewCount from videoPrimaryInfoRenderer first, # then from videoDetails, as the latter is "0" for livestreams (we want # to get the amount of viewers watching). - views_txt = video_primary_renderer - .try &.dig?("viewCount", "videoViewCountRenderer", "viewCount", "runs", 0, "text") - views_txt ||= video_details["viewCount"]? - views = views_txt.try &.as_s.gsub(/\D/, "").to_i64? + views_txt = extract_text( + video_primary_renderer + .try &.dig?("viewCount", "videoViewCountRenderer", "viewCount") + ) + views_txt ||= video_details["viewCount"]?.try &.as_s || "" + views = views_txt.gsub(/\D/, "").to_i64? length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"]) .try &.as_s.to_i64 @@ -309,6 +313,44 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any end end + # Music section + + music_list = [] of VideoMusic + music_desclist = player_response.dig?( + "engagementPanels", 1, "engagementPanelSectionListRenderer", + "content", "structuredDescriptionContentRenderer", "items", 2, + "videoDescriptionMusicSectionRenderer", "carouselLockups" + ) + + music_desclist.try &.as_a.each do |music_desc| + artist = nil + album = nil + music_license = nil + + # Used when the video has multiple songs + if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title") + # "simpleText" for plain text / "runs" when song has a link + song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text") + + # some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU + next if !song + end + + music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc| + desc_title = extract_text(desc.dig?("infoRowRenderer", "title")) + if desc_title == "ARTIST" + artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) + elsif desc_title == "SONG" + song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) + elsif desc_title == "ALBUM" + album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) + elsif desc_title == "LICENSES" + music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata")) + end + end + music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s) + end + # Author infos author = video_details["author"]?.try &.as_s @@ -359,6 +401,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "genre" => JSON::Any.new(genre.try &.as_s || ""), "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), "license" => JSON::Any.new(license.try &.as_s || ""), + # Music section + "music" => JSON.parse(music_list.to_json), # Author infos "author" => JSON::Any.new(author || ""), "ucid" => JSON::Any.new(ucid || ""), diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 22870317..bcba74cf 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -39,6 +39,8 @@ <% end %>
+ + <% if query %> <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%>
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index a29315ef..6e62a471 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -49,6 +49,8 @@ <% end %>
+ +
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 0e959ff2..fa12374f 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,3 +1,5 @@ +<% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %> +
<% case item when %> @@ -40,6 +42,11 @@ <% if item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> + + <% if item_watched %> +
+
+ <% end %>
<% end %>

<%= HTML.escape(item.title) %>

@@ -67,6 +74,11 @@ <% elsif item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> + + <% if item_watched %> +
+
+ <% end %>
<% end %>

<%= HTML.escape(item.title) %>

@@ -124,6 +136,11 @@ <% elsif item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> + + <% if item_watched %> +
+
+ <% end %>
<% end %>

<%= HTML.escape(item.title) %>

diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 89819ef0..548104c8 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -62,6 +62,8 @@ <% end %>
+ +
<% if page > 1 %> diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr index 43173355..2a4b6edd 100644 --- a/src/invidious/views/feeds/playlists.ecr +++ b/src/invidious/views/feeds/playlists.ecr @@ -39,3 +39,5 @@ <%= rendered "components/item" %> <% end %>
+ + diff --git a/src/invidious/views/feeds/popular.ecr b/src/invidious/views/feeds/popular.ecr index e77f35b9..5fbe539c 100644 --- a/src/invidious/views/feeds/popular.ecr +++ b/src/invidious/views/feeds/popular.ecr @@ -16,3 +16,5 @@ <%= rendered "components/item" %> <% end %>
+ + diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 76f2f2bd..9c69c5b0 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -62,6 +62,8 @@ <% end %>
+ +
<% if page > 1 %> diff --git a/src/invidious/views/feeds/trending.ecr b/src/invidious/views/feeds/trending.ecr index a35c4ee3..7dc416c6 100644 --- a/src/invidious/views/feeds/trending.ecr +++ b/src/invidious/views/feeds/trending.ecr @@ -45,3 +45,5 @@ <%= rendered "components/item" %> <% end %>
+ + diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr index 0ecfe832..3351c21c 100644 --- a/src/invidious/views/hashtag.ecr +++ b/src/invidious/views/hashtag.ecr @@ -24,6 +24,8 @@ <%- end -%>
+ +
<%- if page > 1 -%> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index df3112db..a04acf4c 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -106,6 +106,8 @@ <% end %>
+ +
<% if page > 1 %> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 254449a1..a7469e36 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -37,6 +37,8 @@
<%- end -%> + +
<%- if query.page > 1 -%> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index a6f2e524..a3ec94e8 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -181,7 +181,11 @@ we're going to need to do it here in order to allow for translations. <% end %>

<% if video.license %> -

<%= translate(locale, "License: ") %><%= video.license %>

+ <% if video.license.empty? %> +

<%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %>

+ <% else %> +

<%= translate(locale, "License: ") %><%= video.license %>

+ <% end %> <% end %>

<%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %>

@@ -235,6 +239,28 @@ we're going to need to do it here in order to allow for translations.
+ <% if !video.music.empty? %> + + + +
+ <% video.music.each do |music| %> +
+

<%= translate(locale, "Song: ") %><%= music.song %>

+

<%= translate(locale, "Artist: ") %><%= music.artist %>

+

<%= translate(locale, "Album: ") %><%= music.album %>

+
+ <% end %> +
+
+ + <% end %>
<% if nojs %> <%= comment_html %> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 65d107b2..1a37d606 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -18,6 +18,7 @@ private ITEM_PARSERS = { Parsers::CategoryRendererParser, Parsers::RichItemRendererParser, Parsers::ReelItemRendererParser, + Parsers::ItemSectionRendererParser, Parsers::ContinuationItemRendererParser, } @@ -172,7 +173,17 @@ private module Parsers # When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube. # Always simpleText # TODO change default value to nil + subscriber_count = item_contents.dig?("subscriberCountText", "simpleText") + + # Since youtube added channel handles, `VideoCountText` holds the number of + # subscribers and `subscriberCountText` holds the handle, except when the + # channel doesn't have a handle (e.g: some topic music channels). + # See https://github.com/iv-org/invidious/issues/3394#issuecomment-1321261688 + if !subscriber_count || !subscriber_count.as_s.includes? " subscriber" + subscriber_count = item_contents.dig?("videoCountText", "simpleText") + end + subscriber_count = subscriber_count .try { |s| short_text_to_number(s.as_s.split(" ")[0]).to_i32 } || 0 # Auto-generated channels doesn't have videoCountText @@ -367,6 +378,30 @@ private module Parsers end end + # Parses an InnerTube itemSectionRenderer into a SearchVideo. + # Returns nil when the given object isn't a ItemSectionRenderer + # + # A itemSectionRenderer seems to be a simple wrapper for a videoRenderer, used + # by the result page for channel searches. It is located inside a continuationItems + # container.It is very similar to RichItemRendererParser + # + module ItemSectionRendererParser + def self.process(item : JSON::Any, author_fallback : AuthorFallback) + if item_contents = item.dig?("itemSectionRenderer", "contents", 0) + return self.parse(item_contents, author_fallback) + end + end + + private def self.parse(item_contents, author_fallback) + child = VideoRendererParser.process(item_contents, author_fallback) + return child + end + + def self.parser_name + return {{@type.name}} + end + end + # Parses an InnerTube richItemRenderer into a SearchVideo. # Returns nil when the given object isn't a RichItemRenderer # @@ -682,7 +717,11 @@ module HelperExtractors # Returns a 0 when it's unable to do so def self.get_video_count(container : JSON::Any) : Int32 if box = container["videoCountText"]? - return extract_text(box).try &.gsub(/\D/, "").to_i || 0 + if (extracted_text = extract_text(box)) && !extracted_text.includes? " subscriber" + return extracted_text.gsub(/\D/, "").to_i + else + return 0 + end elsif box = container["videoCount"]? return box.as_s.to_i else @@ -759,6 +798,7 @@ end def extract_items(initial_data : InitialData, &block) if unpackaged_data = initial_data["contents"]?.try &.as_h elsif unpackaged_data = initial_data["response"]?.try &.as_h + elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 1).try &.as_h elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 0).try &.as_h else unpackaged_data = initial_data