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

このコミットが含まれているのは:
守矢諏訪子 2022-08-26 14:48:25 +09:00
コミット 77f3b88873
26個のファイルの変更919行の追加515行の削除

ファイルの表示

@ -41,6 +41,7 @@ jobs:
- 1.2.2 - 1.2.2
- 1.3.2 - 1.3.2
- 1.4.0 - 1.4.0
- 1.5.0
include: include:
- crystal: nightly - crystal: nightly
stable: false stable: false

ファイルの表示

@ -27,7 +27,7 @@ jobs:
- name: Install Crystal - name: Install Crystal
uses: crystal-lang/install-crystal@v1.6.0 uses: crystal-lang/install-crystal@v1.6.0
with: with:
crystal: 1.2.2 crystal: 1.5.0
- name: Run lint - name: Run lint
run: | run: |

ファイルの表示

@ -20,7 +20,7 @@
<a href="https://hosted.weblate.org/engage/invidious/"> <a href="https://hosted.weblate.org/engage/invidious/">
<img alt="Translation Status" src="https://hosted.weblate.org/widgets/invidious/-/translations/svg-badge.svg"> <img alt="Translation Status" src="https://hosted.weblate.org/widgets/invidious/-/translations/svg-badge.svg">
</a> </a>
<a href="https://github.com/humanetech-community/awesome-humane-tech"> <a href="https://github.com/humanetech-community/awesome-humane-tech">
<img alt="Awesome Humane Tech" src="https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true"> <img alt="Awesome Humane Tech" src="https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true">
</a> </a>
@ -28,17 +28,17 @@
<h3>An open source alternative front-end to YouTube</h3> <h3>An open source alternative front-end to YouTube</h3>
<a href="https://invidious.io/">Website</a> <a href="https://invidious.io/">Website</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a href="https://instances.invidious.io/">Instances list</a> <a href="https://instances.invidious.io/">Instances list</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a href="https://docs.invidious.io/faq/">FAQ</a> <a href="https://docs.invidious.io/faq/">FAQ</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a href="https://docs.invidious.io/">Documentation</a> <a href="https://docs.invidious.io/">Documentation</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a href="#contribute">Contribute</a> <a href="#contribute">Contribute</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a href="https://invidious.io/donate/">Donate</a> <a href="https://invidious.io/donate/">Donate</a>
<h5>Chat with us:</h5> <h5>Chat with us:</h5>
<a href="https://matrix.to/#/#invidious:matrix.org"> <a href="https://matrix.to/#/#invidious:matrix.org">
<img alt="Matrix" src="https://img.shields.io/matrix/invidious:matrix.org?label=Matrix&color=darkgreen"> <img alt="Matrix" src="https://img.shields.io/matrix/invidious:matrix.org?label=Matrix&color=darkgreen">
@ -153,6 +153,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
- [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch. - [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch.
- [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV. - [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV.
- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. - [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client.
- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API)
## Liability ## Liability

ファイルの表示

@ -259,7 +259,7 @@ function updateCookie(newVolume, newSpeed) {
// Set expiration in 2 year // Set expiration in 2 year
var date = new Date(); var date = new Date();
date.setTime(date.getTime() + 63115200); date.setFullYear(date.getFullYear() + 2);
var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/; var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/;
var domainUsed = location.hostname; var domainUsed = location.hostname;
@ -268,8 +268,10 @@ function updateCookie(newVolume, newSpeed) {
if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost') if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost')
domainUsed = '.' + location.hostname; domainUsed = '.' + location.hostname;
document.cookie = 'PREFS=' + cookieData + '; SameSite=Strict; path=/; domain=' + var secure = location.protocol.startsWith("https") ? " Secure;" : "";
domainUsed + '; expires=' + date.toGMTString() + ';';
document.cookie = 'PREFS=' + cookieData + '; SameSite=Lax; path=/; domain=' +
domainUsed + '; expires=' + date.toGMTString() + ';' + secure;
video_data.params.volume = volumeValue; video_data.params.volume = volumeValue;
video_data.params.speed = speedValue; video_data.params.speed = speedValue;

ファイルの表示

@ -349,13 +349,16 @@ feed_threads: 1
## Enable/Disable the polling job that keeps the decryption ## Enable/Disable the polling job that keeps the decryption
## function (for "secured" videos) up to date. ## function (for "secured" videos) up to date.
## ##
## Note: This part of the code is currently broken, so changing ## Note: This part of the code generate a small amount of data every minute.
## This may not be desired if you have bandwidth limits set by your ISP.
##
## Note 2: This part of the code is currently broken, so changing
## this setting has no impact. ## this setting has no impact.
## ##
## Accepted values: true, false ## Accepted values: true, false
## Default: true ## Default: false
## ##
#decrypt_polling: true #decrypt_polling: false
# ----------------------------- # -----------------------------

ファイルの表示

@ -368,7 +368,7 @@
"footer_donate_page": "تبرّع", "footer_donate_page": "تبرّع",
"preferences_region_label": "بلد المحتوى: ", "preferences_region_label": "بلد المحتوى: ",
"preferences_quality_dash_label": "جودة فيديو DASH المفضلة: ", "preferences_quality_dash_label": "جودة فيديو DASH المفضلة: ",
"preferences_quality_option_dash": "DASH (جودة تكييفية)", "preferences_quality_option_dash": "DASH (الجودة التلقائية)",
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "متوسطة", "preferences_quality_option_medium": "متوسطة",
"preferences_quality_option_small": "صغيرة", "preferences_quality_option_small": "صغيرة",
@ -459,5 +459,81 @@
"Spanish (Spain)": "الإسبانية (إسبانيا)", "Spanish (Spain)": "الإسبانية (إسبانيا)",
"crash_page_search_issue": "بحثت عن <a href=\"`x`\"> المشكلات الموجودة على GitHub </a>", "crash_page_search_issue": "بحثت عن <a href=\"`x`\"> المشكلات الموجودة على GitHub </a>",
"search_filters_title": "معامل الفرز", "search_filters_title": "معامل الفرز",
"search_message_no_results": "لا توجد نتائج." "search_message_no_results": "لا توجد نتائج.",
"search_message_change_filters_or_query": "حاول توسيع استعلام البحث و / أو تغيير عوامل التصفية.",
"search_filters_date_label": "تاريخ الرفع",
"generic_count_weeks_0": "{{count}} أسبوع",
"generic_count_weeks_1": "{{count}} أسبوع",
"generic_count_weeks_2": "{{count}} أسبوع",
"generic_count_weeks_3": "{{count}} أسبوع",
"generic_count_weeks_4": "{{count}} أسابيع",
"generic_count_weeks_5": "{{count}} أسبوع",
"Popular enabled: ": "تم تمكين الشعبية: ",
"search_filters_duration_option_medium": "متوسط (4-20 دقيقة)",
"search_filters_date_option_none": "أي تاريخ",
"search_filters_type_option_all": "أي نوع",
"search_filters_features_option_vr180": "VR180",
"generic_count_minutes_0": "{{count}} دقيقة",
"generic_count_minutes_1": "{{count}} دقيقة",
"generic_count_minutes_2": "{{count}} دقيقة",
"generic_count_minutes_3": "{{count}} دقيقة",
"generic_count_minutes_4": "{{count}} دقائق",
"generic_count_minutes_5": "{{count}} دقيقة",
"generic_count_hours_0": "{{count}} ساعة",
"generic_count_hours_1": "{{count}} ساعة",
"generic_count_hours_2": "{{count}} ساعة",
"generic_count_hours_3": "{{count}} ساعة",
"generic_count_hours_4": "{{count}} ساعات",
"generic_count_hours_5": "{{count}} ساعة",
"comments_view_x_replies_0": "عرض رد {{count}}",
"comments_view_x_replies_1": "عرض رد {{count}}",
"comments_view_x_replies_2": "عرض رد {{count}}",
"comments_view_x_replies_3": "عرض رد {{count}}",
"comments_view_x_replies_4": "عرض الردود {{count}}",
"comments_view_x_replies_5": "عرض رد {{count}}",
"search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
"comments_points_count_0": "{{count}} نقطة",
"comments_points_count_1": "{{count}} نقطة",
"comments_points_count_2": "{{count}} نقطة",
"comments_points_count_3": "{{count}} نقطة",
"comments_points_count_4": "{{count}} نقاط",
"comments_points_count_5": "{{count}} نقطة",
"generic_count_years_0": "{{count}} السنة",
"generic_count_years_1": "{{count}} السنة",
"generic_count_years_2": "{{count}} السنة",
"generic_count_years_3": "{{count}} السنة",
"generic_count_years_4": "{{count}} سنوات",
"generic_count_years_5": "{{count}} السنة",
"tokens_count_0": "الرمز المميز {{count}}",
"tokens_count_1": "الرمز المميز {{count}}",
"tokens_count_2": "الرمز المميز {{count}}",
"tokens_count_3": "الرمز المميز {{count}}",
"tokens_count_4": "الرموز المميزة {{count}}",
"tokens_count_5": "الرمز المميز {{count}}",
"search_filters_apply_button": "تطبيق الفلاتر المحددة",
"search_filters_duration_option_none": "أي مدة",
"subscriptions_unseen_notifs_count_0": "{{count}} إشعار غير مرئي",
"subscriptions_unseen_notifs_count_1": "{{count}} إشعار غير مرئي",
"subscriptions_unseen_notifs_count_2": "{{count}} إشعار غير مرئي",
"subscriptions_unseen_notifs_count_3": "{{count}} إشعار غير مرئي",
"subscriptions_unseen_notifs_count_4": "{{count}} إشعارات غير مرئية",
"subscriptions_unseen_notifs_count_5": "{{count}} إشعار غير مرئي",
"generic_count_days_0": "{{count}} يوم",
"generic_count_days_1": "{{count}} يوم",
"generic_count_days_2": "{{count}} يوم",
"generic_count_days_3": "{{count}} يوم",
"generic_count_days_4": "{{count}} أيام",
"generic_count_days_5": "{{count}} يوم",
"generic_count_months_0": "{{count}} شهر",
"generic_count_months_1": "{{count}} شهر",
"generic_count_months_2": "{{count}} شهر",
"generic_count_months_3": "{{count}} شهر",
"generic_count_months_4": "{{count}} شهور",
"generic_count_months_5": "{{count}} شهر",
"generic_count_seconds_0": "{{count}} ثانية",
"generic_count_seconds_1": "{{count}} ثانية",
"generic_count_seconds_2": "{{count}} ثانية",
"generic_count_seconds_3": "{{count}} ثانية",
"generic_count_seconds_4": "{{count}} ثوانٍ",
"generic_count_seconds_5": "{{count}} ثانية"
} }

ファイルの表示

@ -367,7 +367,7 @@
"adminprefs_modified_source_code_url_label": "URL zum Repositorie des modifizierten Quellcodes", "adminprefs_modified_source_code_url_label": "URL zum Repositorie des modifizierten Quellcodes",
"search_filters_duration_option_short": "Kurz (< 4 Minuten)", "search_filters_duration_option_short": "Kurz (< 4 Minuten)",
"preferences_region_label": "Land der Inhalte: ", "preferences_region_label": "Land der Inhalte: ",
"preferences_quality_option_dash": "DASH (automatische Qualität)", "preferences_quality_option_dash": "DASH (adaptive Qualität)",
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "Mittel", "preferences_quality_option_medium": "Mittel",
"preferences_quality_option_small": "Niedrig", "preferences_quality_option_small": "Niedrig",
@ -460,5 +460,16 @@
"Chinese (Taiwan)": "Chinesisch (Taiwan)", "Chinese (Taiwan)": "Chinesisch (Taiwan)",
"Korean (auto-generated)": "Koreanisch (automatisch generiert)", "Korean (auto-generated)": "Koreanisch (automatisch generiert)",
"Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)", "Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
"search_filters_title": "Filtern" "search_filters_title": "Filtern",
"search_message_change_filters_or_query": "Versuchen Sie, Ihre Suchanfrage zu erweitern und/oder die Filter zu ändern.",
"search_message_use_another_instance": " Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
"Popular enabled: ": "„Beliebt“-Seite aktiviert: ",
"search_message_no_results": "Keine Ergebnisse gefunden.",
"search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)",
"search_filters_features_option_vr180": "VR180",
"search_filters_type_option_all": "Beliebiger Typ",
"search_filters_apply_button": "Ausgewählte Filter anwenden",
"search_filters_duration_option_none": "Beliebige Länge",
"search_filters_date_label": "Upload-Datum",
"search_filters_date_option_none": "Beliebiges Datum"
} }

ファイルの表示

@ -449,5 +449,6 @@
"videoinfo_invidious_embed_link": "Σύνδεσμος Ενσωμάτωσης", "videoinfo_invidious_embed_link": "Σύνδεσμος Ενσωμάτωσης",
"search_filters_type_option_show": "Μπάρα προόδου διαβάσματος", "search_filters_type_option_show": "Μπάρα προόδου διαβάσματος",
"preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ", "preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ",
"search_filters_title": "Φίλτρο" "search_filters_title": "Φίλτρο",
"search_message_no_results": "Δεν"
} }

ファイルの表示

@ -126,7 +126,7 @@
"revoke": "cabut", "revoke": "cabut",
"Subscriptions": "Langganan", "Subscriptions": "Langganan",
"subscriptions_unseen_notifs_count_0": "{{count}} pemberitahuan belum dilihat", "subscriptions_unseen_notifs_count_0": "{{count}} pemberitahuan belum dilihat",
"search": "cari", "search": "Telusuri",
"Log out": "Keluar", "Log out": "Keluar",
"Released under the AGPLv3 on Github.": "Dirilis di bawah AGPLv3 di GitHub.", "Released under the AGPLv3 on Github.": "Dirilis di bawah AGPLv3 di GitHub.",
"Source available here.": "Sumber tersedia di sini.", "Source available here.": "Sumber tersedia di sini.",
@ -447,5 +447,6 @@
"Dutch (auto-generated)": "Belanda (dihasilkan secara otomatis)", "Dutch (auto-generated)": "Belanda (dihasilkan secara otomatis)",
"search_filters_date_option_none": "Tanggal berapa pun", "search_filters_date_option_none": "Tanggal berapa pun",
"search_filters_duration_option_none": "Durasi berapa pun", "search_filters_duration_option_none": "Durasi berapa pun",
"search_filters_duration_option_medium": "Sedang (4 - 20 menit)" "search_filters_duration_option_medium": "Sedang (4 - 20 menit)",
"Cantonese (Hong Kong)": "Bahasa Kanton (Hong Kong)"
} }

ファイルの表示

@ -14,7 +14,7 @@
"newest": "più recente", "newest": "più recente",
"oldest": "più vecchio", "oldest": "più vecchio",
"popular": "Tendenze", "popular": "Tendenze",
"last": "durare", "last": "ultimo",
"Next page": "Pagina successiva", "Next page": "Pagina successiva",
"Previous page": "Pagina precedente", "Previous page": "Pagina precedente",
"Clear watch history?": "Eliminare la cronologia dei video guardati?", "Clear watch history?": "Eliminare la cronologia dei video guardati?",
@ -158,7 +158,7 @@
"generic_views_count_plural": "{{count}} visualizzazioni", "generic_views_count_plural": "{{count}} visualizzazioni",
"Premieres in `x`": "In anteprima in `x`", "Premieres in `x`": "In anteprima in `x`",
"Premieres `x`": "In anteprima `x`", "Premieres `x`": "In anteprima `x`",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao! Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti. Considera che potrebbe volerci più tempo.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.",
"View YouTube comments": "Visualizza i commenti da YouTube", "View YouTube comments": "Visualizza i commenti da YouTube",
"View more comments on Reddit": "Visualizza più commenti su Reddit", "View more comments on Reddit": "Visualizza più commenti su Reddit",
"View `x` comments": { "View `x` comments": {
@ -212,7 +212,7 @@
"Azerbaijani": "Azero", "Azerbaijani": "Azero",
"Bangla": "Bengalese", "Bangla": "Bengalese",
"Basque": "Basco", "Basque": "Basco",
"Belarusian": "Biellorusso", "Belarusian": "Bielorusso",
"Bosnian": "Bosniaco", "Bosnian": "Bosniaco",
"Bulgarian": "Bulgaro", "Bulgarian": "Bulgaro",
"Burmese": "Birmano", "Burmese": "Birmano",
@ -238,10 +238,10 @@
"Haitian Creole": "Creolo haitiano", "Haitian Creole": "Creolo haitiano",
"Hausa": "Lingua hausa", "Hausa": "Lingua hausa",
"Hawaiian": "Hawaiano", "Hawaiian": "Hawaiano",
"Hebrew": "Ebreo", "Hebrew": "Ebraico",
"Hindi": "Hindi", "Hindi": "Hindi",
"Hmong": "Hmong", "Hmong": "Hmong",
"Hungarian": "Ungarese", "Hungarian": "Ungherese",
"Icelandic": "Islandese", "Icelandic": "Islandese",
"Igbo": "Igbo", "Igbo": "Igbo",
"Indonesian": "Indonesiano", "Indonesian": "Indonesiano",
@ -254,7 +254,7 @@
"Khmer": "Khmer", "Khmer": "Khmer",
"Korean": "Coreano", "Korean": "Coreano",
"Kurdish": "Curdo", "Kurdish": "Curdo",
"Kyrgyz": "Kirghize", "Kyrgyz": "Kirghiso",
"Lao": "Lao", "Lao": "Lao",
"Latin": "Latino", "Latin": "Latino",
"Latvian": "Lettone", "Latvian": "Lettone",
@ -269,7 +269,7 @@
"Marathi": "Marathi", "Marathi": "Marathi",
"Mongolian": "Mongolo", "Mongolian": "Mongolo",
"Nepali": "Nepalese", "Nepali": "Nepalese",
"Norwegian Bokmål": "Norvegese", "Norwegian Bokmål": "Norvegese bokmål",
"Nyanja": "Nyanja", "Nyanja": "Nyanja",
"Pashto": "Pashtu", "Pashto": "Pashtu",
"Persian": "Persiano", "Persian": "Persiano",
@ -278,7 +278,7 @@
"Punjabi": "Punjabi", "Punjabi": "Punjabi",
"Romanian": "Rumeno", "Romanian": "Rumeno",
"Russian": "Russo", "Russian": "Russo",
"Samoan": "Samoan", "Samoan": "Samoano",
"Scottish Gaelic": "Gaelico scozzese", "Scottish Gaelic": "Gaelico scozzese",
"Serbian": "Serbo", "Serbian": "Serbo",
"Shona": "Shona", "Shona": "Shona",
@ -293,15 +293,15 @@
"Sundanese": "Sudanese", "Sundanese": "Sudanese",
"Swahili": "Swahili", "Swahili": "Swahili",
"Swedish": "Svedese", "Swedish": "Svedese",
"Tajik": "Tajik", "Tajik": "Tagico",
"Tamil": "Tamil", "Tamil": "Tamil",
"Telugu": "Telugu", "Telugu": "Telugu",
"Thai": "Thaï", "Thai": "Thailandese",
"Turkish": "Turco", "Turkish": "Turco",
"Ukrainian": "Ucraino", "Ukrainian": "Ucraino",
"Urdu": "Urdu", "Urdu": "Urdu",
"Uzbek": "Uzbeco", "Uzbek": "Uzbeco",
"Vietnamese": "Vietnamese", "Vietnamese": "Vietnamita",
"Welsh": "Gallese", "Welsh": "Gallese",
"Western Frisian": "Frisone occidentale", "Western Frisian": "Frisone occidentale",
"Xhosa": "Xhosa", "Xhosa": "Xhosa",
@ -364,7 +364,7 @@
"search_filters_type_option_channel": "Canale", "search_filters_type_option_channel": "Canale",
"search_filters_type_option_playlist": "Playlist", "search_filters_type_option_playlist": "Playlist",
"search_filters_type_option_movie": "Film", "search_filters_type_option_movie": "Film",
"search_filters_features_option_hd": "AD", "search_filters_features_option_hd": "HD",
"search_filters_features_option_subtitles": "Sottotitoli / CC", "search_filters_features_option_subtitles": "Sottotitoli / CC",
"search_filters_features_option_c_commons": "Creative Commons", "search_filters_features_option_c_commons": "Creative Commons",
"search_filters_features_option_three_d": "3D", "search_filters_features_option_three_d": "3D",
@ -383,7 +383,7 @@
"preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_4320p": "4320p",
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"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.": "Pubblicato su GitHub con licenza AGPLv3.",
"preferences_quality_option_medium": "Media", "preferences_quality_option_medium": "Media",
"preferences_quality_option_small": "Limitata", "preferences_quality_option_small": "Limitata",
"preferences_quality_dash_option_best": "Migliore", "preferences_quality_dash_option_best": "Migliore",
@ -430,7 +430,7 @@
"comments_view_x_replies_plural": "Vedi {{count}} risposte", "comments_view_x_replies_plural": "Vedi {{count}} risposte",
"comments_points_count": "{{count}} punto", "comments_points_count": "{{count}} punto",
"comments_points_count_plural": "{{count}} punti", "comments_points_count_plural": "{{count}} punti",
"Portuguese (auto-generated)": "Portoghese (auto-generato)", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)",
"crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "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_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_before_reporting": "Prima di segnalare un bug, assicurati di aver:",
@ -441,7 +441,7 @@
"English (United Kingdom)": "Inglese (Regno Unito)", "English (United Kingdom)": "Inglese (Regno Unito)",
"Portuguese (Brazil)": "Portoghese (Brasile)", "Portuguese (Brazil)": "Portoghese (Brasile)",
"preferences_watch_history_label": "Attiva cronologia di riproduzione: ", "preferences_watch_history_label": "Attiva cronologia di riproduzione: ",
"French (auto-generated)": "Francese (auto-generato)", "French (auto-generated)": "Francese (generati automaticamente)",
"search_message_use_another_instance": " Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.", "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_no_results": "Nessun risultato trovato.",
"search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.", "search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.",
@ -451,15 +451,15 @@
"Chinese (China)": "Cinese (Cina)", "Chinese (China)": "Cinese (Cina)",
"Chinese (Hong Kong)": "Cinese (Hong Kong)", "Chinese (Hong Kong)": "Cinese (Hong Kong)",
"Chinese (Taiwan)": "Cinese (Taiwan)", "Chinese (Taiwan)": "Cinese (Taiwan)",
"Dutch (auto-generated)": "Olandese (auto-generato)", "Dutch (auto-generated)": "Olandese (generati automaticamente)",
"German (auto-generated)": "Tedesco (auto-generato)", "German (auto-generated)": "Tedesco (generati automaticamente)",
"Indonesian (auto-generated)": "Indonesiano (auto-generato)", "Indonesian (auto-generated)": "Indonesiano (generati automaticamente)",
"Interlingue": "Interlingua", "Interlingue": "Interlingua",
"Italian (auto-generated)": "Italiano (auto-generato)", "Italian (auto-generated)": "Italiano (generati automaticamente)",
"Japanese (auto-generated)": "Giapponese (auto-generato)", "Japanese (auto-generated)": "Giapponese (generati automaticamente)",
"Korean (auto-generated)": "Coreano (auto-generato)", "Korean (auto-generated)": "Coreano (generati automaticamente)",
"Russian (auto-generated)": "Russo (auto-generato)", "Russian (auto-generated)": "Russo (generati automaticamente)",
"Spanish (auto-generated)": "Spagnolo (auto-generato)", "Spanish (auto-generated)": "Spagnolo (generati automaticamente)",
"Spanish (Mexico)": "Spagnolo (Messico)", "Spanish (Mexico)": "Spagnolo (Messico)",
"Spanish (Spain)": "Spagnolo (Spagna)", "Spanish (Spain)": "Spagnolo (Spagna)",
"Turkish (auto-generated)": "Turco (auto-generato)", "Turkish (auto-generated)": "Turco (auto-generato)",

ファイルの表示

@ -461,12 +461,12 @@
"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: ", "Popular enabled: ": "Populære aktiv: ",
"search_message_change_filters_or_query": "Prøv ett mindre snevert søk og/eller endre filterne.", "search_message_change_filters_or_query": "Prøv ett mindre snevert søk og/eller endre filterne.",
"search_filters_duration_option_medium": "Middels (420 minutter)", "search_filters_duration_option_medium": "Middels (420 minutter)",
"search_message_no_results": "Resultatløst.", "search_message_no_results": "Resultatløst.",
"search_filters_type_option_all": "Alle typer", "search_filters_type_option_all": "Alle typer",
"search_filters_duration_option_none": "Uvilkårlig varighet", "search_filters_duration_option_none": "Enhver varighet",
"search_message_use_another_instance": " Du kan også <a href=\"`x`\">søke på en annen instans</a>.", "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_date_label": "Opplastningsdato",
"search_filters_apply_button": "Bruk valgte filtre", "search_filters_apply_button": "Bruk valgte filtre",

ファイルの表示

@ -408,5 +408,59 @@
"preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_480p": "480p", "preferences_quality_dash_option_480p": "480p",
"preferences_quality_dash_option_360p": "360p", "preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_240p": "240p" "preferences_quality_dash_option_240p": "240p",
"Video unavailable": "Vídeo indisponível",
"Russian (auto-generated)": "Russo (geradas automaticamente)",
"comments_view_x_replies": "Ver {{count}} resposta",
"comments_view_x_replies_plural": "Ver {{count}} respostas",
"comments_points_count": "{{count}} ponto",
"comments_points_count_plural": "{{count}} pontos",
"English (United Kingdom)": "Inglês (Reino Unido)",
"Chinese (Hong Kong)": "Chinês (Hong Kong)",
"Chinese (Taiwan)": "Chinês (Taiwan)",
"Dutch (auto-generated)": "Holandês (geradas automaticamente)",
"French (auto-generated)": "Francês (geradas automaticamente)",
"German (auto-generated)": "Alemão (geradas automaticamente)",
"Indonesian (auto-generated)": "Indonésio (geradas automaticamente)",
"Interlingue": "Interlingue",
"Italian (auto-generated)": "Italiano (geradas automaticamente)",
"Japanese (auto-generated)": "Japonês (geradas automaticamente)",
"Korean (auto-generated)": "Coreano (geradas automaticamente)",
"Portuguese (auto-generated)": "Português (geradas automaticamente)",
"Portuguese (Brazil)": "Português (Brasil)",
"Spanish (Spain)": "Espanhol (Espanha)",
"Vietnamese (auto-generated)": "Vietnamita (geradas automaticamente)",
"search_filters_type_option_all": "Qualquer tipo",
"search_filters_duration_option_none": "Qualquer duração",
"search_filters_duration_option_short": "Curto (< 4 minutos)",
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
"search_filters_duration_option_long": "Longo (> 20 minutos)",
"search_filters_features_option_purchased": "Comprado",
"search_filters_apply_button": "Aplicar filtros selecionados",
"videoinfo_watch_on_youTube": "Ver no YouTube",
"videoinfo_youTube_embed_link": "Embutir",
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte modificado",
"videoinfo_invidious_embed_link": "Ligação embutida",
"none": "nenhum",
"videoinfo_started_streaming_x_ago": "Entrou em direto há `x`",
"download_subtitles": "Legendas - `x` (.vtt)",
"user_created_playlists": "`x` listas de reprodução criadas",
"user_saved_playlists": "`x` listas de reprodução guardadas",
"preferences_save_player_pos_label": "Guardar posição de reprodução: ",
"Turkish (auto-generated)": "Turco (geradas automaticamente)",
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
"Chinese (China)": "Chinês (China)",
"Spanish (auto-generated)": "Espanhol (geradas automaticamente)",
"Spanish (Mexico)": "Espanhol (México)",
"English (United States)": "Inglês (Estados Unidos)",
"footer_donate_page": "Doar",
"footer_documentation": "Documentação",
"footer_source_code": "Código-fonte",
"footer_original_source_code": "Código-fonte original",
"footer_modfied_source_code": "Código-fonte modificado",
"Chinese": "Chinês",
"search_filters_date_label": "Data de carregamento",
"search_filters_date_option_none": "Qualquer data",
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_vr180": "VR180"
} }

ファイルの表示

@ -102,13 +102,13 @@
"Manage tokens": "Управление токенами", "Manage tokens": "Управление токенами",
"Watch history": "История просмотров", "Watch history": "История просмотров",
"Delete account": "Удалить аккаунт", "Delete account": "Удалить аккаунт",
"preferences_category_admin": "Администраторские настройки", "preferences_category_admin": "Настройки администратора",
"preferences_default_home_label": "Главная страница по умолчанию: ", "preferences_default_home_label": "Главная страница по умолчанию: ",
"preferences_feed_menu_label": "Меню ленты видео: ", "preferences_feed_menu_label": "Меню ленты видео: ",
"preferences_show_nick_label": "Показать ник вверху: ", "preferences_show_nick_label": "Показать ник вверху: ",
"Top enabled: ": "Включить топ видео? ", "Top enabled: ": "Включить топ видео? ",
"CAPTCHA enabled: ": "Включить капчу? ", "CAPTCHA enabled: ": "Включить капчу? ",
"Login enabled: ": "Включить авторизацию? ", "Login enabled: ": "Включить авторизацию: ",
"Registration enabled: ": "Включить регистрацию? ", "Registration enabled: ": "Включить регистрацию? ",
"Report statistics: ": "Сообщать статистику? ", "Report statistics: ": "Сообщать статистику? ",
"Save preferences": "Сохранить настройки", "Save preferences": "Сохранить настройки",
@ -195,7 +195,7 @@
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»",
"Erroneous challenge": "Неправильный ответ в «challenge»", "Erroneous challenge": "Неправильный ответ в «challenge»",
"Erroneous token": "Неправильный токен", "Erroneous token": "Неправильный токен",
"No such user": "Недопустимое имя пользователя", "No such user": "Пользователь не найден",
"Token is expired, please try again": "Срок действия токена истёк, попробуйте позже", "Token is expired, please try again": "Срок действия токена истёк, попробуйте позже",
"English": "Английский", "English": "Английский",
"English (auto-generated)": "Английский (созданы автоматически)", "English (auto-generated)": "Английский (созданы автоматически)",

126
locales/si.json ノーマルファイル
ファイルの表示

@ -0,0 +1,126 @@
{
"generic_views_count": "බැලීම් {{count}}",
"generic_views_count_plural": "බැලීම් {{count}}",
"generic_videos_count": "{{count}} වීඩියෝව",
"generic_videos_count_plural": "වීඩියෝ {{count}}",
"generic_subscribers_count": "ග්‍රාහකයන් {{count}}",
"generic_subscribers_count_plural": "ග්‍රාහකයන් {{count}}",
"generic_subscriptions_count": "දායකත්ව {{count}}",
"generic_subscriptions_count_plural": "දායකත්ව {{count}}",
"Shared `x` ago": "`x` පෙර බෙදා ගන්නා ලදී",
"Unsubscribe": "දායක නොවන්න",
"View playlist on YouTube": "YouTube හි ධාවන ලැයිස්තුව බලන්න",
"newest": "අලුත්ම",
"oldest": "පැරණිතම",
"popular": "ජනප්‍රිය",
"last": "අවසන්",
"Cannot change password for Google accounts": "Google ගිණුම් සඳහා මුරපදය වෙනස් කළ නොහැක",
"Authorize token?": "ටෝකනය අනුමත කරනවා ද?",
"Authorize token for `x`?": "`x` සඳහා ටෝකනය අනුමත කරනවා ද?",
"Yes": "ඔව්",
"Import and Export Data": "දත්ත ආනයනය සහ අපනයනය කිරීම",
"Import": "ආනයන",
"Import Invidious data": "Invidious JSON දත්ත ආයාත කරන්න",
"Import FreeTube subscriptions (.db)": "FreeTube දායකත්වයන් (.db) ආයාත කරන්න",
"Import NewPipe subscriptions (.json)": "NewPipe දායකත්වයන් (.json) ආයාත කරන්න",
"Import NewPipe data (.zip)": "NewPipe දත්ත (.zip) ආයාත කරන්න",
"Export": "අපනයන",
"Export data as JSON": "Invidious දත්ත JSON ලෙස අපනයනය කරන්න",
"Delete account?": "ගිණුම මකාදමනවා ද?",
"History": "ඉතිහාසය",
"An alternative front-end to YouTube": "YouTube සඳහා විකල්ප ඉදිරිපස අන්තයක්",
"source": "මූලාශ්‍රය",
"Log in/register": "පුරන්න/ලියාපදිංචිවන්න",
"Log in with Google": "Google සමඟ පුරන්න",
"Password": "මුරපදය",
"Time (h:mm:ss):": "වේලාව (h:mm:ss):",
"Sign In": "පුරන්න",
"Preferences": "මනාපයන්",
"preferences_category_player": "වීඩියෝ ධාවක මනාපයන්",
"preferences_video_loop_label": "නැවත නැවතත්: ",
"preferences_autoplay_label": "ස්වයංක්‍රීය වාදනය: ",
"preferences_continue_label": "මීලඟට වාදනය කරන්න: ",
"preferences_continue_autoplay_label": "මීළඟ වීඩියෝව ස්වයංක්‍රීයව ධාවනය කරන්න: ",
"preferences_local_label": "Proxy වීඩියෝ: ",
"preferences_watch_history_label": "නැරඹුම් ඉතිහාසය සබල කරන්න: ",
"preferences_speed_label": "පෙරනිමි වේගය: ",
"preferences_quality_option_dash": "DASH (අනුවර්තිත ගුණත්වය)",
"preferences_quality_option_medium": "මධ්‍යස්ථ",
"preferences_quality_dash_label": "කැමති DASH වීඩියෝ ගුණත්වය: ",
"preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_480p": "480p",
"preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_144p": "144p",
"preferences_volume_label": "ධාවකයේ හඬ: ",
"preferences_comments_label": "පෙරනිමි අදහස්: ",
"youtube": "YouTube",
"reddit": "Reddit",
"invidious": "Invidious",
"preferences_captions_label": "පෙරනිමි උපසිරැසි: ",
"preferences_related_videos_label": "අදාළ වීඩියෝ පෙන්වන්න: ",
"preferences_annotations_label": "අනුසටහන් පෙන්වන්න: ",
"preferences_vr_mode_label": "අන්තර්ක්‍රියාකාරී අංශක 360 වීඩියෝ (WebGL අවශ්‍යයි): ",
"preferences_region_label": "අන්තර්ගත රට: ",
"preferences_player_style_label": "වීඩියෝ ධාවක විලාසය: ",
"Dark mode: ": "අඳුරු මාදිලිය: ",
"preferences_dark_mode_label": "තේමාව: ",
"light": "ආලෝකමත්",
"generic_playlists_count": "{{count}} ධාවන ලැයිස්තුව",
"generic_playlists_count_plural": "ධාවන ලැයිස්තු {{count}}",
"LIVE": "සජීව",
"Subscribe": "දායක වන්න",
"View channel on YouTube": "YouTube හි නාලිකාව බලන්න",
"Next page": "ඊළඟ පිටුව",
"Previous page": "පෙර පිටුව",
"Clear watch history?": "නැරඹුම් ඉතිහාසය මකාදමනවා ද?",
"No": "නැත",
"Log in": "පුරන්න",
"New password": "නව මුරපදය",
"Import YouTube subscriptions": "YouTube/OPML දායකත්වයන් ආයාත කරන්න",
"Register": "ලියාපදිංචිවන්න",
"New passwords must match": "නව මුරපද ගැලපිය යුතුය",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML ලෙස දායකත්වයන් අපනයනය කරන්න (NewPipe සහ FreeTube සඳහා)",
"Export subscriptions as OPML": "දායකත්වයන් OPML ලෙස අපනයනය කරන්න",
"JavaScript license information": "JavaScript බලපත්‍ර තොරතුරු",
"User ID": "පරිශීලක කේතය",
"Text CAPTCHA": "CAPTCHA පෙල",
"Image CAPTCHA": "CAPTCHA රූපය",
"Google verification code": "Google සත්‍යාපන කේතය",
"E-mail": "විද්‍යුත් තැපෑල",
"preferences_quality_label": "කැමති වීඩියෝ ගුණත්වය: ",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_dash_option_auto": "ස්වයංක්‍රීය",
"preferences_quality_option_small": "කුඩා",
"preferences_quality_dash_option_best": "උසස්",
"preferences_quality_dash_option_2160p": "2160p",
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_720p": "720p",
"preferences_quality_dash_option_240p": "240p",
"preferences_extend_desc_label": "වීඩියෝ විස්තරය ස්වයංක්‍රීයව දිගහරින්න: ",
"preferences_category_visual": "දෘශ්‍ය මනාපයන්",
"dark": "අඳුරු",
"preferences_category_misc": "විවිධ මනාප",
"preferences_category_subscription": "දායකත්ව මනාප",
"Redirect homepage to feed: ": "මුල් පිටුව පෝෂණය වෙත හරවා යවන්න: ",
"preferences_max_results_label": "සංග්‍රහයේ පෙන්වන වීඩියෝ ගණන: ",
"preferences_sort_label": "වීඩියෝ වර්ග කරන්න: ",
"alphabetically": "අකාරාදී ලෙස",
"alphabetically - reverse": "අකාරාදී - ආපසු",
"channel name": "නාලිකාවේ නම",
"Only show latest video from channel: ": "නාලිකාවේ නවතම වීඩියෝව පමණක් පෙන්වන්න: ",
"preferences_unseen_only_label": "නොබැලූ පමණක් පෙන්වන්න: ",
"Enable web notifications": "වෙබ් දැනුම්දීම් සබල කරන්න",
"Import/export data": "දත්ත ආනයනය / අපනයනය",
"Change password": "මුරපදය වෙනස් කරන්න",
"Manage subscriptions": "දායකත්ව කළමනාකරණය",
"Manage tokens": "ටෝකන කළමනාකරණය",
"Watch history": "නැරඹුම් ඉතිහාසය",
"Save preferences": "මනාප සුරකින්න",
"Token": "ටෝකනය",
"View privacy policy.": "රහස්‍යතා ප්‍රතිපත්තිය බලන්න.",
"Only show latest unwatched video from channel: ": "නාලිකාවේ නවතම නැරඹන නොලද වීඩියෝව පමණක් පෙන්වන්න: ",
"preferences_category_data": "දත්ත මනාප",
"Clear watch history": "නැරඹුම් ඉතිහාසය මකාදැමීම",
"Subscriptions": "දායකත්ව"
}

ファイルの表示

@ -111,7 +111,7 @@ module Kemal
if @fallthrough if @fallthrough
call_next(context) call_next(context)
else else
context.response.status_code = 405 context.response.status = HTTP::Status::METHOD_NOT_ALLOWED
context.response.headers.add("Allow", "GET, HEAD") context.response.headers.add("Allow", "GET, HEAD")
end end
return return
@ -124,7 +124,7 @@ module Kemal
# File path cannot contains '\0' (NUL) because all filesystem I know # File path cannot contains '\0' (NUL) because all filesystem I know
# don't accept '\0' character as file name. # don't accept '\0' character as file name.
if request_path.includes? '\0' if request_path.includes? '\0'
context.response.status_code = 400 context.response.status = HTTP::Status::BAD_REQUEST
return return
end end
@ -143,13 +143,15 @@ module Kemal
add_cache_headers(context.response.headers, last_modified) add_cache_headers(context.response.headers, last_modified)
if cache_request?(context, last_modified) if cache_request?(context, last_modified)
context.response.status_code = 304 context.response.status = HTTP::Status::NOT_MODIFIED
return return
end end
send_file(context, file_path, file[:data], file[:filestat]) send_file(context, file_path, file[:data], file[:filestat])
else else
is_dir = Dir.exists? file_path file_info = File.info?(file_path)
is_dir = file_info.try &.directory? || false
is_file = file_info.try &.file? || false
if request_path != expanded_path if request_path != expanded_path
redirect_to context, expanded_path redirect_to context, expanded_path
@ -157,19 +159,21 @@ module Kemal
redirect_to context, expanded_path + '/' redirect_to context, expanded_path + '/'
end end
if Dir.exists?(file_path) return call_next(context) if file_info.nil?
if is_dir
if config.is_a?(Hash) && config["dir_listing"] == true if config.is_a?(Hash) && config["dir_listing"] == true
context.response.content_type = "text/html" context.response.content_type = "text/html"
directory_listing(context.response, request_path, file_path) directory_listing(context.response, request_path, file_path)
else else
call_next(context) call_next(context)
end end
elsif File.exists?(file_path) elsif is_file
last_modified = modification_time(file_path) last_modified = file_info.modification_time
add_cache_headers(context.response.headers, last_modified) add_cache_headers(context.response.headers, last_modified)
if cache_request?(context, last_modified) if cache_request?(context, last_modified)
context.response.status_code = 304 context.response.status = HTTP::Status::NOT_MODIFIED
return return
end end
@ -177,14 +181,12 @@ module Kemal
data = Bytes.new(size) data = Bytes.new(size)
File.open(file_path, &.read(data)) File.open(file_path, &.read(data))
filestat = File.info(file_path) @cached_files[file_path] = {data: data, filestat: file_info}
send_file(context, file_path, data, file_info)
@cached_files[file_path] = {data: data, filestat: filestat}
send_file(context, file_path, data, filestat)
else else
send_file(context, file_path) send_file(context, file_path)
end end
else else # Not a normal file (FIFO/device/socket)
call_next(context) call_next(context)
end end
end end

ファイルの表示

@ -178,305 +178,19 @@ def popular_videos
Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get
end end
# Routing
before_all do |env| before_all do |env|
preferences = Preferences.from_json("{}") Invidious::Routes::BeforeAll.handle(env)
begin
if prefs_cookie = env.request.cookies["PREFS"]?
preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
else
if language_header = env.request.headers["Accept-Language"]?
if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
preferences.locale = language.header
end
end
end
rescue
preferences = Preferences.from_json("{}")
end
env.set "preferences", preferences
env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff"
# Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed
if CONFIG.disabled?("local") || !preferences.local
extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443"
else
extra_media_csp = ""
end
# Only allow the pages at /embed/* to be embedded
if env.request.resource.starts_with?("/embed")
frame_ancestors = "'self' http: https:"
else
frame_ancestors = "'none'"
end
# TODO: Remove style-src's 'unsafe-inline', requires to remove all
# inline styles (<style> [..] </style>, style=" [..] ")
env.response.headers["Content-Security-Policy"] = {
"default-src 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"font-src 'self' data:",
"connect-src 'self'",
"manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp,
"child-src 'self' blob:",
"frame-src 'self'",
"frame-ancestors " + frame_ancestors,
}.join("; ")
env.response.headers["Referrer-Policy"] = "same-origin"
# Ask the chrom*-based browsers to disable FLoC
# See: https://blog.runcloud.io/google-floc/
env.response.headers["Permissions-Policy"] = "interest-cohort=()"
if (Kemal.config.ssl || CONFIG.https_only) && CONFIG.hsts
env.response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
end
next if {
"/sb/",
"/vi/",
"/s_p/",
"/yts/",
"/ggpht/",
"/api/manifest/",
"/videoplayback",
"/latest_version",
"/download",
}.any? { |r| env.request.resource.starts_with? r }
if env.request.cookies.has_key? "SID"
sid = env.request.cookies["SID"].value
if sid.starts_with? "v1:"
raise "Cannot use token as SID"
end
# Invidious users only have SID
if !env.request.cookies.has_key? "SSID"
if email = Invidious::Database::SessionIDs.select_email(sid)
user = Invidious::Database::Users.select!(email: email)
csrf_token = generate_response(sid, {
":authorize_token",
":playlist_ajax",
":signout",
":subscription_ajax",
":token_ajax",
":watch_ajax",
}, HMAC_KEY, 1.week)
preferences = user.preferences
env.set "preferences", preferences
env.set "sid", sid
env.set "csrf_token", csrf_token
env.set "user", user
end
else
headers = HTTP::Headers.new
headers["Cookie"] = env.request.headers["Cookie"]
begin
user, sid = get_user(sid, headers, false)
csrf_token = generate_response(sid, {
":authorize_token",
":playlist_ajax",
":signout",
":subscription_ajax",
":token_ajax",
":watch_ajax",
}, HMAC_KEY, 1.week)
preferences = user.preferences
env.set "preferences", preferences
env.set "sid", sid
env.set "csrf_token", csrf_token
env.set "user", user
rescue ex
end
end
end
dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s
thin_mode = thin_mode == "true"
locale = env.params.query["hl"]? || preferences.locale
preferences.dark_mode = dark_mode
preferences.thin_mode = thin_mode
preferences.locale = locale
env.set "preferences", preferences
current_page = env.request.path
if env.request.query
query = HTTP::Params.parse(env.request.query.not_nil!)
if query["referer"]?
query["referer"] = get_referer(env, "/")
end
current_page += "?#{query}"
end
env.set "current_page", URI.encode_www_form(current_page)
end end
{% unless flag?(:api_only) %} Invidious::Routing.register_all
Invidious::Routing.get "/", Invidious::Routes::Misc, :home
Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy
Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses
Invidious::Routing.get "/channel/:ucid", Invidious::Routes::Channels, :home
Invidious::Routing.get "/channel/:ucid/home", Invidious::Routes::Channels, :home
Invidious::Routing.get "/channel/:ucid/videos", Invidious::Routes::Channels, :videos
Invidious::Routing.get "/channel/:ucid/playlists", Invidious::Routes::Channels, :playlists
Invidious::Routing.get "/channel/:ucid/community", Invidious::Routes::Channels, :community
Invidious::Routing.get "/channel/:ucid/about", Invidious::Routes::Channels, :about
Invidious::Routing.get "/channel/:ucid/live", Invidious::Routes::Channels, :live
Invidious::Routing.get "/user/:user/live", Invidious::Routes::Channels, :live
Invidious::Routing.get "/c/:user/live", Invidious::Routes::Channels, :live
["", "/videos", "/playlists", "/community", "/about"].each do |path|
# /c/LinusTechTips
Invidious::Routing.get "/c/:user#{path}", Invidious::Routes::Channels, :brand_redirect
# /user/linustechtips | Not always the same as /c/
Invidious::Routing.get "/user/:user#{path}", Invidious::Routes::Channels, :brand_redirect
# /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
Invidious::Routing.get "/attribution_link#{path}", Invidious::Routes::Channels, :brand_redirect
# /profile?user=linustechtips
Invidious::Routing.get "/profile/#{path}", Invidious::Routes::Channels, :profile
end
Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
Invidious::Routing.post "/watch_ajax", Invidious::Routes::Watch, :mark_watched
Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect
Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect
Invidious::Routing.get "/clip/:clip", Invidious::Routes::Watch, :clip
Invidious::Routing.get "/w/:id", Invidious::Routes::Watch, :redirect
Invidious::Routing.get "/v/:id", Invidious::Routes::Watch, :redirect
Invidious::Routing.get "/e/:id", Invidious::Routes::Watch, :redirect
Invidious::Routing.get "/redirect", Invidious::Routes::Misc, :cross_instance_redirect
Invidious::Routing.post "/download", Invidious::Routes::Watch, :download
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new
Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create
Invidious::Routing.get "/subscribe_playlist", Invidious::Routes::Playlists, :subscribe
Invidious::Routing.get "/delete_playlist", Invidious::Routes::Playlists, :delete_page
Invidious::Routing.post "/delete_playlist", Invidious::Routes::Playlists, :delete
Invidious::Routing.get "/edit_playlist", Invidious::Routes::Playlists, :edit
Invidious::Routing.post "/edit_playlist", Invidious::Routes::Playlists, :update
Invidious::Routing.get "/add_playlist_items", Invidious::Routes::Playlists, :add_playlist_items_page
Invidious::Routing.post "/playlist_ajax", Invidious::Routes::Playlists, :playlist_ajax
Invidious::Routing.get "/playlist", Invidious::Routes::Playlists, :show
Invidious::Routing.get "/mix", Invidious::Routes::Playlists, :mix
Invidious::Routing.get "/watch_videos", Invidious::Routes::Playlists, :watch_videos
Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch
Invidious::Routing.get "/results", Invidious::Routes::Search, :results
Invidious::Routing.get "/search", Invidious::Routes::Search, :search
Invidious::Routing.get "/hashtag/:hashtag", Invidious::Routes::Search, :hashtag
# User routes
define_user_routes()
# Feeds
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Feeds, :view_all_playlists_redirect
Invidious::Routing.get "/feed/playlists", Invidious::Routes::Feeds, :playlists
Invidious::Routing.get "/feed/popular", Invidious::Routes::Feeds, :popular
Invidious::Routing.get "/feed/trending", Invidious::Routes::Feeds, :trending
Invidious::Routing.get "/feed/subscriptions", Invidious::Routes::Feeds, :subscriptions
Invidious::Routing.get "/feed/history", Invidious::Routes::Feeds, :history
# RSS Feeds
Invidious::Routing.get "/feed/channel/:ucid", Invidious::Routes::Feeds, :rss_channel
Invidious::Routing.get "/feed/private", Invidious::Routes::Feeds, :rss_private
Invidious::Routing.get "/feed/playlist/:plid", Invidious::Routes::Feeds, :rss_playlist
Invidious::Routing.get "/feeds/videos.xml", Invidious::Routes::Feeds, :rss_videos
# Support push notifications via PubSubHubbub
Invidious::Routing.get "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_get
Invidious::Routing.post "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_post
Invidious::Routing.get "/modify_notifications", Invidious::Routes::Notifications, :modify
Invidious::Routing.post "/subscription_ajax", Invidious::Routes::Subscriptions, :toggle_subscription
Invidious::Routing.get "/subscription_manager", Invidious::Routes::Subscriptions, :subscription_manager
{% end %}
Invidious::Routing.get "/ggpht/*", Invidious::Routes::Images, :ggpht
Invidious::Routing.options "/sb/:authority/:id/:storyboard/:index", Invidious::Routes::Images, :options_storyboard
Invidious::Routing.get "/sb/:authority/:id/:storyboard/:index", Invidious::Routes::Images, :get_storyboard
Invidious::Routing.get "/s_p/:id/:name", Invidious::Routes::Images, :s_p_image
Invidious::Routing.get "/yts/img/:name", Invidious::Routes::Images, :yts_image
Invidious::Routing.get "/vi/:id/:name", Invidious::Routes::Images, :thumbnails
# API routes (macro)
define_v1_api_routes()
# Video playback (macros)
define_api_manifest_routes()
define_video_playback_routes()
error 404 do |env| error 404 do |env|
if md = env.request.path.match(/^\/(?<id>([a-zA-Z0-9_-]{11})|(\w+))$/) Invidious::Routes::ErrorRoutes.error_404(env)
item = md["id"]
# Check if item is branding URL e.g. https://youtube.com/gaming
response = YT_POOL.client &.get("/#{item}")
if response.status_code == 301
response = YT_POOL.client &.get(URI.parse(response.headers["Location"]).request_target)
end
if response.body.empty?
env.response.headers["Location"] = "/"
halt env, status_code: 302
end
html = XML.parse_html(response.body)
ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1]
if ucid
env.response.headers["Location"] = "/channel/#{ucid}"
halt env, status_code: 302
end
params = [] of String
env.params.query.each do |k, v|
params << "#{k}=#{v}"
end
params = params.join("&")
url = "/watch?v=#{item}"
if !params.empty?
url += "&#{params}"
end
# Check if item is video ID
if item.match(/^[a-zA-Z0-9_-]{11}$/) && YT_POOL.client &.head("/watch?v=#{item}").status_code != 404
env.response.headers["Location"] = url
halt env, status_code: 302
end
end
env.response.headers["Location"] = "/"
halt env, status_code: 302
end end
error 500 do |env, ex| error 500 do |env, ex|
locale = env.get("preferences").as(Preferences).locale
error_template(500, ex) error_template(500, ex)
end end
@ -484,6 +198,8 @@ static_headers do |response|
response.headers.add("Cache-Control", "max-age=2629800") response.headers.add("Cache-Control", "max-age=2629800")
end end
# Init Kemal
public_folder "assets" public_folder "assets"
Kemal.config.powered_by_header = false Kemal.config.powered_by_header = false

ファイルの表示

@ -75,7 +75,7 @@ class Config
@[YAML::Field(converter: Preferences::URIConverter)] @[YAML::Field(converter: Preferences::URIConverter)]
property database_url : URI = URI.parse("") property database_url : URI = URI.parse("")
# Use polling to keep decryption function up to date # Use polling to keep decryption function up to date
property decrypt_polling : Bool = true property decrypt_polling : Bool = false
# Used for crawling channels: threads should check all videos uploaded by a channel # Used for crawling channels: threads should check all videos uploaded by a channel
property full_refresh : Bool = false property full_refresh : Bool = false
# Used to tell Invidious it is behind a proxy, so links to resources should be https:// # Used to tell Invidious it is behind a proxy, so links to resources should be https://

ファイルの表示

@ -30,3 +30,6 @@ end
# Exception threw when an element is not found. # Exception threw when an element is not found.
class NotFoundException < InfoException class NotFoundException < InfoException
end end
class VideoNotAvailableException < Exception
end

152
src/invidious/routes/before_all.cr ノーマルファイル
ファイルの表示

@ -0,0 +1,152 @@
module Invidious::Routes::BeforeAll
def self.handle(env)
preferences = Preferences.from_json("{}")
begin
if prefs_cookie = env.request.cookies["PREFS"]?
preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
else
if language_header = env.request.headers["Accept-Language"]?
if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
preferences.locale = language.header
end
end
end
rescue
preferences = Preferences.from_json("{}")
end
env.set "preferences", preferences
env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff"
# Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed
if CONFIG.disabled?("local") || !preferences.local
extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443"
else
extra_media_csp = ""
end
# Only allow the pages at /embed/* to be embedded
if env.request.resource.starts_with?("/embed")
frame_ancestors = "'self' http: https:"
else
frame_ancestors = "'none'"
end
# TODO: Remove style-src's 'unsafe-inline', requires to remove all
# inline styles (<style> [..] </style>, style=" [..] ")
env.response.headers["Content-Security-Policy"] = {
"default-src 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"font-src 'self' data:",
"connect-src 'self'",
"manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp,
"child-src 'self' blob:",
"frame-src 'self'",
"frame-ancestors " + frame_ancestors,
}.join("; ")
env.response.headers["Referrer-Policy"] = "same-origin"
# Ask the chrom*-based browsers to disable FLoC
# See: https://blog.runcloud.io/google-floc/
env.response.headers["Permissions-Policy"] = "interest-cohort=()"
if (Kemal.config.ssl || CONFIG.https_only) && CONFIG.hsts
env.response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
end
return if {
"/sb/",
"/vi/",
"/s_p/",
"/yts/",
"/ggpht/",
"/api/manifest/",
"/videoplayback",
"/latest_version",
"/download",
}.any? { |r| env.request.resource.starts_with? r }
if env.request.cookies.has_key? "SID"
sid = env.request.cookies["SID"].value
if sid.starts_with? "v1:"
raise "Cannot use token as SID"
end
# Invidious users only have SID
if !env.request.cookies.has_key? "SSID"
if email = Invidious::Database::SessionIDs.select_email(sid)
user = Invidious::Database::Users.select!(email: email)
csrf_token = generate_response(sid, {
":authorize_token",
":playlist_ajax",
":signout",
":subscription_ajax",
":token_ajax",
":watch_ajax",
}, HMAC_KEY, 1.week)
preferences = user.preferences
env.set "preferences", preferences
env.set "sid", sid
env.set "csrf_token", csrf_token
env.set "user", user
end
else
headers = HTTP::Headers.new
headers["Cookie"] = env.request.headers["Cookie"]
begin
user, sid = get_user(sid, headers, false)
csrf_token = generate_response(sid, {
":authorize_token",
":playlist_ajax",
":signout",
":subscription_ajax",
":token_ajax",
":watch_ajax",
}, HMAC_KEY, 1.week)
preferences = user.preferences
env.set "preferences", preferences
env.set "sid", sid
env.set "csrf_token", csrf_token
env.set "user", user
rescue ex
end
end
end
dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s
thin_mode = thin_mode == "true"
locale = env.params.query["hl"]? || preferences.locale
preferences.dark_mode = dark_mode
preferences.thin_mode = thin_mode
preferences.locale = locale
env.set "preferences", preferences
current_page = env.request.path
if env.request.query
query = HTTP::Params.parse(env.request.query.not_nil!)
if query["referer"]?
query["referer"] = get_referer(env, "/")
end
current_page += "?#{query}"
end
env.set "current_page", URI.encode_www_form(current_page)
end
end

47
src/invidious/routes/errors.cr ノーマルファイル
ファイルの表示

@ -0,0 +1,47 @@
module Invidious::Routes::ErrorRoutes
def self.error_404(env)
if md = env.request.path.match(/^\/(?<id>([a-zA-Z0-9_-]{11})|(\w+))$/)
item = md["id"]
# Check if item is branding URL e.g. https://youtube.com/gaming
response = YT_POOL.client &.get("/#{item}")
if response.status_code == 301
response = YT_POOL.client &.get(URI.parse(response.headers["Location"]).request_target)
end
if response.body.empty?
env.response.headers["Location"] = "/"
haltf env, status_code: 302
end
html = XML.parse_html(response.body)
ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1]
if ucid
env.response.headers["Location"] = "/channel/#{ucid}"
haltf env, status_code: 302
end
params = [] of String
env.params.query.each do |k, v|
params << "#{k}=#{v}"
end
params = params.join("&")
url = "/watch?v=#{item}"
if !params.empty?
url += "&#{params}"
end
# Check if item is video ID
if item.match(/^[a-zA-Z0-9_-]{11}$/) && YT_POOL.client &.head("/watch?v=#{item}").status_code != 404
env.response.headers["Location"] = url
haltf env, status_code: 302
end
end
env.response.headers["Location"] = "/"
haltf env, status_code: 302
end
end

ファイルの表示

@ -204,6 +204,12 @@ module Invidious::Routes::Feeds
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" } xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
end end
xml.element("image") do
xml.element("url") { xml.text channel.author_thumbnail }
xml.element("title") { xml.text channel.author }
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
end
videos.each do |video| videos.each do |video|
video.to_xml(channel.auto_generated, params, xml) video.to_xml(channel.auto_generated, params, xml)
end end

ファイルの表示

@ -1,130 +1,273 @@
module Invidious::Routing module Invidious::Routing
{% for http_method in {"get", "post", "delete", "options", "patch", "put", "head"} %} extend self
{% for http_method in {"get", "post", "delete", "options", "patch", "put"} %}
macro {{http_method.id}}(path, controller, method = :handle) macro {{http_method.id}}(path, controller, method = :handle)
{{http_method.id}} \{{ path }} do |env| unless Kemal::Utils.path_starts_with_slash?(\{{path}})
raise Kemal::Exceptions::InvalidPathStartException.new({{http_method}}, \{{path}})
end
Kemal::RouteHandler::INSTANCE.add_route({{http_method.upcase}}, \{{path}}) do |env|
\{{ controller }}.\{{ method.id }}(env) \{{ controller }}.\{{ method.id }}(env)
end end
end end
{% end %} {% end %}
end
def register_all
macro define_user_routes {% unless flag?(:api_only) %}
# User login/out get "/", Routes::Misc, :home
Invidious::Routing.get "/login", Invidious::Routes::Login, :login_page get "/privacy", Routes::Misc, :privacy
Invidious::Routing.post "/login", Invidious::Routes::Login, :login get "/licenses", Routes::Misc, :licenses
Invidious::Routing.post "/signout", Invidious::Routes::Login, :signout get "/redirect", Routes::Misc, :cross_instance_redirect
Invidious::Routing.get "/Captcha", Invidious::Routes::Login, :captcha
self.register_channel_routes
# User preferences self.register_watch_routes
Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :show
Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update self.register_iv_playlist_routes
Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme self.register_yt_playlist_routes
Invidious::Routing.get "/data_control", Invidious::Routes::PreferencesRoute, :data_control
Invidious::Routing.post "/data_control", Invidious::Routes::PreferencesRoute, :update_data_control self.register_search_routes
# User account management self.register_user_routes
Invidious::Routing.get "/change_password", Invidious::Routes::Account, :get_change_password self.register_feed_routes
Invidious::Routing.post "/change_password", Invidious::Routes::Account, :post_change_password
Invidious::Routing.get "/delete_account", Invidious::Routes::Account, :get_delete # Support push notifications via PubSubHubbub
Invidious::Routing.post "/delete_account", Invidious::Routes::Account, :post_delete get "/feed/webhook/:token", Routes::Feeds, :push_notifications_get
Invidious::Routing.get "/clear_watch_history", Invidious::Routes::Account, :get_clear_history post "/feed/webhook/:token", Routes::Feeds, :push_notifications_post
Invidious::Routing.post "/clear_watch_history", Invidious::Routes::Account, :post_clear_history
Invidious::Routing.get "/authorize_token", Invidious::Routes::Account, :get_authorize_token get "/modify_notifications", Routes::Notifications, :modify
Invidious::Routing.post "/authorize_token", Invidious::Routes::Account, :post_authorize_token {% end %}
Invidious::Routing.get "/token_manager", Invidious::Routes::Account, :token_manager
Invidious::Routing.post "/token_ajax", Invidious::Routes::Account, :token_ajax self.register_image_routes
end self.register_api_v1_routes
self.register_api_manifest_routes
macro define_v1_api_routes self.register_video_playback_routes
{{namespace = Invidious::Routes::API::V1}} end
# Videos
Invidious::Routing.get "/api/v1/videos/:id", {{namespace}}::Videos, :videos # -------------------
Invidious::Routing.get "/api/v1/storyboards/:id", {{namespace}}::Videos, :storyboards # Invidious routes
Invidious::Routing.get "/api/v1/captions/:id", {{namespace}}::Videos, :captions # -------------------
Invidious::Routing.get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
Invidious::Routing.get "/api/v1/comments/:id", {{namespace}}::Videos, :comments def register_user_routes
# User login/out
# Feeds get "/login", Routes::Login, :login_page
Invidious::Routing.get "/api/v1/trending", {{namespace}}::Feeds, :trending post "/login", Routes::Login, :login
Invidious::Routing.get "/api/v1/popular", {{namespace}}::Feeds, :popular post "/signout", Routes::Login, :signout
get "/Captcha", Routes::Login, :captcha
# Channels
Invidious::Routing.get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home # User preferences
{% for route in {"videos", "latest", "playlists", "community", "search"} %} get "/preferences", Routes::PreferencesRoute, :show
Invidious::Routing.get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}} post "/preferences", Routes::PreferencesRoute, :update
Invidious::Routing.get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}} get "/toggle_theme", Routes::PreferencesRoute, :toggle_theme
{% end %} get "/data_control", Routes::PreferencesRoute, :data_control
post "/data_control", Routes::PreferencesRoute, :update_data_control
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
Invidious::Routing.get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect # User account management
Invidious::Routing.get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect get "/change_password", Routes::Account, :get_change_password
post "/change_password", Routes::Account, :post_change_password
get "/delete_account", Routes::Account, :get_delete
# Search post "/delete_account", Routes::Account, :post_delete
Invidious::Routing.get "/api/v1/search", {{namespace}}::Search, :search get "/clear_watch_history", Routes::Account, :get_clear_history
Invidious::Routing.get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions post "/clear_watch_history", Routes::Account, :post_clear_history
get "/authorize_token", Routes::Account, :get_authorize_token
# Authenticated post "/authorize_token", Routes::Account, :post_authorize_token
get "/token_manager", Routes::Account, :token_manager
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr post "/token_ajax", Routes::Account, :token_ajax
# post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
# Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications get "/subscription_manager", Routes::Subscriptions, :subscription_manager
# Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications end
Invidious::Routing.get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences def register_iv_playlist_routes
Invidious::Routing.post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences get "/create_playlist", Routes::Playlists, :new
post "/create_playlist", Routes::Playlists, :create
Invidious::Routing.get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed get "/subscribe_playlist", Routes::Playlists, :subscribe
get "/delete_playlist", Routes::Playlists, :delete_page
Invidious::Routing.get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions post "/delete_playlist", Routes::Playlists, :delete
Invidious::Routing.post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel get "/edit_playlist", Routes::Playlists, :edit
Invidious::Routing.delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel post "/edit_playlist", Routes::Playlists, :update
get "/add_playlist_items", Routes::Playlists, :add_playlist_items_page
post "/playlist_ajax", Routes::Playlists, :playlist_ajax
Invidious::Routing.get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists end
Invidious::Routing.post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
Invidious::Routing.patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute def register_feed_routes
Invidious::Routing.delete "/api/v1/auth/playlists/:plid", {{namespace}}::Authenticated, :delete_playlist # Feeds
get "/view_all_playlists", Routes::Feeds, :view_all_playlists_redirect
get "/feed/playlists", Routes::Feeds, :playlists
Invidious::Routing.post "/api/v1/auth/playlists/:plid/videos", {{namespace}}::Authenticated, :insert_video_into_playlist get "/feed/popular", Routes::Feeds, :popular
Invidious::Routing.delete "/api/v1/auth/playlists/:plid/videos/:index", {{namespace}}::Authenticated, :delete_video_in_playlist get "/feed/trending", Routes::Feeds, :trending
get "/feed/subscriptions", Routes::Feeds, :subscriptions
Invidious::Routing.get "/api/v1/auth/tokens", {{namespace}}::Authenticated, :get_tokens get "/feed/history", Routes::Feeds, :history
Invidious::Routing.post "/api/v1/auth/tokens/register", {{namespace}}::Authenticated, :register_token
Invidious::Routing.post "/api/v1/auth/tokens/unregister", {{namespace}}::Authenticated, :unregister_token # RSS Feeds
get "/feed/channel/:ucid", Routes::Feeds, :rss_channel
Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications get "/feed/private", Routes::Feeds, :rss_private
Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications get "/feed/playlist/:plid", Routes::Feeds, :rss_playlist
get "/feeds/videos.xml", Routes::Feeds, :rss_videos
# Misc end
Invidious::Routing.get "/api/v1/stats", {{namespace}}::Misc, :stats
Invidious::Routing.get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist # -------------------
Invidious::Routing.get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist # Youtube routes
Invidious::Routing.get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes # -------------------
end
def register_channel_routes
macro define_api_manifest_routes get "/channel/:ucid", Routes::Channels, :home
Invidious::Routing.get "/api/manifest/dash/id/:id", Invidious::Routes::API::Manifest, :get_dash_video_id get "/channel/:ucid/home", Routes::Channels, :home
get "/channel/:ucid/videos", Routes::Channels, :videos
Invidious::Routing.get "/api/manifest/dash/id/videoplayback", Invidious::Routes::API::Manifest, :get_dash_video_playback get "/channel/:ucid/playlists", Routes::Channels, :playlists
Invidious::Routing.get "/api/manifest/dash/id/videoplayback/*", Invidious::Routes::API::Manifest, :get_dash_video_playback_greedy get "/channel/:ucid/community", Routes::Channels, :community
get "/channel/:ucid/about", Routes::Channels, :about
Invidious::Routing.options "/api/manifest/dash/id/videoplayback", Invidious::Routes::API::Manifest, :options_dash_video_playback get "/channel/:ucid/live", Routes::Channels, :live
Invidious::Routing.options "/api/manifest/dash/id/videoplayback/*", Invidious::Routes::API::Manifest, :options_dash_video_playback get "/user/:user/live", Routes::Channels, :live
get "/c/:user/live", Routes::Channels, :live
Invidious::Routing.get "/api/manifest/hls_playlist/*", Invidious::Routes::API::Manifest, :get_hls_playlist
Invidious::Routing.get "/api/manifest/hls_variant/*", Invidious::Routes::API::Manifest, :get_hls_variant ["", "/videos", "/playlists", "/community", "/about"].each do |path|
end # /c/LinusTechTips
get "/c/:user#{path}", Routes::Channels, :brand_redirect
macro define_video_playback_routes # /user/linustechtips | Not always the same as /c/
Invidious::Routing.get "/videoplayback", Invidious::Routes::VideoPlayback, :get_video_playback get "/user/:user#{path}", Routes::Channels, :brand_redirect
Invidious::Routing.get "/videoplayback/*", Invidious::Routes::VideoPlayback, :get_video_playback_greedy # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
get "/attribution_link#{path}", Routes::Channels, :brand_redirect
Invidious::Routing.options "/videoplayback", Invidious::Routes::VideoPlayback, :options_video_playback # /profile?user=linustechtips
Invidious::Routing.options "/videoplayback/*", Invidious::Routes::VideoPlayback, :options_video_playback get "/profile/#{path}", Routes::Channels, :profile
end
Invidious::Routing.get "/latest_version", Invidious::Routes::VideoPlayback, :latest_version end
def register_watch_routes
get "/watch", Routes::Watch, :handle
post "/watch_ajax", Routes::Watch, :mark_watched
get "/watch/:id", Routes::Watch, :redirect
get "/shorts/:id", Routes::Watch, :redirect
get "/clip/:clip", Routes::Watch, :clip
get "/w/:id", Routes::Watch, :redirect
get "/v/:id", Routes::Watch, :redirect
get "/e/:id", Routes::Watch, :redirect
post "/download", Routes::Watch, :download
get "/embed/", Routes::Embed, :redirect
get "/embed/:id", Routes::Embed, :show
end
def register_yt_playlist_routes
get "/playlist", Routes::Playlists, :show
get "/mix", Routes::Playlists, :mix
get "/watch_videos", Routes::Playlists, :watch_videos
end
def register_search_routes
get "/opensearch.xml", Routes::Search, :opensearch
get "/results", Routes::Search, :results
get "/search", Routes::Search, :search
get "/hashtag/:hashtag", Routes::Search, :hashtag
end
# -------------------
# Media proxy routes
# -------------------
def register_api_manifest_routes
get "/api/manifest/dash/id/:id", Routes::API::Manifest, :get_dash_video_id
get "/api/manifest/dash/id/videoplayback", Routes::API::Manifest, :get_dash_video_playback
get "/api/manifest/dash/id/videoplayback/*", Routes::API::Manifest, :get_dash_video_playback_greedy
options "/api/manifest/dash/id/videoplayback", Routes::API::Manifest, :options_dash_video_playback
options "/api/manifest/dash/id/videoplayback/*", Routes::API::Manifest, :options_dash_video_playback
get "/api/manifest/hls_playlist/*", Routes::API::Manifest, :get_hls_playlist
get "/api/manifest/hls_variant/*", Routes::API::Manifest, :get_hls_variant
end
def register_video_playback_routes
get "/videoplayback", Routes::VideoPlayback, :get_video_playback
get "/videoplayback/*", Routes::VideoPlayback, :get_video_playback_greedy
options "/videoplayback", Routes::VideoPlayback, :options_video_playback
options "/videoplayback/*", Routes::VideoPlayback, :options_video_playback
get "/latest_version", Routes::VideoPlayback, :latest_version
end
def register_image_routes
get "/ggpht/*", Routes::Images, :ggpht
options "/sb/:authority/:id/:storyboard/:index", Routes::Images, :options_storyboard
get "/sb/:authority/:id/:storyboard/:index", Routes::Images, :get_storyboard
get "/s_p/:id/:name", Routes::Images, :s_p_image
get "/yts/img/:name", Routes::Images, :yts_image
get "/vi/:id/:name", Routes::Images, :thumbnails
end
# -------------------
# API routes
# -------------------
def register_api_v1_routes
{% begin %}
{{namespace = Routes::API::V1}}
# Videos
get "/api/v1/videos/:id", {{namespace}}::Videos, :videos
get "/api/v1/storyboards/:id", {{namespace}}::Videos, :storyboards
get "/api/v1/captions/:id", {{namespace}}::Videos, :captions
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
# Feeds
get "/api/v1/trending", {{namespace}}::Feeds, :trending
get "/api/v1/popular", {{namespace}}::Feeds, :popular
# Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
{% for route in {"videos", "latest", "playlists", "community", "search"} %}
get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}}
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
{% end %}
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect
get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect
# Search
get "/api/v1/search", {{namespace}}::Search, :search
get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions
# Authenticated
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
#
# Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
# Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences
get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed
get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute
delete "/api/v1/auth/playlists/:plid", {{namespace}}::Authenticated, :delete_playlist
post "/api/v1/auth/playlists/:plid/videos", {{namespace}}::Authenticated, :insert_video_into_playlist
delete "/api/v1/auth/playlists/:plid/videos/:index", {{namespace}}::Authenticated, :delete_video_in_playlist
get "/api/v1/auth/tokens", {{namespace}}::Authenticated, :get_tokens
post "/api/v1/auth/tokens/register", {{namespace}}::Authenticated, :register_token
post "/api/v1/auth/tokens/unregister", {{namespace}}::Authenticated, :unregister_token
get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
# Misc
get "/api/v1/stats", {{namespace}}::Misc, :stats
get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist
get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist
get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes
{% end %}
end
end end

ファイルの表示

@ -18,7 +18,7 @@ struct Invidious::User
expires: Time.utc + 2.years, expires: Time.utc + 2.years,
secure: SECURE, secure: SECURE,
http_only: true, http_only: true,
samesite: HTTP::Cookie::SameSite::Strict samesite: HTTP::Cookie::SameSite::Lax
) )
end end
@ -32,7 +32,7 @@ struct Invidious::User
expires: Time.utc + 2.years, expires: Time.utc + 2.years,
secure: SECURE, secure: SECURE,
http_only: false, http_only: false,
samesite: HTTP::Cookie::SameSite::Strict samesite: HTTP::Cookie::SameSite::Lax
) )
end end
end end

ファイルの表示

@ -909,6 +909,10 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
"reason" => JSON::Any.new(reason), "reason" => JSON::Any.new(reason),
} }
end end
elsif video_id != player_response.dig("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
raise VideoNotAvailableException.new("The video returned by YouTube isn't the requested one. (WEB client)")
else else
reason = nil reason = nil
end end
@ -933,10 +937,14 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
end end
android_player = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) android_player = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
# Sometime, the video is available from the web client, but not on Android, so check # Sometimes, the video is available from the web client, but not on Android, so check
# that here, and fallback to the streaming data from the web client if needed. # that here, and fallback to the streaming data from the web client if needed.
# See: https://github.com/iv-org/invidious/issues/2549 # See: https://github.com/iv-org/invidious/issues/2549
if android_player["playabilityStatus"]["status"] == "OK" if video_id != android_player.dig("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
raise VideoNotAvailableException.new("The video returned by YouTube isn't the requested one. (ANDROID client)")
elsif android_player["playabilityStatus"]["status"] == "OK"
params["streamingData"] = android_player["streamingData"]? || JSON::Any.new("") params["streamingData"] = android_player["streamingData"]? || JSON::Any.new("")
else else
params["streamingData"] = player_response["streamingData"]? || JSON::Any.new("") params["streamingData"] = player_response["streamingData"]? || JSON::Any.new("")
@ -1012,7 +1020,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
if toplevel_buttons if toplevel_buttons
likes_button = toplevel_buttons.as_a likes_button = toplevel_buttons.as_a
.find(&.dig("toggleButtonRenderer", "defaultIcon", "iconType").as_s.== "LIKE") .find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
.try &.["toggleButtonRenderer"] .try &.["toggleButtonRenderer"]
if likes_button if likes_button

ファイルの表示

@ -435,20 +435,22 @@ private module Extractors
raw_items = [] of JSON::Any raw_items = [] of JSON::Any
content = extract_selected_tab(target["tabs"])["content"] content = extract_selected_tab(target["tabs"])["content"]
content["sectionListRenderer"]["contents"].as_a.each do |renderer_container| if section_list_contents = content.dig?("sectionListRenderer", "contents")
renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0] section_list_contents.as_a.each do |renderer_container|
renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0]
# Category extraction # Category extraction
if items_container = renderer_container_contents["shelfRenderer"]? if items_container = renderer_container_contents["shelfRenderer"]?
raw_items << renderer_container_contents raw_items << renderer_container_contents
next next
elsif items_container = renderer_container_contents["gridRenderer"]? elsif items_container = renderer_container_contents["gridRenderer"]?
else else
items_container = renderer_container_contents items_container = renderer_container_contents
end end
items_container["items"]?.try &.as_a.each do |item| items_container["items"]?.try &.as_a.each do |item|
raw_items << item raw_items << item
end
end end
end end

ファイルの表示

@ -5,15 +5,28 @@
module YoutubeAPI module YoutubeAPI
extend self extend self
private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
private ANDROID_APP_VERSION = "17.29.35"
private ANDROID_SDK_VERSION = 30_i64
private IOS_APP_VERSION = "17.30.1"
# Enumerate used to select one of the clients supported by the API # Enumerate used to select one of the clients supported by the API
enum ClientType enum ClientType
Web Web
WebEmbeddedPlayer WebEmbeddedPlayer
WebMobile WebMobile
WebScreenEmbed WebScreenEmbed
Android Android
AndroidEmbeddedPlayer AndroidEmbeddedPlayer
AndroidScreenEmbed AndroidScreenEmbed
IOS
IOSEmbedded
IOSMusic
TvHtml5
TvHtml5ScreenEmbed TvHtml5ScreenEmbed
end end
@ -21,50 +34,78 @@ module YoutubeAPI
HARDCODED_CLIENTS = { HARDCODED_CLIENTS = {
ClientType::Web => { ClientType::Web => {
name: "WEB", name: "WEB",
version: "2.20210721.00.00", version: "2.20220804.07.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "WATCH_FULL_SCREEN", screen: "WATCH_FULL_SCREEN",
}, },
ClientType::WebEmbeddedPlayer => { ClientType::WebEmbeddedPlayer => {
name: "WEB_EMBEDDED_PLAYER", # 56 name: "WEB_EMBEDDED_PLAYER", # 56
version: "1.20210721.1.0", version: "1.20220803.01.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "EMBED", screen: "EMBED",
}, },
ClientType::WebMobile => { ClientType::WebMobile => {
name: "MWEB", name: "MWEB",
version: "2.20210726.08.00", version: "2.20220805.01.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "", # None
}, },
ClientType::WebScreenEmbed => { ClientType::WebScreenEmbed => {
name: "WEB", name: "WEB",
version: "2.20210721.00.00", version: "2.20220804.00.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "EMBED", screen: "EMBED",
}, },
# Android
ClientType::Android => { ClientType::Android => {
name: "ANDROID", name: "ANDROID",
version: "16.20", version: ANDROID_APP_VERSION,
api_key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w", api_key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w",
screen: "", # ?? android_sdk_version: ANDROID_SDK_VERSION,
}, },
ClientType::AndroidEmbeddedPlayer => { ClientType::AndroidEmbeddedPlayer => {
name: "ANDROID_EMBEDDED_PLAYER", # 55 name: "ANDROID_EMBEDDED_PLAYER", # 55
version: "16.20", version: ANDROID_APP_VERSION,
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "", # None?
}, },
ClientType::AndroidScreenEmbed => { ClientType::AndroidScreenEmbed => {
name: "ANDROID", # 3 name: "ANDROID", # 3
version: "16.20", version: ANDROID_APP_VERSION,
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "EMBED", screen: "EMBED",
android_sdk_version: ANDROID_SDK_VERSION,
},
# IOS
ClientType::IOS => {
name: "IOS", # 5
version: IOS_APP_VERSION,
api_key: "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc",
},
ClientType::IOSEmbedded => {
name: "IOS_MESSAGES_EXTENSION", # 66
version: IOS_APP_VERSION,
api_key: DEFAULT_API_KEY,
},
ClientType::IOSMusic => {
name: "IOS_MUSIC", # 26
version: "4.32",
api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s",
},
# TV app
ClientType::TvHtml5 => {
name: "TVHTML5", # 7
version: "7.20220325",
api_key: DEFAULT_API_KEY,
}, },
ClientType::TvHtml5ScreenEmbed => { ClientType::TvHtml5ScreenEmbed => {
name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER", name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER", # 85
version: "2.0", version: "2.0",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", api_key: DEFAULT_API_KEY,
screen: "EMBED", screen: "EMBED",
}, },
} }
@ -131,7 +172,11 @@ module YoutubeAPI
# :ditto: # :ditto:
def screen : String def screen : String
HARDCODED_CLIENTS[@client_type][:screen] HARDCODED_CLIENTS[@client_type][:screen]? || ""
end
def android_sdk_version : Int64?
HARDCODED_CLIENTS[@client_type][:android_sdk_version]?
end end
# Convert to string, for logging purposes # Convert to string, for logging purposes
@ -163,7 +208,7 @@ module YoutubeAPI
"gl" => client_config.region || "US", # Can't be empty! "gl" => client_config.region || "US", # Can't be empty!
"clientName" => client_config.name, "clientName" => client_config.name,
"clientVersion" => client_config.version, "clientVersion" => client_config.version,
}, } of String => String | Int64,
} }
# Add some more context if it exists in the client definitions # Add some more context if it exists in the client definitions
@ -174,7 +219,11 @@ module YoutubeAPI
if client_config.screen == "EMBED" if client_config.screen == "EMBED"
client_context["thirdParty"] = { client_context["thirdParty"] = {
"embedUrl" => "https://www.youtube.com/embed/dQw4w9WgXcQ", "embedUrl" => "https://www.youtube.com/embed/dQw4w9WgXcQ",
} } of String => String | Int64
end
if android_sdk_version = client_config.android_sdk_version
client_context["client"]["androidSdkVersion"] = android_sdk_version
end end
return client_context return client_context