Merge branch 'master' of github.com:iv-org/invidious
このコミットが含まれているのは:
コミット
77f3b88873
|
@ -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>
|
||||||
•
|
•
|
||||||
<a href="https://instances.invidious.io/">Instances list</a>
|
<a href="https://instances.invidious.io/">Instances list</a>
|
||||||
•
|
•
|
||||||
<a href="https://docs.invidious.io/faq/">FAQ</a>
|
<a href="https://docs.invidious.io/faq/">FAQ</a>
|
||||||
•
|
•
|
||||||
<a href="https://docs.invidious.io/">Documentation</a>
|
<a href="https://docs.invidious.io/">Documentation</a>
|
||||||
•
|
•
|
||||||
<a href="#contribute">Contribute</a>
|
<a href="#contribute">Contribute</a>
|
||||||
•
|
•
|
||||||
<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 (4–20 minutter)",
|
"search_filters_duration_option_medium": "Middels (4–20 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)": "Английский (созданы автоматически)",
|
||||||
|
|
|
@ -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
|
||||||
|
|
298
src/invidious.cr
298
src/invidious.cr
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
読み込み中…
新しいイシューから参照