Merge branch 'master' of github.com:iv-org/invidious

このコミットが含まれているのは:
守矢諏訪子 2022-07-21 23:15:35 +09:00
コミット fd23352bf2
55個のファイルの変更420行の追加171行の削除

ファイルの表示

@ -204,7 +204,8 @@ img.thumbnail {
margin: 1px; margin: 1px;
border: 1px solid; border: 1px solid;
border-color: #0000 #0000 #CCC #0000; border-color: rgba(0,0,0,0);
border-bottom-color: #CCC;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
@ -214,7 +215,8 @@ img.thumbnail {
.searchbar input[type="search"]:focus { .searchbar input[type="search"]:focus {
margin: 0 0 0.5px 0; margin: 0 0 0.5px 0;
border: 2px solid; 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 */ /* https://stackoverflow.com/a/55170420 */
@ -234,7 +236,7 @@ input[type="search"]::-webkit-search-cancel-button {
} }
.user-field div { .user-field div {
width: initial; width: auto;
} }
.user-field div:not(:last-child) { .user-field div:not(:last-child) {
@ -527,3 +529,9 @@ p,
/* Center the "invidious" logo on the search page */ /* Center the "invidious" logo on the search page */
#logo > h1 { text-align: center; } #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; }

ファイルの表示

@ -101,23 +101,27 @@ ul.vjs-menu-content::-webkit-scrollbar {
order: 2; order: 2;
} }
.vjs-quality-selector, .vjs-audio-button {
.video-js .vjs-http-source-selector {
order: 3; order: 3;
} }
.vjs-playback-rate { .vjs-quality-selector,
.video-js .vjs-http-source-selector {
order: 4; order: 4;
} }
.vjs-share-control { .vjs-playback-rate {
order: 5; order: 5;
} }
.vjs-fullscreen-control { .vjs-share-control {
order: 6; order: 6;
} }
.vjs-fullscreen-control {
order: 7;
}
.vjs-playback-rate > .vjs-menu { .vjs-playback-rate > .vjs-menu {
width: 50px; width: 50px;
} }

ファイルの表示

@ -68,7 +68,10 @@ fieldset, legend {
.filter-options label { margin: 0 10px; } .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 */ /* Error message */

ファイルの表示

@ -17,6 +17,7 @@ var options = {
'remainingTimeDisplay', 'remainingTimeDisplay',
'Spacer', 'Spacer',
'captionsButton', 'captionsButton',
'audioTrackButton',
'qualitySelector', 'qualitySelector',
'playbackRateMenuButton', 'playbackRateMenuButton',
'fullscreenToggle' 'fullscreenToggle'
@ -145,11 +146,12 @@ function isMobile() {
} }
if (isMobile()) { if (isMobile()) {
player.mobileUi(); player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } });
var buttons = ['playToggle', 'volumePanel', 'captionsButton']; 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 // Create new control bar object for operation buttons
const ControlBar = videojs.getComponent('controlBar'); const ControlBar = videojs.getComponent('controlBar');
@ -176,7 +178,7 @@ if (isMobile()) {
var share_element = document.getElementsByClassName('vjs-share-control')[0]; var share_element = document.getElementsByClassName('vjs-share-control')[0];
operations_bar_element.append(share_element); 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]; var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0];
operations_bar_element.append(http_source_selector); operations_bar_element.append(http_source_selector);
} }
@ -274,6 +276,9 @@ function updateCookie(newVolume, newSpeed) {
player.on('ratechange', function () { player.on('ratechange', function () {
updateCookie(null, player.playbackRate()); updateCookie(null, player.playbackRate());
if (isMobile()) {
player.mobileUi({ touchControls: { seekSeconds: 5 * player.playbackRate() } });
}
}); });
player.on('volumechange', function () { player.on('volumechange', function () {
@ -673,7 +678,12 @@ if (player.share) player.share(shareOptions);
// show the preferred caption by default // show the preferred caption by default
if (player_data.preferred_caption_found) { if (player_data.preferred_caption_found) {
player.ready(function () { player.ready(function () {
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'; player.textTracks()[1].mode = 'showing';
} else {
player.textTracks()[0].mode = 'showing';
}
}); });
} }

ファイルの表示

@ -2,7 +2,7 @@
# Using it will build an image from the locally cloned repository. # 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 # 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" version: "3"
services: services:

ファイルの表示

@ -1,5 +1,5 @@
FROM alpine:edge AS builder FROM alpine:3.16 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 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 ARG release
@ -34,7 +34,7 @@ RUN if [ ${release} == 1 ] ; then \
--link-flags "-lxml2 -llzma"; \ --link-flags "-lxml2 -llzma"; \
fi fi
FROM alpine:edge FROM alpine:3.16
RUN apk add --no-cache librsvg ttf-opensans RUN apk add --no-cache librsvg ttf-opensans
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \

ファイルの表示

@ -88,7 +88,7 @@
"Only show latest unwatched video from channel: ": "Zobrazit jen nejnovější nezhlédnuté video z daného kanálu: ", "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_unseen_only_label": "Zobrazit jen již nezhlédnuté: ",
"preferences_notifications_only_label": "Zobrazit pouze upozornění (pokud nějaká jsou): ", "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` uploaded a video": "`x` nahrál(a) video",
"`x` is live": "`x` je živě", "`x` is live": "`x` je živě",
"preferences_category_data": "Nastavení dat", "preferences_category_data": "Nastavení dat",
@ -486,5 +486,6 @@
"search_filters_features_option_purchased": "Zakoupeno", "search_filters_features_option_purchased": "Zakoupeno",
"search_filters_sort_label": "Řadit dle", "search_filters_sort_label": "Řadit dle",
"search_filters_sort_option_relevance": "Relevantnost", "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: "
} }

ファイルの表示

@ -470,5 +470,6 @@
"search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)", "search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)",
"search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.", "search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
"search_filters_date_option_none": "Milloin tahansa", "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ä: "
} }

ファイルの表示

@ -107,7 +107,7 @@
"preferences_feed_menu_label": "Izbornik za feedove: ", "preferences_feed_menu_label": "Izbornik za feedove: ",
"preferences_show_nick_label": "Prikaži nadimak na vrhu: ", "preferences_show_nick_label": "Prikaži nadimak na vrhu: ",
"Top enabled: ": "Najbolji aktivirani: ", "Top enabled: ": "Najbolji aktivirani: ",
"CAPTCHA enabled: ": "Aktivirani CAPTCHA: ", "CAPTCHA enabled: ": "CAPTCHA aktiviran: ",
"Login enabled: ": "Prijava aktivirana: ", "Login enabled: ": "Prijava aktivirana: ",
"Registration enabled: ": "Registracija aktivirana: ", "Registration enabled: ": "Registracija aktivirana: ",
"Report statistics: ": "Izvještaj o statistici: ", "Report statistics: ": "Izvještaj o statistici: ",
@ -486,5 +486,6 @@
"search_filters_duration_option_none": "Bilo koje duljine", "search_filters_duration_option_none": "Bilo koje duljine",
"search_filters_duration_option_medium": "Srednje (4 20 minuta)", "search_filters_duration_option_medium": "Srednje (4 20 minuta)",
"search_filters_apply_button": "Primijeni odabrane filtre", "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: "
} }

ファイルの表示

@ -346,7 +346,7 @@
"Community": "Komunitas", "Community": "Komunitas",
"search_filters_sort_option_relevance": "Relevansi", "search_filters_sort_option_relevance": "Relevansi",
"search_filters_sort_option_rating": "Penilaian", "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_sort_option_views": "Jumlah ditonton",
"search_filters_type_label": "Tipe", "search_filters_type_label": "Tipe",
"search_filters_duration_label": "Durasi", "search_filters_duration_label": "Durasi",
@ -421,5 +421,31 @@
"search_filters_title": "Saring", "search_filters_title": "Saring",
"search_message_no_results": "Tidak ada hasil yang ditemukan.", "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_change_filters_or_query": "Coba perbanyak kueri pencarian dan/atau ubah filter Anda.",
"search_message_use_another_instance": " Anda juga bisa <a href=\"`x`\">mencari di peladen lain</a>." "search_message_use_another_instance": " Anda juga bisa <a href=\"`x`\">mencari di peladen lain</a>.",
"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)"
} }

ファイルの表示

@ -28,7 +28,7 @@
"Import and Export Data": "Importazione ed esportazione dati", "Import and Export Data": "Importazione ed esportazione dati",
"Import": "Importa", "Import": "Importa",
"Import Invidious data": "Importa dati Invidious in formato JSON", "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 FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)",
"Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)", "Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)",
@ -340,7 +340,7 @@
"%A %B %-d, %Y": "%A %-d %B %Y", "%A %B %-d, %Y": "%A %-d %B %Y",
"(edited)": "(modificato)", "(edited)": "(modificato)",
"YouTube comment permalink": "Link permanente al commento di YouTube", "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 ❤", "`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤",
"Audio mode": "Modalità audio", "Audio mode": "Modalità audio",
"Video mode": "Modalità video", "Video mode": "Modalità video",
@ -385,7 +385,7 @@
"preferences_quality_dash_option_144p": "144p", "preferences_quality_dash_option_144p": "144p",
"Released under the AGPLv3 on Github.": "Rilasciato su GitHub con licenza AGPLv3.", "Released under the AGPLv3 on Github.": "Rilasciato su GitHub con licenza AGPLv3.",
"preferences_quality_option_medium": "Media", "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_best": "Migliore",
"preferences_quality_dash_option_worst": "Peggiore", "preferences_quality_dash_option_worst": "Peggiore",
"invidious": "Invidious", "invidious": "Invidious",
@ -393,7 +393,7 @@
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
"preferences_quality_dash_option_auto": "Automatica", "preferences_quality_dash_option_auto": "Automatica",
"videoinfo_watch_on_youTube": "Guarda su YouTube", "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: ", "preferences_vr_mode_label": "Video interattivi a 360 gradi: ",
"Show less": "Mostra di meno", "Show less": "Mostra di meno",
"Switch Invidious Instance": "Cambia istanza Invidious", "Switch Invidious Instance": "Cambia istanza Invidious",
@ -425,5 +425,51 @@
"search_filters_type_option_show": "Serie", "search_filters_type_option_show": "Serie",
"search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_short": "Corto (< 4 minuti)",
"search_filters_duration_option_long": "Lungo (> 20 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 <a href=\"`x`\">usare un'altra istanza</a>",
"crash_page_before_reporting": "Prima di segnalare un bug, assicurati di aver:",
"crash_page_read_the_faq": "letto le <a href=\"`x`\">domande più frequenti (FAQ)</a>",
"crash_page_search_issue": "cercato tra <a href=\"`x`\"> i problemi esistenti su GitHub</a>",
"crash_page_report_issue": "Se niente di tutto ciò ha aiutato, per favore <a href=\"`x`\">apri un nuovo problema su GitHub</a> (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 <a href=\"`x`\">cercare in un'altra istanza</a>.",
"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 <a href=\"`x`\">ricaricare la pagina</a>"
} }

ファイルの表示

@ -433,5 +433,10 @@
"Spanish (Spain)": "スペイン語 (スペイン)", "Spanish (Spain)": "スペイン語 (スペイン)",
"Vietnamese (auto-generated)": "ベトナム語 (自動生成)", "Vietnamese (auto-generated)": "ベトナム語 (自動生成)",
"search_filters_title": "フィルタ", "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"
} }

ファイルの表示

@ -460,5 +460,16 @@
"Russian (auto-generated)": "Russisk (laget automatisk)", "Russian (auto-generated)": "Russisk (laget automatisk)",
"Dutch (auto-generated)": "Nederlandsk (laget automatisk)", "Dutch (auto-generated)": "Nederlandsk (laget automatisk)",
"Turkish (auto-generated)": "Tyrkisk (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 (420 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å <a href=\"`x`\">søke på en annen instans</a>.",
"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"
} }

ファイルの表示

@ -470,5 +470,6 @@
"Spanish (Spain)": "Espanhol (Espanha)", "Spanish (Spain)": "Espanhol (Espanha)",
"Turkish (auto-generated)": "Turco (gerado automaticamente)", "Turkish (auto-generated)": "Turco (gerado automaticamente)",
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)", "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: "
} }

ファイルの表示

@ -21,15 +21,15 @@
"No": "Não", "No": "Não",
"Import and Export Data": "Importar e exportar dados", "Import and Export Data": "Importar e exportar dados",
"Import": "Importar", "Import": "Importar",
"Import Invidious data": "Importar dados do Invidious", "Import Invidious data": "Importar dados JSON do Invidious",
"Import YouTube subscriptions": "Importar subscrições do YouTube", "Import YouTube subscriptions": "Importar subscrições OPML ou do YouTube",
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
"Export": "Exportar", "Export": "Exportar",
"Export subscriptions as OPML": "Exportar subscrições como OPML", "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 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?", "Delete account?": "Eliminar conta?",
"History": "Histórico", "History": "Histórico",
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube", "An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
@ -60,13 +60,13 @@
"preferences_volume_label": "Volume da reprodução: ", "preferences_volume_label": "Volume da reprodução: ",
"preferences_comments_label": "Preferência dos comentários: ", "preferences_comments_label": "Preferência dos comentários: ",
"youtube": "YouTube", "youtube": "YouTube",
"reddit": "reddit", "reddit": "Reddit",
"preferences_captions_label": "Legendas predefinidas: ", "preferences_captions_label": "Legendas predefinidas: ",
"Fallback captions: ": "Legendas alternativas: ", "Fallback captions: ": "Legendas alternativas: ",
"preferences_related_videos_label": "Mostrar vídeos relacionados: ", "preferences_related_videos_label": "Mostrar vídeos relacionados: ",
"preferences_annotations_label": "Mostrar anotações sempre: ", "preferences_annotations_label": "Mostrar anotações sempre: ",
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", "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_category_visual": "Preferências visuais",
"preferences_player_style_label": "Estilo do reprodutor: ", "preferences_player_style_label": "Estilo do reprodutor: ",
"Dark mode: ": "Modo escuro: ", "Dark mode: ": "Modo escuro: ",
@ -374,5 +374,39 @@
"next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar", "next_steps_error_message_refresh": "Atualizar",
"next_steps_error_message_go_to_youtube": "Ir ao YouTube", "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"
} }

ファイルの表示

@ -470,5 +470,6 @@
"search_filters_date_label": "Data de publicação", "search_filters_date_label": "Data de publicação",
"search_filters_date_option_none": "Qualquer data", "search_filters_date_option_none": "Qualquer data",
"search_filters_type_option_all": "Qualquer tipo", "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: "
} }

ファイルの表示

@ -75,11 +75,11 @@
"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": "Настройки подписок",
"preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ", "preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ",
"Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ", "Redirect homepage to feed: ": "Показывать подписки на главной странице: ",
"preferences_max_results_label": "Число видео, на которые вы подписаны, в ленте: ", "preferences_max_results_label": "Число видео в ленте: ",
"preferences_sort_label": "Сортировать видео: ", "preferences_sort_label": "Сортировать видео: ",
"published": "по дате публикации", "published": "по дате публикации",
"published - reverse": "по дате публикации в обратном порядке", "published - reverse": "по дате публикации в обратном порядке",
@ -158,7 +158,7 @@
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
"View `x` comments": { "View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Показано `x` комментариев", "([^.,0-9]|^)1([^.,0-9]|$)": "Показано `x` комментариев",
"": "Показано`x` комментариев" "": "Показано `x` комментариев"
}, },
"View Reddit comments": "Смотреть комментарии с Reddit", "View Reddit comments": "Смотреть комментарии с Reddit",
"Hide replies": "Скрыть ответы", "Hide replies": "Скрыть ответы",
@ -186,7 +186,7 @@
"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.": "Плейлист не существует.",
@ -486,5 +486,6 @@
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.", "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.",
"search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_duration_option_medium": "Средние (4 - 20 минут)",
"search_filters_apply_button": "Применить фильтры" "search_filters_apply_button": "Применить фильтры",
"Popular enabled: ": "Популярное включено: "
} }

ファイルの表示

@ -502,5 +502,6 @@
"crash_page_refresh": "poskušal/a <a href=\"`x`\">osvežiti stran</a>", "crash_page_refresh": "poskušal/a <a href=\"`x`\">osvežiti stran</a>",
"crash_page_before_reporting": "Preden prijaviš napako, se prepričaj, da si:", "crash_page_before_reporting": "Preden prijaviš napako, se prepričaj, da si:",
"crash_page_search_issue": "preiskal/a <a href=\"`x`\">obstoječe težave na GitHubu</a>", "crash_page_search_issue": "preiskal/a <a href=\"`x`\">obstoječe težave na GitHubu</a>",
"crash_page_report_issue": "Če nič od navedenega ni pomagalo, prosim <a href=\"`x`\">odpri novo težavo v GitHubu</a> (po možnosti v angleščini) in v svoje sporočilo vključi naslednje besedilo (tega besedila NE prevajaj):" "crash_page_report_issue": "Če nič od navedenega ni pomagalo, prosim <a href=\"`x`\">odpri novo težavo v GitHubu</a> (po možnosti v angleščini) in v svoje sporočilo vključi naslednje besedilo (tega besedila NE prevajaj):",
"Popular enabled: ": "Priljubljeni omogočeni: "
} }

ファイルの表示

@ -470,5 +470,6 @@
"search_filters_duration_option_medium": "Orta (4 - 20 dakika)", "search_filters_duration_option_medium": "Orta (4 - 20 dakika)",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_filters_title": "Filtreler", "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: "
} }

ファイルの表示

@ -486,5 +486,6 @@
"search_filters_features_option_purchased": "Придбано", "search_filters_features_option_purchased": "Придбано",
"search_filters_sort_option_relevance": "Відповідні", "search_filters_sort_option_relevance": "Відповідні",
"search_filters_sort_option_rating": "Рейтингові", "search_filters_sort_option_rating": "Рейтингові",
"search_filters_sort_option_views": "Популярні" "search_filters_sort_option_views": "Популярні",
"Popular enabled: ": "Популярне ввімкнено: "
} }

ファイルの表示

@ -454,5 +454,6 @@
"search_message_change_filters_or_query": "尝试扩大你的搜索查询和/或更改过滤器。", "search_message_change_filters_or_query": "尝试扩大你的搜索查询和/或更改过滤器。",
"search_filters_duration_option_none": "任意时长", "search_filters_duration_option_none": "任意时长",
"search_filters_type_option_all": "任意类型", "search_filters_type_option_all": "任意类型",
"search_filters_features_option_vr180": "VR180" "search_filters_features_option_vr180": "VR180",
"Popular enabled: ": "已启用流行度: "
} }

ファイルの表示

@ -454,5 +454,6 @@
"search_filters_title": "過濾條件", "search_filters_title": "過濾條件",
"search_filters_date_label": "上傳日期", "search_filters_date_label": "上傳日期",
"search_filters_type_option_all": "任何類型", "search_filters_type_option_all": "任何類型",
"search_filters_date_option_none": "任何日期" "search_filters_date_option_none": "任何日期",
"Popular enabled: ": "已啟用人氣: "
} }

ファイルの表示

@ -6,7 +6,7 @@
interactive=true interactive=true
if [ "$1" == "--no-interactive" ]; then if [ "$1" = "--no-interactive" ]; then
interactive=false interactive=false
fi fi
@ -21,7 +21,7 @@ sudo systemctl enable postgresql.service
# Create databse and user # Create databse and user
# #
if [ "$interactive" == "true" ]; then if [ "$interactive" = "true" ]; then
sudo -u postgres -- createuser -P kemal sudo -u postgres -- createuser -P kemal
sudo -u postgres -- createdb -O kemal invidious sudo -u postgres -- createdb -O kemal invidious
else else

ファイルの表示

@ -74,7 +74,7 @@ install_apt() {
sudo apt-get install --yes --no-install-recommends \ sudo apt-get install --yes --no-install-recommends \
libssl-dev libxml2-dev libyaml-dev libgmp-dev libevent-dev \ libssl-dev libxml2-dev libyaml-dev libgmp-dev libevent-dev \
libpcre3-dev libreadline-dev libsqlite3-dev zlib1g-dev \ libpcre3-dev libreadline-dev libsqlite3-dev zlib1g-dev \
crystal postgres git librsvg2-bin make crystal postgresql-13 git librsvg2-bin make
} }
install_yum() { install_yum() {

ファイルの表示

@ -197,4 +197,46 @@ Spectator.describe Invidious::Search::Query do
) )
end end
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 end

ファイルの表示

@ -133,12 +133,13 @@ Invidious::Database.check_integrity(CONFIG)
# Running the script by itself would show some colorful feedback while this doesn't. # 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? # 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) %} {% if flag?(:minified_player_dependencies) %}
{% puts run("../scripts/fetch-player-dependencies.cr", "--minified").stringify %} {% puts run("../scripts/fetch-player-dependencies.cr", "--minified").stringify %}
{% else %} {% else %}
{% puts run("../scripts/fetch-player-dependencies.cr").stringify %} {% puts run("../scripts/fetch-player-dependencies.cr").stringify %}
{% end %} {% end %}
{% puts "\nDone checking player dependencies, now compiling Invidious...\n" %}
{% end %} {% end %}
# Start jobs # Start jobs

ファイルの表示

@ -31,7 +31,12 @@ def get_about_info(ucid, locale) : AboutChannel
end end
if initdata.dig?("alerts", 0, "alertRenderer", "type") == "ERROR" 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 end
if browse_endpoint = initdata["onResponseReceivedActions"]?.try &.[0]?.try &.["navigateAction"]?.try &.["endpoint"]?.try &.["browseEndpoint"]? 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? banner = banners.try &.[-1]?.try &.["url"].as_s?
description_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] 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 else
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
@ -74,13 +76,17 @@ def get_about_info(ucid, locale) : AboutChannel
# end # end
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
end
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map(&.as_s)
end allowed_regions = initdata
.dig?("microformat", "microformatDataRenderer", "availableCountries")
.try &.as_a.map(&.as_s) || [] of String
description = !description_node.nil? ? description_node.as_s : "" description = !description_node.nil? ? description_node.as_s : ""
description_html = HTML.escape(description) description_html = HTML.escape(description)
if !description_node.nil? if !description_node.nil?
if description_node.as_h?.nil? if description_node.as_h?.nil?
description_node = text_to_parsed_content(description_node.as_s) description_node = text_to_parsed_content(description_node.as_s)

ファイルの表示

@ -6,20 +6,18 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
end end
if response.status_code != 200 if response.status_code != 200
raise InfoException.new("This channel does not exist.") raise NotFoundException.new("This channel does not exist.")
end end
ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"] ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"]
if !continuation || continuation.empty? if !continuation || continuation.empty?
initial_data = extract_initial_data(response.body) 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 if !body
raise InfoException.new("Could not extract community tab.") raise InfoException.new("Could not extract community tab.")
end end
body = body["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]
else else
continuation = produce_channel_community_continuation(ucid, continuation) continuation = produce_channel_community_continuation(ucid, continuation)
@ -49,8 +47,12 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
error_message = (message["text"]["simpleText"]? || error_message = (message["text"]["simpleText"]? ||
message["text"]["runs"]?.try &.[0]?.try &.["text"]?) message["text"]["runs"]?.try &.[0]?.try &.["text"]?)
.try &.as_s || "" .try &.as_s || ""
if error_message == "This channel does not exist."
raise NotFoundException.new(error_message)
else
raise InfoException.new(error_message) raise InfoException.new(error_message)
end end
end
response = JSON.build do |json| response = JSON.build do |json|
json.object do json.object do

ファイルの表示

@ -95,7 +95,7 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b
contents = body["contents"]? contents = body["contents"]?
header = body["header"]? header = body["header"]?
else else
raise InfoException.new("Could not fetch comments") raise NotFoundException.new("Comments not found.")
end end
if !contents 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) thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink)
else else
raise InfoException.new("Could not fetch comments") raise NotFoundException.new("Comments not found.")
end end
client.close client.close

ファイルの表示

@ -18,3 +18,7 @@ class BrokenTubeException < Exception
return "Missing JSON element \"#{@element}\"" return "Missing JSON element \"#{@element}\""
end end
end end
# Exception threw when an element is not found.
class NotFoundException < InfoException
end

ファイルの表示

@ -317,7 +317,7 @@ def get_playlist(plid : String)
if playlist = Invidious::Database::Playlists.select(id: plid) if playlist = Invidious::Database::Playlists.select(id: plid)
return playlist return playlist
else else
raise InfoException.new("Playlist does not exist.") raise NotFoundException.new("Playlist does not exist.")
end end
else else
return fetch_playlist(plid) return fetch_playlist(plid)

ファイルの表示

@ -16,6 +16,8 @@ module Invidious::Routes::API::Manifest
video = get_video(id, region: region) video = get_video(id, region: region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
return env.redirect env.request.resource.gsub(id, ex.video_id) return env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex : NotFoundException
haltf env, status_code: 404
rescue ex rescue ex
haltf env, status_code: 403 haltf env, status_code: 403
end end
@ -46,7 +48,7 @@ module Invidious::Routes::API::Manifest
end end
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! 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| 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 } mime_streams = audio_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
next if mime_streams.empty? next if mime_streams.empty?
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true) do
mime_streams.each do |fmt| mime_streams.each do |fmt|
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415) # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange")) 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('"') codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i itag = fmt["itag"].as_i
url = fmt["url"].as_s 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("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
value: "2") value: "2")
@ -79,10 +87,9 @@ module Invidious::Routes::API::Manifest
end end
end end
end end
end
i += 1 i += 1
end end
end
potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144} potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144}

ファイルの表示

@ -237,6 +237,8 @@ module Invidious::Routes::API::V1::Authenticated
begin begin
video = get_video(video_id) video = get_video(video_id)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end

ファイルの表示

@ -13,6 +13,8 @@ module Invidious::Routes::API::V1::Channels
rescue ex : ChannelRedirect rescue ex : ChannelRedirect
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id})
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
@ -170,6 +172,8 @@ module Invidious::Routes::API::V1::Channels
rescue ex : ChannelRedirect rescue ex : ChannelRedirect
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id})
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
@ -205,6 +209,8 @@ module Invidious::Routes::API::V1::Channels
rescue ex : ChannelRedirect rescue ex : ChannelRedirect
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id}) return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id})
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end

ファイルの表示

@ -12,6 +12,8 @@ module Invidious::Routes::API::V1::Videos
rescue ex : VideoRedirect rescue ex : VideoRedirect
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id})
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
@ -42,6 +44,8 @@ module Invidious::Routes::API::V1::Videos
rescue ex : VideoRedirect rescue ex : VideoRedirect
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id})
rescue ex : NotFoundException
haltf env, 404
rescue ex rescue ex
haltf env, 500 haltf env, 500
end end
@ -167,6 +171,8 @@ module Invidious::Routes::API::V1::Videos
rescue ex : VideoRedirect rescue ex : VideoRedirect
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id) env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
return error_json(302, "Video is unavailable", {"videoId" => ex.video_id}) return error_json(302, "Video is unavailable", {"videoId" => ex.video_id})
rescue ex : NotFoundException
haltf env, 404
rescue ex rescue ex
haltf env, 500 haltf env, 500
end end
@ -324,6 +330,8 @@ module Invidious::Routes::API::V1::Videos
begin begin
comments = fetch_youtube_comments(id, continuation, format, locale, thin_mode, region, sort_by: sort_by) 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 rescue ex
return error_json(500, ex) return error_json(500, ex)
end end

ファイルの表示

@ -85,6 +85,9 @@ module Invidious::Routes::Channels
rescue ex : InfoException rescue ex : InfoException
env.response.status_code = 500 env.response.status_code = 500
error_message = ex.message error_message = ex.message
rescue ex : NotFoundException
env.response.status_code = 404
error_message = ex.message
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end 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}" : ""}") 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"] ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
rescue ex : InfoException | KeyError 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 end
selected_tab = env.request.path.split("/")[-1] selected_tab = env.request.path.split("/")[-1]
@ -141,7 +144,7 @@ module Invidious::Routes::Channels
user = env.params.query["user"]? user = env.params.query["user"]?
if !user if !user
raise InfoException.new("This channel does not exist.") return error_template(404, "This channel does not exist.")
else else
env.redirect "/user/#{user}#{uri_params}" env.redirect "/user/#{user}#{uri_params}"
end end
@ -197,6 +200,8 @@ module Invidious::Routes::Channels
channel = get_about_info(ucid, locale) channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect rescue ex : ChannelRedirect
return env.redirect env.request.resource.gsub(ucid, ex.channel_id) return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end

ファイルの表示

@ -7,6 +7,8 @@ module Invidious::Routes::Embed
playlist = get_playlist(plid) playlist = get_playlist(plid)
offset = env.params.query["index"]?.try &.to_i? || 0 offset = env.params.query["index"]?.try &.to_i? || 0
videos = get_playlist_videos(playlist, offset: offset) videos = get_playlist_videos(playlist, offset: offset)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end
@ -60,6 +62,8 @@ module Invidious::Routes::Embed
playlist = get_playlist(plid) playlist = get_playlist(plid)
offset = env.params.query["index"]?.try &.to_i? || 0 offset = env.params.query["index"]?.try &.to_i? || 0
videos = get_playlist_videos(playlist, offset: offset) videos = get_playlist_videos(playlist, offset: offset)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end
@ -119,6 +123,8 @@ module Invidious::Routes::Embed
video = get_video(id, region: params.region) video = get_video(id, region: params.region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
return env.redirect env.request.resource.gsub(id, ex.video_id) return env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end

ファイルの表示

@ -150,6 +150,8 @@ module Invidious::Routes::Feeds
channel = get_about_info(ucid, locale) channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect rescue ex : ChannelRedirect
return env.redirect env.request.resource.gsub(ucid, ex.channel_id) return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex : NotFoundException
return error_atom(404, ex)
rescue ex rescue ex
return error_atom(500, ex) return error_atom(500, ex)
end end

ファイルの表示

@ -66,7 +66,13 @@ module Invidious::Routes::Playlists
user = user.as(User) user = user.as(User)
playlist_id = env.params.query["list"] playlist_id = env.params.query["list"]
begin
playlist = get_playlist(playlist_id) 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) subscribe_playlist(user, playlist)
env.redirect "/playlist?list=#{playlist.id}" env.redirect "/playlist?list=#{playlist.id}"
@ -304,6 +310,8 @@ module Invidious::Routes::Playlists
playlist_id = env.params.query["playlist_id"] playlist_id = env.params.query["playlist_id"]
playlist = get_playlist(playlist_id).as(InvidiousPlaylist) playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
raise "Invalid user" if playlist.author != user.email raise "Invalid user" if playlist.author != user.email
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
if redirect if redirect
return error_template(400, ex) return error_template(400, ex)
@ -334,6 +342,8 @@ module Invidious::Routes::Playlists
begin begin
video = get_video(video_id) video = get_video(video_id)
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
if redirect if redirect
return error_template(500, ex) return error_template(500, ex)
@ -394,6 +404,8 @@ module Invidious::Routes::Playlists
begin begin
playlist = get_playlist(plid) playlist = get_playlist(plid)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex rescue ex
return error_template(500, ex) return error_template(500, ex)
end end

ファイルの表示

@ -59,6 +59,12 @@ module Invidious::Routes::Search
return error_template(500, ex) return error_template(500, ex)
end 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 env.set "search", query.text
templated "search" templated "search"
end end

ファイルの表示

@ -265,7 +265,13 @@ module Invidious::Routes::VideoPlayback
return error_template(403, "Administrator has disabled this endpoint.") return error_template(403, "Administrator has disabled this endpoint.")
end end
begin
video = get_video(id, region: region) 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 } 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 url = fmt.try &.["url"]?.try &.as_s

ファイルの表示

@ -63,6 +63,9 @@ module Invidious::Routes::Watch
video = get_video(id, region: params.region) video = get_video(id, region: params.region)
rescue ex : VideoRedirect rescue ex : VideoRedirect
return env.redirect env.request.resource.gsub(id, ex.video_id) 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 rescue ex
LOGGER.error("get_video: #{id} : #{ex.message}") LOGGER.error("get_video: #{id} : #{ex.message}")
return error_template(500, ex) return error_template(500, ex)

ファイルの表示

@ -57,7 +57,7 @@ module Invidious::Search
# Get the page number (also common to all search types) # Get the page number (also common to all search types)
@page = params["page"]?.try &.to_i? || 1 @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? # NOTE: maybe raise in the future?
return if self.empty_raw_query? return if self.empty_raw_query?
@ -127,6 +127,16 @@ module Invidious::Search
return items return items
end 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 # TODO: clean code
private def unnest_items(all_items) : Array(SearchItem) private def unnest_items(all_items) : Array(SearchItem)
items = [] of SearchItem items = [] of SearchItem

ファイルの表示

@ -323,7 +323,7 @@ struct Video
json.field "viewCount", self.views json.field "viewCount", self.views
json.field "likeCount", self.likes json.field "likeCount", self.likes
json.field "dislikeCount", self.dislikes json.field "dislikeCount", 0_i64
json.field "paid", self.paid json.field "paid", self.paid
json.field "premium", self.premium json.field "premium", self.premium
@ -354,7 +354,7 @@ struct Video
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", self.length_seconds
json.field "allowRatings", self.allow_ratings 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 "isListed", self.is_listed
json.field "liveNow", self.live_now json.field "liveNow", self.live_now
json.field "isUpcoming", self.is_upcoming json.field "isUpcoming", self.is_upcoming
@ -556,11 +556,6 @@ struct Video
info["dislikes"]?.try &.as_i64 || 0_i64 info["dislikes"]?.try &.as_i64 || 0_i64
end 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 def published : Time
info info
.dig?("microformat", "playerMicroformatRenderer", "publishDate") .dig?("microformat", "playerMicroformatRenderer", "publishDate")
@ -813,14 +808,6 @@ struct Video
return info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s return info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s
end 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? def reason : String?
info["reason"]?.try &.as_s info["reason"]?.try &.as_s
end end
@ -908,14 +895,21 @@ 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) 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") subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
reason = subreason.try &.[]?("simpleText").try &.as_s reason = subreason.try &.[]?("simpleText").try &.as_s
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
reason ||= player_response.dig("playabilityStatus", "reason").as_s reason ||= player_response.dig("playabilityStatus", "reason").as_s
params["reason"] = JSON::Any.new(reason) params["reason"] = JSON::Any.new(reason)
# Stop here if video is not a scheduled livestream
if playability_status != "LIVE_STREAM_OFFLINE"
return params return params
end end
end
params["shortDescription"] = player_response.dig?("videoDetails", "shortDescription") || JSON::Any.new(nil) 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) params["relatedVideos"] = JSON::Any.new(related)
# Likes/dislikes # Likes
toplevel_buttons = video_primary_renderer toplevel_buttons = video_primary_renderer
.try &.dig?("videoActions", "menuRenderer", "topLevelButtons") .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.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"")
LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes
end 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 end
params["likes"] = JSON::Any.new(likes || 0_i64) params["likes"] = JSON::Any.new(likes || 0_i64)
params["dislikes"] = JSON::Any.new(dislikes || 0_i64) params["dislikes"] = JSON::Any.new(0_i64)
# Description # Description
@ -1158,8 +1132,12 @@ def fetch_video(id, region)
end end
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable"
raise NotFoundException.new(reason.as_s || "")
else
raise InfoException.new(reason.as_s || "") raise InfoException.new(reason.as_s || "")
end end
end
video = Video.new({ video = Video.new({
id: id, id: id,

ファイルの表示

@ -5,7 +5,7 @@
<a href="/channel/<%= item.ucid %>"> <a href="/channel/<%= item.ucid %>">
<% if !env.get("preferences").as(Preferences).thin_mode %> <% if !env.get("preferences").as(Preferences).thin_mode %>
<center> <center>
<img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/> <img loading="lazy" tabindex="-1" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
</center> </center>
<% end %> <% end %>
<p dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></p> <p dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></p>
@ -23,7 +23,7 @@
<a style="width:100%" href="<%= url %>"> <a style="width:100%" href="<%= url %>">
<% if !env.get("preferences").as(Preferences).thin_mode %> <% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail"> <div class="thumbnail">
<img loading="lazy" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>"/> <img loading="lazy" tabindex="-1" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>"/>
<p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> <p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p>
</div> </div>
<% end %> <% end %>
@ -36,7 +36,7 @@
<a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>"> <a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>">
<% if !env.get("preferences").as(Preferences).thin_mode %> <% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail"> <div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/> <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if item.length_seconds != 0 %> <% if item.length_seconds != 0 %>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p> <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %> <% end %>
@ -51,16 +51,13 @@
<a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>"> <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>">
<% if !env.get("preferences").as(Preferences).thin_mode %> <% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail"> <div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/> <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if plid_form = env.get?("remove_playlist_items") %> <% if plid_form = env.get?("remove_playlist_items") %>
<form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched"> <p class="watched">
<a data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>" href="javascript:void(0)"> <button type="submit" style="all:unset" data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>"><i class="icon ion-md-trash"></i></button>
<button type="submit" style="all:unset">
<i class="icon ion-md-trash"></i>
</button>
</a>
</p> </p>
</form> </form>
<% end %> <% end %>
@ -103,29 +100,21 @@
<a style="width:100%" href="/watch?v=<%= item.id %>"> <a style="width:100%" href="/watch?v=<%= item.id %>">
<% if !env.get("preferences").as(Preferences).thin_mode %> <% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail"> <div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/> <img loading="lazy" tabindex="-1" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if env.get? "show_watched" %> <% if env.get? "show_watched" %>
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched"> <p class="watched">
<a data-onclick="mark_watched" data-id="<%= item.id %>" href="javascript:void(0)"> <button type="submit" style="all:unset" data-onclick="mark_watched" data-id="<%= item.id %>">
<button type="submit" style="all:unset"> <i data-mouse="switch_classes" data-switch-classes="ion-ios-eye-off,ion-ios-eye" class="icon ion-ios-eye"></i>
<i data-mouse="switch_classes" data-switch-classes="ion-ios-eye-off,ion-ios-eye"
class="icon ion-ios-eye">
</i>
</button> </button>
</a>
</p> </p>
</form> </form>
<% elsif plid_form = env.get? "add_playlist_items" %> <% elsif plid_form = env.get? "add_playlist_items" %>
<form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched"> <p class="watched">
<a data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>" href="javascript:void(0)"> <button type="submit" style="all:unset" data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button>
<button type="submit" style="all:unset">
<i class="icon ion-md-add"></i>
</button>
</a>
</p> </p>
</form> </form>
<% end %> <% end %>

ファイルの表示

@ -7,14 +7,25 @@
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream"> <source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
<% else %> <% else %>
<% if params.listen %> <% 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 = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local src_url += "&local=true" if params.local
bitrate = fmt["bitrate"] bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s) mimetype = HTML.escape(fmt["mimeType"].as_s)
selected = i == 0 ? true : false selected = (i == best_m4a_stream_index)
%> %>
<source src="<%= src_url %>" type='<%= mimetype %>' label="<%= bitrate %>k" selected="<%= selected %>"> <source src="<%= src_url %>" type='<%= mimetype %>' label="<%= bitrate %>k" selected="<%= selected %>">
<% if !params.local && !CONFIG.disabled?("local") %> <% if !params.local && !CONFIG.disabled?("local") %>

ファイルの表示

@ -11,6 +11,7 @@
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/embed.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/embed.css?v=<%= ASSET_COMMIT %>">
<title><%= HTML.escape(video.title) %> - Invidious</title> <title><%= HTML.escape(video.title) %> - Invidious</title>
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head> </head>
<body class="dark-theme"> <body class="dark-theme">

ファイルの表示

@ -38,9 +38,7 @@
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched"> <p class="watched">
<a data-onclick="mark_unwatched" data-id="<%= item %>" href="javascript:void(0)"> <button type="submit" style="all:unset" data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button>
<button type="submit" style="all:unset"><i class="icon ion-md-trash"></i></button>
</a>
</p> </p>
</form> </form>
</div> </div>

ファイルの表示

@ -3,16 +3,6 @@
<link rel="stylesheet" href="/css/search.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/search.css?v=<%= ASSET_COMMIT %>">
<% end %> <% 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)
-%>
<!-- Search redirection and filtering UI --> <!-- Search redirection and filtering UI -->
<%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %>
<hr/> <hr/>

ファイルの表示

@ -69,7 +69,7 @@
</div> </div>
<% if env.get("preferences").as(Preferences).show_nick %> <% if env.get("preferences").as(Preferences).show_nick %>
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<span id="user_name"><%= env.get("user").as(Invidious::User).email %></span> <span id="user_name"><%= HTML.escape(env.get("user").as(Invidious::User).email) %></span>
</div> </div>
<% end %> <% end %>
<div class="pure-u-1-4"> <div class="pure-u-1-4">

ファイルの表示

@ -39,9 +39,7 @@
<h3 style="padding-right:0.5em"> <h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<a data-onclick="remove_subscription" data-ucid="<%= channel.id %>" href="#"> <input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= translate(locale, "unsubscribe") %>">
<input style="all:unset" type="submit" value="<%= translate(locale, "unsubscribe") %>">
</a>
</form> </form>
</h3> </h3>
</div> </div>

ファイルの表示

@ -31,9 +31,7 @@
<h3 style="padding-right:0.5em"> <h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post"> <form data-onsubmit="return_false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>"> <input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<a data-onclick="revoke_token" data-session="<%= token[:session] %>" href="#"> <input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= translate(locale, "revoke") %>">
<input style="all:unset" type="submit" value="<%= translate(locale, "revoke") %>">
</a>
</form> </form>
</h3> </h3>
</div> </div>

ファイルの表示

@ -173,7 +173,7 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p> <p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p id="dislikes"></p> <p id="dislikes" style="display: none; visibility: hidden;"></p>
<p id="genre"><%= translate(locale, "Genre: ") %> <p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %> <% if !video.genre_url %>
<%= video.genre %> <%= video.genre %>
@ -185,9 +185,9 @@ we're going to need to do it here in order to allow for translations.
<p id="license"><%= translate(locale, "License: ") %><%= video.license %></p> <p id="license"><%= translate(locale, "License: ") %><%= video.license %></p>
<% end %> <% end %>
<p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p> <p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p>
<p id="wilson"><%= translate(locale, "Wilson score: ") %><%= video.wilson_score %></p> <p id="wilson" style="display: none; visibility: hidden;"></p>
<p id="rating"></p> <p id="rating" style="display: none; visibility: hidden;"></p>
<p id="engagement"><%= translate(locale, "Engagement: ") %><%= video.engagement %>%</p> <p id="engagement" style="display: none; visibility: hidden;"></p>
<% if video.allowed_regions.size != REGIONS.size %> <% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions"> <p id="allowed_regions">
<% if video.allowed_regions.size < REGIONS.size // 2 %> <% if video.allowed_regions.size < REGIONS.size // 2 %>

ファイルの表示

@ -417,7 +417,7 @@ private module Extractors
# {"tabRenderer": { # {"tabRenderer": {
# "endpoint": {...} # "endpoint": {...}
# "title": "Playlists", # "title": "Playlists",
# "selected": true, # "selected": true, # Is nil unless tab is selected
# "content": {...}, # "content": {...},
# ... # ...
# }} # }}

ファイルの表示

@ -84,7 +84,7 @@ end
def extract_selected_tab(tabs) def extract_selected_tab(tabs)
# Extract the selected tab from the array of tabs Youtube returns # 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 end
def fetch_continuation_token(items : Array(JSON::Any)) def fetch_continuation_token(items : Array(JSON::Any))