diff --git a/assets/css/default.css b/assets/css/default.css index 61b7819f..9ffff960 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -204,7 +204,8 @@ img.thumbnail { margin: 1px; border: 1px solid; - border-color: #0000 #0000 #CCC #0000; + border-color: rgba(0,0,0,0); + border-bottom-color: #CCC; border-radius: 0; box-shadow: none; @@ -214,7 +215,8 @@ img.thumbnail { .searchbar input[type="search"]:focus { margin: 0 0 0.5px 0; border: 2px solid; - border-color: #0000 #0000 #FED #0000; + border-color: rgba(0,0,0,0); + border-bottom-color: #FED; } /* https://stackoverflow.com/a/55170420 */ @@ -234,7 +236,7 @@ input[type="search"]::-webkit-search-cancel-button { } .user-field div { - width: initial; + width: auto; } .user-field div:not(:last-child) { @@ -527,3 +529,9 @@ p, /* Center the "invidious" logo on the search page */ #logo > h1 { text-align: center; } + +/* IE11 fixes */ +:-ms-input-placeholder { color: #888; } + +/* Wider settings name to less word wrap */ +.pure-form-aligned .pure-control-group label { width: 19em; } diff --git a/assets/css/player.css b/assets/css/player.css index 304375b5..8a7cfdab 100644 --- a/assets/css/player.css +++ b/assets/css/player.css @@ -101,23 +101,27 @@ ul.vjs-menu-content::-webkit-scrollbar { order: 2; } -.vjs-quality-selector, -.video-js .vjs-http-source-selector { +.vjs-audio-button { order: 3; } -.vjs-playback-rate { +.vjs-quality-selector, +.video-js .vjs-http-source-selector { order: 4; } -.vjs-share-control { +.vjs-playback-rate { order: 5; } -.vjs-fullscreen-control { +.vjs-share-control { order: 6; } +.vjs-fullscreen-control { + order: 7; +} + .vjs-playback-rate > .vjs-menu { width: 50px; } diff --git a/assets/css/search.css b/assets/css/search.css index 5ca141d0..7036fd28 100644 --- a/assets/css/search.css +++ b/assets/css/search.css @@ -68,7 +68,10 @@ fieldset, legend { .filter-options label { margin: 0 10px; } -#filters-apply { text-align: end; } +#filters-apply { + text-align: right; /* IE11 only */ + text-align: end; /* Override for compatible browsers */ +} /* Error message */ diff --git a/assets/js/player.js b/assets/js/player.js index 7d099e66..287b7ea1 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -17,6 +17,7 @@ var options = { 'remainingTimeDisplay', 'Spacer', 'captionsButton', + 'audioTrackButton', 'qualitySelector', 'playbackRateMenuButton', 'fullscreenToggle' @@ -145,11 +146,12 @@ function isMobile() { } if (isMobile()) { - player.mobileUi(); + player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); var buttons = ['playToggle', 'volumePanel', 'captionsButton']; - if (video_data.params.quality !== 'dash') buttons.push('qualitySelector'); + if (!video_data.params.listen && video_data.params.quality === 'dash') buttons.push('audioTrackButton'); + if (video_data.params.listen || video_data.params.quality !== 'dash') buttons.push('qualitySelector'); // Create new control bar object for operation buttons const ControlBar = videojs.getComponent('controlBar'); @@ -176,7 +178,7 @@ if (isMobile()) { var share_element = document.getElementsByClassName('vjs-share-control')[0]; operations_bar_element.append(share_element); - if (video_data.params.quality === 'dash') { + if (!video_data.params.listen && video_data.params.quality === 'dash') { var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0]; operations_bar_element.append(http_source_selector); } @@ -274,6 +276,9 @@ function updateCookie(newVolume, newSpeed) { player.on('ratechange', function () { updateCookie(null, player.playbackRate()); + if (isMobile()) { + player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } }); + } }); player.on('volumechange', function () { @@ -673,7 +678,12 @@ if (player.share) player.share(shareOptions); // show the preferred caption by default if (player_data.preferred_caption_found) { player.ready(function () { - player.textTracks()[1].mode = 'showing'; + if (!video_data.params.listen && video_data.params.quality === 'dash') { + // play.textTracks()[0] on DASH mode is showing some debug messages + player.textTracks()[1].mode = 'showing'; + } else { + player.textTracks()[0].mode = 'showing'; + } }); } diff --git a/docker-compose.yml b/docker-compose.yml index fa14a8e8..eb83b020 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ # Using it will build an image from the locally cloned repository. # # If you want to use Invidious in production, see the docker-compose.yml file provided -# in the installation documentation: https://docs.invidious.io/Installation.md +# in the installation documentation: https://docs.invidious.io/installation/ version: "3" services: diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index a703e870..35d3fa7b 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -1,5 +1,5 @@ -FROM alpine:edge AS builder -RUN apk add --no-cache 'crystal=1.4.1-r1' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev +FROM alpine:3.16 AS builder +RUN apk add --no-cache 'crystal=1.4.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev ARG release @@ -34,7 +34,7 @@ RUN if [ ${release} == 1 ] ; then \ --link-flags "-lxml2 -llzma"; \ fi -FROM alpine:edge +FROM alpine:3.16 RUN apk add --no-cache librsvg ttf-opensans WORKDIR /invidious RUN addgroup -g 1000 -S invidious && \ diff --git a/locales/cs.json b/locales/cs.json index d590b5b8..97f108d7 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -88,7 +88,7 @@ "Only show latest unwatched video from channel: ": "Zobrazit jen nejnovější nezhlédnuté video z daného kanálu: ", "preferences_unseen_only_label": "Zobrazit jen již nezhlédnuté: ", "preferences_notifications_only_label": "Zobrazit pouze upozornění (pokud nějaká jsou): ", - "Enable web notifications": "Povolit webové upozornění", + "Enable web notifications": "Povolit webová upozornění", "`x` uploaded a video": "`x` nahrál(a) video", "`x` is live": "`x` je živě", "preferences_category_data": "Nastavení dat", @@ -486,5 +486,6 @@ "search_filters_features_option_purchased": "Zakoupeno", "search_filters_sort_label": "Řadit dle", "search_filters_sort_option_relevance": "Relevantnost", - "search_filters_apply_button": "Použít vybrané filtry" + "search_filters_apply_button": "Použít vybrané filtry", + "Popular enabled: ": "Populární povoleno: " } diff --git a/locales/fi.json b/locales/fi.json index 2aa64ea7..cbb18825 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -470,5 +470,6 @@ "search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)", "search_message_use_another_instance": " Voit myös hakea toisella instanssilla.", "search_filters_date_option_none": "Milloin tahansa", - "search_filters_type_option_all": "Mikä tahansa tyyppi" + "search_filters_type_option_all": "Mikä tahansa tyyppi", + "Popular enabled: ": "Suosittu käytössä: " } diff --git a/locales/hr.json b/locales/hr.json index 7eb065dc..54eef7f9 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -107,7 +107,7 @@ "preferences_feed_menu_label": "Izbornik za feedove: ", "preferences_show_nick_label": "Prikaži nadimak na vrhu: ", "Top enabled: ": "Najbolji aktivirani: ", - "CAPTCHA enabled: ": "Aktivirani CAPTCHA: ", + "CAPTCHA enabled: ": "CAPTCHA aktiviran: ", "Login enabled: ": "Prijava aktivirana: ", "Registration enabled: ": "Registracija aktivirana: ", "Report statistics: ": "Izvještaj o statistici: ", @@ -486,5 +486,6 @@ "search_filters_duration_option_none": "Bilo koje duljine", "search_filters_duration_option_medium": "Srednje (4 – 20 minuta)", "search_filters_apply_button": "Primijeni odabrane filtre", - "search_filters_type_option_all": "Bilo koja vrsta" + "search_filters_type_option_all": "Bilo koja vrsta", + "Popular enabled: ": "Popularni aktivirani: " } diff --git a/locales/id.json b/locales/id.json index c96495c3..d150cece 100644 --- a/locales/id.json +++ b/locales/id.json @@ -346,7 +346,7 @@ "Community": "Komunitas", "search_filters_sort_option_relevance": "Relevansi", "search_filters_sort_option_rating": "Penilaian", - "search_filters_sort_option_date": "Tanggal unggah", + "search_filters_sort_option_date": "Tanggal Unggah", "search_filters_sort_option_views": "Jumlah ditonton", "search_filters_type_label": "Tipe", "search_filters_duration_label": "Durasi", @@ -421,5 +421,31 @@ "search_filters_title": "Saring", "search_message_no_results": "Tidak ada hasil yang ditemukan.", "search_message_change_filters_or_query": "Coba perbanyak kueri pencarian dan/atau ubah filter Anda.", - "search_message_use_another_instance": " Anda juga bisa mencari di peladen lain." + "search_message_use_another_instance": " Anda juga bisa mencari di peladen lain.", + "Indonesian (auto-generated)": "Indonesia (dibuat secara otomatis)", + "Japanese (auto-generated)": "Jepang (dibuat secara otomatis)", + "Korean (auto-generated)": "Korea (dibuat secara otomatis)", + "Portuguese (Brazil)": "Portugis (Brasil)", + "Russian (auto-generated)": "Rusia (dibuat secara otomatis)", + "Spanish (Mexico)": "Spanyol (Meksiko)", + "Spanish (Spain)": "Spanyol (Spanyol)", + "Vietnamese (auto-generated)": "Vietnam (dibuat secara otomatis)", + "search_filters_features_option_vr180": "VR180", + "Spanish (auto-generated)": "Spanyol (dibuat secara otomatis)", + "Chinese": "Bahasa Cina", + "Chinese (Taiwan)": "Bahasa Cina (Taiwan)", + "Chinese (Hong Kong)": "Bahasa Cina (Hong Kong)", + "Chinese (China)": "Bahasa Cina (China)", + "French (auto-generated)": "Perancis (dibuat secara otomatis)", + "German (auto-generated)": "Jerman (dibuat secara otomatis)", + "Italian (auto-generated)": "Italia (dibuat secara otomatis)", + "Portuguese (auto-generated)": "Portugis (dibuat secara otomatis)", + "Turkish (auto-generated)": "Turki (dibuat secara otomatis)", + "search_filters_date_label": "Tanggal unggah", + "search_filters_type_option_all": "Segala jenis", + "search_filters_apply_button": "Terapkan saringan yang dipilih", + "Dutch (auto-generated)": "Belanda (dihasilkan secara otomatis)", + "search_filters_date_option_none": "Tanggal berapa pun", + "search_filters_duration_option_none": "Durasi berapa pun", + "search_filters_duration_option_medium": "Sedang (4 - 20 menit)" } diff --git a/locales/it.json b/locales/it.json index 7ba5ff2d..ac83ac58 100644 --- a/locales/it.json +++ b/locales/it.json @@ -28,7 +28,7 @@ "Import and Export Data": "Importazione ed esportazione dati", "Import": "Importa", "Import Invidious data": "Importa dati Invidious in formato JSON", - "Import YouTube subscriptions": "Importa le iscrizioni da YouTube", + "Import YouTube subscriptions": "Importa le iscrizioni da YouTube/OPML", "Import FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)", "Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)", @@ -340,7 +340,7 @@ "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(modificato)", "YouTube comment permalink": "Link permanente al commento di YouTube", - "permalink": "permalink", + "permalink": "perma-collegamento", "`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤", "Audio mode": "Modalità audio", "Video mode": "Modalità video", @@ -385,7 +385,7 @@ "preferences_quality_dash_option_144p": "144p", "Released under the AGPLv3 on Github.": "Rilasciato su GitHub con licenza AGPLv3.", "preferences_quality_option_medium": "Media", - "preferences_quality_option_small": "Piccola", + "preferences_quality_option_small": "Limitata", "preferences_quality_dash_option_best": "Migliore", "preferences_quality_dash_option_worst": "Peggiore", "invidious": "Invidious", @@ -393,7 +393,7 @@ "preferences_quality_option_hd720": "HD720", "preferences_quality_dash_option_auto": "Automatica", "videoinfo_watch_on_youTube": "Guarda su YouTube", - "preferences_extend_desc_label": "Espandi automaticamente la descrizione del video: ", + "preferences_extend_desc_label": "Estendi automaticamente la descrizione del video: ", "preferences_vr_mode_label": "Video interattivi a 360 gradi: ", "Show less": "Mostra di meno", "Switch Invidious Instance": "Cambia istanza Invidious", @@ -425,5 +425,51 @@ "search_filters_type_option_show": "Serie", "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", - "search_filters_features_option_purchased": "Acquistato" + "search_filters_features_option_purchased": "Acquistato", + "comments_view_x_replies": "Vedi {{count}} risposta", + "comments_view_x_replies_plural": "Vedi {{count}} risposte", + "comments_points_count": "{{count}} punto", + "comments_points_count_plural": "{{count}} punti", + "Portuguese (auto-generated)": "Portoghese (auto-generato)", + "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", + "crash_page_switch_instance": "provato a usare un'altra istanza", + "crash_page_before_reporting": "Prima di segnalare un bug, assicurati di aver:", + "crash_page_read_the_faq": "letto le domande più frequenti (FAQ)", + "crash_page_search_issue": "cercato tra i problemi esistenti su GitHub", + "crash_page_report_issue": "Se niente di tutto ciò ha aiutato, per favore apri un nuovo problema su GitHub (preferibilmente in inglese) e includi il seguente testo nel tuo messaggio (NON tradurre il testo):", + "Popular enabled: ": "Popolare attivato: ", + "English (United Kingdom)": "Inglese (Regno Unito)", + "Portuguese (Brazil)": "Portoghese (Brasile)", + "preferences_watch_history_label": "Attiva cronologia di riproduzione: ", + "French (auto-generated)": "Francese (auto-generato)", + "search_message_use_another_instance": " Puoi anche cercare in un'altra istanza.", + "search_message_no_results": "Nessun risultato trovato.", + "search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.", + "English (United States)": "Inglese (Stati Uniti)", + "Cantonese (Hong Kong)": "Cantonese (Hong Kong)", + "Chinese": "Cinese", + "Chinese (China)": "Cinese (Cina)", + "Chinese (Hong Kong)": "Cinese (Hong Kong)", + "Chinese (Taiwan)": "Cinese (Taiwan)", + "Dutch (auto-generated)": "Olandese (auto-generato)", + "German (auto-generated)": "Tedesco (auto-generato)", + "Indonesian (auto-generated)": "Indonesiano (auto-generato)", + "Interlingue": "Interlingua", + "Italian (auto-generated)": "Italiano (auto-generato)", + "Japanese (auto-generated)": "Giapponese (auto-generato)", + "Korean (auto-generated)": "Coreano (auto-generato)", + "Russian (auto-generated)": "Russo (auto-generato)", + "Spanish (auto-generated)": "Spagnolo (auto-generato)", + "Spanish (Mexico)": "Spagnolo (Messico)", + "Spanish (Spain)": "Spagnolo (Spagna)", + "Turkish (auto-generated)": "Turco (auto-generato)", + "Vietnamese (auto-generated)": "Vietnamita (auto-generato)", + "search_filters_date_label": "Data caricamento", + "search_filters_date_option_none": "Qualunque data", + "search_filters_type_option_all": "Qualunque tipo", + "search_filters_duration_option_none": "Qualunque durata", + "search_filters_duration_option_medium": "Media (4 - 20 minuti)", + "search_filters_features_option_vr180": "VR180", + "search_filters_apply_button": "Applica filtri selezionati", + "crash_page_refresh": "provato a ricaricare la pagina" } diff --git a/locales/ja.json b/locales/ja.json index 20d3c20e..7918fe95 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -433,5 +433,10 @@ "Spanish (Spain)": "スペイン語 (スペイン)", "Vietnamese (auto-generated)": "ベトナム語 (自動生成)", "search_filters_title": "フィルタ", - "search_filters_features_option_three_sixty": "360°" + "search_filters_features_option_three_sixty": "360°", + "search_message_change_filters_or_query": "別のキーワードを試してみるか、検索フィルタを削除してください", + "search_message_no_results": "一致する検索結果はありませんでした", + "English (United States)": "英語 (アメリカ)", + "search_filters_date_label": "アップロード日", + "search_filters_features_option_vr180": "VR180" } diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 8d80c10c..77c688d5 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -460,5 +460,16 @@ "Russian (auto-generated)": "Russisk (laget automatisk)", "Dutch (auto-generated)": "Nederlandsk (laget automatisk)", "Turkish (auto-generated)": "Tyrkisk (laget automatisk)", - "search_filters_title": "Filtrer" + "search_filters_title": "Filtrer", + "Popular enabled: ": "Populære påskrudd: ", + "search_message_change_filters_or_query": "Prøv ett mindre snevert søk og/eller endre filterne.", + "search_filters_duration_option_medium": "Middels (4–20 minutter)", + "search_message_no_results": "Resultatløst.", + "search_filters_type_option_all": "Alle typer", + "search_filters_duration_option_none": "Uvilkårlig varighet", + "search_message_use_another_instance": " Du kan også søke på en annen instans.", + "search_filters_date_label": "Opplastningsdato", + "search_filters_apply_button": "Bruk valgte filtre", + "search_filters_date_option_none": "Siden begynnelsen", + "search_filters_features_option_vr180": "VR180" } diff --git a/locales/pt-BR.json b/locales/pt-BR.json index df149564..9576d646 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -470,5 +470,6 @@ "Spanish (Spain)": "Espanhol (Espanha)", "Turkish (auto-generated)": "Turco (gerado automaticamente)", "search_filters_duration_option_medium": "Médio (4 - 20 minutos)", - "search_filters_features_option_vr180": "VR180" + "search_filters_features_option_vr180": "VR180", + "Popular enabled: ": "Popular habilitado: " } diff --git a/locales/pt-PT.json b/locales/pt-PT.json index a57a2939..b00ebc72 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -21,15 +21,15 @@ "No": "Não", "Import and Export Data": "Importar e exportar dados", "Import": "Importar", - "Import Invidious data": "Importar dados do Invidious", - "Import YouTube subscriptions": "Importar subscrições do YouTube", + "Import Invidious data": "Importar dados JSON do Invidious", + "Import YouTube subscriptions": "Importar subscrições OPML ou do YouTube", "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", "Export": "Exportar", "Export subscriptions as OPML": "Exportar subscrições como OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)", - "Export data as JSON": "Exportar dados como JSON", + "Export data as JSON": "Exportar dados do Invidious como JSON", "Delete account?": "Eliminar conta?", "History": "Histórico", "An alternative front-end to YouTube": "Uma interface alternativa ao YouTube", @@ -60,13 +60,13 @@ "preferences_volume_label": "Volume da reprodução: ", "preferences_comments_label": "Preferência dos comentários: ", "youtube": "YouTube", - "reddit": "reddit", + "reddit": "Reddit", "preferences_captions_label": "Legendas predefinidas: ", "Fallback captions: ": "Legendas alternativas: ", "preferences_related_videos_label": "Mostrar vídeos relacionados: ", "preferences_annotations_label": "Mostrar anotações sempre: ", "preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", - "preferences_vr_mode_label": "Vídeos interativos de 360 graus: ", + "preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ", "preferences_category_visual": "Preferências visuais", "preferences_player_style_label": "Estilo do reprodutor: ", "Dark mode: ": "Modo escuro: ", @@ -374,5 +374,39 @@ "next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message_refresh": "Atualizar", "next_steps_error_message_go_to_youtube": "Ir ao YouTube", - "search_filters_title": "Filtro" + "search_filters_title": "Filtro", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "generic_playlists_count": "{{count}} lista de reprodução", + "generic_playlists_count_plural": "{{count}} listas de reprodução", + "generic_subscriptions_count": "{{count}} subscrição", + "generic_subscriptions_count_plural": "{{count}} subscrições", + "generic_views_count": "{{count}} visualização", + "generic_views_count_plural": "{{count}} visualizações", + "generic_subscribers_count": "{{count}} subscritor", + "generic_subscribers_count_plural": "{{count}} subscritores", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_dash_label": "Qualidade de vídeo DASH preferencial ", + "preferences_quality_dash_option_2160p": "2160p", + "subscriptions_unseen_notifs_count": "{{count}} notificação por ver", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificações por ver", + "Popular enabled: ": "Página \"Popular\" ativada: ", + "search_message_no_results": "Nenhum resultado encontrado.", + "preferences_quality_dash_option_auto": "Automática", + "preferences_region_label": "País para o conteúdo: ", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_720p": "720p", + "preferences_watch_history_label": "Ativar histórico de visualizações ", + "preferences_quality_dash_option_best": "Melhor", + "preferences_quality_dash_option_worst": "Pior", + "preferences_quality_dash_option_144p": "144p", + "invidious": "Invidious", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_option_dash": "DASH (qualidade adaptativa)", + "preferences_quality_option_medium": "Média", + "preferences_quality_option_small": "Pequena", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_360p": "360p", + "preferences_quality_dash_option_240p": "240p" } diff --git a/locales/pt.json b/locales/pt.json index 1abe46fa..654cfdeb 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -470,5 +470,6 @@ "search_filters_date_label": "Data de publicação", "search_filters_date_option_none": "Qualquer data", "search_filters_type_option_all": "Qualquer tipo", - "search_filters_duration_option_none": "Qualquer duração" + "search_filters_duration_option_none": "Qualquer duração", + "Popular enabled: ": "Página \"popular\" ativada: " } diff --git a/locales/ru.json b/locales/ru.json index 00d24502..4680e350 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -75,11 +75,11 @@ "light": "светлая", "preferences_thin_mode_label": "Облегчённое оформление: ", "preferences_category_misc": "Прочие настройки", - "preferences_automatic_instance_redirect_label": "Автоматическое перенаправление на зеркало сайта (переход на redirect.invidious.io): ", + "preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ", "preferences_category_subscription": "Настройки подписок", "preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ", - "Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ", - "preferences_max_results_label": "Число видео, на которые вы подписаны, в ленте: ", + "Redirect homepage to feed: ": "Показывать подписки на главной странице: ", + "preferences_max_results_label": "Число видео в ленте: ", "preferences_sort_label": "Сортировать видео: ", "published": "по дате публикации", "published - reverse": "по дате публикации в обратном порядке", @@ -158,7 +158,7 @@ "View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "Показано `x` комментариев", - "": "Показано`x` комментариев" + "": "Показано `x` комментариев" }, "View Reddit comments": "Смотреть комментарии с Reddit", "Hide replies": "Скрыть ответы", @@ -186,7 +186,7 @@ "Could not fetch comments": "Не удаётся загрузить комментарии", "`x` ago": "`x` назад", "Load more": "Загрузить ещё", - "Could not create mix.": "Не удаётся создать микс.", + "Could not create mix.": "Не удалось создать микс.", "Empty playlist": "Плейлист пуст", "Not a playlist.": "Некорректный плейлист.", "Playlist does not exist.": "Плейлист не существует.", @@ -486,5 +486,6 @@ "search_filters_features_option_vr180": "VR180", "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.", "search_filters_duration_option_medium": "Средние (4 - 20 минут)", - "search_filters_apply_button": "Применить фильтры" + "search_filters_apply_button": "Применить фильтры", + "Popular enabled: ": "Популярное включено: " } diff --git a/locales/sl.json b/locales/sl.json index 9165e714..288f8da5 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -502,5 +502,6 @@ "crash_page_refresh": "poskušal/a osvežiti stran", "crash_page_before_reporting": "Preden prijaviš napako, se prepričaj, da si:", "crash_page_search_issue": "preiskal/a obstoječe težave na GitHubu", - "crash_page_report_issue": "Če nič od navedenega ni pomagalo, prosim odpri novo težavo v GitHubu (po možnosti v angleščini) in v svoje sporočilo vključi naslednje besedilo (tega besedila NE prevajaj):" + "crash_page_report_issue": "Če nič od navedenega ni pomagalo, prosim odpri novo težavo v GitHubu (po možnosti v angleščini) in v svoje sporočilo vključi naslednje besedilo (tega besedila NE prevajaj):", + "Popular enabled: ": "Priljubljeni omogočeni: " } diff --git a/locales/tr.json b/locales/tr.json index b1991c35..bd499746 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -470,5 +470,6 @@ "search_filters_duration_option_medium": "Orta (4 - 20 dakika)", "search_filters_features_option_vr180": "VR180", "search_filters_title": "Filtreler", - "search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin." + "search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin.", + "Popular enabled: ": "Popüler etkin: " } diff --git a/locales/uk.json b/locales/uk.json index 23f56c9a..0cc14579 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -486,5 +486,6 @@ "search_filters_features_option_purchased": "Придбано", "search_filters_sort_option_relevance": "Відповідні", "search_filters_sort_option_rating": "Рейтингові", - "search_filters_sort_option_views": "Популярні" + "search_filters_sort_option_views": "Популярні", + "Popular enabled: ": "Популярне ввімкнено: " } diff --git a/locales/zh-CN.json b/locales/zh-CN.json index ed180628..ff48e101 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -454,5 +454,6 @@ "search_message_change_filters_or_query": "尝试扩大你的搜索查询和/或更改过滤器。", "search_filters_duration_option_none": "任意时长", "search_filters_type_option_all": "任意类型", - "search_filters_features_option_vr180": "VR180" + "search_filters_features_option_vr180": "VR180", + "Popular enabled: ": "已启用流行度: " } diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 4b6fa71b..90614e48 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -454,5 +454,6 @@ "search_filters_title": "過濾條件", "search_filters_date_label": "上傳日期", "search_filters_type_option_all": "任何類型", - "search_filters_date_option_none": "任何日期" + "search_filters_date_option_none": "任何日期", + "Popular enabled: ": "已啟用人氣: " } diff --git a/scripts/deploy-database.sh b/scripts/deploy-database.sh index ed9464e6..fa24b8f0 100644 --- a/scripts/deploy-database.sh +++ b/scripts/deploy-database.sh @@ -6,7 +6,7 @@ interactive=true -if [ "$1" == "--no-interactive" ]; then +if [ "$1" = "--no-interactive" ]; then interactive=false fi @@ -21,7 +21,7 @@ sudo systemctl enable postgresql.service # Create databse and user # -if [ "$interactive" == "true" ]; then +if [ "$interactive" = "true" ]; then sudo -u postgres -- createuser -P kemal sudo -u postgres -- createdb -O kemal invidious else diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index 27e0bf15..1e67bdaf 100644 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -74,7 +74,7 @@ install_apt() { sudo apt-get install --yes --no-install-recommends \ libssl-dev libxml2-dev libyaml-dev libgmp-dev libevent-dev \ libpcre3-dev libreadline-dev libsqlite3-dev zlib1g-dev \ - crystal postgres git librsvg2-bin make + crystal postgresql-13 git librsvg2-bin make } install_yum() { diff --git a/spec/invidious/search/query_spec.cr b/spec/invidious/search/query_spec.cr index 4853e9e9..063b69f1 100644 --- a/spec/invidious/search/query_spec.cr +++ b/spec/invidious/search/query_spec.cr @@ -197,4 +197,46 @@ Spectator.describe Invidious::Search::Query do ) end end + + describe "#to_http_params" do + it "formats regular search" do + query = described_class.new( + HTTP::Params.parse("q=The+Simpsons+hiding+in+bush&duration=short"), + Invidious::Search::Query::Type::Regular, nil + ) + + params = query.to_http_params + + expect(params).to have_key("duration") + expect(params["duration"]?).to eq("short") + + expect(params).to have_key("q") + expect(params["q"]?).to eq("The Simpsons hiding in bush") + + # Check if there aren't other parameters + params.delete("duration") + params.delete("q") + expect(params).to be_empty + end + + it "formats channel search" do + query = described_class.new( + HTTP::Params.parse("q=channel:UC2DjFE7Xf11URZqWBigcVOQ%20multimeter"), + Invidious::Search::Query::Type::Regular, nil + ) + + params = query.to_http_params + + expect(params).to have_key("channel") + expect(params["channel"]?).to eq("UC2DjFE7Xf11URZqWBigcVOQ") + + expect(params).to have_key("q") + expect(params["q"]?).to eq("multimeter") + + # Check if there aren't other parameters + params.delete("channel") + params.delete("q") + expect(params).to be_empty + end + end end diff --git a/src/invidious.cr b/src/invidious.cr index 4952b365..070b4d18 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -133,12 +133,13 @@ Invidious::Database.check_integrity(CONFIG) # Running the script by itself would show some colorful feedback while this doesn't. # Perhaps we should just move the script to runtime in order to get that feedback? - {% puts "\nChecking player dependencies...\n" %} + {% puts "\nChecking player dependencies, this may take more than 20 minutes... If it is stuck, check your internet connection.\n" %} {% if flag?(:minified_player_dependencies) %} {% puts run("../scripts/fetch-player-dependencies.cr", "--minified").stringify %} {% else %} {% puts run("../scripts/fetch-player-dependencies.cr").stringify %} {% end %} + {% puts "\nDone checking player dependencies, now compiling Invidious...\n" %} {% end %} # Start jobs diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index 565f2bca..f60ee7af 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -31,7 +31,12 @@ def get_about_info(ucid, locale) : AboutChannel end if initdata.dig?("alerts", 0, "alertRenderer", "type") == "ERROR" - raise InfoException.new(initdata["alerts"][0]["alertRenderer"]["text"]["simpleText"].as_s) + error_message = initdata["alerts"][0]["alertRenderer"]["text"]["simpleText"].as_s + if error_message == "This channel does not exist." + raise NotFoundException.new(error_message) + else + raise InfoException.new(error_message) + end end if browse_endpoint = initdata["onResponseReceivedActions"]?.try &.[0]?.try &.["navigateAction"]?.try &.["endpoint"]?.try &.["browseEndpoint"]? @@ -54,9 +59,6 @@ def get_about_info(ucid, locale) : AboutChannel banner = banners.try &.[-1]?.try &.["url"].as_s? description_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] - - is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool - allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map(&.as_s) else author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s @@ -74,13 +76,17 @@ def get_about_info(ucid, locale) : AboutChannel # end description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? - - is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool - allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map(&.as_s) end + is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool + + allowed_regions = initdata + .dig?("microformat", "microformatDataRenderer", "availableCountries") + .try &.as_a.map(&.as_s) || [] of String + description = !description_node.nil? ? description_node.as_s : "" description_html = HTML.escape(description) + if !description_node.nil? if description_node.as_h?.nil? description_node = text_to_parsed_content(description_node.as_s) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index a13d879c..053c3190 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -6,20 +6,18 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end if response.status_code != 200 - raise InfoException.new("This channel does not exist.") + raise NotFoundException.new("This channel does not exist.") end ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"] if !continuation || continuation.empty? initial_data = extract_initial_data(response.body) - body = initial_data["contents"]?.try &.["twoColumnBrowseResultsRenderer"]["tabs"].as_a.select { |tab| tab["tabRenderer"]?.try &.["selected"].as_bool.== true }[0]? + body = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"] if !body raise InfoException.new("Could not extract community tab.") end - - body = body["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"] else continuation = produce_channel_community_continuation(ucid, continuation) @@ -49,7 +47,11 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) error_message = (message["text"]["simpleText"]? || message["text"]["runs"]?.try &.[0]?.try &.["text"]?) .try &.as_s || "" - raise InfoException.new(error_message) + if error_message == "This channel does not exist." + raise NotFoundException.new(error_message) + else + raise InfoException.new(error_message) + end end response = JSON.build do |json| diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 593189fd..5112ad3d 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -95,7 +95,7 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b contents = body["contents"]? header = body["header"]? else - raise InfoException.new("Could not fetch comments") + raise NotFoundException.new("Comments not found.") end if !contents @@ -290,7 +290,7 @@ def fetch_reddit_comments(id, sort_by = "confidence") thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink) else - raise InfoException.new("Could not fetch comments") + raise NotFoundException.new("Comments not found.") end client.close diff --git a/src/invidious/exceptions.cr b/src/invidious/exceptions.cr index bfaa3fd5..471a199a 100644 --- a/src/invidious/exceptions.cr +++ b/src/invidious/exceptions.cr @@ -18,3 +18,7 @@ class BrokenTubeException < Exception return "Missing JSON element \"#{@element}\"" end end + +# Exception threw when an element is not found. +class NotFoundException < InfoException +end diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index aefa34cc..c4eb7507 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -317,7 +317,7 @@ def get_playlist(plid : String) if playlist = Invidious::Database::Playlists.select(id: plid) return playlist else - raise InfoException.new("Playlist does not exist.") + raise NotFoundException.new("Playlist does not exist.") end else return fetch_playlist(plid) diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr index 8bc36946..bfb8a377 100644 --- a/src/invidious/routes/api/manifest.cr +++ b/src/invidious/routes/api/manifest.cr @@ -16,6 +16,8 @@ module Invidious::Routes::API::Manifest video = get_video(id, region: region) rescue ex : VideoRedirect return env.redirect env.request.resource.gsub(id, ex.video_id) + rescue ex : NotFoundException + haltf env, status_code: 404 rescue ex haltf env, status_code: 403 end @@ -46,7 +48,7 @@ module Invidious::Routes::API::Manifest end end - audio_streams = video.audio_streams + audio_streams = video.audio_streams.sort_by { |stream| {stream["bitrate"].as_i} }.reverse! video_streams = video.video_streams.sort_by { |stream| {stream["width"].as_i, stream["fps"].as_i} }.reverse! manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml| @@ -60,16 +62,22 @@ module Invidious::Routes::API::Manifest mime_streams = audio_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type } next if mime_streams.empty? - xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true) do - mime_streams.each do |fmt| - # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415) - next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange")) + mime_streams.each do |fmt| + # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415) + next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange")) + # Different representations of the same audio should be groupped into one AdaptationSet. + # However, most players don't support auto quality switching, so we have to trick them + # into providing a quality selector. + # See https://github.com/iv-org/invidious/issues/3074 for more details. + xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: fmt["bitrate"].to_s + "k") do codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"') bandwidth = fmt["bitrate"].as_i itag = fmt["itag"].as_i url = fmt["url"].as_s + xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: i == 0 ? "main" : "alternate") + xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", value: "2") @@ -79,9 +87,8 @@ module Invidious::Routes::API::Manifest end end end + i += 1 end - - i += 1 end potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144} diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index b559a01a..1f5ad8ef 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -237,6 +237,8 @@ module Invidious::Routes::API::V1::Authenticated begin video = get_video(video_id) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex return error_json(500, ex) end diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 8650976d..6b81c546 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -13,6 +13,8 @@ module Invidious::Routes::API::V1::Channels rescue ex : ChannelRedirect env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex return error_json(500, ex) end @@ -170,6 +172,8 @@ module Invidious::Routes::API::V1::Channels rescue ex : ChannelRedirect env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex return error_json(500, ex) end @@ -205,6 +209,8 @@ module Invidious::Routes::API::V1::Channels rescue ex : ChannelRedirect env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex return error_json(500, ex) end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index a9f891f5..1b7b4fa7 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -12,6 +12,8 @@ module Invidious::Routes::API::V1::Videos rescue ex : VideoRedirect env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex return error_json(500, ex) end @@ -42,6 +44,8 @@ module Invidious::Routes::API::V1::Videos rescue ex : VideoRedirect env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) + rescue ex : NotFoundException + haltf env, 404 rescue ex haltf env, 500 end @@ -167,6 +171,8 @@ module Invidious::Routes::API::V1::Videos rescue ex : VideoRedirect env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) + rescue ex : NotFoundException + haltf env, 404 rescue ex haltf env, 500 end @@ -324,6 +330,8 @@ module Invidious::Routes::API::V1::Videos begin comments = fetch_youtube_comments(id, continuation, format, locale, thin_mode, region, sort_by: sort_by) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex return error_json(500, ex) end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index cd2e3323..c6e02cbd 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -85,6 +85,9 @@ module Invidious::Routes::Channels rescue ex : InfoException env.response.status_code = 500 error_message = ex.message + rescue ex : NotFoundException + env.response.status_code = 404 + error_message = ex.message rescue ex return error_template(500, ex) end @@ -118,7 +121,7 @@ module Invidious::Routes::Channels resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}") ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"] rescue ex : InfoException | KeyError - raise InfoException.new(translate(locale, "This channel does not exist.")) + return error_template(404, translate(locale, "This channel does not exist.")) end selected_tab = env.request.path.split("/")[-1] @@ -141,7 +144,7 @@ module Invidious::Routes::Channels user = env.params.query["user"]? if !user - raise InfoException.new("This channel does not exist.") + return error_template(404, "This channel does not exist.") else env.redirect "/user/#{user}#{uri_params}" end @@ -197,6 +200,8 @@ module Invidious::Routes::Channels channel = get_about_info(ucid, locale) rescue ex : ChannelRedirect return env.redirect env.request.resource.gsub(ucid, ex.channel_id) + rescue ex : NotFoundException + return error_template(404, ex) rescue ex return error_template(500, ex) end diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr index 207970b0..84da9993 100644 --- a/src/invidious/routes/embed.cr +++ b/src/invidious/routes/embed.cr @@ -7,6 +7,8 @@ module Invidious::Routes::Embed playlist = get_playlist(plid) offset = env.params.query["index"]?.try &.to_i? || 0 videos = get_playlist_videos(playlist, offset: offset) + rescue ex : NotFoundException + return error_template(404, ex) rescue ex return error_template(500, ex) end @@ -60,6 +62,8 @@ module Invidious::Routes::Embed playlist = get_playlist(plid) offset = env.params.query["index"]?.try &.to_i? || 0 videos = get_playlist_videos(playlist, offset: offset) + rescue ex : NotFoundException + return error_template(404, ex) rescue ex return error_template(500, ex) end @@ -119,6 +123,8 @@ module Invidious::Routes::Embed video = get_video(id, region: params.region) rescue ex : VideoRedirect return env.redirect env.request.resource.gsub(id, ex.video_id) + rescue ex : NotFoundException + return error_template(404, ex) rescue ex return error_template(500, ex) end diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 2e6043f7..44a87175 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -150,6 +150,8 @@ module Invidious::Routes::Feeds channel = get_about_info(ucid, locale) rescue ex : ChannelRedirect return env.redirect env.request.resource.gsub(ucid, ex.channel_id) + rescue ex : NotFoundException + return error_atom(404, ex) rescue ex return error_atom(500, ex) end diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index de981d81..fe7e4e1c 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -66,7 +66,13 @@ module Invidious::Routes::Playlists user = user.as(User) playlist_id = env.params.query["list"] - playlist = get_playlist(playlist_id) + begin + playlist = get_playlist(playlist_id) + rescue ex : NotFoundException + return error_template(404, ex) + rescue ex + return error_template(500, ex) + end subscribe_playlist(user, playlist) env.redirect "/playlist?list=#{playlist.id}" @@ -304,6 +310,8 @@ module Invidious::Routes::Playlists playlist_id = env.params.query["playlist_id"] playlist = get_playlist(playlist_id).as(InvidiousPlaylist) raise "Invalid user" if playlist.author != user.email + rescue ex : NotFoundException + return error_json(404, ex) rescue ex if redirect return error_template(400, ex) @@ -334,6 +342,8 @@ module Invidious::Routes::Playlists begin video = get_video(video_id) + rescue ex : NotFoundException + return error_json(404, ex) rescue ex if redirect return error_template(500, ex) @@ -394,6 +404,8 @@ module Invidious::Routes::Playlists begin playlist = get_playlist(plid) + rescue ex : NotFoundException + return error_template(404, ex) rescue ex return error_template(500, ex) end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 6f8bffea..2a9705cf 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -59,6 +59,12 @@ module Invidious::Routes::Search return error_template(500, ex) end + params = query.to_http_params + url_prev_page = "/search?#{params}&page=#{query.page - 1}" + url_next_page = "/search?#{params}&page=#{query.page + 1}" + + redirect_url = Invidious::Frontend::Misc.redirect_url(env) + env.set "search", query.text templated "search" end diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 3a92ef96..560f9c19 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -265,7 +265,13 @@ module Invidious::Routes::VideoPlayback return error_template(403, "Administrator has disabled this endpoint.") end - video = get_video(id, region: region) + begin + video = get_video(id, region: region) + rescue ex : NotFoundException + return error_template(404, ex) + rescue ex + return error_template(500, ex) + end fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } url = fmt.try &.["url"]?.try &.as_s diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 7280de4f..fe1d8e54 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -63,6 +63,9 @@ module Invidious::Routes::Watch video = get_video(id, region: params.region) rescue ex : VideoRedirect return env.redirect env.request.resource.gsub(id, ex.video_id) + rescue ex : NotFoundException + LOGGER.error("get_video not found: #{id} : #{ex.message}") + return error_template(404, ex) rescue ex LOGGER.error("get_video: #{id} : #{ex.message}") return error_template(500, ex) diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr index 34b36b1d..24e79609 100644 --- a/src/invidious/search/query.cr +++ b/src/invidious/search/query.cr @@ -57,7 +57,7 @@ module Invidious::Search # Get the page number (also common to all search types) @page = params["page"]?.try &.to_i? || 1 - # Stop here is raw query in empty + # Stop here if raw query is empty # NOTE: maybe raise in the future? return if self.empty_raw_query? @@ -127,6 +127,16 @@ module Invidious::Search return items end + # Return the HTTP::Params corresponding to this Query (invidious format) + def to_http_params : HTTP::Params + params = @filters.to_iv_params + + params["q"] = @query + params["channel"] = @channel if !@channel.empty? + + return params + end + # TODO: clean code private def unnest_items(all_items) : Array(SearchItem) items = [] of SearchItem diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 1504e390..19ee064c 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -323,7 +323,7 @@ struct Video json.field "viewCount", self.views json.field "likeCount", self.likes - json.field "dislikeCount", self.dislikes + json.field "dislikeCount", 0_i64 json.field "paid", self.paid json.field "premium", self.premium @@ -354,7 +354,7 @@ struct Video json.field "lengthSeconds", self.length_seconds json.field "allowRatings", self.allow_ratings - json.field "rating", self.average_rating + json.field "rating", 0_i64 json.field "isListed", self.is_listed json.field "liveNow", self.live_now json.field "isUpcoming", self.is_upcoming @@ -556,11 +556,6 @@ struct Video info["dislikes"]?.try &.as_i64 || 0_i64 end - def average_rating : Float64 - # (likes / (likes + dislikes) * 4 + 1) - info["videoDetails"]["averageRating"]?.try { |t| t.as_f? || t.as_i64?.try &.to_f64 }.try &.round(4) || 0.0 - end - def published : Time info .dig?("microformat", "playerMicroformatRenderer", "publishDate") @@ -813,14 +808,6 @@ struct Video return info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s end - def wilson_score : Float64 - ci_lower_bound(likes, likes + dislikes).round(4) - end - - def engagement : Float64 - (((likes + dislikes) / views) * 100).round(4) - end - def reason : String? info["reason"]?.try &.as_s end @@ -908,13 +895,20 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_ player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) - if player_response.dig?("playabilityStatus", "status").try &.as_s != "OK" + playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s + + if playability_status != "OK" subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason") reason = subreason.try &.[]?("simpleText").try &.as_s reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") reason ||= player_response.dig("playabilityStatus", "reason").as_s + params["reason"] = JSON::Any.new(reason) - return params + + # Stop here if video is not a scheduled livestream + if playability_status != "LIVE_STREAM_OFFLINE" + return params + end end params["shortDescription"] = player_response.dig?("videoDetails", "shortDescription") || JSON::Any.new(nil) @@ -1005,7 +999,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_ params["relatedVideos"] = JSON::Any.new(related) - # Likes/dislikes + # Likes toplevel_buttons = video_primary_renderer .try &.dig?("videoActions", "menuRenderer", "topLevelButtons") @@ -1023,30 +1017,10 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_ LOGGER.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"") LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes end - - dislikes_button = toplevel_buttons.as_a - .find(&.dig("toggleButtonRenderer", "defaultIcon", "iconType").as_s.== "DISLIKE") - .try &.["toggleButtonRenderer"] - - if dislikes_button - dislikes_txt = (dislikes_button["defaultText"]? || dislikes_button["toggledText"]?) - .try &.dig?("accessibility", "accessibilityData", "label") - dislikes = dislikes_txt.as_s.gsub(/\D/, "").to_i64? if dislikes_txt - - LOGGER.trace("extract_video_info: Found \"dislikes\" button. Button text is \"#{dislikes_txt}\"") - LOGGER.debug("extract_video_info: Dislikes count is #{dislikes}") if dislikes - end - end - - if likes && likes != 0_i64 && (!dislikes || dislikes == 0_i64) - if rating = player_response.dig?("videoDetails", "averageRating").try { |x| x.as_i64? || x.as_f? } - dislikes = (likes * ((5 - rating)/(rating - 1))).round.to_i64 - LOGGER.debug("extract_video_info: Dislikes count (using fallback method) is #{dislikes}") - end end params["likes"] = JSON::Any.new(likes || 0_i64) - params["dislikes"] = JSON::Any.new(dislikes || 0_i64) + params["dislikes"] = JSON::Any.new(0_i64) # Description @@ -1158,7 +1132,11 @@ def fetch_video(id, region) end if reason = info["reason"]? - raise InfoException.new(reason.as_s || "") + if reason == "Video unavailable" + raise NotFoundException.new(reason.as_s || "") + else + raise InfoException.new(reason.as_s || "") + end end video = Video.new({ diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index fb7ad1dc..0e959ff2 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -5,7 +5,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
- "/> + "/>
<% end %>

<%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

@@ -23,7 +23,7 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- "/> + "/>

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %> @@ -36,7 +36,7 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- + <% if item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> @@ -51,16 +51,13 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- + + <% if plid_form = env.get?("remove_playlist_items") %>
" method="post"> ">

- - - +

<% end %> @@ -103,29 +100,21 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
- + <% if env.get? "show_watched" %>
" method="post"> ">

- - - +

<% elsif plid_form = env.get? "add_playlist_items" %>
" method="post"> ">

- - - +

<% end %> diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index fffefc9a..c3c02df0 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -7,14 +7,25 @@ <% else %> <% if params.listen %> - <% audio_streams.each_with_index do |fmt, i| + <% # default to 128k m4a stream + best_m4a_stream_index = 0 + best_m4a_stream_bitrate = 0 + audio_streams.each_with_index do |fmt, i| + bandwidth = fmt["bitrate"].as_i + if (fmt["mimeType"].as_s.starts_with?("audio/mp4") && bandwidth > best_m4a_stream_bitrate) + best_m4a_stream_bitrate = bandwidth + best_m4a_stream_index = i + end + end + + audio_streams.each_with_index do |fmt, i| src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}" src_url += "&local=true" if params.local bitrate = fmt["bitrate"] mimetype = HTML.escape(fmt["mimeType"].as_s) - selected = i == 0 ? true : false + selected = (i == best_m4a_stream_index) %> <% if !params.local && !CONFIG.disabled?("local") %> diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index ce5ff7f0..1bf5cc3e 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -11,6 +11,7 @@ <%= HTML.escape(video.title) %> - Invidious + diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 6c1243c5..471d21db 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -38,9 +38,7 @@
" method="post"> ">

- - - +

diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 7110703e..254449a1 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -3,16 +3,6 @@ <% end %> -<%- - search_query_encoded = URI.encode_www_form(query.text, space_to_plus: true) - filter_params = query.filters.to_iv_params - - url_prev_page = "/search?q=#{search_query_encoded}&#{filter_params}&page=#{query.page - 1}" - url_next_page = "/search?q=#{search_query_encoded}&#{filter_params}&page=#{query.page + 1}" - - redirect_url = Invidious::Frontend::Misc.redirect_url(env) --%> - <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 0338f1c4..9eb65c96 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -69,7 +69,7 @@
<% if env.get("preferences").as(Preferences).show_nick %>
- <%= env.get("user").as(Invidious::User).email %> + <%= HTML.escape(env.get("user").as(Invidious::User).email) %>
<% end %>
diff --git a/src/invidious/views/user/subscription_manager.ecr b/src/invidious/views/user/subscription_manager.ecr index c2a89ca2..c9801f09 100644 --- a/src/invidious/views/user/subscription_manager.ecr +++ b/src/invidious/views/user/subscription_manager.ecr @@ -39,9 +39,7 @@

" method="post"> "> - - "> - + ">

diff --git a/src/invidious/views/user/token_manager.ecr b/src/invidious/views/user/token_manager.ecr index 79f905a1..a73fa048 100644 --- a/src/invidious/views/user/token_manager.ecr +++ b/src/invidious/views/user/token_manager.ecr @@ -31,9 +31,7 @@

" method="post"> "> - - "> - + ">

diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index d1fdcce2..50c63d21 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -173,7 +173,7 @@ we're going to need to do it here in order to allow for translations.

<%= number_with_separator(video.views) %>

<%= number_with_separator(video.likes) %>

-

+

<%= translate(locale, "Genre: ") %> <% if !video.genre_url %> <%= video.genre %> @@ -185,9 +185,9 @@ we're going to need to do it here in order to allow for translations.

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

<% end %>

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

-

<%= translate(locale, "Wilson score: ") %><%= video.wilson_score %>

-

-

<%= translate(locale, "Engagement: ") %><%= video.engagement %>%

+ + + <% if video.allowed_regions.size != REGIONS.size %>

<% if video.allowed_regions.size < REGIONS.size // 2 %> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index c4326cab..b9609eb9 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -417,7 +417,7 @@ private module Extractors # {"tabRenderer": { # "endpoint": {...} # "title": "Playlists", - # "selected": true, + # "selected": true, # Is nil unless tab is selected # "content": {...}, # ... # }} diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr index 3d5e5787..f8245160 100644 --- a/src/invidious/yt_backend/extractors_utils.cr +++ b/src/invidious/yt_backend/extractors_utils.cr @@ -84,7 +84,7 @@ end def extract_selected_tab(tabs) # Extract the selected tab from the array of tabs Youtube returns - return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]["tabRenderer"] + return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"] end def fetch_continuation_token(items : Array(JSON::Any))