diff options
45 files changed, 626 insertions, 190 deletions
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" @@ -154,6 +154,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [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 ## Liability diff --git a/assets/css/default.css b/assets/css/default.css index f6b94431..3deaebe1 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -508,8 +508,9 @@ hr { } /* Description Expansion Styling*/ -#descexpansionbutton { - display: none +#descexpansionbutton, +#music-desc-expansion { + display: none; } #descexpansionbutton ~ div { @@ -527,7 +528,8 @@ hr { margin-top: 20px; } -label[for="descexpansionbutton"]:hover { +label[for="descexpansionbutton"]:hover, +label[for="music-desc-expansion"]:hover { cursor: pointer; } @@ -539,14 +541,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 */ 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 e31a0e28..181ff933 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -536,5 +536,12 @@ "generic_count_seconds_3": "{{count}} ثوانٍ", "generic_count_seconds_4": "{{count}} ثانية", "generic_count_seconds_5": "{{count}} ثانية", - "error_video_not_in_playlist": "الفيديو المطلوب غير موجود في قائمة التشغيل هذه. <a href=\"`x`\"> انقر هنا للحصول على الصفحة الرئيسية لقائمة التشغيل. </a>" + "error_video_not_in_playlist": "الفيديو المطلوب غير موجود في قائمة التشغيل هذه. <a href=\"`x`\"> انقر هنا للحصول على الصفحة الرئيسية لقائمة التشغيل. </a>", + "channel_tab_shorts_label": "الفيديوهات القصيرة", + "channel_tab_streams_label": "البث المباشر", + "channel_tab_playlists_label": "قوائم التشغيل", + "channel_tab_channels_label": "القنوات", + "Music in this video": "الموسيقى في هذا الفيديو", + "Album: ": "الألبوم: ", + "Artist: ": "الفنان: " } diff --git a/locales/cs.json b/locales/cs.json index 466a3058..51db1550 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", @@ -488,5 +488,12 @@ "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. <a href=\"`x`\">Klikněte sem pro navštívení domovské stránky playlistu.</a>" + "error_video_not_in_playlist": "Požadované video v tomto playlistu neexistuje. <a href=\"`x`\">Klikněte sem pro navštívení domovské stránky playlistu.</a>", + "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: " } diff --git a/locales/en-US.json b/locales/en-US.json index 12955665..a5c16fd7 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -188,6 +188,9 @@ "Engagement: ": "Engagement: ", "Whitelisted regions: ": "Whitelisted regions: ", "Blacklisted regions: ": "Blacklisted regions: ", + "Music in this video": "Music in this video", + "Artist: ": "Artist: ", + "Album: ": "Album: ", "Shared `x`": "Shared `x`", "Premieres in `x`": "Premieres in `x`", "Premieres `x`": "Premieres `x`", diff --git a/locales/eo.json b/locales/eo.json index 1a5d9938..9f37c7cb 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -325,7 +325,7 @@ "`x` marked it with a ❤": "`x` markis ĝin per ❤", "Audio mode": "Aŭda reĝimo", "Video mode": "Videa reĝimo", - "channel_tab_videos_label": "Filmetoj", + "channel_tab_videos_label": "Videoj", "Playlists": "Ludlistoj", "channel_tab_community_label": "Komunumo", "search_filters_sort_option_relevance": "rilateco", @@ -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 dc63619e..6cf721f3 100644 --- a/locales/es.json +++ b/locales/es.json @@ -472,5 +472,12 @@ "search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.", "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. <a href=\"`x`\">Haga clic aquí para acceder a la página de inicio de la lista de reproducción.</a>" + "error_video_not_in_playlist": "El vídeo solicitado no existe en esta lista de reproducción. <a href=\"`x`\">Haga clic aquí para acceder a la página de inicio de la lista de reproducción.</a>", + "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 vídeo", + "Artist: ": "Artista: ", + "Album: ": "Álbum: " } diff --git a/locales/fa.json b/locales/fa.json index f2ca2745..fe72a1e8 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -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,26 @@ "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)": "اسپانیایی (مکزیک)" } diff --git a/locales/fr.json b/locales/fr.json index 59a960d0..9d3e117f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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. <a href=\"`x`\">Cliquez ici pour retourner à la liste de lecture.</a>" + "error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. <a href=\"`x`\">Cliquez ici pour retourner à la liste de lecture.</a>", + "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/hr.json b/locales/hr.json index c8414322..72cd6a8e 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", @@ -488,5 +488,12 @@ "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. <a href=\"`x`\">Pritisni ovdje za početnu stranicu zbirke.</a>" + "error_video_not_in_playlist": "Traženi video ne postoji u ovoj zbirci. <a href=\"`x`\">Pritisni ovdje za početnu stranicu zbirke.</a>", + "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č: " } diff --git a/locales/it.json b/locales/it.json index 1a0d8efc..c60f760b 100644 --- a/locales/it.json +++ b/locales/it.json @@ -346,7 +346,6 @@ "Video mode": "Modalità video", "channel_tab_videos_label": "Video", "Playlists": "Playlist", - "channel_tab_community_label": "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 <a href=\"`x`\">ricaricare la pagina</a>", - "error_video_not_in_playlist": "Il video richiesto non esiste in questa playlist. <a href=\"`x`\">Fai clic qui per la pagina iniziale della playlist.</a>" + "error_video_not_in_playlist": "Il video richiesto non esiste in questa playlist. <a href=\"`x`\">Fai clic qui per la pagina iniziale della playlist.</a>", + "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 a392abfe..3ad4b494 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -53,12 +53,12 @@ "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_autoplay_label": "次の動画を自動再生: ", - "preferences_listen_label": "デフォルトでオーディオモードを使用: ", + "preferences_listen_label": "デフォルトで音声モードを使用: ", "preferences_local_label": "動画をプロキシーに通す: ", "preferences_speed_label": "デフォルトの再生速度: ", "preferences_quality_label": "優先する画質: ", @@ -73,14 +73,14 @@ "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: ": "ホームからフィードにリダイレクト: ", @@ -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,24 +136,24 @@ "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: ": "ブラックリストの地域: ", @@ -181,11 +181,11 @@ "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.": "このチャンネルは存在しません。", @@ -194,18 +194,18 @@ "comments_view_x_replies_0": "{{count}} 件の返信を見る", "`x` ago": "`x`前", "Load more": "もっと読み込む", - "comments_points_count_0": "{{count}} ポイント", + "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": "ビデオモード", + "`x` marked it with a ❤": "`x` が❤を送りました", + "Audio mode": "音声モード", + "Video mode": "動画モード", "channel_tab_videos_label": "動画", - "Playlists": "プレイリスト", + "Playlists": "再生リスト", "channel_tab_community_label": "コミュニティ", - "search_filters_sort_option_relevance": "関連", + "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": "今月", @@ -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,34 @@ "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": "<a href=\"`x`\">別のインスタンスを使用</a>しようとしました", + "crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す", "crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む", "Popular enabled: ": "人気動画を有効化 ", - "search_message_use_another_instance": " <a href=\"`x`\">別のインスタンスで検索</a>することもできます。", + "search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上でも検索</a>できます。", "search_filters_apply_button": "選択したフィルターを適用", - "user_saved_playlists": "`x` 個の保存済みプレイリスト", + "user_saved_playlists": "`x` 個の保存した再生リスト", "crash_page_you_found_a_bug": "Invidious でバグを見つけたようです。", - "crash_page_refresh": "<a href=\"`x`\">ページを更新</a>しようとしました", - "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 分)", + "crash_page_refresh": "<a href=\"`x`\">ページを更新</a>を試す", + "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": "上記が助けにならないなら、<a href=\"`x`\">GitHub</a> に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてください(テキストは翻訳しない)。", + "crash_page_search_issue": "<a href=\"`x`\">GitHub の既存の問題 (issue)</a> を検索", + "channel_tab_streams_label": "ライブ", + "channel_tab_playlists_label": "再生リスト", + "error_video_not_in_playlist": "要求された動画はこの再生リスト内に存在しません。<a href=\"`x`\">再生リストのホームへ。</a>", + "channel_tab_shorts_label": "ショート", + "channel_tab_channels_label": "チャンネル", + "Music in this video": "この動画の音楽", + "Artist: ": "アーティスト: ", + "Album: ": "アルバム: " } diff --git a/locales/ko.json b/locales/ko.json index af19fd02..d4f3a711 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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": "기본으로 주석 표시: ", @@ -150,7 +150,7 @@ "Subscription manager": "구독 관리자", "Save preferences": "설정 저장", "Report statistics: ": "통계 보고: ", - "Registration enabled: ": "등록 활성화: ", + "Registration enabled: ": "회원가입 활성화: ", "Login enabled: ": "로그인 활성화: ", "CAPTCHA enabled: ": "캡차 활성화: ", "Top enabled: ": "Top 활성화: ", @@ -187,8 +187,8 @@ "Polish": "폴란드어", "Persian": "페르시아어", "Pashto": "파슈토어", - "Nyanja": "체와어", - "Norwegian Bokmål": "보크몰", + "Nyanja": "냔자어", + "Norwegian Bokmål": "노르웨이 부크몰어", "Nepali": "네팔어", "Mongolian": "몽골어", "Marathi": "마라티어", @@ -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": "위의 방법 중 어느 것도 도움이 되지 않았다면, <a href=\"`x`\">깃허브에서 새 이슈를 열고</a>(가능하면 영어로) 메시지에 다음 텍스트를 포함하세요(해당 텍스트를 번역하지 마십시오):", "videoinfo_youTube_embed_link": "임베드", "videoinfo_invidious_embed_link": "임베드 링크", - "error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. <a href=\"`x`\">재생목록 목록을 보려면 여기를 클릭하십시오.</a>" + "error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. <a href=\"`x`\">재생목록 목록을 보려면 여기를 클릭하십시오.</a>", + "channel_tab_shorts_label": "쇼츠", + "channel_tab_streams_label": "실시간 스트리밍", + "channel_tab_channels_label": "채널", + "channel_tab_playlists_label": "재생목록" } diff --git a/locales/pl.json b/locales/pl.json index 6c642475..2dd3ed87 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: ", @@ -324,7 +324,7 @@ "`x` marked it with a ❤": "`x` oznaczonych ❤", "Audio mode": "Tryb audio", "Video mode": "Tryb wideo", - "channel_tab_videos_label": "Filmy", + "channel_tab_videos_label": "Wideo", "Playlists": "Playlisty", "channel_tab_community_label": "Społeczność", "search_filters_sort_option_relevance": "Trafność", @@ -443,7 +443,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 +481,19 @@ "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 <a href=\"`x`\">wyszukać w innej instancji</a>.", "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: " } diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 112ed4b7..afd31ede 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -472,5 +472,9 @@ "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. <a href=\"`x`\">Clique aqui para acessar a página inicial da playlist.</a>" + "error_video_not_in_playlist": "O vídeo solicitado não existe nesta playlist. <a href=\"`x`\">Clique aqui para acessar a página inicial da playlist.</a>", + "channel_tab_channels_label": "Canais", + "channel_tab_playlists_label": "Listas de reprodução", + "channel_tab_shorts_label": "Curtos", + "channel_tab_streams_label": "Ao Vivo" } diff --git a/locales/pt.json b/locales/pt.json index 2facba94..b6b6c110 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -472,5 +472,12 @@ "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. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>" + "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>", + "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: " } diff --git a/locales/ru.json b/locales/ru.json index e54937a6..733e0be1 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Версия для встраивания", @@ -488,5 +488,12 @@ "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры", "Popular enabled: ": "Популярное включено: ", - "error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. <a href=\"`x`\">Нажмите тут, чтобы вернуться к странице плейлиста.</a>" + "error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. <a href=\"`x`\">Нажмите тут, чтобы вернуться к странице плейлиста.</a>", + "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 f27bb20d..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", @@ -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", @@ -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 <a href=\"`x`\">uporabiti drugo instanco</a>", "download_subtitles": "Podnapisi - `x` (.vtt)", @@ -504,5 +504,12 @@ "crash_page_search_issue": "preiskal/a <a href=\"`x`\">obstoječe težave na GitHubu</a>", "crash_page_report_issue": "Če nič od navedenega ni pomagalo, prosim <a href=\"`x`\">odpri novo težavo v GitHubu</a> (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. <a href=\"`x`\">Klikni tukaj za domačo stran seznama predvajanja.</a>" + "error_video_not_in_playlist": "Zahtevani videoposnetek ne obstaja na tem seznamu predvajanja. <a href=\"`x`\">Klikni tukaj za domačo stran seznama predvajanja.</a>", + "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 b8651316..15025750 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -463,5 +463,10 @@ "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" } diff --git a/locales/tr.json b/locales/tr.json index 7dc256a9..d98e2038 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -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,12 @@ "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. <a href=\"`x`\">Oynatma listesi ana sayfası için buraya tıklayın.</a>" + "error_video_not_in_playlist": "İstenen video bu oynatma listesinde yok. <a href=\"`x`\">Oynatma listesi ana sayfası için buraya tıklayın.</a>", + "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çı: " } diff --git a/locales/uk.json b/locales/uk.json index d063799e..b44d237f 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": "Заголовок", @@ -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,12 @@ "search_filters_sort_option_rating": "Рейтингові", "search_filters_sort_option_views": "Популярні", "Popular enabled: ": "Популярне ввімкнено: ", - "error_video_not_in_playlist": "Запитуваного відео в цьому списку відтворення не існує. <a href=\"`x`\">Клацніть тут, щоб переглянути домашню сторінку списку відтворення.</a>" + "error_video_not_in_playlist": "Запитуваного відео в цьому списку відтворення не існує. <a href=\"`x`\">Клацніть тут, щоб переглянути домашню сторінку списку відтворення.</a>", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Прямі трансляції", + "channel_tab_playlists_label": "Добірки", + "channel_tab_channels_label": "Канали", + "Music in this video": "Музика в цьому відео", + "Artist: ": "Виконавець: ", + "Album: ": "Альбом: " } diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 385f16bd..aff6dd3e 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -456,5 +456,12 @@ "search_filters_type_option_all": "任意类型", "search_filters_features_option_vr180": "VR180", "Popular enabled: ": "已启用流行度: ", - "error_video_not_in_playlist": "此播放列表中不存在请求的视频。 <a href=\"`x`\">单击析出查看播放列表主页。</a>" + "error_video_not_in_playlist": "此播放列表中不存在请求的视频。 <a href=\"`x`\">单击析出查看播放列表主页。</a>", + "Music in this video": "此视频中的音乐", + "channel_tab_playlists_label": "播放列表", + "Artist: ": "艺术家: ", + "channel_tab_streams_label": "直播", + "Album: ": "专辑: ", + "channel_tab_shorts_label": "短视频", + "channel_tab_channels_label": "频道" } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 584d4a0a..8aa9869a 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -456,5 +456,12 @@ "search_filters_type_option_all": "任何類型", "search_filters_date_option_none": "任何日期", "Popular enabled: ": "已啟用人氣: ", - "error_video_not_in_playlist": "此播放清單不存在請求的影片。<a href=\"`x`\">點擊此處檢視播放清單首頁。</a>" + "error_video_not_in_playlist": "此播放清單不存在請求的影片。<a href=\"`x`\">點擊此處檢視播放清單首頁。</a>", + "channel_tab_shorts_label": "短片", + "channel_tab_playlists_label": "播放清單", + "channel_tab_channels_label": "頻道", + "channel_tab_streams_label": "直播", + "Artist: ": "藝術家: ", + "Album: ": "專輯: ", + "Music in this video": "此影片中的音樂" } diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 8e300288..13af2d8b 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -69,7 +69,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 @@ -189,6 +189,32 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) # when .has_key?("pollRenderer") # attachment = attachment["pollRenderer"] # json.field "type", "poll" + 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") + + qualities = {320, 560, 640, 1280, 2000} + + 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/comments.cr b/src/invidious/comments.cr index d691ca36..357a461c 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -181,6 +181,8 @@ 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 "published", published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) @@ -670,6 +672,7 @@ def content_to_comment_html(content, video_id : String? = "") end text = "<b>#{text}</b>" if run["bold"]? + text = "<s>#{text}</s>" if run["strikethrough"]? text = "<i>#{text}</i>" if run["italics"]? text 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 = /(?<number>\d+(\.\d+)?)\s?(?<suffix>[mMkKbB])?/.match(short_text) + matches = /(?<number>\d+(\.\d+)?)\s?(?<suffix>[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/routes/account.cr b/src/invidious/routes/account.cr index 9bb73136..8f69df94 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"] = user.email url.query = query.to_s env.redirect url.to_s diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index 421355bb..6b935312 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -31,6 +31,29 @@ 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.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 79f7bd3f..f312211e 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -93,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?("<?xml") + webvtt = caption.timedtext_to_vtt(caption_xml, tlang) + else + caption_xml = XML.parse(caption_xml) + webvtt = String.build do |str| + str << <<-END_VTT + WEBVTT + Kind: captions + Language: #{tlang || caption.language_code} - END_VTT - caption_nodes = caption_xml.xpath_nodes("//transcript/text") - caption_nodes.each_with_index do |node, i| - start_time = node["start"].to_f.seconds - duration = node["dur"]?.try &.to_f.seconds - duration ||= start_time + END_VTT - if caption_nodes.size > i + 1 - end_time = caption_nodes[i + 1]["start"].to_f.seconds - else - end_time = start_time + duration - end + caption_nodes = caption_xml.xpath_nodes("//transcript/text") + caption_nodes.each_with_index do |node, i| + start_time = node["start"].to_f.seconds + duration = node["dur"]?.try &.to_f.seconds + duration ||= start_time - 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')}" + if caption_nodes.size > i + 1 + end_time = caption_nodes[i + 1]["start"].to_f.seconds + else + end_time = start_time + duration + end - text = HTML.unescape(node.content) - text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "") - text = text.gsub(/<\/font>/, "") - if md = text.match(/(?<name>.*) : (?<text>.*)/) - text = "<v #{md["name"]}>#{md["text"]}</v>" - 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')}" - str << <<-END_CUE - #{start_time} --> #{end_time} - #{text} + text = HTML.unescape(node.content) + text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "") + text = text.gsub(/<\/font>/, "") + if md = text.match(/(?<name>.*) : (?<text>.*)/) + text = "<v #{md["name"]}>#{md["text"]}</v>" + end + str << <<-END_CUE + #{start_time} --> #{end_time} + #{text} - END_CUE + + END_CUE + end end end else @@ -141,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?("<?xml") + webvtt = caption.timedtext_to_vtt(webvtt) + else + webvtt = YT_POOL.client &.get("#{url}&format=vtt").body + .gsub(/([0-9:.]{12} --> [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/routing.cr b/src/invidious/routing.cr index 491022a5..dca2f117 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,9 @@ 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/feed", {{namespace}}::Authenticated, :feed get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions @@ -279,6 +284,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..436ac82d 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -247,6 +247,12 @@ struct Video info["reason"]?.try &.as_s end + def music : Array(VideoMusic) + info["music"].as_a.map { |music_json| + VideoMusic.new(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..402ae46f --- /dev/null +++ b/src/invidious/videos/music.cr @@ -0,0 +1,12 @@ +require "json" + +struct VideoMusic + include JSON::Serializable + + property album : String + property artist : String + property license : String + + def initialize(@album : String, @artist : String, @license : String) + end +end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 5c323975..cf43f1be 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -311,6 +311,33 @@ 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 + + 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 == "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(album.to_s, artist.to_s, music_license.to_s) + end + # Author infos author = video_details["author"]?.try &.as_s @@ -361,6 +388,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/watch.ecr b/src/invidious/views/watch.ecr index a6f2e524..666eb3b0 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -235,6 +235,28 @@ we're going to need to do it here in order to allow for translations. <hr> + <% if !video.music.empty? %> + <input id="music-desc-expansion" type="checkbox"/> + <label for="music-desc-expansion"> + <h3 id="music-description-title"> + <%= translate(locale, "Music in this video") %> + <span class="icon ion-ios-arrow-up"></span> + <span class="icon ion-ios-arrow-down"></span> + </h3> + </label> + + <div id="music-description-box"> + <% video.music.each do |music| %> + <div class="music-item"> + <p id="music-artist"><%= translate(locale, "Artist: ") %><%= music.artist %></p> + <p id="music-album"><%= translate(locale, "Album: ") %><%= music.album %></p> + <p id="music-license"><%= translate(locale, "License: ") %><%= music.license %></p> + </div> + <% end %> + </div> + <hr> + + <% end %> <div id="comments"> <% if nojs %> <%= comment_html %> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 65d107b2..b14ad7b9 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -172,7 +172,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 @@ -682,7 +692,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 |
