Merge branch 'master' of github.com:iv-org/invidious
このコミットが含まれているのは:
コミット
fd23352bf2
|
@ -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 () {
|
||||||
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';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (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å <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"]?
|
||||||
|
|
||||||
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
|
|
||||||
allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map(&.as_s)
|
|
||||||
end
|
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 = !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,7 +47,11 @@ 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 || ""
|
||||||
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
|
end
|
||||||
|
|
||||||
response = JSON.build do |json|
|
response = JSON.build do |json|
|
||||||
|
|
|
@ -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,9 +87,8 @@ module Invidious::Routes::API::Manifest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
i += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
i += 1
|
|
||||||
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"]
|
||||||
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)
|
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
|
||||||
|
|
||||||
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 }
|
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,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)
|
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)
|
||||||
return params
|
|
||||||
|
# Stop here if video is not a scheduled livestream
|
||||||
|
if playability_status != "LIVE_STREAM_OFFLINE"
|
||||||
|
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,7 +1132,11 @@ def fetch_video(id, region)
|
||||||
end
|
end
|
||||||
|
|
||||||
if reason = info["reason"]?
|
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
|
end
|
||||||
|
|
||||||
video = Video.new({
|
video = Video.new({
|
||||||
|
|
|
@ -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 %> <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 %> <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"
|
</button>
|
||||||
class="icon ion-ios-eye">
|
|
||||||
</i>
|
|
||||||
</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))
|
||||||
|
|
読み込み中…
新しいイシューから参照