Merge branch 'iv-org:master' into CICD

This commit is contained in:
John Wong 2023-05-05 20:25:20 +08:00 committed by GitHub
commit 380e48859b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 234 additions and 228 deletions

View file

@ -545,5 +545,7 @@
"Album: ": "الألبوم: ", "Album: ": "الألبوم: ",
"Artist: ": "الفنان: ", "Artist: ": "الفنان: ",
"Song: ": "أغنية: ", "Song: ": "أغنية: ",
"Channel Sponsor": "راعي القناة" "Channel Sponsor": "راعي القناة",
"Standard YouTube license": "ترخيص YouTube القياسي",
"Download is disabled": "تم تعطيل التحميلات"
} }

View file

@ -66,7 +66,7 @@
"Malay": "Malai", "Malay": "Malai",
"Persian": "Persa", "Persian": "Persa",
"Slovak": "Eslovac", "Slovak": "Eslovac",
"Search": "Busca", "Search": "Cerca",
"Show annotations": "Mostra anotacions", "Show annotations": "Mostra anotacions",
"preferences_region_label": "País del contingut: ", "preferences_region_label": "País del contingut: ",
"preferences_sort_label": "Ordena vídeos per: ", "preferences_sort_label": "Ordena vídeos per: ",
@ -75,7 +75,7 @@
"Title": "Títol", "Title": "Títol",
"Belarusian": "Bielorús", "Belarusian": "Bielorús",
"Enable web notifications": "Activa notificacions web", "Enable web notifications": "Activa notificacions web",
"search": "cerca", "search": "Cerca",
"Catalan": "Català", "Catalan": "Català",
"Croatian": "Croat", "Croatian": "Croat",
"preferences_category_admin": "Preferències d'administrador", "preferences_category_admin": "Preferències d'administrador",
@ -122,8 +122,8 @@
"search_filters_features_option_location": "Ubicació", "search_filters_features_option_location": "Ubicació",
"search_filters_apply_button": "Aplica els filtres seleccionats", "search_filters_apply_button": "Aplica els filtres seleccionats",
"videoinfo_started_streaming_x_ago": "Ha començat el directe fa `x`", "videoinfo_started_streaming_x_ago": "Ha començat el directe fa `x`",
"next_steps_error_message_go_to_youtube": "Anar a YouTube", "next_steps_error_message_go_to_youtube": "Vés a YouTube",
"footer_donate_page": "Donar", "footer_donate_page": "Feu un donatiu",
"footer_original_source_code": "Codi font original", "footer_original_source_code": "Codi font original",
"videoinfo_watch_on_youTube": "Veure a YouTube", "videoinfo_watch_on_youTube": "Veure a YouTube",
"user_saved_playlists": "`x` llistes de reproducció guardades", "user_saved_playlists": "`x` llistes de reproducció guardades",
@ -164,7 +164,7 @@
"crash_page_report_issue": "Si cap de les anteriors no ha ajudat, <a href=\"`x`\">obre un nou issue a GitHub</a> (preferiblement en anglès) i inclou el text següent al missatge (NO tradueixis aquest text):", "crash_page_report_issue": "Si cap de les anteriors no ha ajudat, <a href=\"`x`\">obre un nou issue a GitHub</a> (preferiblement en anglès) i inclou el text següent al missatge (NO tradueixis aquest text):",
"generic_subscriptions_count": "{{count}} subscripció", "generic_subscriptions_count": "{{count}} subscripció",
"generic_subscriptions_count_plural": "{{count}} subscripcions", "generic_subscriptions_count_plural": "{{count}} subscripcions",
"error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. <a href=\"`x`\">Fes clic aquí per a la pàgina d'inici de la llista de reproducció.</a>", "error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. <a href=\"`x`\">Feu clic aquí per a la pàgina d'inici de la llista de reproducció.</a>",
"comments_points_count": "{{count}} punt", "comments_points_count": "{{count}} punt",
"comments_points_count_plural": "{{count}} punts", "comments_points_count_plural": "{{count}} punts",
"%A %B %-d, %Y": "%A %B %-d, %Y", "%A %B %-d, %Y": "%A %B %-d, %Y",
@ -175,7 +175,7 @@
"preferences_unseen_only_label": "Mostra només no vistos: ", "preferences_unseen_only_label": "Mostra només no vistos: ",
"preferences_listen_label": "Escolta per defecte: ", "preferences_listen_label": "Escolta per defecte: ",
"Import": "Importar", "Import": "Importar",
"Token": "Senyal", "Token": "Testimoni",
"Wilson score: ": "Puntuació de Wilson: ", "Wilson score: ": "Puntuació de Wilson: ",
"search_filters_date_label": "Data de càrrega", "search_filters_date_label": "Data de càrrega",
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
@ -184,10 +184,10 @@
"preferences_comments_label": "Comentaris per defecte: ", "preferences_comments_label": "Comentaris per defecte: ",
"`x` uploaded a video": "`x` ha penjat un vídeo", "`x` uploaded a video": "`x` ha penjat un vídeo",
"Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.", "Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.",
"Token manager": "Gestor de tokens", "Token manager": "Gestor de testimonis",
"Watch history": "Historial de reproduccions", "Watch history": "Historial de reproduccions",
"Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google", "Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google",
"Authorize token?": "Autoritzar senyal?", "Authorize token?": "Autoritzar testimoni?",
"Source available here.": "Font disponible aquí.", "Source available here.": "Font disponible aquí.",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)",
"Log in": "Inicia sessió", "Log in": "Inicia sessió",
@ -197,7 +197,7 @@
"Public": "Públic", "Public": "Públic",
"View all playlists": "Veure totes les llistes de reproducció", "View all playlists": "Veure totes les llistes de reproducció",
"reddit": "Reddit", "reddit": "Reddit",
"Manage tokens": "Gestiona senyals", "Manage tokens": "Gestiona testimonis",
"Not a playlist.": "No és una llista de reproducció.", "Not a playlist.": "No és una llista de reproducció.",
"preferences_local_label": "Vídeos de Proxy: ", "preferences_local_label": "Vídeos de Proxy: ",
"View channel on YouTube": "Veure canal a Youtube", "View channel on YouTube": "Veure canal a Youtube",
@ -272,7 +272,7 @@
"Khmer": "Khmer", "Khmer": "Khmer",
"This channel does not exist.": "Aquest canal no existeix.", "This channel does not exist.": "Aquest canal no existeix.",
"Song: ": "Cançó: ", "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.", "Login failed. This may be because two-factor authentication is not turned on for your account.": "S'ha produït un error en iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.",
"channel:`x`": "canal: `x`", "channel:`x`": "canal: `x`",
"Deleted or invalid channel": "Canal suprimit o no vàlid", "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 get channel info.": "No s'ha pogut obtenir la informació del canal.",
@ -298,10 +298,10 @@
"generic_views_count_plural": "{{count}} visualitzacions", "generic_views_count_plural": "{{count}} visualitzacions",
"generic_videos_count": "{{count}} vídeo", "generic_videos_count": "{{count}} vídeo",
"generic_videos_count_plural": "{{count}} vídeos", "generic_videos_count_plural": "{{count}} vídeos",
"Token is expired, please try again": "La senyal ha caducat, torna-ho a provar", "Token is expired, please try again": "El testimoni ha caducat, torna-ho a provar",
"English": "Anglès", "English": "Anglès",
"Kannada": "Kanarès", "Kannada": "Kanarès",
"Erroneous token": "Senyal errònia", "Erroneous token": "Testimoni erroni",
"`x` ago": "fa `x`", "`x` ago": "fa `x`",
"Empty playlist": "Llista de reproducció buida", "Empty playlist": "Llista de reproducció buida",
"Playlist does not exist.": "La llista de reproducció no existeix.", "Playlist does not exist.": "La llista de reproducció no existeix.",
@ -376,7 +376,7 @@
"Clear watch history": "Neteja l'historial de reproduccions", "Clear watch history": "Neteja l'historial de reproduccions",
"Mongolian": "Mongol", "Mongolian": "Mongol",
"preferences_quality_dash_option_best": "Millor", "preferences_quality_dash_option_best": "Millor",
"Authorize token for `x`?": "Autoritzar senyal per a `x`?", "Authorize token for `x`?": "Autoritzar testimoni per a `x`?",
"Report statistics: ": "Estadístiques de l'informe: ", "Report statistics: ": "Estadístiques de l'informe: ",
"Switch Invidious Instance": "Canvia la instància d'Invidious", "Switch Invidious Instance": "Canvia la instància d'Invidious",
"History": "Historial", "History": "Historial",
@ -410,7 +410,7 @@
"Export": "Exportar", "Export": "Exportar",
"preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_4320p": "4320p",
"JavaScript license information": "Informació de la llicència de JavaScript", "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", "Hidden field \"token\" is a required field": "El camp ocult \"testimoni\" és un camp obligatori",
"Shona": "Xona", "Shona": "Xona",
"Family friendly? ": "Apte per a tots els públics? ", "Family friendly? ": "Apte per a tots els públics? ",
"preferences_quality_dash_label": "Qualitat de vídeo DASH preferida: ", "preferences_quality_dash_label": "Qualitat de vídeo DASH preferida: ",
@ -443,7 +443,7 @@
"unsubscribe": "cancel·la la subscripció", "unsubscribe": "cancel·la la subscripció",
"View playlist on YouTube": "Veure llista de reproducció a YouTube", "View playlist on YouTube": "Veure llista de reproducció a YouTube",
"Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)",
"crash_page_you_found_a_bug": "Sembla que has trobat un error a Invidious!", "crash_page_you_found_a_bug": "Heu trobat un error a Invidious!",
"Subscribe": "Subscriu-me", "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", "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": "{{count}} dia",
@ -468,8 +468,8 @@
"revoke": "revocar", "revoke": "revocar",
"English (United Kingdom)": "Anglès (Regne Unit)", "English (United Kingdom)": "Anglès (Regne Unit)",
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
"tokens_count": "{{count}} senyal", "tokens_count": "{{count}} testimoni",
"tokens_count_plural": "{{count}} senyals", "tokens_count_plural": "{{count}} testimonis",
"subscriptions_unseen_notifs_count": "{{count}} notificació no vista", "subscriptions_unseen_notifs_count": "{{count}} notificació no vista",
"subscriptions_unseen_notifs_count_plural": "{{count}} notificacions no vistes", "subscriptions_unseen_notifs_count_plural": "{{count}} notificacions no vistes",
"generic_subscribers_count": "{{count}} subscriptor", "generic_subscribers_count": "{{count}} subscriptor",
@ -481,5 +481,7 @@
"Top": "Millors", "Top": "Millors",
"preferences_max_results_label": "Nombre de vídeos mostrats al feed: ", "preferences_max_results_label": "Nombre de vídeos mostrats al feed: ",
"Engagement: ": "Atracció: ", "Engagement: ": "Atracció: ",
"Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: " "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ",
"Standard YouTube license": "Llicència estàndard de YouTube",
"Download is disabled": "Les baixades s'han inhabilitat"
} }

View file

@ -497,5 +497,7 @@
"Artist: ": "Umělec: ", "Artist: ": "Umělec: ",
"Album: ": "Album: ", "Album: ": "Album: ",
"Channel Sponsor": "Sponzor kanálu", "Channel Sponsor": "Sponzor kanálu",
"Song: ": "Skladba: " "Song: ": "Skladba: ",
"Standard YouTube license": "Standardní licence YouTube",
"Download is disabled": "Stahování je zakázáno"
} }

View file

@ -479,5 +479,8 @@
"Artist: ": "Künstler: ", "Artist: ": "Künstler: ",
"Album: ": "Album: ", "Album: ": "Album: ",
"channel_tab_playlists_label": "Wiedergabelisten", "channel_tab_playlists_label": "Wiedergabelisten",
"channel_tab_channels_label": "Kanäle" "channel_tab_channels_label": "Kanäle",
"Channel Sponsor": "Kanalsponsor",
"Standard YouTube license": "Standard YouTube-Lizenz",
"Song: ": "Musik: "
} }

View file

@ -479,5 +479,9 @@
"channel_tab_shorts_label": "Mallongaj", "channel_tab_shorts_label": "Mallongaj",
"Music in this video": "Muziko en ĉi tiu video", "Music in this video": "Muziko en ĉi tiu video",
"Artist: ": "Artisto: ", "Artist: ": "Artisto: ",
"Album: ": "Albumo: " "Album: ": "Albumo: ",
"Channel Sponsor": "Kanala sponsoro",
"Song: ": "Muzikaĵo: ",
"Standard YouTube license": "Implicita YouTube-licenco",
"Download is disabled": "Elŝuto estas malebligita"
} }

View file

@ -482,5 +482,7 @@
"Artist: ": "Artista: ", "Artist: ": "Artista: ",
"Album: ": "Álbum: ", "Album: ": "Álbum: ",
"Song: ": "Canción: ", "Song: ": "Canción: ",
"Channel Sponsor": "Patrocinador del canal" "Channel Sponsor": "Patrocinador del canal",
"Standard YouTube license": "Licencia de YouTube estándar",
"Download is disabled": "La descarga está deshabilitada"
} }

View file

@ -450,5 +450,8 @@
"Music in this video": "آهنگ در این ویدیو", "Music in this video": "آهنگ در این ویدیو",
"Artist: ": "هنرمند: ", "Artist: ": "هنرمند: ",
"Album: ": "آلبوم: ", "Album: ": "آلبوم: ",
"Song: ": "آهنگ: " "Song: ": "آهنگ: ",
"Channel Sponsor": "اسپانسر کانال",
"Standard YouTube license": "پروانه استاندارد YouTube",
"search_message_use_another_instance": " شما همچنین می‌توانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>."
} }

View file

@ -474,7 +474,14 @@
"search_filters_duration_option_none": "Toutes les durées", "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_shorts_label": "Clips",
"channel_tab_streams_label": "En direct", "channel_tab_streams_label": "Vidéos en direct",
"channel_tab_playlists_label": "Listes de lecture", "channel_tab_playlists_label": "Listes de lecture",
"channel_tab_channels_label": "Chaînes" "channel_tab_channels_label": "Chaînes",
"Song: ": "Chanson : ",
"Artist: ": "Artiste : ",
"Album: ": "Album : ",
"Standard YouTube license": "Licence YouTube Standard",
"Music in this video": "Musique dans cette vidéo",
"Channel Sponsor": "Soutien de la chaîne",
"Download is disabled": "Le téléchargement est désactivé"
} }

View file

@ -497,5 +497,7 @@
"Album: ": "Album: ", "Album: ": "Album: ",
"Artist: ": "Izvođač: ", "Artist: ": "Izvođač: ",
"Channel Sponsor": "Sponzor kanala", "Channel Sponsor": "Sponzor kanala",
"Song: ": "Pjesma: " "Song: ": "Pjesma: ",
"Standard YouTube license": "Standardna YouTube licenca",
"Download is disabled": "Preuzimanje je deaktivirano"
} }

View file

@ -453,5 +453,6 @@
"crash_page_switch_instance": "mencoba untuk <a href=\"`x`\">menggunakan peladen lainnya</a>", "crash_page_switch_instance": "mencoba untuk <a href=\"`x`\">menggunakan peladen lainnya</a>",
"crash_page_read_the_faq": "baca <a href=\"`x`\">Soal Sering Ditanya (SSD/FAQ)</a>", "crash_page_read_the_faq": "baca <a href=\"`x`\">Soal Sering Ditanya (SSD/FAQ)</a>",
"crash_page_search_issue": "mencari <a href=\"`x`\">isu yang ada di GitHub</a>", "crash_page_search_issue": "mencari <a href=\"`x`\">isu yang ada di GitHub</a>",
"crash_page_report_issue": "Jika yang di atas tidak membantu, <a href=\"`x`\">buka isu baru di GitHub</a> (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):" "crash_page_report_issue": "Jika yang di atas tidak membantu, <a href=\"`x`\">buka isu baru di GitHub</a> (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):",
"Popular enabled: ": "Populer diaktifkan: "
} }

View file

@ -479,5 +479,9 @@
"channel_tab_community_label": "Comunità", "channel_tab_community_label": "Comunità",
"Music in this video": "Musica in questo video", "Music in this video": "Musica in questo video",
"Artist: ": "Artista: ", "Artist: ": "Artista: ",
"Album: ": "Album: " "Album: ": "Album: ",
"Download is disabled": "Il download è disabilitato",
"Song: ": "Canzone: ",
"Standard YouTube license": "Licenza standard di YouTube",
"Channel Sponsor": "Sponsor del canale"
} }

View file

@ -465,5 +465,6 @@
"Artist: ": "アーティスト: ", "Artist: ": "アーティスト: ",
"Album: ": "アルバム: ", "Album: ": "アルバム: ",
"Song: ": "曲: ", "Song: ": "曲: ",
"Channel Sponsor": "チャンネルのスポンサー" "Channel Sponsor": "チャンネルのスポンサー",
"Standard YouTube license": "標準 Youtube ライセンス"
} }

View file

@ -488,5 +488,6 @@
"preferences_save_player_pos_label": "Išsaugoti atkūrimo padėtį: ", "preferences_save_player_pos_label": "Išsaugoti atkūrimo padėtį: ",
"videoinfo_youTube_embed_link": "Įterpti", "videoinfo_youTube_embed_link": "Įterpti",
"videoinfo_invidious_embed_link": "Įterpti nuorodą", "videoinfo_invidious_embed_link": "Įterpti nuorodą",
"crash_page_refresh": "pabandėte <a href=\"`x`\">atnaujinti puslapį</a>" "crash_page_refresh": "pabandėte <a href=\"`x`\">atnaujinti puslapį</a>",
"Album: ": "Albumas "
} }

View file

@ -498,5 +498,6 @@
"Artist: ": "Wykonawca: ", "Artist: ": "Wykonawca: ",
"Album: ": "Album: ", "Album: ": "Album: ",
"Song: ": "Piosenka: ", "Song: ": "Piosenka: ",
"Channel Sponsor": "Sponsor kanału" "Channel Sponsor": "Sponsor kanału",
"Standard YouTube license": "Standardowa licencja YouTube"
} }

View file

@ -481,5 +481,7 @@
"Artist: ": "Artista: ", "Artist: ": "Artista: ",
"Album: ": "Álbum: ", "Album: ": "Álbum: ",
"Song: ": "Canção: ", "Song: ": "Canção: ",
"Channel Sponsor": "Patrocinador do canal" "Channel Sponsor": "Patrocinador do canal",
"Standard YouTube license": "Licença padrão do YouTube",
"Download is disabled": "A descarga está desativada"
} }

View file

@ -4,7 +4,7 @@
"Unsubscribe": "Отписаться", "Unsubscribe": "Отписаться",
"Subscribe": "Подписаться", "Subscribe": "Подписаться",
"View channel on YouTube": "Смотреть канал на YouTube", "View channel on YouTube": "Смотреть канал на YouTube",
"View playlist on YouTube": осмотреть плейлист на YouTube", "View playlist on YouTube": росмотреть подборку на ютубе",
"newest": "сначала новые", "newest": "сначала новые",
"oldest": "сначала старые", "oldest": "сначала старые",
"popular": "популярные", "popular": "популярные",
@ -14,7 +14,7 @@
"Clear watch history?": "Очистить историю просмотров?", "Clear watch history?": "Очистить историю просмотров?",
"New password": "Новый пароль", "New password": "Новый пароль",
"New passwords must match": "Новые пароли не совпадают", "New passwords must match": "Новые пароли не совпадают",
"Cannot change password for Google accounts": "Изменить пароль аккаунта Google невозможно", "Cannot change password for Google accounts": "Изменить пароль учётной записи Google невозможно",
"Authorize token?": "Авторизовать токен?", "Authorize token?": "Авторизовать токен?",
"Authorize token for `x`?": "Авторизовать токен для `x`?", "Authorize token for `x`?": "Авторизовать токен для `x`?",
"Yes": "Да", "Yes": "Да",
@ -30,7 +30,7 @@
"Export subscriptions as OPML": "Экспортировать подписки в формате OPML", "Export subscriptions as OPML": "Экспортировать подписки в формате OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)",
"Export data as JSON": "Экспортировать данные Invidious в формате JSON", "Export data as JSON": "Экспортировать данные Invidious в формате JSON",
"Delete account?": "Удалить аккаунт?", "Delete account?": "Удалить учётку?",
"History": "История", "History": "История",
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube", "An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
"JavaScript license information": "Информация о лицензиях JavaScript", "JavaScript license information": "Информация о лицензиях JavaScript",
@ -38,14 +38,14 @@
"Log in": "Войти", "Log in": "Войти",
"Log in/register": "Войти или зарегистрироваться", "Log in/register": "Войти или зарегистрироваться",
"Log in with Google": "Войти через Google", "Log in with Google": "Войти через Google",
"User ID": "ID пользователя", "User ID": "ИД пользователя",
"Password": "Пароль", "Password": "Пароль",
"Time (h:mm:ss):": "Время (ч:мм:сс):", "Time (h:mm:ss):": "Время (ч:мм:сс):",
"Text CAPTCHA": "Текстовая капча (англ.)", "Text CAPTCHA": "Текстовая капча (англ.)",
"Image CAPTCHA": "Капча-картинка", "Image CAPTCHA": "Капча-картинка",
"Sign In": "Войти", "Sign In": "Войти",
"Register": "Зарегистрироваться", "Register": "Зарегистрироваться",
"E-mail": "Электронная почта", "E-mail": "Эл. почта",
"Google verification code": "Код подтверждения Google", "Google verification code": "Код подтверждения Google",
"Preferences": "Настройки", "Preferences": "Настройки",
"preferences_category_player": "Настройки проигрывателя", "preferences_category_player": "Настройки проигрывателя",
@ -69,11 +69,11 @@
"preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ", "preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ",
"preferences_category_visual": "Настройки сайта", "preferences_category_visual": "Настройки сайта",
"preferences_player_style_label": "Стиль проигрывателя: ", "preferences_player_style_label": "Стиль проигрывателя: ",
"Dark mode: ": "Темное оформление: ", "Dark mode: ": "Тёмное оформление: ",
"preferences_dark_mode_label": "Тема: ", "preferences_dark_mode_label": "Тема: ",
"dark": емная", "dark": ёмная",
"light": "светлая", "light": "светлая",
"preferences_thin_mode_label": "Облегченное оформление: ", "preferences_thin_mode_label": "Облегчённое оформление: ",
"preferences_category_misc": "Прочие настройки", "preferences_category_misc": "Прочие настройки",
"preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ", "preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ",
"preferences_category_subscription": "Настройки подписок", "preferences_category_subscription": "Настройки подписок",
@ -129,14 +129,14 @@
"Public": "Публичный", "Public": "Публичный",
"Unlisted": "Нет в списке", "Unlisted": "Нет в списке",
"Private": "Приватный", "Private": "Приватный",
"View all playlists": осмотреть все плейлисты", "View all playlists": росмотреть все подборки",
"Updated `x` ago": "Обновлено `x` назад", "Updated `x` ago": "Обновлено `x` назад",
"Delete playlist `x`?": "Удалить плейлист `x`?", "Delete playlist `x`?": "Удалить подборку `x`?",
"Delete playlist": "Удалить плейлист", "Delete playlist": "Удалить подборку",
"Create playlist": "Создать плейлист", "Create playlist": "Создать подборку",
"Title": "Заголовок", "Title": "Заголовок",
"Playlist privacy": "Видимость плейлиста", "Playlist privacy": "Видимость подборки",
"Editing playlist `x`": "Редактирование плейлиста `x`", "Editing playlist `x`": "Изменение подборки `x`",
"Show more": "Развернуть", "Show more": "Развернуть",
"Show less": "Свернуть", "Show less": "Свернуть",
"Watch on YouTube": "Смотреть на YouTube", "Watch on YouTube": "Смотреть на YouTube",
@ -147,13 +147,13 @@
"License: ": "Лицензия: ", "License: ": "Лицензия: ",
"Family friendly? ": "Семейный просмотр: ", "Family friendly? ": "Семейный просмотр: ",
"Wilson score: ": "Оценка Уилсона: ", "Wilson score: ": "Оценка Уилсона: ",
"Engagement: ": "Вовлеченность: ", "Engagement: ": "Вовлечённость: ",
"Whitelisted regions: ": "Доступно в регионах: ", "Whitelisted regions: ": "Доступно в регионах: ",
"Blacklisted regions: ": "Недоступно в регионах: ", "Blacklisted regions: ": "Недоступно в регионах: ",
"Shared `x`": "Опубликовано `x`", "Shared `x`": "Опубликовано `x`",
"Premieres in `x`": "Премьера через `x`", "Premieres in `x`": "Премьера через `x`",
"Premieres `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 YouTube comments": "Показать комментарии с YouTube",
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
"View `x` comments": { "View `x` comments": {
@ -171,7 +171,7 @@
"Wrong answer": "Неправильный ответ", "Wrong answer": "Неправильный ответ",
"Erroneous CAPTCHA": "Неправильная капча", "Erroneous CAPTCHA": "Неправильная капча",
"CAPTCHA is a required field": "Необходимо решить капчу", "CAPTCHA is a required field": "Необходимо решить капчу",
"User ID is a required field": "Необходимо ввести ID пользователя", "User ID is a required field": "Необходимо ввести идентификатор пользователя",
"Password is a required field": "Необходимо ввести пароль", "Password is a required field": "Необходимо ввести пароль",
"Wrong username or password": "Неправильный логин или пароль", "Wrong username or password": "Неправильный логин или пароль",
"Please sign in using 'Log in with Google'": "Пожалуйста, нажмите «Войти через Google»", "Please sign in using 'Log in with Google'": "Пожалуйста, нажмите «Войти через Google»",
@ -180,23 +180,23 @@
"Please log in": "Пожалуйста, войдите", "Please log in": "Пожалуйста, войдите",
"Invidious Private Feed for `x`": "Приватная лента Invidious для `x`", "Invidious Private Feed for `x`": "Приватная лента Invidious для `x`",
"channel:`x`": "канал: `x`", "channel:`x`": "канал: `x`",
"Deleted or invalid channel": "Канал удален или не найден", "Deleted or invalid channel": "Канал удалён или не найден",
"This channel does not exist.": "Такого канала не существует.", "This channel does not exist.": "Такого канала не существует.",
"Could not get channel info.": "Не удается получить информацию об этом канале.", "Could not get channel info.": "Не удаётся получить информацию об этом канале.",
"Could not fetch comments": "Не удается загрузить комментарии", "Could not fetch comments": "Не удаётся загрузить комментарии",
"`x` ago": "`x` назад", "`x` ago": "`x` назад",
"Load more": "Загрузить еще", "Load more": "Загрузить ещё",
"Could not create mix.": "Не удалось создать микс.", "Could not create mix.": "Не удалось создать микс.",
"Empty playlist": лейлист пуст", "Empty playlist": одборка пуста",
"Not a playlist.": "Это не плейлист.", "Not a playlist.": "Это не подборка.",
"Playlist does not exist.": лейлист не существует.", "Playlist does not exist.": одборка не существует.",
"Could not pull trending pages.": "Не удается загрузить страницы «в тренде».", "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».",
"Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»",
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»",
"Erroneous challenge": "Неправильный ответ в «challenge»", "Erroneous challenge": "Неправильный ответ в «challenge»",
"Erroneous token": "Неправильный токен", "Erroneous token": "Неправильный токен",
"No such user": "Пользователь не найден", "No such user": "Пользователь не найден",
"Token is expired, please try again": "Срок действия токена истек, попробуйте позже", "Token is expired, please try again": "Срок действия токена истёк, попробуйте позже",
"English": "Английский", "English": "Английский",
"English (auto-generated)": "Английский (созданы автоматически)", "English (auto-generated)": "Английский (созданы автоматически)",
"Afrikaans": "Африкаанс", "Afrikaans": "Африкаанс",
@ -213,7 +213,7 @@
"Burmese": "Бирманский", "Burmese": "Бирманский",
"Catalan": "Каталонский", "Catalan": "Каталонский",
"Cebuano": "Себуанский", "Cebuano": "Себуанский",
"Chinese (Simplified)": "Китайский (упрощенный)", "Chinese (Simplified)": "Китайский (упрощённый)",
"Chinese (Traditional)": "Китайский (традиционный)", "Chinese (Traditional)": "Китайский (традиционный)",
"Corsican": "Корсиканский", "Corsican": "Корсиканский",
"Croatian": "Хорватский", "Croatian": "Хорватский",
@ -310,7 +310,7 @@
"About": "О сайте", "About": "О сайте",
"Rating: ": "Рейтинг: ", "Rating: ": "Рейтинг: ",
"preferences_locale_label": "Язык: ", "preferences_locale_label": "Язык: ",
"View as playlist": "Смотреть как плейлист", "View as playlist": "Смотреть как подборку",
"Default": "По умолчанию", "Default": "По умолчанию",
"Music": "Музыка", "Music": "Музыка",
"Gaming": "Игры", "Gaming": "Игры",
@ -326,7 +326,7 @@
"Audio mode": "Аудио режим", "Audio mode": "Аудио режим",
"Video mode": "Видео режим", "Video mode": "Видео режим",
"channel_tab_videos_label": "Видео", "channel_tab_videos_label": "Видео",
"Playlists": лейлисты", "Playlists": одборки",
"channel_tab_community_label": "Сообщество", "channel_tab_community_label": "Сообщество",
"search_filters_sort_option_relevance": "по актуальности", "search_filters_sort_option_relevance": "по актуальности",
"search_filters_sort_option_rating": "по рейтингу", "search_filters_sort_option_rating": "по рейтингу",
@ -343,7 +343,7 @@
"search_filters_date_option_year": "Этот год", "search_filters_date_option_year": "Этот год",
"search_filters_type_option_video": "Видео", "search_filters_type_option_video": "Видео",
"search_filters_type_option_channel": "Канал", "search_filters_type_option_channel": "Канал",
"search_filters_type_option_playlist": лейлист", "search_filters_type_option_playlist": одборка",
"search_filters_type_option_movie": "Фильм", "search_filters_type_option_movie": "Фильм",
"search_filters_type_option_show": "Сериал", "search_filters_type_option_show": "Сериал",
"search_filters_features_option_hd": "HD", "search_filters_features_option_hd": "HD",
@ -379,13 +379,13 @@
"Turkish (auto-generated)": "Турецкий (созданы автоматически)", "Turkish (auto-generated)": "Турецкий (созданы автоматически)",
"Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)", "Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)",
"footer_documentation": "Документация", "footer_documentation": "Документация",
"adminprefs_modified_source_code_url_label": "URL-адрес репозитория измененного исходного кода", "adminprefs_modified_source_code_url_label": "Ссылка на репозиторий с измененными исходными кодами",
"none": "ничего", "none": "ничего",
"videoinfo_watch_on_youTube": "Смотреть на YouTube", "videoinfo_watch_on_youTube": "Смотреть на YouTube",
"videoinfo_youTube_embed_link": "Версия для встраивания", "videoinfo_youTube_embed_link": "Версия для встраивания",
"videoinfo_invidious_embed_link": "Ссылка для встраивания", "videoinfo_invidious_embed_link": "Ссылка для встраивания",
"download_subtitles": "Субтитры - `x` (.vtt)", "download_subtitles": "Субтитры - `x` (.vtt)",
"user_created_playlists": "`x` созданных плейлистов", "user_created_playlists": "`x` созданных подборок",
"crash_page_you_found_a_bug": "Похоже, вы нашли ошибку в Invidious!", "crash_page_you_found_a_bug": "Похоже, вы нашли ошибку в Invidious!",
"crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:", "crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:",
"crash_page_refresh": "пробовали <a href=\"`x`\"> перезагрузить страницу</a>", "crash_page_refresh": "пробовали <a href=\"`x`\"> перезагрузить страницу</a>",
@ -393,9 +393,9 @@
"generic_videos_count_0": "{{count}} видео", "generic_videos_count_0": "{{count}} видео",
"generic_videos_count_1": "{{count}} видео", "generic_videos_count_1": "{{count}} видео",
"generic_videos_count_2": "{{count}} видео", "generic_videos_count_2": "{{count}} видео",
"generic_playlists_count_0": "{{count}} плейлист", "generic_playlists_count_0": "{{count}} подборка",
"generic_playlists_count_1": "{{count}} плейлиста", "generic_playlists_count_1": "{{count}} подборки",
"generic_playlists_count_2": "{{count}} плейлистов", "generic_playlists_count_2": "{{count}} подборок",
"tokens_count_0": "{{count}} токен", "tokens_count_0": "{{count}} токен",
"tokens_count_1": "{{count}} токена", "tokens_count_1": "{{count}} токена",
"tokens_count_2": "{{count}} токенов", "tokens_count_2": "{{count}} токенов",
@ -453,8 +453,8 @@
"Portuguese (Brazil)": "Португальский (Бразилия)", "Portuguese (Brazil)": "Португальский (Бразилия)",
"footer_source_code": "Исходный код", "footer_source_code": "Исходный код",
"footer_original_source_code": "Оригинальный исходный код", "footer_original_source_code": "Оригинальный исходный код",
"footer_modfied_source_code": "Измененный исходный код", "footer_modfied_source_code": "Изменённый исходный код",
"user_saved_playlists": "`x` сохраненных плейлистов", "user_saved_playlists": "`x` сохранённых подборок",
"crash_page_search_issue": "поискали <a href=\"`x`\">похожую проблему на GitHub</a>", "crash_page_search_issue": "поискали <a href=\"`x`\">похожую проблему на GitHub</a>",
"comments_points_count_0": "{{count}} плюс", "comments_points_count_0": "{{count}} плюс",
"comments_points_count_1": "{{count}} плюса", "comments_points_count_1": "{{count}} плюса",
@ -488,12 +488,16 @@
"search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_duration_option_medium": "Средние (4 - 20 минут)",
"search_filters_apply_button": "Применить фильтры", "search_filters_apply_button": "Применить фильтры",
"Popular enabled: ": "Популярное включено: ", "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_playlists_label": одборки",
"channel_tab_channels_label": "Каналы", "channel_tab_channels_label": "Каналы",
"channel_tab_streams_label": "Живое вещание", "channel_tab_streams_label": "Живое вещание",
"channel_tab_shorts_label": "Shorts", "channel_tab_shorts_label": "Shorts",
"Music in this video": "Музыка в этом видео", "Music in this video": "Музыка в этом видео",
"Artist: ": "Исполнитель: ", "Artist: ": "Исполнитель: ",
"Album: ": "Альбом: " "Album: ": "Альбом: ",
"Song: ": "Композиция: ",
"Standard YouTube license": "Стандартная лицензия YouTube",
"Channel Sponsor": "Спонсор канала",
"Download is disabled": "Загрузка отключена"
} }

View file

@ -511,5 +511,9 @@
"channel_tab_streams_label": "Prenosi v živo", "channel_tab_streams_label": "Prenosi v živo",
"Artist: ": "Umetnik/ca: ", "Artist: ": "Umetnik/ca: ",
"Music in this video": "Glasba v tem videoposnetku", "Music in this video": "Glasba v tem videoposnetku",
"Album: ": "Album: " "Album: ": "Album: ",
"Song: ": "Pesem: ",
"Standard YouTube license": "Standardna licenca YouTube",
"Channel Sponsor": "Sponzor kanala",
"Download is disabled": "Prenos je onemogočen"
} }

View file

@ -481,5 +481,7 @@
"Music in this video": "Bu videodaki müzik", "Music in this video": "Bu videodaki müzik",
"Artist: ": "Sanatçı: ", "Artist: ": "Sanatçı: ",
"Channel Sponsor": "Kanal Sponsoru", "Channel Sponsor": "Kanal Sponsoru",
"Song: ": "Şarkı: " "Song: ": "Şarkı: ",
"Standard YouTube license": "Standart YouTube lisansı",
"Download is disabled": "İndirme devre dışı"
} }

View file

@ -497,5 +497,7 @@
"Artist: ": "Виконавець: ", "Artist: ": "Виконавець: ",
"Album: ": "Альбом: ", "Album: ": "Альбом: ",
"Song: ": "Пісня: ", "Song: ": "Пісня: ",
"Channel Sponsor": "Спонсор каналу" "Channel Sponsor": "Спонсор каналу",
"Standard YouTube license": "Стандартна ліцензія YouTube",
"Download is disabled": "Завантаження вимкнено"
} }

View file

@ -465,5 +465,7 @@
"channel_tab_shorts_label": "短视频", "channel_tab_shorts_label": "短视频",
"channel_tab_channels_label": "频道", "channel_tab_channels_label": "频道",
"Song: ": "歌曲: ", "Song: ": "歌曲: ",
"Channel Sponsor": "频道赞助者" "Channel Sponsor": "频道赞助者",
"Standard YouTube license": "标准 YouTube 许可证",
"Download is disabled": "已禁用下载"
} }

View file

@ -465,5 +465,7 @@
"Album: ": "專輯: ", "Album: ": "專輯: ",
"Music in this video": "此影片中的音樂", "Music in this video": "此影片中的音樂",
"Channel Sponsor": "頻道贊助者", "Channel Sponsor": "頻道贊助者",
"Song: ": "歌曲: " "Song: ": "歌曲: ",
"Standard YouTube license": "標準 YouTube 授權條款",
"Download is disabled": "已停用下載"
} }

View file

@ -31,18 +31,16 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
session_token: session_token, session_token: session_token,
} }
response = YT_POOL.client &.post("/comment_service_ajax?action_get_comments=1&ctoken=#{continuation}&continuation=#{continuation}&hl=en&gl=US", headers, form: post_req) body = YoutubeAPI.browse(continuation)
body = JSON.parse(response.body)
body = body["response"]["continuationContents"]["itemSectionContinuation"]? || body = body.dig?("continuationContents", "itemSectionContinuation") ||
body["response"]["continuationContents"]["backstageCommentsContinuation"]? body.dig?("continuationContents", "backstageCommentsContinuation")
if !body if !body
raise InfoException.new("Could not extract continuation.") raise InfoException.new("Could not extract continuation.")
end end
end end
continuation = body["continuations"]?.try &.[0]["nextContinuationData"]["continuation"].as_s
posts = body["contents"].as_a posts = body["contents"].as_a
if message = posts[0]["messageRenderer"]? if message = posts[0]["messageRenderer"]?
@ -270,10 +268,8 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
end end
end end
end end
if cont = posts.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token")
if body["continuations"]? json.field "continuation", extract_channel_community_cursor(cont.as_s)
continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
json.field "continuation", extract_channel_community_cursor(continuation)
end end
end end
end end

View file

@ -604,7 +604,7 @@ def text_to_parsed_content(text : String) : JSON::Any
currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}} currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}}
currentNodes << (JSON.parse(currentNode.to_json)) currentNodes << (JSON.parse(currentNode.to_json))
# If text remain after match create new simple node with text after match # If text remain after match create new simple node with text after match
afterNode = {"text" => splittedLastNode.size > 0 ? splittedLastNode[1] : ""} afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""}
currentNodes << (JSON.parse(afterNode.to_json)) currentNodes << (JSON.parse(afterNode.to_json))
end end
@ -635,55 +635,8 @@ def content_to_comment_html(content, video_id : String? = "")
text = HTML.escape(run["text"].as_s) text = HTML.escape(run["text"].as_s)
if run["navigationEndpoint"]? if navigationEndpoint = run.dig?("navigationEndpoint")
if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s text = parse_link_endpoint(navigationEndpoint, text, video_id)
url = URI.parse(url)
displayed_url = text
if url.host == "youtu.be"
url = "/watch?v=#{url.request_target.lstrip('/')}"
elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com")
if url.path == "/redirect"
# Sometimes, links can be corrupted (why?) so make sure to fallback
# nicely. See https://github.com/iv-org/invidious/issues/2682
url = url.query_params["q"]? || ""
displayed_url = url
else
url = url.request_target
displayed_url = "youtube.com#{url}"
end
end
text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>)
elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]?
start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i
link_video_id = watch_endpoint["videoId"].as_s
url = "/watch?v=#{link_video_id}"
url += "&t=#{start_time}" if !start_time.nil?
# If the current video ID (passed through from the caller function)
# is the same as the video ID in the link, add HTML attributes for
# the JS handler function that bypasses page reload.
#
# See: https://github.com/iv-org/invidious/issues/3063
if link_video_id == video_id
start_time ||= 0
text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>)
else
text = %(<a href="#{url}">#{text}</a>)
end
elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s
if text.starts_with?(/\s?[@#]/)
# Handle "pings" in comments and hasthags differently
# See:
# - https://github.com/iv-org/invidious/issues/3038
# - https://github.com/iv-org/invidious/issues/3062
text = %(<a href="#{url}">#{text}</a>)
else
text = %(<a href="#{url}">#{reduce_uri(url)}</a>)
end
end
end end
text = "<b>#{text}</b>" if run["bold"]? text = "<b>#{text}</b>" if run["bold"]?

View file

@ -52,7 +52,7 @@ module Invidious::Database::Users
def mark_watched(user : User, vid : String) def mark_watched(user : User, vid : String)
request = <<-SQL request = <<-SQL
UPDATE users UPDATE users
SET watched = array_append(watched, $1) SET watched = array_append(array_remove(watched, $1), $1)
WHERE email = $2 WHERE email = $2
SQL SQL

View file

@ -389,3 +389,56 @@ def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "
end end
return str return str
end end
# Get the html link from a NavigationEndpoint or an innertubeCommand
def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
if url = endpoint.dig?("urlEndpoint", "url").try &.as_s
url = URI.parse(url)
displayed_url = text
if url.host == "youtu.be"
url = "/watch?v=#{url.request_target.lstrip('/')}"
elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com")
if url.path == "/redirect"
# Sometimes, links can be corrupted (why?) so make sure to fallback
# nicely. See https://github.com/iv-org/invidious/issues/2682
url = url.query_params["q"]? || ""
displayed_url = url
else
url = url.request_target
displayed_url = "youtube.com#{url}"
end
end
text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>)
elsif watch_endpoint = endpoint.dig?("watchEndpoint")
start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i
link_video_id = watch_endpoint["videoId"].as_s
url = "/watch?v=#{link_video_id}"
url += "&t=#{start_time}" if !start_time.nil?
# If the current video ID (passed through from the caller function)
# is the same as the video ID in the link, add HTML attributes for
# the JS handler function that bypasses page reload.
#
# See: https://github.com/iv-org/invidious/issues/3063
if link_video_id == video_id
start_time ||= 0
text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>)
else
text = %(<a href="#{url}">#{text}</a>)
end
elsif url = endpoint.dig?("commandMetadata", "webCommandMetadata", "url").try &.as_s
if text.starts_with?(/\s?[@#]/)
# Handle "pings" in comments and hasthags differently
# See:
# - https://github.com/iv-org/invidious/issues/3038
# - https://github.com/iv-org/invidious/issues/3062
text = %(<a href="#{url}">#{text}</a>)
else
text = %(<a href="#{url}">#{reduce_uri(url)}</a>)
end
end
return text
end

View file

@ -76,7 +76,7 @@ module Invidious::Routes::Watch
end end
env.params.query.delete_all("iv_load_policy") env.params.query.delete_all("iv_load_policy")
if watched && preferences.watch_history && !watched.includes? id if watched && preferences.watch_history
Invidious::Database::Users.mark_watched(user.as(User), id) Invidious::Database::Users.mark_watched(user.as(User), id)
end end
@ -259,9 +259,7 @@ module Invidious::Routes::Watch
case action case action
when "action_mark_watched" when "action_mark_watched"
if !user.watched.includes? id
Invidious::Database::Users.mark_watched(user, id) Invidious::Database::Users.mark_watched(user, id)
end
when "action_mark_unwatched" when "action_mark_unwatched"
Invidious::Database::Users.mark_unwatched(user, id) Invidious::Database::Users.mark_unwatched(user, id)
else else

View file

@ -10,7 +10,7 @@ module Invidious::Search
initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config) initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config)
items, _ = extract_items(initial_data) items, _ = extract_items(initial_data)
return items return items.reject!(Category)
end end
# Search a youtube channel # Search a youtube channel
@ -32,7 +32,7 @@ module Invidious::Search
response_json = YoutubeAPI.browse(continuation) response_json = YoutubeAPI.browse(continuation)
items, _ = extract_items(response_json, "", ucid) items, _ = extract_items(response_json, "", ucid)
return items return items.reject!(Category)
end end
# Search inside of user subscriptions # Search inside of user subscriptions

View file

@ -113,7 +113,7 @@ module Invidious::Search
case @type case @type
when .regular?, .playlist? when .regular?, .playlist?
items = unnest_items(Processors.regular(self)) items = Processors.regular(self)
# #
when .channel? when .channel?
items = Processors.channel(self) items = Processors.channel(self)
@ -136,26 +136,5 @@ module Invidious::Search
return params return params
end end
# TODO: clean code
private def unnest_items(all_items) : Array(SearchItem)
items = [] of SearchItem
# Light processing to flatten search results out of Categories.
# They should ideally be supported in the future.
all_items.each do |i|
if i.is_a? Category
i.contents.each do |nest_i|
if !nest_i.is_a? Video
items << nest_i
end
end
else
items << i
end
end
return items
end
end end
end end

View file

@ -17,7 +17,24 @@ def fetch_trending(trending_type, region, locale)
client_config = YoutubeAPI::ClientConfig.new(region: region) client_config = YoutubeAPI::ClientConfig.new(region: region)
initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config) initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config)
trending = extract_videos(initial_data)
return {trending, plid} items, _ = extract_items(initial_data)
extracted = [] of SearchItem
items.each do |itm|
if itm.is_a?(Category)
# Ignore the smaller categories, as they generally contain a sponsored
# channel, which brings a lot of noise on the trending page.
# See: https://github.com/iv-org/invidious/issues/2989
next if itm.contents.size < 24
extracted.concat extract_category(itm)
else
extracted << itm
end
end
# Deduplicate items before returning results
return extracted.select(SearchVideo).uniq!(&.id), plid
end end

View file

@ -48,7 +48,7 @@ struct Invidious::User
if data["watch_history"]? if data["watch_history"]?
user.watched += data["watch_history"].as_a.map(&.as_s) user.watched += data["watch_history"].as_a.map(&.as_s)
user.watched.uniq! user.watched.reverse!.uniq!.reverse!
Invidious::Database::Users.update_watch_history(user) Invidious::Database::Users.update_watch_history(user)
end end

View file

@ -1,51 +1,6 @@
require "json" require "json"
require "uri" require "uri"
def parse_command(command : JSON::Any?, string : String) : String?
on_tap = command.dig?("onTap", "innertubeCommand")
# 3rd party URL, extract original URL from YouTube tracking URL
if url_endpoint = on_tap.try &.["urlEndpoint"]?
youtube_url = URI.parse url_endpoint["url"].as_s
original_url = youtube_url.query_params["q"]?
if original_url.nil?
return ""
else
return "<a href=\"#{original_url}\">#{original_url}</a>"
end
# 1st party watch URL
elsif watch_endpoint = on_tap.try &.["watchEndpoint"]?
video_id = watch_endpoint["videoId"].as_s
time = watch_endpoint["startTimeSeconds"].as_i
url = "/watch?v=#{video_id}&t=#{time}s"
# if string is a timestamp, use the string instead
# this is a lazy regex for validating timestamps
if /(?:\d{1,2}:){1,2}\d{2}/ =~ string
return "<a href=\"#{url}\">#{string}</a>"
else
return "<a href=\"#{url}\">#{url}</a>"
end
# hashtag/other browse URLs
elsif browse_endpoint = on_tap.try &.dig?("commandMetadata", "webCommandMetadata")
url = browse_endpoint["url"].try &.as_s
# remove unnecessary character in a channel name
if browse_endpoint["webPageType"]?.try &.as_s == "WEB_PAGE_TYPE_CHANNEL"
name = string.match(/@[\w\d.-]+/)
if name.try &.[0]?
return "<a href=\"#{url}\">#{name.try &.[0]}</a>"
end
end
return "<a href=\"#{url}\">#{string}</a>"
end
return "(unknown YouTube desc command)"
end
private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int
copied = 0 copied = 0
while copied < count while copied < count
@ -62,7 +17,7 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
return copied return copied
end end
def parse_description(desc : JSON::Any?) : String? def parse_description(desc, video_id : String) : String?
return "" if desc.nil? return "" if desc.nil?
content = desc["content"].as_s content = desc["content"].as_s
@ -94,7 +49,11 @@ def parse_description(desc : JSON::Any?) : String?
copy_string(str2, iter, cmd_length) copy_string(str2, iter, cmd_length)
end end
str << parse_command(command, cmd_content) link = cmd_content
if on_tap = command.dig?("onTap", "innertubeCommand")
link = parse_link_endpoint(on_tap, cmd_content, video_id)
end
str << link
index += cmd_length index += cmd_length
end end

View file

@ -287,7 +287,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
# description_html = video_secondary_renderer.try &.dig?("description", "runs") # description_html = video_secondary_renderer.try &.dig?("description", "runs")
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) } # .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription")) description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
# Video metadata # Video metadata

View file

@ -68,19 +68,17 @@ rescue ex
return false return false
end end
def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil) : Array(SearchVideo) # This function extracts SearchVideo items from a Category.
extracted, _ = extract_items(initial_data, author_fallback, author_id_fallback) # Categories are commonly returned in search results and trending pages.
def extract_category(category : Category) : Array(SearchVideo)
target = [] of (SearchItem | Continuation) return category.contents.select(SearchVideo)
extracted.each do |i|
if i.is_a?(Category)
i.contents.each { |cate_i| target << cate_i if !cate_i.is_a? Video }
else
target << i
end
end end
return target.select(SearchVideo) # :ditto:
def extract_category(category : Category, &)
category.contents.select(SearchVideo).each do |item|
yield item
end
end end
def extract_selected_tab(tabs) def extract_selected_tab(tabs)