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

このコミットが含まれているのは:
守矢諏訪子 2023-02-06 17:33:45 +09:00
コミット eac9e6aaa4
103個のファイルの変更1813行の追加1308行の削除

ファイルの表示

@ -490,8 +490,9 @@ hr {
} }
/* Description Expansion Styling*/ /* Description Expansion Styling*/
#descexpansionbutton { #descexpansionbutton,
display: none #music-desc-expansion {
display: none;
} }
#descexpansionbutton ~ div { #descexpansionbutton ~ div {
@ -509,6 +510,11 @@ hr {
margin-top: 20px; margin-top: 20px;
} }
label[for="descexpansionbutton"]:hover,
label[for="music-desc-expansion"]:hover {
cursor: pointer;
}
/* Bidi (bidirectional text) support */ /* Bidi (bidirectional text) support */
h1, h1,
h2, h2,
@ -517,14 +523,38 @@ h4,
h5, h5,
p, p,
#descriptionWrapper, #descriptionWrapper,
#description-box { #description-box,
unicode-bidi: plaintext; #music-description-box {
text-align: start; unicode-bidi: plaintext;
text-align: start;
} }
#descriptionWrapper { #descriptionWrapper {
max-width: 600px; max-width: 600px;
white-space: pre-wrap; white-space: pre-wrap;
}
#music-description-box {
display: none;
}
#music-desc-expansion:checked ~ #music-description-box {
display: block;
}
#music-desc-expansion ~ label > h3 > .ion-ios-arrow-up,
#music-desc-expansion:checked ~ label > h3 > .ion-ios-arrow-down {
display: none;
}
#music-desc-expansion:checked ~ label > h3 > .ion-ios-arrow-up,
#music-desc-expansion ~ label > h3 > .ion-ios-arrow-down {
display: inline;
}
/* Select all the music items except the first one */
.music-item + .music-item {
border-top: 1px solid #ffffff;
} }
/* Center the "invidious" logo on the search page */ /* Center the "invidious" logo on the search page */

ファイルの表示

@ -295,6 +295,17 @@ https_only: false
## ##
#admins: [""] #admins: [""]
##
## Enable/Disable the user notifications for all users
##
## Note: On large instances, it is recommended to set this option to 'false'
## in order to reduce the amount of data written to the database, and hence
## improve the overall performance of the instance.
##
## Accepted values: true, false
## Default: true
##
#enable_user_notifications: true
# ----------------------------- # -----------------------------
# Background jobs # Background jobs

ファイルの表示

@ -43,7 +43,7 @@ RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
FROM alpine:3.16 FROM alpine:3.16
RUN apk add --no-cache librsvg ttf-opensans RUN apk add --no-cache librsvg ttf-opensans tini
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious adduser -u 1000 -S invidious -G invidious
@ -58,4 +58,5 @@ RUN chmod o+rX -R ./assets ./config ./locales
EXPOSE 3000 EXPOSE 3000
USER invidious USER invidious
ENTRYPOINT ["/sbin/tini", "--"]
CMD [ "/invidious/invidious" ] CMD [ "/invidious/invidious" ]

ファイルの表示

@ -42,7 +42,7 @@ RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
fi fi
FROM alpine:3.16 FROM alpine:3.16
RUN apk add --no-cache librsvg ttf-opensans RUN apk add --no-cache librsvg ttf-opensans tini
WORKDIR /invidious WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \ RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious adduser -u 1000 -S invidious -G invidious
@ -57,4 +57,5 @@ RUN chmod o+rX -R ./assets ./config ./locales
EXPOSE 3000 EXPOSE 3000
USER invidious USER invidious
ENTRYPOINT ["/sbin/tini", "--"]
CMD [ "/invidious/invidious" ] CMD [ "/invidious/invidious" ]

ファイルの表示

@ -1,6 +1,6 @@
dependencies: dependencies:
- name: postgresql - name: postgresql
repository: https://charts.bitnami.com/bitnami/ repository: https://charts.bitnami.com/bitnami/
version: 11.1.3 version: 12.1.9
digest: sha256:79061645472b6fb342d45e8e5b3aacd018ef5067193e46a060bccdc99fe7f6e1 digest: sha256:71ff342a6c0a98bece3d7fe199983afb2113f8db65a3e3819de875af2c45add7
generated: "2022-03-02T05:57:20.081432389+13:00" generated: "2023-01-20T20:42:32.757707004Z"

ファイルの表示

@ -17,6 +17,6 @@ maintainers:
email: mail@leonklingele.de email: mail@leonklingele.de
dependencies: dependencies:
- name: postgresql - name: postgresql
version: ~11.1.3 version: ~12.1.6
repository: "https://charts.bitnami.com/bitnami/" repository: "https://charts.bitnami.com/bitnami/"
engine: gotpl engine: gotpl

ファイルの表示

@ -34,6 +34,8 @@ securityContext:
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql # See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
postgresql: postgresql:
image:
tag: 13
auth: auth:
username: kemal username: kemal
password: kemal password: kemal

ファイルの表示

@ -1,11 +1,11 @@
{ {
"LIVE": "مُباشِر", "LIVE": "مُباشِر",
"Shared `x` ago": "تمَّ رفع المقطع المرئيّ مُنذ `x`", "Shared `x` ago": "تمَّ الرفع مُنذ `x`",
"Unsubscribe": "إلغاء الاشتراك", "Unsubscribe": "إلغاء الاشتراك",
"Subscribe": "الإشتراك", "Subscribe": "الاشتراك",
"View channel on YouTube": "زيارة القناة على موقع يوتيوب", "View channel on YouTube": "زيارة القناة على يوتيوب",
"View playlist on YouTube": "عرض قائمة التشغيل على اليوتيوب", "View playlist on YouTube": "عرض قائمة التشغيل على يوتيوب",
"newest": "الأجدد", "newest": "الأحدث",
"oldest": "الأقدم", "oldest": "الأقدم",
"popular": "الأكثر شعبية", "popular": "الأكثر شعبية",
"last": "الأخيرة", "last": "الأخيرة",
@ -96,8 +96,8 @@
"`x` is live": "`x` في بث مباشر", "`x` is live": "`x` في بث مباشر",
"preferences_category_data": "إعدادات التفضيلات", "preferences_category_data": "إعدادات التفضيلات",
"Clear watch history": "حذف سجل المشاهدة", "Clear watch history": "حذف سجل المشاهدة",
"Import/export data": ضافة\\استخراج البيانات", "Import/export data": ستيراد و تصدير البيانات",
"Change password": "غير كلمة السر", "Change password": "تغير كلمة السر",
"Manage subscriptions": "إدارة الاشتراكات", "Manage subscriptions": "إدارة الاشتراكات",
"Manage tokens": "إدارة الرموز", "Manage tokens": "إدارة الرموز",
"Watch history": "سجل المشاهدة", "Watch history": "سجل المشاهدة",
@ -137,7 +137,7 @@
"Title": "العنوان", "Title": "العنوان",
"Playlist privacy": "إعدادات الخصوصية", "Playlist privacy": "إعدادات الخصوصية",
"Editing playlist `x`": "تعديل قائمة التشغيل `x`", "Editing playlist `x`": "تعديل قائمة التشغيل `x`",
"Show more": "إظهار المزيد", "Show more": "عرض المزيد",
"Show less": "عرض اقل", "Show less": "عرض اقل",
"Watch on YouTube": "مشاهدة الفيديو على اليوتيوب", "Watch on YouTube": "مشاهدة الفيديو على اليوتيوب",
"Switch Invidious Instance": "تبديل المثيل Invidious", "Switch Invidious Instance": "تبديل المثيل Invidious",
@ -147,20 +147,20 @@
"License: ": "التراخيص: ", "License: ": "التراخيص: ",
"Family friendly? ": "محتوى عائلي؟ ", "Family friendly? ": "محتوى عائلي؟ ",
"Wilson score: ": "درجة ويلسون: ", "Wilson score: ": "درجة ويلسون: ",
"Engagement: ": "نسبة المشاركة: ", "Engagement: ": "نسبة التفاعل: ",
"Whitelisted regions: ": "الدول المسموح فيها هذا الفيديو: ", "Whitelisted regions: ": "الدول المسموح فيها هذا الفيديو: ",
"Blacklisted regions: ": "الدول المحظور فيها هذا الفيديو: ", "Blacklisted regions: ": "الدول المحظور فيها هذا الفيديو: ",
"Shared `x`": "شارك منذ `x`", "Shared `x`": "تمت المشاركة في `x`",
"Premieres in `x`": "يعرض فى `x`", "Premieres in `x`": "يعرض فى `x`",
"Premieres `x`": "يعرض `x`", "Premieres `x`": "يعرض `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.": "أهلًا! يبدو أن جافاسكريبت معطلٌ لديك. اضغط هنا لعرض التعليقات، وَضَع في اعتبارك أنها ستأخذ وقتًا أطول للتحميل.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "أهلًا! يبدو أن جافاسكريبت معطلٌ لديك. اضغط هنا لعرض التعليقات، وَضَع في اعتبارك أنها ستأخذ وقتًا أطول للتحميل.",
"View YouTube comments": "عرض تعليقات اليوتيوب", "View YouTube comments": "عرض تعليقات اليوتيوب",
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit", "View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت",
"View `x` comments": { "View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات", "([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات",
"": "عرض `x` تعليقات" "": "عرض `x` تعليقات"
}, },
"View Reddit comments": "عرض تعليقات ريدإت Reddit", "View Reddit comments": "عرض تعليقات ريديت",
"Hide replies": "إخفاء الردود", "Hide replies": "إخفاء الردود",
"Show replies": "عرض الردود", "Show replies": "عرض الردود",
"Incorrect password": "كلمة السر غير صحيحة", "Incorrect password": "كلمة السر غير صحيحة",
@ -182,20 +182,20 @@
"channel:`x`": "قناة:`x`", "channel:`x`": "قناة:`x`",
"Deleted or invalid channel": "قناة ممسوحة او غير صالحة", "Deleted or invalid channel": "قناة ممسوحة او غير صالحة",
"This channel does not exist.": "هذه القناة غير موجودة.", "This channel does not exist.": "هذه القناة غير موجودة.",
"Could not get channel info.": "لم يستطع الحصول على معلومات القناة.", "Could not get channel info.": "لم يتمكن الحصول على معلومات القناة.",
"Could not fetch comments": م يتمكن من إحضار التعليقات", "Could not fetch comments": ا يتمكن إحضار التعليقات",
"`x` ago": "`x` منذ", "`x` ago": "`x` منذ",
"Load more": "عرض المزيد", "Load more": "تحميل المزيد",
"Could not create mix.": "تعذر إنشاء مزيج.", "Could not create mix.": "تعذر إنشاء مزيج.",
"Empty playlist": "قائمة التشغيل فارغة", "Empty playlist": "قائمة التشغيل فارغة",
"Not a playlist.": "قائمة التشغيل غير صالحة.", "Not a playlist.": "قائمة التشغيل غير صالحة.",
"Playlist does not exist.": "قائمة التشغيل غير موجودة.", "Playlist does not exist.": "قائمة التشغيل غير موجودة.",
"Could not pull trending pages.": م يستطع عرض الصفحات الراجئة.", "Could not pull trending pages.": ا يتمكن عرض الصفحات الراجئة.",
"Hidden field \"challenge\" is a required field": "مكان مخفي \"تحدي\" مكان مطلوب", "Hidden field \"challenge\" is a required field": "الحقل المخفي \"تحدي\" حقل مطلوب",
"Hidden field \"token\" is a required field": "مكان مخفي \"رمز\" مكان مطلوب", "Hidden field \"token\" is a required field": "الحقل المخفي \"رمز\" حقل مطلوب",
"Erroneous challenge": "تحدي غير صالح", "Erroneous 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)": "إنجليزي (تم إنشائه تلقائيًا)",
@ -325,15 +325,15 @@
"`x` marked it with a ❤": "`x` أعجب بهذا", "`x` marked it with a ❤": "`x` أعجب بهذا",
"Audio mode": "الوضع الصوتي", "Audio mode": "الوضع الصوتي",
"Video mode": "وضع الفيديو", "Video mode": "وضع الفيديو",
"Videos": "الفيديوهات", "channel_tab_videos_label": "الفيديوهات",
"Playlists": "قوائم التشغيل", "Playlists": "قوائم التشغيل",
"Community": "المجتمع", "channel_tab_community_label": "المجتمع",
"search_filters_sort_option_relevance": "ملاؤم", "search_filters_sort_option_relevance": "ملائمة",
"search_filters_sort_option_rating": "تقييم", "search_filters_sort_option_rating": "تقييم",
"search_filters_sort_option_date": "التاريخ", "search_filters_sort_option_date": "التاريخ",
"search_filters_sort_option_views": "مشاهدات", "search_filters_sort_option_views": "مشاهدات",
"search_filters_type_label": "نوع المحتوى", "search_filters_type_label": "نوع المحتوى",
"search_filters_duration_label": "المدة الزمنية", "search_filters_duration_label": "المدة",
"search_filters_features_label": "الميزات", "search_filters_features_label": "الميزات",
"search_filters_sort_label": "فرز", "search_filters_sort_label": "فرز",
"search_filters_date_option_hour": "آخر ساعة", "search_filters_date_option_hour": "آخر ساعة",
@ -351,8 +351,8 @@
"search_filters_features_option_c_commons": "المشاع الإبداعي", "search_filters_features_option_c_commons": "المشاع الإبداعي",
"search_filters_features_option_three_d": "ثلاثي الأبعاد", "search_filters_features_option_three_d": "ثلاثي الأبعاد",
"search_filters_features_option_live": "مباشر", "search_filters_features_option_live": "مباشر",
"search_filters_features_option_four_k": "4k", "search_filters_features_option_four_k": "4K",
"search_filters_features_option_location": "الأماكن", "search_filters_features_option_location": "المكان",
"search_filters_features_option_hdr": "وضع التباين العالي", "search_filters_features_option_hdr": "وضع التباين العالي",
"Current version: ": "الإصدار الحالي: ", "Current version: ": "الإصدار الحالي: ",
"next_steps_error_message": "بعد ذلك يجب أن تحاول: ", "next_steps_error_message": "بعد ذلك يجب أن تحاول: ",
@ -360,10 +360,10 @@
"next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب", "next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب",
"search_filters_duration_option_short": "قصير (< 4 دقائق)", "search_filters_duration_option_short": "قصير (< 4 دقائق)",
"search_filters_duration_option_long": "طويل (> 20 دقيقة)", "search_filters_duration_option_long": "طويل (> 20 دقيقة)",
"footer_source_code": "شفرة المصدر", "footer_source_code": "الكود المصدر",
"footer_original_source_code": "كود المصدر الأصلي", "footer_original_source_code": "الكود المصدر الأصلي",
"footer_modfied_source_code": "شفرة المصدر المعدلة", "footer_modfied_source_code": "الكود المصدر المعدل",
"adminprefs_modified_source_code_url_label": "URL إلى مستودع التعليمات البرمجية المصدرية المعدلة", "adminprefs_modified_source_code_url_label": "URL إلى مستودع الكود المصدر المعدل",
"footer_documentation": "التوثيق", "footer_documentation": "التوثيق",
"footer_donate_page": "تبرّع", "footer_donate_page": "تبرّع",
"preferences_region_label": "بلد المحتوى: ", "preferences_region_label": "بلد المحتوى: ",
@ -398,31 +398,31 @@
"invidious": "الخيالي", "invidious": "الخيالي",
"preferences_save_player_pos_label": "حفظ موضع التشغيل: ", "preferences_save_player_pos_label": "حفظ موضع التشغيل: ",
"crash_page_you_found_a_bug": "يبدو أنك قد وجدت خطأً برمجيًّا في Invidious!", "crash_page_you_found_a_bug": "يبدو أنك قد وجدت خطأً برمجيًّا في Invidious!",
"generic_videos_count_0": "لا فيديوهات", "generic_videos_count_0": "لا يوجد فيديوهات",
"generic_videos_count_1": "فيديو واحد", "generic_videos_count_1": "فيديو واحد",
"generic_videos_count_2": "فيديوهين", "generic_videos_count_2": "فيديوهين",
"generic_videos_count_3": "{{count}} فيديوهات", "generic_videos_count_3": "{{count}} فيديوهات",
"generic_videos_count_4": "{{count}} فيديو", "generic_videos_count_4": "{{count}} فيديو",
"generic_videos_count_5": "{{count}} فيديو", "generic_videos_count_5": "{{count}} فيديو",
"generic_subscribers_count_0": "لا مشتركين", "generic_subscribers_count_0": "لا يوجد مشترك",
"generic_subscribers_count_1": "مشترك واحد", "generic_subscribers_count_1": "مشترك واحد",
"generic_subscribers_count_2": "مشتركان", "generic_subscribers_count_2": "مشتركان",
"generic_subscribers_count_3": "{{count}} مشتركين", "generic_subscribers_count_3": "{{count}} مشتركين",
"generic_subscribers_count_4": "{{count}} مشترك", "generic_subscribers_count_4": "{{count}} مشترك",
"generic_subscribers_count_5": "{{count}} مشترك", "generic_subscribers_count_5": "{{count}} مشترك",
"generic_views_count_0": "لا مشاهدات", "generic_views_count_0": "لا يوجد مشاهدة",
"generic_views_count_1": "مشاهدة واحدة", "generic_views_count_1": "مشاهدة واحدة",
"generic_views_count_2": "مشاهدتان", "generic_views_count_2": "مشاهدتان",
"generic_views_count_3": "{{count}} مشاهدات", "generic_views_count_3": "{{count}} مشاهدات",
"generic_views_count_4": "{{count}} مشاهدة", "generic_views_count_4": "{{count}} مشاهدة",
"generic_views_count_5": "{{count}} مشاهدة", "generic_views_count_5": "{{count}} مشاهدة",
"generic_subscriptions_count_0": "لا اشتراكات", "generic_subscriptions_count_0": "لا يوجد اشتراك",
"generic_subscriptions_count_1": "اشتراك واحد", "generic_subscriptions_count_1": "اشتراك واحد",
"generic_subscriptions_count_2": "اشتراكان", "generic_subscriptions_count_2": "اشتراكان",
"generic_subscriptions_count_3": "{{count}} اشتراكات", "generic_subscriptions_count_3": "{{count}} اشتراكات",
"generic_subscriptions_count_4": "{{count}} اشتراك", "generic_subscriptions_count_4": "{{count}} اشتراك",
"generic_subscriptions_count_5": "{{count}} اشتراك", "generic_subscriptions_count_5": "{{count}} اشتراك",
"generic_playlists_count_0": "لا قوائم تشغيل", "generic_playlists_count_0": "لا يوجد قوائم تشغيل",
"generic_playlists_count_1": "قائمة تشغيل واحدة", "generic_playlists_count_1": "قائمة تشغيل واحدة",
"generic_playlists_count_2": "قائمتا تشغيل", "generic_playlists_count_2": "قائمتا تشغيل",
"generic_playlists_count_3": "{{count}} قوائم تشغيل", "generic_playlists_count_3": "{{count}} قوائم تشغيل",
@ -463,10 +463,10 @@
"search_message_change_filters_or_query": "حاول توسيع استعلام البحث و / أو تغيير عوامل التصفية.", "search_message_change_filters_or_query": "حاول توسيع استعلام البحث و / أو تغيير عوامل التصفية.",
"search_filters_date_label": "تاريخ الرفع", "search_filters_date_label": "تاريخ الرفع",
"generic_count_weeks_0": "{{count}} أسبوع", "generic_count_weeks_0": "{{count}} أسبوع",
"generic_count_weeks_1": "{{count}} أسبوع", "generic_count_weeks_1": "أسبوع واحد",
"generic_count_weeks_2": "{{count}} أسبوع", "generic_count_weeks_2": "أسبوعين",
"generic_count_weeks_3": "{{count}} أسبوع", "generic_count_weeks_3": "{{count}} أسابيع",
"generic_count_weeks_4": "{{count}} أسابيع", "generic_count_weeks_4": "{{count}} أسبوع",
"generic_count_weeks_5": "{{count}} أسبوع", "generic_count_weeks_5": "{{count}} أسبوع",
"Popular enabled: ": "تم تمكين الشعبية: ", "Popular enabled: ": "تم تمكين الشعبية: ",
"search_filters_duration_option_medium": "متوسط (4-20 دقيقة)", "search_filters_duration_option_medium": "متوسط (4-20 دقيقة)",
@ -474,16 +474,16 @@
"search_filters_type_option_all": "أي نوع", "search_filters_type_option_all": "أي نوع",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"generic_count_minutes_0": "{{count}} دقيقة", "generic_count_minutes_0": "{{count}} دقيقة",
"generic_count_minutes_1": "{{count}} دقيقة", "generic_count_minutes_1": "دقيقة واحدة",
"generic_count_minutes_2": "{{count}} دقيقة", "generic_count_minutes_2": "دقيقتين",
"generic_count_minutes_3": "{{count}} دقيقة", "generic_count_minutes_3": "{{count}} دقائق",
"generic_count_minutes_4": "{{count}} دقائق", "generic_count_minutes_4": "{{count}} دقيقة",
"generic_count_minutes_5": "{{count}} دقيقة", "generic_count_minutes_5": "{{count}} دقيقة",
"generic_count_hours_0": "{{count}} ساعة", "generic_count_hours_0": "{{count}} ساعة",
"generic_count_hours_1": "{{count}} ساعة", "generic_count_hours_1": "ساعة واحدة",
"generic_count_hours_2": "{{count}} ساعة", "generic_count_hours_2": "ساعتين",
"generic_count_hours_3": "{{count}} ساعة", "generic_count_hours_3": "{{count}} ساعات",
"generic_count_hours_4": "{{count}} ساعات", "generic_count_hours_4": "{{count}} ساعة",
"generic_count_hours_5": "{{count}} ساعة", "generic_count_hours_5": "{{count}} ساعة",
"comments_view_x_replies_0": "عرض رد {{count}}", "comments_view_x_replies_0": "عرض رد {{count}}",
"comments_view_x_replies_1": "عرض رد {{count}}", "comments_view_x_replies_1": "عرض رد {{count}}",
@ -493,10 +493,10 @@
"comments_view_x_replies_5": "عرض رد {{count}}", "comments_view_x_replies_5": "عرض رد {{count}}",
"search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.", "search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
"comments_points_count_0": "{{count}} نقطة", "comments_points_count_0": "{{count}} نقطة",
"comments_points_count_1": "{{count}} نقطة", "comments_points_count_1": "نقطة واحدة",
"comments_points_count_2": "{{count}} نقطة", "comments_points_count_2": "نقطتان",
"comments_points_count_3": "{{count}} نقطة", "comments_points_count_3": "{{count}} نقط",
"comments_points_count_4": "{{count}} نقاط", "comments_points_count_4": "{{count}} نقطة",
"comments_points_count_5": "{{count}} نقطة", "comments_points_count_5": "{{count}} نقطة",
"generic_count_years_0": "{{count}} السنة", "generic_count_years_0": "{{count}} السنة",
"generic_count_years_1": "{{count}} السنة", "generic_count_years_1": "{{count}} السنة",
@ -512,17 +512,17 @@
"tokens_count_5": "الرمز المميز {{count}}", "tokens_count_5": "الرمز المميز {{count}}",
"search_filters_apply_button": "تطبيق الفلاتر المحددة", "search_filters_apply_button": "تطبيق الفلاتر المحددة",
"search_filters_duration_option_none": "أي مدة", "search_filters_duration_option_none": "أي مدة",
"subscriptions_unseen_notifs_count_0": "{{count}} إشعار غير مرئي", "subscriptions_unseen_notifs_count_0": "{{count}} إشعار جديد",
"subscriptions_unseen_notifs_count_1": "{{count}} إشعار غير مرئي", "subscriptions_unseen_notifs_count_1": "إشعار واحد جديد",
"subscriptions_unseen_notifs_count_2": "{{count}} إشعار غير مرئي", "subscriptions_unseen_notifs_count_2": "إشعارين جديدين",
"subscriptions_unseen_notifs_count_3": "{{count}} إشعار غير مرئي", "subscriptions_unseen_notifs_count_3": "{{count}} إشعارات جديدة",
"subscriptions_unseen_notifs_count_4": "{{count}} إشعارات غير مرئية", "subscriptions_unseen_notifs_count_4": "{{count}} إشعارا جديد",
"subscriptions_unseen_notifs_count_5": "{{count}} إشعار غير مرئي", "subscriptions_unseen_notifs_count_5": "{{count}} إشعار جديد",
"generic_count_days_0": "{{count}} يوم", "generic_count_days_0": "{{count}} يوم",
"generic_count_days_1": "{{count}} يوم", "generic_count_days_1": "يوم واحد",
"generic_count_days_2": "{{count}} يوم", "generic_count_days_2": "يومين",
"generic_count_days_3": "{{count}} يوم", "generic_count_days_3": "{{count}} أيام",
"generic_count_days_4": "{{count}} أيام", "generic_count_days_4": "{{count}} يوم",
"generic_count_days_5": "{{count}} يوم", "generic_count_days_5": "{{count}} يوم",
"generic_count_months_0": "{{count}} شهر", "generic_count_months_0": "{{count}} شهر",
"generic_count_months_1": "{{count}} شهر", "generic_count_months_1": "{{count}} شهر",
@ -531,10 +531,14 @@
"generic_count_months_4": "{{count}} شهور", "generic_count_months_4": "{{count}} شهور",
"generic_count_months_5": "{{count}} شهر", "generic_count_months_5": "{{count}} شهر",
"generic_count_seconds_0": "{{count}} ثانية", "generic_count_seconds_0": "{{count}} ثانية",
"generic_count_seconds_1": "{{count}} ثانية", "generic_count_seconds_1": "ثانية واحدة",
"generic_count_seconds_2": "{{count}} ثانية", "generic_count_seconds_2": "ثانيتين",
"generic_count_seconds_3": "{{count}} ثانية", "generic_count_seconds_3": "{{count}} ثوانٍ",
"generic_count_seconds_4": "{{count}} ثوانٍ", "generic_count_seconds_4": "{{count}} ثانية",
"generic_count_seconds_5": "{{count}} ثانية", "generic_count_seconds_5": "{{count}} ثانية",
"error_video_not_in_playlist": "الفيديو المطلوب غير موجود في قائمة التشغيل هذه. <a href=\"`x`\"> انقر هنا للحصول على الصفحة الرئيسية لقائمة التشغيل. </a>" "error_video_not_in_playlist": "الفيديو المطلوب غير موجود في قائمة التشغيل هذه. <a href=\"`x`\"> انقر هنا للحصول على الصفحة الرئيسية لقائمة التشغيل. </a>",
"channel_tab_shorts_label": "الفيديوهات القصيرة",
"channel_tab_streams_label": "البث المباشر",
"channel_tab_playlists_label": "قوائم التشغيل",
"channel_tab_channels_label": "القنوات"
} }

ファイルの表示

@ -51,7 +51,7 @@
"Movies": "Películes", "Movies": "Películes",
"Download": "Descarrega", "Download": "Descarrega",
"Download as: ": "Descarrega com: ", "Download as: ": "Descarrega com: ",
"Videos": "Vídeos", "channel_tab_videos_label": "Vídeos",
"search_filters_type_label": "Tipus", "search_filters_type_label": "Tipus",
"search_filters_duration_label": "Duració", "search_filters_duration_label": "Duració",
"search_filters_sort_label": "Ordena per", "search_filters_sort_label": "Ordena per",

ファイルの表示

@ -63,7 +63,7 @@
"reddit": "Reddit", "reddit": "Reddit",
"preferences_captions_label": "Výchozí titulky: ", "preferences_captions_label": "Výchozí titulky: ",
"Fallback captions: ": "Záložní titulky: ", "Fallback captions: ": "Záložní titulky: ",
"preferences_related_videos_label": "Zobrazit podobné videa: ", "preferences_related_videos_label": "Zobrazit podobná videa: ",
"preferences_annotations_label": "Zobrazovat poznámky ve výchozím nastavení: ", "preferences_annotations_label": "Zobrazovat poznámky ve výchozím nastavení: ",
"preferences_extend_desc_label": "Rozšířit automaticky popis u videa: ", "preferences_extend_desc_label": "Rozšířit automaticky popis u videa: ",
"preferences_category_visual": "Nastavení vzhledu", "preferences_category_visual": "Nastavení vzhledu",
@ -260,8 +260,8 @@
"`x` marked it with a ❤": "`x` to označil(a) se ❤", "`x` marked it with a ❤": "`x` to označil(a) se ❤",
"Audio mode": "Audiový režim", "Audio mode": "Audiový režim",
"Video mode": "Videový režim", "Video mode": "Videový režim",
"Videos": "Videa", "channel_tab_videos_label": "Videa",
"Community": "Komunita", "channel_tab_community_label": "Komunita",
"search_filters_sort_option_rating": "Hodnocení", "search_filters_sort_option_rating": "Hodnocení",
"search_filters_sort_option_date": "Datum nahrání", "search_filters_sort_option_date": "Datum nahrání",
"search_filters_sort_option_views": "Počet zhlédnutí", "search_filters_sort_option_views": "Počet zhlédnutí",
@ -488,5 +488,9 @@
"search_filters_sort_option_relevance": "Relevantnost", "search_filters_sort_option_relevance": "Relevantnost",
"search_filters_apply_button": "Použít vybrané filtry", "search_filters_apply_button": "Použít vybrané filtry",
"Popular enabled: ": "Populární povoleno: ", "Popular enabled: ": "Populární povoleno: ",
"error_video_not_in_playlist": "Požadované video v tomto playlistu neexistuje. <a href=\"`x`\">Klikněte sem pro navštívení domovské stránky playlistu.</a>" "error_video_not_in_playlist": "Požadované video v tomto playlistu neexistuje. <a href=\"`x`\">Klikněte sem pro navštívení domovské stránky playlistu.</a>",
"channel_tab_shorts_label": "Shorts",
"channel_tab_playlists_label": "Playlisty",
"channel_tab_channels_label": "Kanály",
"channel_tab_streams_label": "Živé přenosy"
} }

ファイルの表示

@ -187,7 +187,7 @@
"Esperanto": "Esperanto", "Esperanto": "Esperanto",
"Czech": "Tjekkisk", "Czech": "Tjekkisk",
"Danish": "Dansk", "Danish": "Dansk",
"Community": "Samfund", "channel_tab_community_label": "Samfund",
"Afrikaans": "Afrikansk", "Afrikaans": "Afrikansk",
"Portuguese": "Portugisisk", "Portuguese": "Portugisisk",
"Ukrainian": "Ukrainsk", "Ukrainian": "Ukrainsk",
@ -267,7 +267,7 @@
"search_filters_sort_option_rating": "Bedømmelse", "search_filters_sort_option_rating": "Bedømmelse",
"Yoruba": "Yoruba", "Yoruba": "Yoruba",
"Erroneous token": "Fejlagtig token", "Erroneous token": "Fejlagtig token",
"Videos": "Videoer", "channel_tab_videos_label": "Videoer",
"search_filters_type_option_show": "Vis", "search_filters_type_option_show": "Vis",
"Luxembourgish": "Luxemboursk", "Luxembourgish": "Luxemboursk",
"Vietnamese": "Vietnamesisk", "Vietnamese": "Vietnamesisk",

ファイルの表示

@ -325,9 +325,9 @@
"`x` marked it with a ❤": "`x` markierte es mit einem ❤", "`x` marked it with a ❤": "`x` markierte es mit einem ❤",
"Audio mode": "Audiomodus", "Audio mode": "Audiomodus",
"Video mode": "Videomodus", "Video mode": "Videomodus",
"Videos": "Videos", "channel_tab_videos_label": "Videos",
"Playlists": "Wiedergabelisten", "Playlists": "Wiedergabelisten",
"Community": "Gemeinschaft", "channel_tab_community_label": "Gemeinschaft",
"search_filters_sort_option_relevance": "Relevanz", "search_filters_sort_option_relevance": "Relevanz",
"search_filters_sort_option_rating": "Bewertung", "search_filters_sort_option_rating": "Bewertung",
"search_filters_sort_option_date": "Datum", "search_filters_sort_option_date": "Datum",
@ -471,5 +471,6 @@
"search_filters_apply_button": "Ausgewählte Filter anwenden", "search_filters_apply_button": "Ausgewählte Filter anwenden",
"search_filters_duration_option_none": "Beliebige Länge", "search_filters_duration_option_none": "Beliebige Länge",
"search_filters_date_label": "Upload-Datum", "search_filters_date_label": "Upload-Datum",
"search_filters_date_option_none": "Beliebiges Datum" "search_filters_date_option_none": "Beliebiges Datum",
"error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. <a href=\"`x`\">Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen.</a>"
} }

ファイルの表示

@ -315,9 +315,9 @@
"`x` marked it with a ❤": "Ο χρηστης `x` έβαλε ❤", "`x` marked it with a ❤": "Ο χρηστης `x` έβαλε ❤",
"Audio mode": "Λειτουργία ήχου", "Audio mode": "Λειτουργία ήχου",
"Video mode": "Λειτουργία βίντεο", "Video mode": "Λειτουργία βίντεο",
"Videos": "Βίντεο", "channel_tab_videos_label": "Βίντεο",
"Playlists": "Λίστες Αναπαραγωγής", "Playlists": "Λίστες Αναπαραγωγής",
"Community": "Κοινότητα", "channel_tab_community_label": "Κοινότητα",
"Current version: ": "Τρέχουσα έκδοση: ", "Current version: ": "Τρέχουσα έκδοση: ",
"generic_playlists_count": "{{count}} λίστα αναπαραγωγής", "generic_playlists_count": "{{count}} λίστα αναπαραγωγής",
"generic_playlists_count_plural": "{{count}} λίστες αναπαραγωγής", "generic_playlists_count_plural": "{{count}} λίστες αναπαραγωγής",

ファイルの表示

@ -188,6 +188,9 @@
"Engagement: ": "Engagement: ", "Engagement: ": "Engagement: ",
"Whitelisted regions: ": "Whitelisted regions: ", "Whitelisted regions: ": "Whitelisted regions: ",
"Blacklisted regions: ": "Blacklisted regions: ", "Blacklisted regions: ": "Blacklisted regions: ",
"Music in this video": "Music in this video",
"Artist: ": "Artist: ",
"Album: ": "Album: ",
"Shared `x`": "Shared `x`", "Shared `x`": "Shared `x`",
"Premieres in `x`": "Premieres in `x`", "Premieres in `x`": "Premieres in `x`",
"Premieres `x`": "Premieres `x`", "Premieres `x`": "Premieres `x`",
@ -404,9 +407,7 @@
"`x` marked it with a ❤": "`x` marked it with a ❤", "`x` marked it with a ❤": "`x` marked it with a ❤",
"Audio mode": "Audio mode", "Audio mode": "Audio mode",
"Video mode": "Video mode", "Video mode": "Video mode",
"Videos": "Videos",
"Playlists": "Playlists", "Playlists": "Playlists",
"Community": "Community",
"search_filters_title": "Filters", "search_filters_title": "Filters",
"search_filters_date_label": "Upload date", "search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date", "search_filters_date_option_none": "Any date",
@ -472,5 +473,11 @@
"crash_page_read_the_faq": "read the <a href=\"`x`\">Frequently Asked Questions (FAQ)</a>", "crash_page_read_the_faq": "read the <a href=\"`x`\">Frequently Asked Questions (FAQ)</a>",
"crash_page_search_issue": "searched for <a href=\"`x`\">existing issues on GitHub</a>", "crash_page_search_issue": "searched for <a href=\"`x`\">existing issues on GitHub</a>",
"crash_page_report_issue": "If none of the above helped, please <a href=\"`x`\">open a new issue on GitHub</a> (preferably in English) and include the following text in your message (do NOT translate that text):", "crash_page_report_issue": "If none of the above helped, please <a href=\"`x`\">open a new issue on GitHub</a> (preferably in English) and include the following text in your message (do NOT translate that text):",
"error_video_not_in_playlist": "The requested video doesn't exist in this playlist. <a href=\"`x`\">Click here for the playlist home page.</a>" "error_video_not_in_playlist": "The requested video doesn't exist in this playlist. <a href=\"`x`\">Click here for the playlist home page.</a>",
"channel_tab_videos_label": "Videos",
"channel_tab_shorts_label": "Shorts",
"channel_tab_streams_label": "Livestreams",
"channel_tab_playlists_label": "Playlists",
"channel_tab_community_label": "Community",
"channel_tab_channels_label": "Channels"
} }

ファイルの表示

@ -5,8 +5,8 @@
"Subscribe": "Abonu", "Subscribe": "Abonu",
"View channel on YouTube": "Vidu kanalon en JuTubo", "View channel on YouTube": "Vidu kanalon en JuTubo",
"View playlist on YouTube": "Vidu ludliston en JuTubo", "View playlist on YouTube": "Vidu ludliston en JuTubo",
"newest": "pli novaj", "newest": "plej novaj",
"oldest": "pli malnovaj", "oldest": "plej malnovaj",
"popular": "popularaj", "popular": "popularaj",
"last": "lasta", "last": "lasta",
"Next page": "Sekva paĝo", "Next page": "Sekva paĝo",
@ -325,9 +325,9 @@
"`x` marked it with a ❤": "`x` markis ĝin per ❤", "`x` marked it with a ❤": "`x` markis ĝin per ❤",
"Audio mode": "Aŭda reĝimo", "Audio mode": "Aŭda reĝimo",
"Video mode": "Videa reĝimo", "Video mode": "Videa reĝimo",
"Videos": "Filmetoj", "channel_tab_videos_label": "Videoj",
"Playlists": "Ludlistoj", "Playlists": "Ludlistoj",
"Community": "Komunumo", "channel_tab_community_label": "Komunumo",
"search_filters_sort_option_relevance": "rilateco", "search_filters_sort_option_relevance": "rilateco",
"search_filters_sort_option_rating": "takso", "search_filters_sort_option_rating": "takso",
"search_filters_sort_option_date": "dato", "search_filters_sort_option_date": "dato",
@ -472,5 +472,9 @@
"generic_subscribers_count_plural": "{{count}} abonantoj", "generic_subscribers_count_plural": "{{count}} abonantoj",
"generic_count_months": "{{count}} monato", "generic_count_months": "{{count}} monato",
"generic_count_months_plural": "{{count}} monatoj", "generic_count_months_plural": "{{count}} monatoj",
"preferences_save_player_pos_label": "Konservi ludadan pozicion: " "preferences_save_player_pos_label": "Konservi ludadan pozicion: ",
"channel_tab_streams_label": "Tujelsendoj",
"channel_tab_playlists_label": "Ludlistoj",
"channel_tab_channels_label": "Kanaloj",
"channel_tab_shorts_label": "Mallongaj"
} }

ファイルの表示

@ -325,9 +325,9 @@
"`x` marked it with a ❤": "`x` lo ha marcado con un ❤", "`x` marked it with a ❤": "`x` lo ha marcado con un ❤",
"Audio mode": "Modo de audio", "Audio mode": "Modo de audio",
"Video mode": "Modo de vídeo", "Video mode": "Modo de vídeo",
"Videos": "Vídeos", "channel_tab_videos_label": "Vídeos",
"Playlists": "Listas de reproducción", "Playlists": "Listas de reproducción",
"Community": "Comunidad", "channel_tab_community_label": "Comunidad",
"search_filters_sort_option_relevance": "relevancia", "search_filters_sort_option_relevance": "relevancia",
"search_filters_sort_option_rating": "valoración", "search_filters_sort_option_rating": "valoración",
"search_filters_sort_option_date": "fecha", "search_filters_sort_option_date": "fecha",
@ -472,5 +472,9 @@
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.", "search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
"search_filters_duration_option_medium": "Medio (4 - 20 minutes)", "search_filters_duration_option_medium": "Medio (4 - 20 minutes)",
"Popular enabled: ": "¿Habilitar la sección popular? ", "Popular enabled: ": "¿Habilitar la sección popular? ",
"error_video_not_in_playlist": "El vídeo solicitado no existe en esta lista de reproducción. <a href=\"`x`\">Haga clic aquí para acceder a la página de inicio de la lista de reproducción.</a>" "error_video_not_in_playlist": "El vídeo solicitado no existe en esta lista de reproducción. <a href=\"`x`\">Haga clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
"channel_tab_streams_label": "Directos",
"channel_tab_channels_label": "Canales",
"channel_tab_shorts_label": "Cortos",
"channel_tab_playlists_label": "Listas de reproducción"
} }

ファイルの表示

@ -296,8 +296,8 @@
"Corsican": "Korsika", "Corsican": "Korsika",
"Javanese": "Jaava", "Javanese": "Jaava",
"Lithuanian": "Leedu", "Lithuanian": "Leedu",
"Videos": "Videod", "channel_tab_videos_label": "Videod",
"Community": "Kogukond", "channel_tab_community_label": "Kogukond",
"CAPTCHA is a required field": "CAPTCHA on kohustuslik väli", "CAPTCHA is a required field": "CAPTCHA on kohustuslik väli",
"comments_points_count": "{{count}} punkt", "comments_points_count": "{{count}} punkt",
"comments_points_count_plural": "{{count}} punkti", "comments_points_count_plural": "{{count}} punkti",

ファイルの表示

@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` نشان گذاری شده با یک ❤", "`x` marked it with a ❤": "`x` نشان گذاری شده با یک ❤",
"Audio mode": "حالت صدا", "Audio mode": "حالت صدا",
"Video mode": "حالت ویدیو", "Video mode": "حالت ویدیو",
"Videos": "ویدیو ها", "channel_tab_videos_label": "ویدیو ها",
"Playlists": "سیاهه‌های پخش", "Playlists": "سیاهه‌های پخش",
"Community": "اجتماع", "channel_tab_community_label": "اجتماع",
"search_filters_sort_option_relevance": "مرتبط بودن", "search_filters_sort_option_relevance": "مرتبط بودن",
"search_filters_sort_option_rating": "امتیاز", "search_filters_sort_option_rating": "امتیاز",
"search_filters_sort_option_date": "تاریخ بارگذاری", "search_filters_sort_option_date": "تاریخ بارگذاری",
@ -411,5 +411,18 @@
"search_filters_duration_option_long": "بلند (> 20 دقیقه)", "search_filters_duration_option_long": "بلند (> 20 دقیقه)",
"adminprefs_modified_source_code_url_label": "URL مخزن کد منبع ویریش شده", "adminprefs_modified_source_code_url_label": "URL مخزن کد منبع ویریش شده",
"search_filters_duration_option_short": "کوتاه (< 4 دقیقه)", "search_filters_duration_option_short": "کوتاه (< 4 دقیقه)",
"search_filters_title": "پالایه" "search_filters_title": "پالایه",
"Chinese (Hong Kong)": "چینی (هنگ‌کنگ)",
"Dutch (auto-generated)": "هلندی (تولید خودکار)",
"preferences_watch_history_label": "فعال‌سازی تاریخچه‌ی پخش ",
"Indonesian (auto-generated)": "اندونزیایی (تولید خودکار)",
"English (United States)": "انگلیسی (ایالات متحده)",
"Chinese": "چینی",
"Chinese (Taiwan)": "چینی (تایوان)",
"French (auto-generated)": "فرانسوی (تولید خودکار)",
"English (United Kingdom)": "انگلیسی (ایالات بریتانیا)",
"search_message_no_results": "نتیجه‌ای یافت نشد.",
"search_message_change_filters_or_query": "سعی کنید جست‌و‌جوی خود را وسیع‌تر کنید و/یا فیلترها را تغییر دهید.",
"Chinese (China)": "چینی (چین)",
"German (auto-generated)": "آلمانی (تولید خودکار)"
} }

ファイルの表示

@ -324,9 +324,9 @@
"`x` marked it with a ❤": "`x` merkkasi ❤:llä", "`x` marked it with a ❤": "`x` merkkasi ❤:llä",
"Audio mode": "Äänitila", "Audio mode": "Äänitila",
"Video mode": "Videotila", "Video mode": "Videotila",
"Videos": "Videot", "channel_tab_videos_label": "Videot",
"Playlists": "Soittolistat", "Playlists": "Soittolistat",
"Community": "Yhteisö", "channel_tab_community_label": "Yhteisö",
"search_filters_sort_option_relevance": "Osuvuus", "search_filters_sort_option_relevance": "Osuvuus",
"search_filters_sort_option_rating": "Arvostelu", "search_filters_sort_option_rating": "Arvostelu",
"search_filters_sort_option_date": "Latauspäivämäärä", "search_filters_sort_option_date": "Latauspäivämäärä",
@ -471,5 +471,6 @@
"search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.", "search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
"search_filters_date_option_none": "Milloin tahansa", "search_filters_date_option_none": "Milloin tahansa",
"search_filters_type_option_all": "Mikä tahansa tyyppi", "search_filters_type_option_all": "Mikä tahansa tyyppi",
"Popular enabled: ": "Suosittu käytössä: " "Popular enabled: ": "Suosittu käytössä: ",
"error_video_not_in_playlist": "Pyydettyä videota ei löydy tästä soittolistasta. <a href=\"`x`\">Klikkaa tähän päästäksesi soittolistan etusivulle.</a>"
} }

ファイルの表示

@ -358,9 +358,9 @@
"`x` marked it with a ❤": "`x` l'a marqué d'un ❤", "`x` marked it with a ❤": "`x` l'a marqué d'un ❤",
"Audio mode": "Mode audio", "Audio mode": "Mode audio",
"Video mode": "Mode vidéo", "Video mode": "Mode vidéo",
"Videos": "Vidéos", "channel_tab_videos_label": "Vidéos",
"Playlists": "Listes de lecture", "Playlists": "Listes de lecture",
"Community": "Communauté", "channel_tab_community_label": "Communauté",
"search_filters_sort_option_relevance": "Pertinence", "search_filters_sort_option_relevance": "Pertinence",
"search_filters_sort_option_rating": "Notation", "search_filters_sort_option_rating": "Notation",
"search_filters_sort_option_date": "Date d'ajout", "search_filters_sort_option_date": "Date d'ajout",
@ -472,5 +472,9 @@
"search_filters_date_label": "Date d'ajout", "search_filters_date_label": "Date d'ajout",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_filters_duration_option_none": "Toutes les durées", "search_filters_duration_option_none": "Toutes les durées",
"error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. <a href=\"`x`\">Cliquez ici pour retourner à la liste de lecture.</a>" "error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. <a href=\"`x`\">Cliquez ici pour retourner à la liste de lecture.</a>",
"channel_tab_shorts_label": "Clips",
"channel_tab_streams_label": "En direct",
"channel_tab_playlists_label": "Listes de lecture",
"channel_tab_channels_label": "Chaînes"
} }

ファイルの表示

@ -271,9 +271,9 @@
"`x` marked it with a ❤": "סומנה ב־❤ על ידי `x`", "`x` marked it with a ❤": "סומנה ב־❤ על ידי `x`",
"Audio mode": "Audio mode", "Audio mode": "Audio mode",
"Video mode": "Video mode", "Video mode": "Video mode",
"Videos": "סרטונים", "channel_tab_videos_label": "סרטונים",
"Playlists": "פלייליסטים", "Playlists": "פלייליסטים",
"Community": "קהילה", "channel_tab_community_label": "קהילה",
"search_filters_sort_option_relevance": "רלוונטיות", "search_filters_sort_option_relevance": "רלוונטיות",
"search_filters_sort_option_rating": "דירוג", "search_filters_sort_option_rating": "דירוג",
"search_filters_sort_option_date": "תאריך העלאה", "search_filters_sort_option_date": "תאריך העלאה",

ファイルの表示

@ -401,12 +401,12 @@
"(edited)": "(संपादित)", "(edited)": "(संपादित)",
"YouTube comment permalink": "YouTube पर टिप्पणी की स्थायी कड़ी", "YouTube comment permalink": "YouTube पर टिप्पणी की स्थायी कड़ी",
"permalink": "स्थायी कड़ी", "permalink": "स्थायी कड़ी",
"Videos": "वीडियो", "channel_tab_videos_label": "वीडियो",
"`x` marked it with a ❤": "`x` ने इसे एक ❤ से चिह्नित किया", "`x` marked it with a ❤": "`x` ने इसे एक ❤ से चिह्नित किया",
"Audio mode": "ऑडियो मोड", "Audio mode": "ऑडियो मोड",
"Playlists": "प्लेलिस्ट्स", "Playlists": "प्लेलिस्ट्स",
"Video mode": "वीडियो मोड", "Video mode": "वीडियो मोड",
"Community": "समुदाय", "channel_tab_community_label": "समुदाय",
"search_filters_title": "फ़िल्टर", "search_filters_title": "फ़िल्टर",
"search_filters_date_label": "अपलोड करने का समय", "search_filters_date_label": "अपलोड करने का समय",
"search_filters_date_option_none": "कोई भी समय", "search_filters_date_option_none": "कोई भी समय",

ファイルの表示

@ -7,8 +7,8 @@
"View playlist on YouTube": "Prikaži zbirku na YouTubeu", "View playlist on YouTube": "Prikaži zbirku na YouTubeu",
"newest": "najnovije", "newest": "najnovije",
"oldest": "najstarije", "oldest": "najstarije",
"popular": "popularni", "popular": "popularne",
"last": "zadnji", "last": "zadnje",
"Next page": "Sljedeća stranica", "Next page": "Sljedeća stranica",
"Previous page": "Prethodna stranica", "Previous page": "Prethodna stranica",
"Clear watch history?": "Izbrisati povijest gledanja?", "Clear watch history?": "Izbrisati povijest gledanja?",
@ -43,9 +43,9 @@
"Time (h:mm:ss):": "Vrijeme (h:mm:ss):", "Time (h:mm:ss):": "Vrijeme (h:mm:ss):",
"Text CAPTCHA": "Tekstualni CAPTCHA", "Text CAPTCHA": "Tekstualni CAPTCHA",
"Image CAPTCHA": "Slikovni CAPTCHA", "Image CAPTCHA": "Slikovni CAPTCHA",
"Sign In": "Prijava", "Sign In": "Prijavi se",
"Register": "Registriraj se", "Register": "Registriraj se",
"E-mail": "E-mail", "E-mail": "E-mail adresa",
"Google verification code": "Googleov potvrdni kod", "Google verification code": "Googleov potvrdni kod",
"Preferences": "Postavke", "Preferences": "Postavke",
"preferences_category_player": "Postavke playera", "preferences_category_player": "Postavke playera",
@ -325,9 +325,9 @@
"`x` marked it with a ❤": "Označeno sa ❤ od `x`", "`x` marked it with a ❤": "Označeno sa ❤ od `x`",
"Audio mode": "Audio modus", "Audio mode": "Audio modus",
"Video mode": "Videomodus", "Video mode": "Videomodus",
"Videos": "Videa", "channel_tab_videos_label": "Videa",
"Playlists": "Zbirke", "Playlists": "Zbirke",
"Community": "Zajednica", "channel_tab_community_label": "Zajednica",
"search_filters_sort_option_relevance": "Značaj", "search_filters_sort_option_relevance": "Značaj",
"search_filters_sort_option_rating": "Ocjena", "search_filters_sort_option_rating": "Ocjena",
"search_filters_sort_option_date": "Datum prijenosa", "search_filters_sort_option_date": "Datum prijenosa",
@ -488,5 +488,9 @@
"search_filters_apply_button": "Primijeni odabrane filtre", "search_filters_apply_button": "Primijeni odabrane filtre",
"search_filters_type_option_all": "Bilo koja vrsta", "search_filters_type_option_all": "Bilo koja vrsta",
"Popular enabled: ": "Popularni aktivirani: ", "Popular enabled: ": "Popularni aktivirani: ",
"error_video_not_in_playlist": "Traženi video ne postoji u ovoj zbirci. <a href=\"`x`\">Pritisni ovdje za početnu stranicu zbirke.</a>" "error_video_not_in_playlist": "Traženi video ne postoji u ovoj zbirci. <a href=\"`x`\">Pritisni ovdje za početnu stranicu zbirke.</a>",
"channel_tab_streams_label": "Prijenosi uživo",
"channel_tab_playlists_label": "Zbirke",
"channel_tab_channels_label": "Kanali",
"channel_tab_shorts_label": "Kratka videa"
} }

ファイルの表示

@ -348,9 +348,9 @@
"`x` marked it with a ❤": "`x` ❤jelet adott a hozzászóláshoz", "`x` marked it with a ❤": "`x` ❤jelet adott a hozzászóláshoz",
"Audio mode": "Csak hanggal", "Audio mode": "Csak hanggal",
"Video mode": "Hanggal és képpel", "Video mode": "Hanggal és képpel",
"Videos": "Videói", "channel_tab_videos_label": "Videói",
"Playlists": "Lejátszási listái", "Playlists": "Lejátszási listái",
"Community": "Közösség", "channel_tab_community_label": "Közösség",
"Current version: ": "Jelenlegi verzió: ", "Current version: ": "Jelenlegi verzió: ",
"preferences_quality_option_medium": "Közepes", "preferences_quality_option_medium": "Közepes",
"preferences_quality_dash_option_auto": "Automatikus", "preferences_quality_dash_option_auto": "Automatikus",
@ -470,5 +470,7 @@
"search_filters_duration_option_none": "Mindegy", "search_filters_duration_option_none": "Mindegy",
"search_filters_duration_option_medium": "Átlagos (4 és 20 perc között)", "search_filters_duration_option_medium": "Átlagos (4 és 20 perc között)",
"search_filters_features_option_vr180": "180°-os virtuális valóság", "search_filters_features_option_vr180": "180°-os virtuális valóság",
"search_filters_apply_button": "Keresés a megadott szűrőkkel" "search_filters_apply_button": "Keresés a megadott szűrőkkel",
"Popular enabled: ": "Népszerű engedélyezve ",
"error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. <a href=\"`x`\">Kattintson ide a lejátszási listához jutáshoz.</a>"
} }

ファイルの表示

@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` telah ditandai dengan ❤", "`x` marked it with a ❤": "`x` telah ditandai dengan ❤",
"Audio mode": "Mode audio", "Audio mode": "Mode audio",
"Video mode": "Mode video", "Video mode": "Mode video",
"Videos": "Video", "channel_tab_videos_label": "Video",
"Playlists": "Daftar putar", "Playlists": "Daftar putar",
"Community": "Komunitas", "channel_tab_community_label": "Komunitas",
"search_filters_sort_option_relevance": "Relevansi", "search_filters_sort_option_relevance": "Relevansi",
"search_filters_sort_option_rating": "Penilaian", "search_filters_sort_option_rating": "Penilaian",
"search_filters_sort_option_date": "Tanggal Unggah", "search_filters_sort_option_date": "Tanggal Unggah",

ファイルの表示

@ -315,9 +315,9 @@
"`x` marked it with a ❤": "`x` merkti það með ❤", "`x` marked it with a ❤": "`x` merkti það með ❤",
"Audio mode": "Hljóð ham", "Audio mode": "Hljóð ham",
"Video mode": "Myndband ham", "Video mode": "Myndband ham",
"Videos": "Myndbönd", "channel_tab_videos_label": "Myndbönd",
"Playlists": "Spilunarlistar", "Playlists": "Spilunarlistar",
"Community": "Samfélag", "channel_tab_community_label": "Samfélag",
"Current version: ": "Núverandi útgáfa: ", "Current version: ": "Núverandi útgáfa: ",
"preferences_watch_history_label": "Virkja áhorfssögu: " "preferences_watch_history_label": "Virkja áhorfssögu: "
} }

ファイルの表示

@ -290,7 +290,7 @@
"Southern Sotho": "Sotho del Sud", "Southern Sotho": "Sotho del Sud",
"Spanish": "Spagnolo", "Spanish": "Spagnolo",
"Spanish (Latin America)": "Spagnolo (America latina)", "Spanish (Latin America)": "Spagnolo (America latina)",
"Sundanese": "Sudanese", "Sundanese": "Sundanese",
"Swahili": "Swahili", "Swahili": "Swahili",
"Swedish": "Svedese", "Swedish": "Svedese",
"Tajik": "Tagico", "Tajik": "Tagico",
@ -344,9 +344,8 @@
"`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤", "`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤",
"Audio mode": "Modalità audio", "Audio mode": "Modalità audio",
"Video mode": "Modalità video", "Video mode": "Modalità video",
"Videos": "Video", "channel_tab_videos_label": "Video",
"Playlists": "Playlist", "Playlists": "Playlist",
"Community": "Comunità",
"search_filters_sort_option_relevance": "Pertinenza", "search_filters_sort_option_relevance": "Pertinenza",
"search_filters_sort_option_rating": "Valutazione", "search_filters_sort_option_rating": "Valutazione",
"search_filters_sort_option_date": "Data di caricamento", "search_filters_sort_option_date": "Data di caricamento",
@ -472,5 +471,10 @@
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_filters_apply_button": "Applica filtri selezionati", "search_filters_apply_button": "Applica filtri selezionati",
"crash_page_refresh": "provato a <a href=\"`x`\">ricaricare la pagina</a>", "crash_page_refresh": "provato a <a href=\"`x`\">ricaricare la pagina</a>",
"error_video_not_in_playlist": "Il video richiesto non esiste in questa playlist. <a href=\"`x`\">Fai clic qui per la pagina iniziale della playlist.</a>" "error_video_not_in_playlist": "Il video richiesto non esiste in questa playlist. <a href=\"`x`\">Fai clic qui per la pagina iniziale della playlist.</a>",
"channel_tab_shorts_label": "Short",
"channel_tab_playlists_label": "Playlist",
"channel_tab_channels_label": "Canali",
"channel_tab_streams_label": "Livestream",
"channel_tab_community_label": "Comunità"
} }

ファイルの表示

@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` が❤を込めてマークしました", "`x` marked it with a ❤": "`x` が❤を込めてマークしました",
"Audio mode": "オーディオモード", "Audio mode": "オーディオモード",
"Video mode": "ビデオモード", "Video mode": "ビデオモード",
"Videos": "動画", "channel_tab_videos_label": "動画",
"Playlists": "プレイリスト", "Playlists": "プレイリスト",
"Community": "コミュニティ", "channel_tab_community_label": "コミュニティ",
"search_filters_sort_option_relevance": "関連", "search_filters_sort_option_relevance": "関連",
"search_filters_sort_option_rating": "評価", "search_filters_sort_option_rating": "評価",
"search_filters_sort_option_date": "時刻", "search_filters_sort_option_date": "時刻",
@ -403,7 +403,7 @@
"none": "なし", "none": "なし",
"download_subtitles": "字幕 - `x` (.vtt)", "download_subtitles": "字幕 - `x` (.vtt)",
"search_filters_features_option_purchased": "購入済み", "search_filters_features_option_purchased": "購入済み",
"preferences_quality_option_dash": "DASH (適切な品質)", "preferences_quality_option_dash": "DASH (適品質)",
"preferences_quality_dash_option_worst": "最悪", "preferences_quality_dash_option_worst": "最悪",
"preferences_quality_dash_option_best": "最高", "preferences_quality_dash_option_best": "最高",
"videoinfo_started_streaming_x_ago": "`x`分前に配信を開始", "videoinfo_started_streaming_x_ago": "`x`分前に配信を開始",
@ -438,5 +438,20 @@
"search_message_no_results": "一致する検索結果はありませんでした", "search_message_no_results": "一致する検索結果はありませんでした",
"English (United States)": "英語 (アメリカ)", "English (United States)": "英語 (アメリカ)",
"search_filters_date_label": "アップロード日", "search_filters_date_label": "アップロード日",
"search_filters_features_option_vr180": "VR180" "search_filters_features_option_vr180": "VR180",
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>しようとしました",
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
"Popular enabled: ": "人気動画を有効化 ",
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンスで検索</a>することもできます。",
"search_filters_apply_button": "選択したフィルターを適用",
"user_saved_playlists": "`x` 個の保存済みプレイリスト",
"crash_page_you_found_a_bug": "Invidious でバグを見つけたようです。",
"crash_page_refresh": "<a href=\"`x`\">ページを更新</a>しようとしました",
"preferences_watch_history_label": "視聴履歴を有効化 ",
"search_filters_date_option_none": "任意の日付",
"search_filters_type_option_all": "いかなるタイプ",
"search_filters_duration_option_none": "任意の期間",
"search_filters_duration_option_medium": "ミディアム (4 ~ 20 分)",
"preferences_save_player_pos_label": "再生位置を保存: ",
"crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。"
} }

ファイルの表示

@ -2,7 +2,7 @@
"preferences_sort_label": "동영상 정렬 기준: ", "preferences_sort_label": "동영상 정렬 기준: ",
"preferences_max_results_label": "피드에 표시된 동영상 수: ", "preferences_max_results_label": "피드에 표시된 동영상 수: ",
"Redirect homepage to feed: ": "피드로 홈페이지 리디렉션: ", "Redirect homepage to feed: ": "피드로 홈페이지 리디렉션: ",
"preferences_annotations_subscribed_label": "구독한 채널에 기본적으로 특수효과를 표시하시겠습니까? ", "preferences_annotations_subscribed_label": "구독한 채널에 기본으로 주석 표시: ",
"preferences_category_subscription": "구독 설정", "preferences_category_subscription": "구독 설정",
"preferences_automatic_instance_redirect_label": "자동 인스턴스 리디렉션 (redirect.invidious.io로 대체): ", "preferences_automatic_instance_redirect_label": "자동 인스턴스 리디렉션 (redirect.invidious.io로 대체): ",
"preferences_thin_mode_label": "단순 모드: ", "preferences_thin_mode_label": "단순 모드: ",
@ -11,7 +11,7 @@
"preferences_dark_mode_label": "테마: ", "preferences_dark_mode_label": "테마: ",
"Dark mode: ": "다크 모드: ", "Dark mode: ": "다크 모드: ",
"preferences_player_style_label": "플레이어 스타일: ", "preferences_player_style_label": "플레이어 스타일: ",
"preferences_category_visual": "시각 설정", "preferences_category_visual": "환경 설정",
"preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ", "preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ",
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ", "preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ",
"preferences_annotations_label": "기본으로 주석 표시: ", "preferences_annotations_label": "기본으로 주석 표시: ",
@ -25,8 +25,8 @@
"preferences_quality_label": "선호하는 비디오 품질: ", "preferences_quality_label": "선호하는 비디오 품질: ",
"preferences_speed_label": "기본 속도: ", "preferences_speed_label": "기본 속도: ",
"preferences_local_label": "비디오를 프록시: ", "preferences_local_label": "비디오를 프록시: ",
"preferences_listen_label": "라디오 모드 활성화: ", "preferences_listen_label": "라디오 모드: ",
"preferences_continue_autoplay_label": "다음 동영상 자동재생 ", "preferences_continue_autoplay_label": "다음 동영상 자동재생: ",
"preferences_continue_label": "다음 동영상으로 이동: ", "preferences_continue_label": "다음 동영상으로 이동: ",
"preferences_autoplay_label": "자동재생: ", "preferences_autoplay_label": "자동재생: ",
"preferences_video_loop_label": "항상 반복: ", "preferences_video_loop_label": "항상 반복: ",
@ -37,8 +37,8 @@
"Register": "회원가입", "Register": "회원가입",
"Sign In": "로그인", "Sign In": "로그인",
"preferences_category_misc": "기타 설정", "preferences_category_misc": "기타 설정",
"Image CAPTCHA": "이미지 CAPTCHA", "Image CAPTCHA": "이미지 캡차",
"Text CAPTCHA": "텍스트 CAPTCHA", "Text CAPTCHA": "텍스트 캡차",
"Time (h:mm:ss):": "시각 (h:mm:ss):", "Time (h:mm:ss):": "시각 (h:mm:ss):",
"Password": "비밀번호", "Password": "비밀번호",
"User ID": "사용자 ID", "User ID": "사용자 ID",
@ -50,15 +50,15 @@
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
"History": "역사", "History": "역사",
"Delete account?": "계정을 삭제 하시겠습니까?", "Delete account?": "계정을 삭제 하시겠습니까?",
"Export data as JSON": "데이터를 JSON으로 내보내기", "Export data as JSON": "JSON으로 데이터 내보내기",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "구독을 OPML로 내보내기 (NewPipe 및 FreeTube 용)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
"Export subscriptions as OPML": "구독을 OPML로 내보내기", "Export subscriptions as OPML": "OPML로 구독 내보내기",
"Export": "내보내기", "Export": "내보내기",
"Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)", "Import NewPipe data (.zip)": "뉴파이프 데이터 가져오기 (.zip)",
"Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)", "Import NewPipe subscriptions (.json)": "뉴파이프 구독 가져오기 (.json)",
"Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)", "Import FreeTube subscriptions (.db)": "프리튜브 구독 가져오기 (.db)",
"Import YouTube subscriptions": "유튜브 구독 가져오기", "Import YouTube subscriptions": "유튜브 구독 가져오기",
"Import Invidious data": "인비디어스 JSON 데이터 가져오기", "Import Invidious data": "인비디어스 데이터 가져오기 (.json)",
"Import": "가져오기", "Import": "가져오기",
"Import and Export Data": "데이터 가져오기 및 내보내기", "Import and Export Data": "데이터 가져오기 및 내보내기",
"No": "아니요", "No": "아니요",
@ -150,9 +150,9 @@
"Subscription manager": "구독 관리자", "Subscription manager": "구독 관리자",
"Save preferences": "설정 저장", "Save preferences": "설정 저장",
"Report statistics: ": "통계 보고: ", "Report statistics: ": "통계 보고: ",
"Registration enabled: ": "등록 활성화: ", "Registration enabled: ": "회원가입 활성화: ",
"Login enabled: ": "로그인 활성화: ", "Login enabled: ": "로그인 활성화: ",
"CAPTCHA enabled: ": "CAPTCHA 활성화: ", "CAPTCHA enabled: ": "캡차 활성화: ",
"Top enabled: ": "Top 활성화: ", "Top enabled: ": "Top 활성화: ",
"preferences_show_nick_label": "상단에 닉네임 표시: ", "preferences_show_nick_label": "상단에 닉네임 표시: ",
"preferences_feed_menu_label": "피드 메뉴: ", "preferences_feed_menu_label": "피드 메뉴: ",
@ -187,8 +187,8 @@
"Polish": "폴란드어", "Polish": "폴란드어",
"Persian": "페르시아어", "Persian": "페르시아어",
"Pashto": "파슈토어", "Pashto": "파슈토어",
"Nyanja": "체와어", "Nyanja": "냔자어",
"Norwegian Bokmål": "보크몰", "Norwegian Bokmål": "노르웨이 부크몰어",
"Nepali": "네팔어", "Nepali": "네팔어",
"Mongolian": "몽골어", "Mongolian": "몽골어",
"Marathi": "마라티어", "Marathi": "마라티어",
@ -284,10 +284,10 @@
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다", "Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
"Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요", "Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요",
"Wrong username or password": "잘못된 사용자 이름 또는 비밀번호", "Wrong username or password": "잘못된 사용자 이름 또는 비밀번호",
"Password is a required field": "비밀번호는 필수 필드입니다", "Password is a required field": "비밀번호는 필수 입력란입니다",
"User ID is a required field": "사용자 ID는 필수 필드입니다", "User ID is a required field": "사용자 ID는 필수 입력란입니다",
"CAPTCHA is a required field": "CAPTCHA는 필수 필드입니다", "CAPTCHA is a required field": "캡차는 필수 입력란입니다",
"Erroneous CAPTCHA": "잘못된 CAPTCHA", "Erroneous CAPTCHA": "잘못된 캡차",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.", "Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.",
"Blacklisted regions: ": "차단된 지역: ", "Blacklisted regions: ": "차단된 지역: ",
"Playlists": "재생목록", "Playlists": "재생목록",
@ -297,7 +297,7 @@
"Empty playlist": "재생목록 비어 있음", "Empty playlist": "재생목록 비어 있음",
"Show annotations": "주석 보이기", "Show annotations": "주석 보이기",
"Hide annotations": "주석 숨기기", "Hide annotations": "주석 숨기기",
"Switch Invidious Instance": "Invidious 인스턴스 변경", "Switch Invidious Instance": "인비디어스 인스턴스 변경",
"Spanish": "스페인어", "Spanish": "스페인어",
"Southern Sotho": "소토어", "Southern Sotho": "소토어",
"Somali": "소말리어", "Somali": "소말리어",
@ -347,8 +347,8 @@
"search_filters_sort_option_date": "업로드 날짜", "search_filters_sort_option_date": "업로드 날짜",
"search_filters_sort_option_rating": "평점", "search_filters_sort_option_rating": "평점",
"search_filters_sort_option_relevance": "관련성", "search_filters_sort_option_relevance": "관련성",
"Community": "커뮤니티", "channel_tab_community_label": "커뮤니티",
"Videos": "동영상", "channel_tab_videos_label": "동영상",
"Video mode": "비디오 모드", "Video mode": "비디오 모드",
"Audio mode": "오디오 모드", "Audio mode": "오디오 모드",
"permalink": "퍼머링크", "permalink": "퍼머링크",
@ -383,7 +383,7 @@
"adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL", "adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL",
"search_filters_title": "필터", "search_filters_title": "필터",
"preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_4320p": "4320p",
"Popular enabled: ": "인기 급상승 활성화: ", "Popular enabled: ": "인기 활성화: ",
"Dutch (auto-generated)": "네덜란드어 (자동 생성됨)", "Dutch (auto-generated)": "네덜란드어 (자동 생성됨)",
"Chinese (Hong Kong)": "중국어 (홍콩)", "Chinese (Hong Kong)": "중국어 (홍콩)",
"Chinese (Taiwan)": "중국어 (대만)", "Chinese (Taiwan)": "중국어 (대만)",
@ -415,7 +415,7 @@
"Spanish (auto-generated)": "스페인어 (자동 생성됨)", "Spanish (auto-generated)": "스페인어 (자동 생성됨)",
"preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_worst": "최저", "preferences_quality_dash_option_worst": "최저",
"preferences_watch_history_label": "시청 기록 활성화: ", "preferences_watch_history_label": "시청 기록 저장: ",
"invidious": "인비디어스", "invidious": "인비디어스",
"preferences_quality_option_small": "낮음", "preferences_quality_option_small": "낮음",
"preferences_quality_dash_option_auto": "자동", "preferences_quality_dash_option_auto": "자동",
@ -439,10 +439,10 @@
"footer_donate_page": "기부하기", "footer_donate_page": "기부하기",
"preferences_quality_option_dash": "DASH (다양한 화질)", "preferences_quality_option_dash": "DASH (다양한 화질)",
"preferences_quality_dash_option_360p": "360p", "preferences_quality_dash_option_360p": "360p",
"preferences_save_player_pos_label": "이어서 보기 활성화: ", "preferences_save_player_pos_label": "이어서 보기: ",
"none": "없음", "none": "없음",
"videoinfo_started_streaming_x_ago": "`x` 전에 스트리밍을 시작했습니다", "videoinfo_started_streaming_x_ago": "`x` 전에 스트리밍을 시작했습니다",
"crash_page_you_found_a_bug": "Invidious에서 버그를 찾은 것 같습니다!", "crash_page_you_found_a_bug": "인비디어스에서 버그를 찾은 것 같습니다!",
"download_subtitles": "자막 - `x`(.vtt)", "download_subtitles": "자막 - `x`(.vtt)",
"user_saved_playlists": "`x`개의 저장된 재생목록", "user_saved_playlists": "`x`개의 저장된 재생목록",
"crash_page_before_reporting": "버그를 보고하기 전에 다음 사항이 있는지 확인합니다:", "crash_page_before_reporting": "버그를 보고하기 전에 다음 사항이 있는지 확인합니다:",
@ -456,5 +456,9 @@
"crash_page_report_issue": "위의 방법 중 어느 것도 도움이 되지 않았다면, <a href=\"`x`\">깃허브에서 새 이슈를 열고</a>(가능하면 영어로) 메시지에 다음 텍스트를 포함하세요(해당 텍스트를 번역하지 마십시오):", "crash_page_report_issue": "위의 방법 중 어느 것도 도움이 되지 않았다면, <a href=\"`x`\">깃허브에서 새 이슈를 열고</a>(가능하면 영어로) 메시지에 다음 텍스트를 포함하세요(해당 텍스트를 번역하지 마십시오):",
"videoinfo_youTube_embed_link": "임베드", "videoinfo_youTube_embed_link": "임베드",
"videoinfo_invidious_embed_link": "임베드 링크", "videoinfo_invidious_embed_link": "임베드 링크",
"error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. <a href=\"`x`\">재생목록 목록을 보려면 여기를 클릭하십시오.</a>" "error_video_not_in_playlist": "요청한 동영상이 이 재생목록에 없습니다. <a href=\"`x`\">재생목록 목록을 보려면 여기를 클릭하십시오.</a>",
"channel_tab_shorts_label": "쇼츠",
"channel_tab_streams_label": "실시간 스트리밍",
"channel_tab_channels_label": "채널",
"channel_tab_playlists_label": "재생목록"
} }

ファイルの表示

@ -325,9 +325,9 @@
"`x` marked it with a ❤": "`x` pažymėjo tai su ❤", "`x` marked it with a ❤": "`x` pažymėjo tai su ❤",
"Audio mode": "Garso rėžimas", "Audio mode": "Garso rėžimas",
"Video mode": "Vaizdo rėžimas", "Video mode": "Vaizdo rėžimas",
"Videos": "Vaizdo įrašai", "channel_tab_videos_label": "Vaizdo įrašai",
"Playlists": "Grojaraiščiai", "Playlists": "Grojaraiščiai",
"Community": "Bendruomenė", "channel_tab_community_label": "Bendruomenė",
"search_filters_sort_option_relevance": "Aktualumas", "search_filters_sort_option_relevance": "Aktualumas",
"search_filters_sort_option_rating": "Reitingas", "search_filters_sort_option_rating": "Reitingas",
"search_filters_sort_option_date": "Įkėlimo data", "search_filters_sort_option_date": "Įkėlimo data",

ファイルの表示

@ -325,9 +325,9 @@
"`x` marked it with a ❤": "`x` levnet et ❤", "`x` marked it with a ❤": "`x` levnet et ❤",
"Audio mode": "Lydmodus", "Audio mode": "Lydmodus",
"Video mode": "Video-modus", "Video mode": "Video-modus",
"Videos": "Videoer", "channel_tab_videos_label": "Videoer",
"Playlists": "Spillelister", "Playlists": "Spillelister",
"Community": "Gemenskap", "channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "relevans", "search_filters_sort_option_relevance": "relevans",
"search_filters_sort_option_rating": "vurdering", "search_filters_sort_option_rating": "vurdering",
"search_filters_sort_option_date": "dato", "search_filters_sort_option_date": "dato",

ファイルの表示

@ -320,9 +320,9 @@
"`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤", "`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤",
"Audio mode": "Audiomodus", "Audio mode": "Audiomodus",
"Video mode": "Videomodus", "Video mode": "Videomodus",
"Videos": "Video's", "channel_tab_videos_label": "Video's",
"Playlists": "Afspeellijsten", "Playlists": "Afspeellijsten",
"Community": "Gemeenschap", "channel_tab_community_label": "Gemeenschap",
"search_filters_sort_option_relevance": "relevantie", "search_filters_sort_option_relevance": "relevantie",
"search_filters_sort_option_rating": "beoordeling", "search_filters_sort_option_rating": "beoordeling",
"search_filters_sort_option_date": "datum", "search_filters_sort_option_date": "datum",

1
locales/or.json ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
{}

ファイルの表示

@ -324,9 +324,9 @@
"`x` marked it with a ❤": "`x` oznaczonych ❤", "`x` marked it with a ❤": "`x` oznaczonych ❤",
"Audio mode": "Tryb audio", "Audio mode": "Tryb audio",
"Video mode": "Tryb wideo", "Video mode": "Tryb wideo",
"Videos": "Filmy", "channel_tab_videos_label": "Wideo",
"Playlists": "Playlisty", "Playlists": "Playlisty",
"Community": "Społeczność", "channel_tab_community_label": "Społeczność",
"search_filters_sort_option_relevance": "Trafność", "search_filters_sort_option_relevance": "Trafność",
"search_filters_sort_option_rating": "Ocena", "search_filters_sort_option_rating": "Ocena",
"search_filters_sort_option_date": "Data przesłania", "search_filters_sort_option_date": "Data przesłania",
@ -488,5 +488,9 @@
"search_message_use_another_instance": " Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.", "search_message_use_another_instance": " Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
"search_filters_type_option_all": "Dowolny typ", "search_filters_type_option_all": "Dowolny typ",
"search_filters_duration_option_none": "Dowolna długość", "search_filters_duration_option_none": "Dowolna długość",
"search_filters_duration_option_medium": "Średnia (4-20 minut)" "search_filters_duration_option_medium": "Średnia (4-20 minut)",
"channel_tab_streams_label": "Na żywo",
"channel_tab_channels_label": "Kanały",
"channel_tab_playlists_label": "Playlisty",
"channel_tab_shorts_label": "Shorts"
} }

ファイルの表示

@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` foi marcado como ❤", "`x` marked it with a ❤": "`x` foi marcado como ❤",
"Audio mode": "Modo de áudio", "Audio mode": "Modo de áudio",
"Video mode": "Modo de vídeo", "Video mode": "Modo de vídeo",
"Videos": "Vídeos", "channel_tab_videos_label": "Vídeos",
"Playlists": "Listas de reprodução", "Playlists": "Listas de reprodução",
"Community": "Comunidade", "channel_tab_community_label": "Comunidade",
"search_filters_sort_option_relevance": "relevância", "search_filters_sort_option_relevance": "relevância",
"search_filters_sort_option_rating": "avaliação", "search_filters_sort_option_rating": "avaliação",
"search_filters_sort_option_date": "data", "search_filters_sort_option_date": "data",
@ -471,5 +471,6 @@
"Turkish (auto-generated)": "Turco (gerado automaticamente)", "Turkish (auto-generated)": "Turco (gerado automaticamente)",
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)", "search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"Popular enabled: ": "Popular habilitado: " "Popular enabled: ": "Popular habilitado: ",
"error_video_not_in_playlist": "O vídeo solicitado não existe nesta playlist. <a href=\"`x`\">Clique aqui para acessar a página inicial da playlist.</a>"
} }

ファイルの表示

@ -22,14 +22,14 @@
"Import and Export Data": "Importar e exportar dados", "Import and Export Data": "Importar e exportar dados",
"Import": "Importar", "Import": "Importar",
"Import Invidious data": "Importar dados JSON do Invidious", "Import Invidious data": "Importar dados JSON do Invidious",
"Import YouTube subscriptions": "Importar subscrições OPML ou do YouTube", "Import YouTube subscriptions": "Importar subscrições do YouTube/OPML",
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
"Export": "Exportar", "Export": "Exportar",
"Export subscriptions as OPML": "Exportar subscrições como OPML", "Export subscriptions as OPML": "Exportar subscrições como OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)",
"Export data as JSON": "Exportar dados do Invidious como JSON", "Export data as JSON": "Exportar dados Invidious como JSON",
"Delete account?": "Eliminar conta?", "Delete account?": "Eliminar conta?",
"History": "Histórico", "History": "Histórico",
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube", "An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` foi marcado como ❤", "`x` marked it with a ❤": "`x` foi marcado como ❤",
"Audio mode": "Modo de áudio", "Audio mode": "Modo de áudio",
"Video mode": "Modo de vídeo", "Video mode": "Modo de vídeo",
"Videos": "Vídeos", "channel_tab_videos_label": "Vídeos",
"Playlists": "Listas de reprodução", "Playlists": "Listas de reprodução",
"Community": "Comunidade", "channel_tab_community_label": "Comunidade",
"search_filters_sort_option_relevance": "Relevância", "search_filters_sort_option_relevance": "Relevância",
"search_filters_sort_option_rating": "Avaliação", "search_filters_sort_option_rating": "Avaliação",
"search_filters_sort_option_date": "Data de envio", "search_filters_sort_option_date": "Data de envio",
@ -379,24 +379,24 @@
"generic_videos_count_plural": "{{count}} vídeos", "generic_videos_count_plural": "{{count}} vídeos",
"generic_playlists_count": "{{count}} lista de reprodução", "generic_playlists_count": "{{count}} lista de reprodução",
"generic_playlists_count_plural": "{{count}} listas de reprodução", "generic_playlists_count_plural": "{{count}} listas de reprodução",
"generic_subscriptions_count": "{{count}} subscrição", "generic_subscriptions_count": "{{count}} inscrição",
"generic_subscriptions_count_plural": "{{count}} subscrições", "generic_subscriptions_count_plural": "{{count}} inscrições",
"generic_views_count": "{{count}} visualização", "generic_views_count": "{{count}} visualização",
"generic_views_count_plural": "{{count}} visualizações", "generic_views_count_plural": "{{count}} visualizações",
"generic_subscribers_count": "{{count}} subscritor", "generic_subscribers_count": "{{count}} inscrito",
"generic_subscribers_count_plural": "{{count}} subscritores", "generic_subscribers_count_plural": "{{count}} inscritos",
"preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_label": "Qualidade de vídeo DASH preferencial ", "preferences_quality_dash_label": "Qualidade de vídeo DASH preferida: ",
"preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_2160p": "2160p",
"subscriptions_unseen_notifs_count": "{{count}} notificação por ver", "subscriptions_unseen_notifs_count": "{{count}} notificação não vista",
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações por ver", "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas",
"Popular enabled: ": "Página \"Popular\" ativada: ", "Popular enabled: ": "Página \"popular\" ativada: ",
"search_message_no_results": "Nenhum resultado encontrado.", "search_message_no_results": "Nenhum resultado encontrado.",
"preferences_quality_dash_option_auto": "Automática", "preferences_quality_dash_option_auto": "Automático",
"preferences_region_label": "País para o conteúdo: ", "preferences_region_label": "País do conteúdo: ",
"preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_720p": "720p", "preferences_quality_dash_option_720p": "720p",
"preferences_watch_history_label": "Ativar histórico de visualizações ", "preferences_watch_history_label": "Ativar histórico de reprodução: ",
"preferences_quality_dash_option_best": "Melhor", "preferences_quality_dash_option_best": "Melhor",
"preferences_quality_dash_option_worst": "Pior", "preferences_quality_dash_option_worst": "Pior",
"preferences_quality_dash_option_144p": "144p", "preferences_quality_dash_option_144p": "144p",
@ -404,13 +404,13 @@
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
"preferences_quality_option_dash": "DASH (qualidade adaptativa)", "preferences_quality_option_dash": "DASH (qualidade adaptativa)",
"preferences_quality_option_medium": "Média", "preferences_quality_option_medium": "Média",
"preferences_quality_option_small": "Pequena", "preferences_quality_option_small": "Baixa",
"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", "Video unavailable": "Vídeo não disponível",
"Russian (auto-generated)": "Russo (geradas automaticamente)", "Russian (auto-generated)": "Russo (gerado automaticamente)",
"comments_view_x_replies": "Ver {{count}} resposta", "comments_view_x_replies": "Ver {{count}} resposta",
"comments_view_x_replies_plural": "Ver {{count}} respostas", "comments_view_x_replies_plural": "Ver {{count}} respostas",
"comments_points_count": "{{count}} ponto", "comments_points_count": "{{count}} ponto",
@ -418,18 +418,18 @@
"English (United Kingdom)": "Inglês (Reino Unido)", "English (United Kingdom)": "Inglês (Reino Unido)",
"Chinese (Hong Kong)": "Chinês (Hong Kong)", "Chinese (Hong Kong)": "Chinês (Hong Kong)",
"Chinese (Taiwan)": "Chinês (Taiwan)", "Chinese (Taiwan)": "Chinês (Taiwan)",
"Dutch (auto-generated)": "Holandês (geradas automaticamente)", "Dutch (auto-generated)": "Holandês (gerado automaticamente)",
"French (auto-generated)": "Francês (geradas automaticamente)", "French (auto-generated)": "Francês (gerado automaticamente)",
"German (auto-generated)": "Alemão (geradas automaticamente)", "German (auto-generated)": "Alemão (gerado automaticamente)",
"Indonesian (auto-generated)": "Indonésio (geradas automaticamente)", "Indonesian (auto-generated)": "Indonésio (gerado automaticamente)",
"Interlingue": "Interlingue", "Interlingue": "Interlíngua",
"Italian (auto-generated)": "Italiano (geradas automaticamente)", "Italian (auto-generated)": "Italiano (gerado automaticamente)",
"Japanese (auto-generated)": "Japonês (geradas automaticamente)", "Japanese (auto-generated)": "Japonês (gerado automaticamente)",
"Korean (auto-generated)": "Coreano (geradas automaticamente)", "Korean (auto-generated)": "Coreano (gerado automaticamente)",
"Portuguese (auto-generated)": "Português (geradas automaticamente)", "Portuguese (auto-generated)": "Português (gerado automaticamente)",
"Portuguese (Brazil)": "Português (Brasil)", "Portuguese (Brazil)": "Português (Brasil)",
"Spanish (Spain)": "Espanhol (Espanha)", "Spanish (Spain)": "Espanhol (Espanha)",
"Vietnamese (auto-generated)": "Vietnamita (geradas automaticamente)", "Vietnamese (auto-generated)": "Vietnamita (gerado automaticamente)",
"search_filters_type_option_all": "Qualquer tipo", "search_filters_type_option_all": "Qualquer tipo",
"search_filters_duration_option_none": "Qualquer duração", "search_filters_duration_option_none": "Qualquer duração",
"search_filters_duration_option_short": "Curto (< 4 minutos)", "search_filters_duration_option_short": "Curto (< 4 minutos)",
@ -438,29 +438,39 @@
"search_filters_features_option_purchased": "Comprado", "search_filters_features_option_purchased": "Comprado",
"search_filters_apply_button": "Aplicar filtros selecionados", "search_filters_apply_button": "Aplicar filtros selecionados",
"videoinfo_watch_on_youTube": "Ver no YouTube", "videoinfo_watch_on_youTube": "Ver no YouTube",
"videoinfo_youTube_embed_link": "Embutir", "videoinfo_youTube_embed_link": "Incorporar",
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte modificado", "adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
"videoinfo_invidious_embed_link": "Ligação embutida", "videoinfo_invidious_embed_link": "Incorporar hiperligação",
"none": "nenhum", "none": "nenhum",
"videoinfo_started_streaming_x_ago": "Entrou em direto há `x`", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
"download_subtitles": "Legendas - `x` (.vtt)", "download_subtitles": "Legendas - `x` (.vtt)",
"user_created_playlists": "`x` listas de reprodução criadas", "user_created_playlists": "`x` listas de reprodução criadas",
"user_saved_playlists": "`x` listas de reprodução guardadas", "user_saved_playlists": "`x` listas de reprodução guardadas",
"preferences_save_player_pos_label": "Guardar posição de reprodução: ", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
"Turkish (auto-generated)": "Turco (geradas automaticamente)", "Turkish (auto-generated)": "Turco (gerado automaticamente)",
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)", "Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
"Chinese (China)": "Chinês (China)", "Chinese (China)": "Chinês (China)",
"Spanish (auto-generated)": "Espanhol (geradas automaticamente)", "Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
"Spanish (Mexico)": "Espanhol (México)", "Spanish (Mexico)": "Espanhol (México)",
"English (United States)": "Inglês (Estados Unidos)", "English (United States)": "Inglês (Estados Unidos)",
"footer_donate_page": "Doar", "footer_donate_page": "Doar",
"footer_documentation": "Documentação", "footer_documentation": "Documentação",
"footer_source_code": "Código-fonte", "footer_source_code": "Código-fonte",
"footer_original_source_code": "Código-fonte original", "footer_original_source_code": "Código-fonte original",
"footer_modfied_source_code": "Código-fonte modificado", "footer_modfied_source_code": "Código-fonte alterado",
"Chinese": "Chinês", "Chinese": "Chinês",
"search_filters_date_label": "Data de carregamento", "search_filters_date_label": "Data de publicação",
"search_filters_date_option_none": "Qualquer data", "search_filters_date_option_none": "Qualquer data",
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_vr180": "VR180" "search_filters_features_option_vr180": "VR180",
"search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!",
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>"
} }

ファイルの表示

@ -267,9 +267,9 @@
"Next page": "Próxima página", "Next page": "Próxima página",
"last": "últimos", "last": "últimos",
"Current version: ": "Versão atual: ", "Current version: ": "Versão atual: ",
"Community": "Comunidade", "channel_tab_community_label": "Comunidade",
"Playlists": "Listas de reprodução", "Playlists": "Listas de reprodução",
"Videos": "Vídeos", "channel_tab_videos_label": "Vídeos",
"Video mode": "Modo de vídeo", "Video mode": "Modo de vídeo",
"Audio mode": "Modo de áudio", "Audio mode": "Modo de áudio",
"`x` marked it with a ❤": "`x` foi marcado como ❤", "`x` marked it with a ❤": "`x` foi marcado como ❤",

ファイルの表示

@ -315,9 +315,9 @@
"`x` marked it with a ❤": "`x` l-a marcat cu o ❤", "`x` marked it with a ❤": "`x` l-a marcat cu o ❤",
"Audio mode": "Mod audio", "Audio mode": "Mod audio",
"Video mode": "Mod video", "Video mode": "Mod video",
"Videos": "Videoclipuri", "channel_tab_videos_label": "Videoclipuri",
"Playlists": "Liste de redare", "Playlists": "Liste de redare",
"Community": "Comunitate", "channel_tab_community_label": "Comunitate",
"Current version: ": "Versiunea actuală: ", "Current version: ": "Versiunea actuală: ",
"crash_page_read_the_faq": "citit lista <a href=\"`x`\">Întrebărilor Frecvente (FAQ)</a>", "crash_page_read_the_faq": "citit lista <a href=\"`x`\">Întrebărilor Frecvente (FAQ)</a>",
"generic_count_days_0": "{{count}} zi", "generic_count_days_0": "{{count}} zi",

ファイルの表示

@ -325,9 +325,9 @@
"`x` marked it with a ❤": "❤ от автора канала \"`x`\"", "`x` marked it with a ❤": "❤ от автора канала \"`x`\"",
"Audio mode": "Аудио режим", "Audio mode": "Аудио режим",
"Video mode": "Видео режим", "Video mode": "Видео режим",
"Videos": "Видео", "channel_tab_videos_label": "Видео",
"Playlists": "Плейлисты", "Playlists": "Плейлисты",
"Community": "Сообщество", "channel_tab_community_label": "Сообщество",
"search_filters_sort_option_relevance": "по актуальности", "search_filters_sort_option_relevance": "по актуальности",
"search_filters_sort_option_rating": "по рейтингу", "search_filters_sort_option_rating": "по рейтингу",
"search_filters_sort_option_date": "по дате загрузки", "search_filters_sort_option_date": "по дате загрузки",

ファイルの表示

@ -222,7 +222,7 @@
"About": "O aplikaciji", "About": "O aplikaciji",
"%A %B %-d, %Y": "%A %-d %B %Y", "%A %B %-d, %Y": "%A %-d %B %Y",
"Audio mode": "Avdio način", "Audio mode": "Avdio način",
"Videos": "Videoposnetki", "channel_tab_videos_label": "Videoposnetki",
"search_filters_date_label": "Datum nalaganja", "search_filters_date_label": "Datum nalaganja",
"search_filters_date_option_today": "Danes", "search_filters_date_option_today": "Danes",
"search_filters_date_option_week": "Ta teden", "search_filters_date_option_week": "Ta teden",
@ -455,7 +455,7 @@
"Download": "Prenesi", "Download": "Prenesi",
"permalink": "stalna povezava", "permalink": "stalna povezava",
"`x` marked it with a ❤": "`x` ga je označil/a z ❤", "`x` marked it with a ❤": "`x` ga je označil/a z ❤",
"Community": "Skupnost", "channel_tab_community_label": "Skupnost",
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"Video mode": "Video način", "Video mode": "Video način",
"search_filters_features_option_c_commons": "Creative Commons", "search_filters_features_option_c_commons": "Creative Commons",

ファイルの表示

@ -259,10 +259,10 @@
"YouTube comment permalink": "Permalidhje komenti YouTube", "YouTube comment permalink": "Permalidhje komenti YouTube",
"Audio mode": "Mënyrë për audion", "Audio mode": "Mënyrë për audion",
"Playlists": "Luajlista", "Playlists": "Luajlista",
"Community": "Bashkësi", "channel_tab_community_label": "Bashkësi",
"search_filters_sort_option_relevance": "Rëndësi", "search_filters_sort_option_relevance": "Rëndësi",
"Video mode": "Mënyrë video", "Video mode": "Mënyrë video",
"Videos": "Video", "channel_tab_videos_label": "Video",
"search_filters_sort_option_rating": "Vlerësim", "search_filters_sort_option_rating": "Vlerësim",
"search_filters_sort_option_date": "Datë ngarkimi", "search_filters_sort_option_date": "Datë ngarkimi",
"search_filters_sort_option_views": "Numër parjesh", "search_filters_sort_option_views": "Numër parjesh",
@ -446,6 +446,22 @@
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML", "Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON", "Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ", "preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
"Shared `x`": "Ndau me të tjerë `x`", "Shared `x`": "Ndarë me të tjerë më `x`",
"search_filters_title": "Filtra" "search_filters_title": "Filtra",
"Popular enabled: ": "Me populloret të aktivizuara: ",
"error_video_not_in_playlist": "Videoja e kërkuar sekziston në këtë luajlistë. <a href=\"`x`\">Klikoni këtu për faqen hyrëse të luajlistës.</a>",
"search_message_use_another_instance": " Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.",
"search_filters_date_label": "Datë ngarkimi",
"preferences_watch_history_label": "Aktivizo historik parjesh: ",
"Top enabled: ": "Me kryesueset të aktivizuara: ",
"preferences_video_loop_label": "Përsërite gjithmonë: ",
"search_message_no_results": "Su gjetën përfundime.",
"Could not pull trending pages.": "Su morën dot faqet në modë.",
"search_filters_date_option_none": "Çfarëdo date",
"search_message_change_filters_or_query": "Provoni të zgjeroni kërkesën tuaj të kërkimit dhe/ose të ndryshoni filtrat.",
"search_filters_type_option_all": "Çfarëdo lloji",
"search_filters_duration_option_none": "Çfarëdo kohëzgjatjeje",
"search_filters_duration_option_medium": "Mesatare (4 - 20 minuta)",
"search_filters_features_option_vr180": "VR180",
"search_filters_apply_button": "Apliko filtrat e përzgjedhur"
} }

ファイルの表示

@ -257,7 +257,7 @@
"preferences_volume_label": "Jačina zvuka: ", "preferences_volume_label": "Jačina zvuka: ",
"preferences_locale_label": "Jezik: ", "preferences_locale_label": "Jezik: ",
"adminprefs_modified_source_code_url_label": "URL veza do skladišta sa Izmenjenom Izvornom Kodom", "adminprefs_modified_source_code_url_label": "URL veza do skladišta sa Izmenjenom Izvornom Kodom",
"Community": "Zajednica", "channel_tab_community_label": "Zajednica",
"Video mode": "Video mod", "Video mode": "Video mod",
"Fallback captions: ": "Titl u slučaju da glavni nije dostupan: ", "Fallback captions: ": "Titl u slučaju da glavni nije dostupan: ",
"Private": "Privatno", "Private": "Privatno",
@ -289,7 +289,7 @@
"Erroneous token": "Pogrešan žeton", "Erroneous token": "Pogrešan žeton",
"Czech": "Češki", "Czech": "Češki",
"Latin": "Latinski", "Latin": "Latinski",
"Videos": "Video klipovi", "channel_tab_videos_label": "Video klipovi",
"search_filters_features_option_four_k": "4К", "search_filters_features_option_four_k": "4К",
"footer_donate_page": "Doniraj", "footer_donate_page": "Doniraj",
"English": "Engleski", "English": "Engleski",

ファイルの表示

@ -245,7 +245,7 @@
"(edited)": "(измењено)", "(edited)": "(измењено)",
"`x` marked it with a ❤": "`x` је означио/ла ово са ❤", "`x` marked it with a ❤": "`x` је означио/ла ово са ❤",
"Audio mode": "Аудио мод", "Audio mode": "Аудио мод",
"Videos": "Видео клипови", "channel_tab_videos_label": "Видео клипови",
"search_filters_sort_option_views": "Број прегледа", "search_filters_sort_option_views": "Број прегледа",
"search_filters_features_label": "Карактеристике", "search_filters_features_label": "Карактеристике",
"search_filters_date_option_today": "Данас", "search_filters_date_option_today": "Данас",
@ -298,7 +298,7 @@
"Ukrainian": "Украјински", "Ukrainian": "Украјински",
"permalink": "трајна веза", "permalink": "трајна веза",
"Pashto": "Паштунски", "Pashto": "Паштунски",
"Community": "Заједница", "channel_tab_community_label": "Заједница",
"Sindhi": "Синди", "Sindhi": "Синди",
"Could not fetch comments": "Узимање коментара није успело", "Could not fetch comments": "Узимање коментара није успело",
"Bangla": "Бангла/Бенгалски", "Bangla": "Бангла/Бенгалски",

ファイルの表示

@ -323,9 +323,9 @@
"`x` marked it with a ❤": "`x` lämnade ett ❤", "`x` marked it with a ❤": "`x` lämnade ett ❤",
"Audio mode": "Ljudläge", "Audio mode": "Ljudläge",
"Video mode": "Videoläge", "Video mode": "Videoläge",
"Videos": "Videor", "channel_tab_videos_label": "Videor",
"Playlists": "Spellistor", "Playlists": "Spellistor",
"Community": "Gemenskap", "channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "Relevans", "search_filters_sort_option_relevance": "Relevans",
"search_filters_sort_option_rating": "Rankning", "search_filters_sort_option_rating": "Rankning",
"search_filters_sort_option_date": "Datum", "search_filters_sort_option_date": "Datum",

ファイルの表示

@ -1,126 +1,126 @@
{ {
"LIVE": "CANLI", "LIVE": "CANLI",
"Shared `x` ago": "`x` önce paylaşıldı", "Shared `x` ago": "`x` Önce Paylaşıldı",
"Unsubscribe": "Abonelikten çık", "Unsubscribe": "Abonelikten Çık",
"Subscribe": "Abone ol", "Subscribe": "Abone Ol",
"View channel on YouTube": "Kanalı YouTube'da görüntüle", "View channel on YouTube": "Kanalı YouTube'da Görüntüle",
"View playlist on YouTube": "Oynatma listesini YouTube'da görüntüle", "View playlist on YouTube": "Oynatma Listesini YouTube'da Görüntüle",
"newest": "en yeni", "newest": "En Yeni",
"oldest": "en eski", "oldest": "En Eski",
"popular": "popüler", "popular": "Popüler",
"last": "son", "last": "Son",
"Next page": "Sonraki sayfa", "Next page": "Sonraki Sayfa",
"Previous page": "Önceki sayfa", "Previous page": "Önceki Sayfa",
"Clear watch history?": "İzleme geçmişi temizlensin mi?", "Clear watch history?": "İzleme geçmişi temizlensin mi?",
"New password": "Yeni parola", "New password": "Yeni Parola",
"New passwords must match": "Yeni parolalar eşleşmek zorunda", "New passwords must match": "Yeni Parolalar Eşleşmek Zorunda",
"Cannot change password for Google accounts": "Google hesapları için parola değiştirilemez", "Cannot change password for Google accounts": "Google Hesapları İçin Parola Değiştirilemez",
"Authorize token?": "Belirteç yetkilendirilsin mi?", "Authorize token?": "Belirteç yetkilendirilsin mi?",
"Authorize token for `x`?": "`x` için belirteç yetkilendirilsin mi?", "Authorize token for `x`?": "`x` için belirteç yetkilendirilsin mi?",
"Yes": "Evet", "Yes": "Evet",
"No": "Hayır", "No": "Hayır",
"Import and Export Data": "Verileri İçe ve Dışa Aktar", "Import and Export Data": "Verileri İçe ve Dışa Aktar",
"Import": "İçe aktar", "Import": "İçe Aktar",
"Import Invidious data": "İnvidious JSON verilerini içe aktar", "Import Invidious data": "Invidious JSON Verilerini İçe Aktar",
"Import YouTube subscriptions": "YouTube/OPML aboneliklerini içe aktar", "Import YouTube subscriptions": "YouTube/OPML Aboneliklerini İçe Aktar",
"Import FreeTube subscriptions (.db)": "FreeTube aboneliklerini içe aktar (.db)", "Import FreeTube subscriptions (.db)": "FreeTube Aboneliklerini İçe Aktar (.db)",
"Import NewPipe subscriptions (.json)": "NewPipe aboneliklerini içe aktar (.json)", "Import NewPipe subscriptions (.json)": "NewPipe Aboneliklerini İçe Aktar (.json)",
"Import NewPipe data (.zip)": "NewPipe verilerini içe aktar (.zip)", "Import NewPipe data (.zip)": "NewPipe Verilerini İçe Aktar (.zip)",
"Export": "Dışa aktar", "Export": "Dışa Aktar",
"Export subscriptions as OPML": "Abonelikleri OPML olarak dışa aktar", "Export subscriptions as OPML": "Abonelikleri OPML Olarak Dışa Aktar",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonelikleri OPML olarak dışa aktar (NewPipe ve FreeTube için)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonelikleri OPML Olarak Dışa Aktar (NewPipe ve FreeTube İçin)",
"Export data as JSON": "Invidious verilerini JSON olarak dışa aktar", "Export data as JSON": "İnvidious Verilerini JSON Olarak Dışa Aktar",
"Delete account?": "Hesap silinsin mi?", "Delete account?": "Hesap silinsin mi?",
"History": "Geçmiş", "History": "Geçmiş",
"An alternative front-end to YouTube": "YouTube için alternatif bir ön-yüz", "An alternative front-end to YouTube": "YouTube İçin Alternatif Bir Ön-Yüz",
"JavaScript license information": "JavaScript lisans bilgileri", "JavaScript license information": "JavaScript Lisans Bilgileri",
"source": "kaynak", "source": "Kaynak",
"Log in": "Oturum aç", "Log in": "Oturum Aç",
"Log in/register": "Oturum aç/kayıt ol", "Log in/register": "Oturum Aç/Kayıt Ol",
"Log in with Google": "Google ile oturum aç", "Log in with Google": "Google İle Oturum Aç",
"User ID": "Kullanıcı kimliği", "User ID": "Kullanıcı Kimliği",
"Password": "Parola", "Password": "Parola",
"Time (h:mm:ss):": "Zaman (h:mm:ss):", "Time (h:mm:ss):": "Zaman (h:mm:ss):",
"Text CAPTCHA": "Metin CAPTCHA", "Text CAPTCHA": "Metin CAPTCHA",
"Image CAPTCHA": "Resim CAPTCHA", "Image CAPTCHA": "Resim CAPTCHA",
"Sign In": "Oturum Aç", "Sign In": "Oturum Aç",
"Register": "Kayıt Ol", "Register": "Kayıt Ol",
"E-mail": "E-posta", "E-mail": "E-Posta",
"Google verification code": "Google doğrulama kodu", "Google verification code": "Google Doğrulama Kodu",
"Preferences": "Tercihler", "Preferences": "Tercihler",
"preferences_category_player": "Oynatıcı tercihleri", "preferences_category_player": "Oynatıcı Tercihleri",
"preferences_video_loop_label": "Sürekli döngü: ", "preferences_video_loop_label": "Sürekli Döngü: ",
"preferences_autoplay_label": "Otomatik oynat: ", "preferences_autoplay_label": "Otomatik Oynat: ",
"preferences_continue_label": "Öntanımlı olarak sonrakini oynat: ", "preferences_continue_label": "Öntanımlı Olarak Sonrakini Oynat: ",
"preferences_continue_autoplay_label": "Sonraki videoyu otomatik oynat: ", "preferences_continue_autoplay_label": "Sonraki Videoyu Otomatik Oynat: ",
"preferences_listen_label": "Öntanımlı olarak dinle: ", "preferences_listen_label": "Öntanımlı Olarak Dinle: ",
"preferences_local_label": "Videoları proxy'le: ", "preferences_local_label": "Videolara Proxy Uygula: ",
"preferences_speed_label": "Öntanımlı hız: ", "preferences_speed_label": "Öntanımlı Hız: ",
"preferences_quality_label": "Tercih edilen video kalitesi: ", "preferences_quality_label": "Tercih Edilen Video Kalitesi: ",
"preferences_volume_label": "Oynatıcı ses seviyesi: ", "preferences_volume_label": "Oynatıcı Ses Seviyesi: ",
"preferences_comments_label": "Öntanımlı yorumlar: ", "preferences_comments_label": "Öntanımlı Yorumlar: ",
"youtube": "YouTube", "youtube": "YouTube",
"reddit": "Reddit", "reddit": "Reddit",
"preferences_captions_label": "Öntanımlı altyazılar: ", "preferences_captions_label": "Öntanımlı Altyazılar: ",
"Fallback captions: ": "Yedek altyazılar: ", "Fallback captions: ": "Yedek Altyazılar: ",
"preferences_related_videos_label": "İlgili videoları göster: ", "preferences_related_videos_label": "İlgili Videoları Göster: ",
"preferences_annotations_label": "Öntanımlı olarak ek açıklamaları göster: ", "preferences_annotations_label": "Öntanımlı Olarak Ek Açıklamaları Göster: ",
"preferences_extend_desc_label": "Video ıklamasını otomatik olarak genişlet: ", "preferences_extend_desc_label": "Video ıklamasını Otomatik Olarak Genişlet: ",
"preferences_vr_mode_label": "Etkileşimli 360 derece videolar (WebGL gerektirir): ", "preferences_vr_mode_label": "Etkileşimli 360 Derece Videolar (WebGL Gerektirir): ",
"preferences_category_visual": "Görsel tercihler", "preferences_category_visual": "Görsel Tercihler",
"preferences_player_style_label": "Oynatıcı biçimi: ", "preferences_player_style_label": "Oynatıcı Biçimi: ",
"Dark mode: ": "Karanlık mod: ", "Dark mode: ": "Koyu Mod: ",
"preferences_dark_mode_label": "Tema: ", "preferences_dark_mode_label": "Tema: ",
"dark": "karanlık", "dark": "Koyu",
"light": "aydınlık", "light": "ık",
"preferences_thin_mode_label": "İnce mod: ", "preferences_thin_mode_label": "İnce Mod: ",
"preferences_category_misc": "Çeşitli tercihler", "preferences_category_misc": "Çeşitli Tercihler",
"preferences_automatic_instance_redirect_label": "Otomatik örnek yeniden yönlendirmesi (yedek: redirect.invidious.io): ", "preferences_automatic_instance_redirect_label": "Otomatik Örnek Yeniden Yönlendirmesi (Yedek: redirect.invidious.io): ",
"preferences_category_subscription": "Abonelik tercihleri", "preferences_category_subscription": "Abonelik Tercihleri",
"preferences_annotations_subscribed_label": "Abone olunan kanallar için ek açıklamaları öntanımlı olarak göster: ", "preferences_annotations_subscribed_label": "Abone Olunan Kanallar İçin Ek Açıklamaları Öntanımlı Olarak Göster: ",
"Redirect homepage to feed: ": "Ana sayfayı akışa yönlendir: ", "Redirect homepage to feed: ": "Ana Sayfayı Akışa Yönlendir: ",
"preferences_max_results_label": "Akışta gösterilen video sayısı: ", "preferences_max_results_label": "Akışta Gösterilen Video Sayısı: ",
"preferences_sort_label": "Videoları sıralama kriteri: ", "preferences_sort_label": "Videoları Sıralama Kriteri: ",
"published": "yayınlandı", "published": "Yayınlandı",
"published - reverse": "yayınlandı - ters", "published - reverse": "Yayınlandı - Ters",
"alphabetically": "alfabetik olarak", "alphabetically": "Alfabetik Olarak",
"alphabetically - reverse": "alfabetik olarak - ters", "alphabetically - reverse": "Alfabetik Olarak - Ters",
"channel name": "kanal adı", "channel name": "Kanal Adı",
"channel name - reverse": "kanal adı - ters", "channel name - reverse": "Kanal Adı - Ters",
"Only show latest video from channel: ": "Sadece kanaldaki en son videoyu göster: ", "Only show latest video from channel: ": "Sadece Kanaldaki En Son Videoyu Göster: ",
"Only show latest unwatched video from channel: ": "Sadece kanaldaki en son izlenmemiş videoyu göster: ", "Only show latest unwatched video from channel: ": "Sadece Kanaldaki En Son İzlenmemiş Videoyu Göster: ",
"preferences_unseen_only_label": "Sadece izlenmemişleri göster: ", "preferences_unseen_only_label": "Sadece İzlenmemişleri Göster: ",
"preferences_notifications_only_label": "Sadece bildirimleri göster (eğer varsa): ", "preferences_notifications_only_label": "Sadece Bildirimleri Göster (Eğer Varsa): ",
"Enable web notifications": "Ağ bildirimlerini etkinleştir", "Enable web notifications": "Ağ Bildirimlerini Etkinleştir",
"`x` uploaded a video": "`x` bir video yükledi", "`x` uploaded a video": "`x` Bir Video Yükledi",
"`x` is live": "`x` canlı yayında", "`x` is live": "`x` Canlı Yayında",
"preferences_category_data": "Veri tercihleri", "preferences_category_data": "Veri Tercihleri",
"Clear watch history": "İzleme geçmişini temizle", "Clear watch history": "İzleme Geçmişini Temizle",
"Import/export data": "Verileri içe/dışa aktar", "Import/export data": "Verileri İçe/Dışa Aktar",
"Change password": "Parolayı değiştir", "Change password": "Parolayı Değiştir",
"Manage subscriptions": "Abonelikleri yönet", "Manage subscriptions": "Abonelikleri Yönet",
"Manage tokens": "Belirteçleri yönet", "Manage tokens": "Belirteçleri Yönet",
"Watch history": "İzleme geçmişi", "Watch history": "İzleme Geçmişi",
"Delete account": "Hesap silme", "Delete account": "Hesap Silme",
"preferences_category_admin": "Yönetici tercihleri", "preferences_category_admin": "Yönetici Tercihleri",
"preferences_default_home_label": "Öntanımlı ana sayfa: ", "preferences_default_home_label": "Öntanımlı Ana Sayfa: ",
"preferences_feed_menu_label": "Akış menüsü: ", "preferences_feed_menu_label": "Akış Menüsü: ",
"preferences_show_nick_label": "Takma adı üstte göster: ", "preferences_show_nick_label": "Takma Adı Üstte Göster: ",
"Top enabled: ": "Top etkin: ", "Top enabled: ": "Top Etkin: ",
"CAPTCHA enabled: ": "CAPTCHA etkin: ", "CAPTCHA enabled: ": "CAPTCHA Etkin: ",
"Login enabled: ": "Oturum açma etkin: ", "Login enabled: ": "Oturum Açma Etkin: ",
"Registration enabled: ": "Kayıt olma etkin: ", "Registration enabled: ": "Kayıt Olma Etkin: ",
"Report statistics: ": "Rapor istatistikleri: ", "Report statistics: ": "Rapor İstatistikleri: ",
"Save preferences": "Tercihleri kaydet", "Save preferences": "Tercihleri Kaydet",
"Subscription manager": "Abonelik yöneticisi", "Subscription manager": "Abonelik Yöneticisi",
"Token manager": "Belirteç yöneticisi", "Token manager": "Belirteç Yöneticisi",
"Token": "Belirteç", "Token": "Belirteç",
"Import/export": "İçe/dışa aktar", "Import/export": "İçe/Dışa Aktar",
"unsubscribe": "abonelikten çık", "unsubscribe": "Abonelikten Çık",
"revoke": "geri al", "revoke": "Geri Al",
"Subscriptions": "Abonelikler", "Subscriptions": "Abonelikler",
"search": "ara", "search": "Ara",
"Log out": ıkış yap", "Log out": ıkış Yap",
"Released under the AGPLv3 on Github.": "GitHub'da AGPLv3 altında yayınlandı.", "Released under the AGPLv3 on Github.": "GitHub'da AGPLv3 altında yayınlandı.",
"Source available here.": "Kaynak kodları burada bulunabilir.", "Source available here.": "Kaynak kodları burada bulunabilir.",
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.", "View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
@ -129,76 +129,76 @@
"Public": "Genel", "Public": "Genel",
"Unlisted": "Listelenmemiş", "Unlisted": "Listelenmemiş",
"Private": "Özel", "Private": "Özel",
"View all playlists": "Tüm oynatma listelerini görüntüle", "View all playlists": "Tüm Oynatma Listelerini Görüntüle",
"Updated `x` ago": "`x` önce güncellendi", "Updated `x` ago": "`x` Önce Güncellendi",
"Delete playlist `x`?": "`x` oynatma listesi silinsin mi?", "Delete playlist `x`?": "`x` oynatma listesi silinsin mi?",
"Delete playlist": "Oynatma listesini sil", "Delete playlist": "Oynatma Listesini Sil",
"Create playlist": "Oynatma listesi oluştur", "Create playlist": "Oynatma Listesi Oluştur",
"Title": "Başlık", "Title": "Başlık",
"Playlist privacy": "Oynatma listesi gizliliği", "Playlist privacy": "Oynatma Listesi Gizliliği",
"Editing playlist `x`": "`x` oynatma listesi düzenleniyor", "Editing playlist `x`": "`x` Oynatma Listesi Düzenleniyor",
"Show more": "Daha fazla göster", "Show more": "Daha Fazla Göster",
"Show less": "Daha az göster", "Show less": "Daha Az Göster",
"Watch on YouTube": "YouTube'da izle", "Watch on YouTube": "YouTube'da İzle",
"Switch Invidious Instance": "Invidious Örneğini Değiştir", "Switch Invidious Instance": "Invidious Örneğini Değiştir",
"Hide annotations": "Ek ıklamaları gizle", "Hide annotations": "Ek ıklamaları Gizle",
"Show annotations": "Ek ıklamaları göster", "Show annotations": "Ek ıklamaları Göster",
"Genre: ": "Tür: ", "Genre: ": "Tür: ",
"License: ": "Lisans: ", "License: ": "Lisans: ",
"Family friendly? ": "Aile için uygun mu? ", "Family friendly? ": "Aile için uygun mu? ",
"Wilson score: ": "Wilson puanı: ", "Wilson score: ": "Wilson Puanı: ",
"Engagement: ": "İzleyenlerin oy verme oranı: ", "Engagement: ": "İzleyenlerin Oy Verme Oranı: ",
"Whitelisted regions: ": "Beyaz listeye alınan bölgeler: ", "Whitelisted regions: ": "Beyaz Listeye Alınan Bölgeler: ",
"Blacklisted regions: ": "Kara listeye alınan bölgeler: ", "Blacklisted regions: ": "Kara Listeye Alınan Bölgeler: ",
"Shared `x`": "`x` paylaşıldı", "Shared `x`": "`x` Paylaşıldı",
"Premieres in `x`": "`x`içinde ilk gösterim", "Premieres in `x`": "`x`İçinde İlk Gösterim",
"Premieres `x`": "`x` ilk gösterim", "Premieres `x`": "`x` İlk Gösterim",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Merhaba! JavaScript'i kapatmış gibi görünüyorsun. Yorumları görüntülemek için buraya tıkla, yüklenmelerinin biraz uzun sürebileceğini unutma.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Merhaba! JavaScript'i kapatmış gibi görünüyorsun. Yorumları görüntülemek için buraya tıkla, yüklenmelerinin biraz uzun sürebileceğini unutma.",
"View YouTube comments": "YouTube yorumlarını görüntüle", "View YouTube comments": "YouTube Yorumlarını Görüntüle",
"View more comments on Reddit": "Reddit'te daha fazla yorum görüntüle", "View more comments on Reddit": "Reddit'te Daha Fazla Yorum Görüntüle",
"View `x` comments": { "View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` yorumu görüntüle", "([^.,0-9]|^)1([^.,0-9]|$)": "`x` Yorumu Görüntüle",
"": "`x` yorumu görüntüle" "": "`x` Yorumu Görüntüle"
}, },
"View Reddit comments": "Reddit yorumlarını görüntüle", "View Reddit comments": "Reddit Yorumlarını Görüntüle",
"Hide replies": "Cevapları gizle", "Hide replies": "Cevapları Gizle",
"Show replies": "Cevapları göster", "Show replies": "Cevapları Göster",
"Incorrect password": "Yanlış parola", "Incorrect password": "Yanlış Parola",
"Quota exceeded, try again in a few hours": "Kota aşıldı, birkaç saat içinde tekrar deneyin", "Quota exceeded, try again in a few hours": "Kota aşıldı, birkaç saat içinde tekrar deneyin.",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Oturum açılamadı, iki faktörlü kimlik doğrulamanın (Authenticator ya da SMS) açık olduğundan emin olun.", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Oturum açılamadı, iki faktörlü kimlik doğrulamanın (Kimlik Doğrulayıcı ya da SMS) açık olduğundan emin olun.",
"Invalid TFA code": "Geçersiz TFA kodu", "Invalid TFA code": "Geçersiz TFA Kodu",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Giriş başarısız. Bunun nedeni, hesabınız için iki faktörlü kimlik doğrulamanın açık olmaması olabilir.", "Login failed. This may be because two-factor authentication is not turned on for your account.": "Giriş başarısız. Bunun nedeni, hesabınız için iki faktörlü kimlik doğrulamanın açık olmaması olabilir.",
"Wrong answer": "Yanlış cevap", "Wrong answer": "Yanlış Cevap",
"Erroneous CAPTCHA": "Hatalı CAPTCHA", "Erroneous CAPTCHA": "Hatalı CAPTCHA",
"CAPTCHA is a required field": "CAPTCHA zorunlu bir alandır", "CAPTCHA is a required field": "CAPTCHA Zorunlu Bir Alandır",
"User ID is a required field": "Kullanıcı kimliği zorunlu bir alandır", "User ID is a required field": "Kullanıcı Kimliği Zorunlu Bir Alandır",
"Password is a required field": "Parola zorunlu bir alandır", "Password is a required field": "Parola Zorunlu Bir Alandır",
"Wrong username or password": "Yanlış kullanıcı adı ya da parola", "Wrong username or password": "Yanlış Kullanıcı Adı ya da Parola",
"Please sign in using 'Log in with Google'": "Lütfen 'Google ile giriş yap' seçeneğini kullanarak oturum açın", "Please sign in using 'Log in with Google'": "Lütfen 'Google İle Giriş Yap' Seçeneğini Kullanarak Oturum Açın",
"Password cannot be empty": "Parola boş olamaz", "Password cannot be empty": "Parola Boş Olamaz",
"Password cannot be longer than 55 characters": "Parola 55 karakterden uzun olamaz", "Password cannot be longer than 55 characters": "Parola 55 Karakterden Uzun Olamaz",
"Please log in": "Lütfen oturum açın", "Please log in": "Lütfen Oturum Açın",
"Invidious Private Feed for `x`": "`x` için İnvidious Özel Akışı", "Invidious Private Feed for `x`": "`x` İçin Invidious Özel Akışı",
"channel:`x`": "kanal:`x`", "channel:`x`": "Kanal:`x`",
"Deleted or invalid channel": "Silinmiş ya da geçersiz kanal", "Deleted or invalid channel": "Silinmiş ya da Geçersiz Kanal",
"This channel does not exist.": "Bu kanal mevcut değil.", "This channel does not exist.": "Bu kanal mevcut değil.",
"Could not get channel info.": "Kanal bilgisi alınamadı.", "Could not get channel info.": "Kanal bilgisi alınamadı.",
"Could not fetch comments": "Yorumlar alınamadı", "Could not fetch comments": "Yorumlar Alınamadı",
"`x` ago": "`x` önce", "`x` ago": "`x` Önce",
"Load more": "Daha fazla yükle", "Load more": "Daha Fazla Yükle",
"Could not create mix.": "Mix oluşturulamadı.", "Could not create mix.": "Mix oluşturulamadı.",
"Empty playlist": "Boş oynatma listesi", "Empty playlist": "Boş Oynatma Listesi",
"Not a playlist.": "Oynatma listesi değil.", "Not a playlist.": "Oynatma listesi değil.",
"Playlist does not exist.": "Oynatma listesi mevcut değil.", "Playlist does not exist.": "Oynatma listesi mevcut değil.",
"Could not pull trending pages.": "Trend sayfaları alınamıyor.", "Could not pull trending pages.": "Trend sayfaları alınamıyor.",
"Hidden field \"challenge\" is a required field": "Gizli alan \"challenge\" zorunlu bir alandır", "Hidden field \"challenge\" is a required field": "Gizli Alan \"Challenge\" Zorunlu Bir Alandır",
"Hidden field \"token\" is a required field": "\"belirteç\" gizli alanı zorunlu bir alandır", "Hidden field \"token\" is a required field": "\"Belirteç\" Gizli Alanı Zorunlu Bir Alandır",
"Erroneous challenge": "Hatalı challenge", "Erroneous challenge": "Hatalı Challenge",
"Erroneous token": "Hatalı belirteç", "Erroneous token": "Hatalı Belirteç",
"No such user": "Böyle bir kullanıcı yok", "No such user": "Böyle Bir Kullanıcı Yok",
"Token is expired, please try again": "Belirtecin süresi doldu, lütfen tekrar deneyin", "Token is expired, please try again": "Belirtecin Süresi Doldu, Lütfen Tekrar Deneyin",
"English": "İngilizce", "English": "İngilizce",
"English (auto-generated)": "İngilizce (otomatik oluşturuldu)", "English (auto-generated)": "İngilizce (Otomatik Oluşturuldu)",
"Afrikaans": "Afrikanca", "Afrikaans": "Afrikanca",
"Albanian": "Arnavutça", "Albanian": "Arnavutça",
"Amharic": "Amharca", "Amharic": "Amharca",
@ -230,9 +230,9 @@
"German": "Almanca", "German": "Almanca",
"Greek": "Yunanca", "Greek": "Yunanca",
"Gujarati": "Guceratça", "Gujarati": "Guceratça",
"Haitian Creole": "Haiti Creole dili", "Haitian Creole": "Haiti Creole Dili",
"Hausa": "Hausaca", "Hausa": "Hausaca",
"Hawaiian": "Hawaii dili", "Hawaiian": "Hawaii Dili",
"Hebrew": "İbranice", "Hebrew": "İbranice",
"Hindi": "Hintçe", "Hindi": "Hintçe",
"Hmong": "Hmong", "Hmong": "Hmong",
@ -244,7 +244,7 @@
"Italian": "İtalyanca", "Italian": "İtalyanca",
"Japanese": "Japonca", "Japanese": "Japonca",
"Javanese": "Cava dili", "Javanese": "Cava dili",
"Kannada": "Kannada dili", "Kannada": "Kannada Dili",
"Kazakh": "Kazakça", "Kazakh": "Kazakça",
"Khmer": "Kmerce", "Khmer": "Kmerce",
"Korean": "Korece", "Korean": "Korece",
@ -258,10 +258,10 @@
"Macedonian": "Makedonca", "Macedonian": "Makedonca",
"Malagasy": "Malgaşça", "Malagasy": "Malgaşça",
"Malay": "Malayca", "Malay": "Malayca",
"Malayalam": "Malayalam dili", "Malayalam": "Malayalam Dili",
"Maltese": "Maltaca", "Maltese": "Maltaca",
"Maori": "Maori dili", "Maori": "Maori Dili",
"Marathi": "Marati dili", "Marathi": "Marati Dili",
"Mongolian": "Moğolca", "Mongolian": "Moğolca",
"Nepali": "Nepalce", "Nepali": "Nepalce",
"Norwegian Bokmål": "Norveççe Bokmål", "Norwegian Bokmål": "Norveççe Bokmål",
@ -270,19 +270,19 @@
"Persian": "Farsça", "Persian": "Farsça",
"Polish": "Lehçe", "Polish": "Lehçe",
"Portuguese": "Portekizce", "Portuguese": "Portekizce",
"Punjabi": "Pencap dili", "Punjabi": "Pencap Dili",
"Romanian": "Rumence", "Romanian": "Rumence",
"Russian": "Rusça", "Russian": "Rusça",
"Samoan": "Samoa dili", "Samoan": "Samoa Dili",
"Scottish Gaelic": "İskoç Galcesi", "Scottish Gaelic": "İskoç Galcesi",
"Serbian": "Sırpça", "Serbian": "Sırpça",
"Shona": "Şona dili", "Shona": "Şona Dili",
"Sindhi": "Sintçe", "Sindhi": "Sintçe",
"Sinhala": "Seylanca", "Sinhala": "Seylanca",
"Slovak": "Slovakça", "Slovak": "Slovakça",
"Slovenian": "Slovence", "Slovenian": "Slovence",
"Somali": "Somalice", "Somali": "Somalice",
"Southern Sotho": "Güney Sotho dili", "Southern Sotho": "Güney Sotho Dili",
"Spanish": "İspanyolca", "Spanish": "İspanyolca",
"Spanish (Latin America)": "İspanyolca (Latin Amerika)", "Spanish (Latin America)": "İspanyolca (Latin Amerika)",
"Sundanese": "Sundaca", "Sundanese": "Sundaca",
@ -290,7 +290,7 @@
"Swedish": "İsveççe", "Swedish": "İsveççe",
"Tajik": "Tacikçe", "Tajik": "Tacikçe",
"Tamil": "Tamilce", "Tamil": "Tamilce",
"Telugu": "Telugu dili", "Telugu": "Telugu Dili",
"Thai": "Tayca", "Thai": "Tayca",
"Turkish": "Türkçe", "Turkish": "Türkçe",
"Ukrainian": "Ukraynaca", "Ukrainian": "Ukraynaca",
@ -299,178 +299,182 @@
"Vietnamese": "Vietnamca", "Vietnamese": "Vietnamca",
"Welsh": "Galce", "Welsh": "Galce",
"Western Frisian": "Batı Frizcesi", "Western Frisian": "Batı Frizcesi",
"Xhosa": "Xhosa dili", "Xhosa": "Xhosa Dili",
"Yiddish": "Yiddiş", "Yiddish": "Yiddiş",
"Yoruba": "Yoruba dili", "Yoruba": "Yoruba Dili",
"Zulu": "Zuluca", "Zulu": "Zuluca",
"Fallback comments: ": "Yedek yorumlar: ", "Fallback comments: ": "Yedek Yorumlar: ",
"Popular": "Popüler", "Popular": "Popüler",
"Search": "Ara", "Search": "Ara",
"Top": "Enler", "Top": "Enler",
"About": "Hakkında", "About": "Hakkında",
"Rating: ": "Değerlendirme: ", "Rating: ": "Değerlendirme: ",
"preferences_locale_label": "Dil: ", "preferences_locale_label": "Dil: ",
"View as playlist": "Oynatma listesi olarak görüntüle", "View as playlist": "Oynatma Listesi Olarak Görüntüle",
"Default": "Öntanımlı", "Default": "Öntanımlı",
"Music": "Müzik", "Music": "Müzik",
"Gaming": "Oyun", "Gaming": "Oyun",
"News": "Haberler", "News": "Haberler",
"Movies": "Filmler", "Movies": "Filmler",
"Download": "İndir", "Download": "İndir",
"Download as: ": "Şu şekilde indir: ", "Download as: ": "Şu Şekilde İndir: ",
"%A %B %-d, %Y": "%A %B %-d, %Y", "%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(düzenlendi)", "(edited)": "(Düzenlendi)",
"YouTube comment permalink": "YouTube yorumu kalıcı linki", "YouTube comment permalink": "YouTube Yorumu Kalıcı Linki",
"permalink": "kalıcı link", "permalink": "Kalıcı Link",
"`x` marked it with a ❤": "`x` ❤ ile işaretledi", "`x` marked it with a ❤": "`x` ❤ İle İşaretledi",
"Audio mode": "Ses modu", "Audio mode": "Ses Modu",
"Video mode": "Video modu", "Video mode": "Video Modu",
"Videos": "Videolar", "channel_tab_videos_label": "Videolar",
"Playlists": "Oynatma listeleri", "Playlists": "Oynatma Listeleri",
"Community": "Topluluk", "channel_tab_community_label": "Topluluk",
"search_filters_sort_option_relevance": "İlgi", "search_filters_sort_option_relevance": "İlgi",
"search_filters_sort_option_rating": "Değerlendirme", "search_filters_sort_option_rating": "Değerlendirme",
"search_filters_sort_option_date": "Yükleme tarihi", "search_filters_sort_option_date": "Yükleme Tarihi",
"search_filters_sort_option_views": "Görüntüleme sayısı", "search_filters_sort_option_views": "Görüntüleme Sayısı",
"search_filters_type_label": "Tür", "search_filters_type_label": "Tür",
"search_filters_duration_label": "Süre", "search_filters_duration_label": "Süre",
"search_filters_features_label": "Özellikler", "search_filters_features_label": "Özellikler",
"search_filters_sort_label": "Sıralama Ölçütü", "search_filters_sort_label": "Sıralama Ölçütü",
"search_filters_date_option_hour": "Son Saat", "search_filters_date_option_hour": "Son Saat",
"search_filters_date_option_today": "Bugün", "search_filters_date_option_today": "Bugün",
"search_filters_date_option_week": "Bu hafta", "search_filters_date_option_week": "Bu Hafta",
"search_filters_date_option_month": "Bu ay", "search_filters_date_option_month": "Bu Ay",
"search_filters_date_option_year": "Bu yıl", "search_filters_date_option_year": "Bu Yıl",
"search_filters_type_option_video": "Video", "search_filters_type_option_video": "Video",
"search_filters_type_option_channel": "Kanal", "search_filters_type_option_channel": "Kanal",
"search_filters_type_option_playlist": "Oynatma listesi", "search_filters_type_option_playlist": "Oynatma Listesi",
"search_filters_type_option_movie": "Film", "search_filters_type_option_movie": "Film",
"search_filters_type_option_show": "Gösteri", "search_filters_type_option_show": "Gösteri",
"search_filters_features_option_hd": "HD", "search_filters_features_option_hd": "HD",
"search_filters_features_option_subtitles": "Alt yazılar", "search_filters_features_option_subtitles": "Alt Yazılar",
"search_filters_features_option_c_commons": "Creative Commons", "search_filters_features_option_c_commons": "Yaratıcı",
"search_filters_features_option_three_d": "3B", "search_filters_features_option_three_d": "3D",
"search_filters_features_option_live": "Canlı", "search_filters_features_option_live": "Canlı",
"search_filters_features_option_four_k": "4K", "search_filters_features_option_four_k": "4K",
"search_filters_features_option_location": "Konum", "search_filters_features_option_location": "Konum",
"search_filters_features_option_hdr": "HDR", "search_filters_features_option_hdr": "HDR",
"Current version: ": "Şu anki sürüm: ", "Current version: ": "Şu Anki Sürüm: ",
"next_steps_error_message": "Bundan sonra şunları denemelisiniz: ", "next_steps_error_message": "Bundan Sonra Şunları Denemelisiniz: ",
"next_steps_error_message_refresh": "Yenile", "next_steps_error_message_refresh": "Yenile",
"next_steps_error_message_go_to_youtube": "YouTube'a git", "next_steps_error_message_go_to_youtube": "YouTube'a Git",
"search_filters_duration_option_short": "Kısa (4 dakikadan az)", "search_filters_duration_option_short": "Kısa (4 Dakikadan Az)",
"search_filters_duration_option_long": "Uzun (20 dakikadan fazla)", "search_filters_duration_option_long": "Uzun (20 Dakikadan Fazla)",
"footer_documentation": "Belgelendirme", "footer_documentation": "Belgelendirme",
"footer_source_code": "Kaynak kodları", "footer_source_code": "Kaynak Kodları",
"footer_original_source_code": "Orijinal kaynak kodları", "footer_original_source_code": "Orijinal Kaynak Kodları",
"footer_modfied_source_code": "Değiştirilmiş kaynak kodları", "footer_modfied_source_code": "Değiştirilmiş Kaynak Kodları",
"adminprefs_modified_source_code_url_label": "Değiştirilmiş kaynak kodları deposunun URL'si", "adminprefs_modified_source_code_url_label": "Değiştirilmiş Kaynak Kodları Deposunun URL'si",
"footer_donate_page": "Bağış yap", "footer_donate_page": "Bağış Yap",
"preferences_region_label": "İçerik ülkesi: ", "preferences_region_label": "İçerik Ülkesi: ",
"preferences_quality_dash_label": "Tercih edilen DASH video kalitesi: ", "preferences_quality_dash_label": "Tercih Edilen DASH Video Kalitesi: ",
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
"preferences_quality_dash_option_best": "En iyi", "preferences_quality_dash_option_best": "En İyi",
"preferences_quality_dash_option_worst": "En kötü", "preferences_quality_dash_option_worst": "En Kötü",
"preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_4320p": "4320P",
"preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_2160p": "2160P",
"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",
"preferences_quality_dash_option_144p": "144p", "preferences_quality_dash_option_144p": "144P",
"invidious": "Invidious", "invidious": "Invidious",
"none": "yok", "none": "Yok",
"videoinfo_started_streaming_x_ago": "`x` önce yayına başladı", "videoinfo_started_streaming_x_ago": "`x` Önce Yayına Başladı",
"videoinfo_youTube_embed_link": "Göm", "videoinfo_youTube_embed_link": "Entegre Et",
"videoinfo_invidious_embed_link": "Bağlantıyı Göm", "videoinfo_invidious_embed_link": "Bağlantıyı Entegre Et",
"user_created_playlists": "`x` oluşturulan oynatma listeleri", "user_created_playlists": "`x` Oluşturulan Oynatma Listeleri",
"user_saved_playlists": "`x` kaydedilen oynatma listeleri", "user_saved_playlists": "`x` Kaydedilen Oynatma Listeleri",
"preferences_quality_option_small": "Küçük", "preferences_quality_option_small": "Küçük",
"preferences_quality_dash_option_720p": "720p", "preferences_quality_dash_option_720p": "720P",
"preferences_quality_option_medium": "Orta", "preferences_quality_option_medium": "Orta",
"preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_1440p": "1440P",
"preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_1080p": "1080P",
"Video unavailable": "Video kullanılamıyor", "Video unavailable": "Video Kullanılamıyor",
"preferences_quality_option_dash": "DASH (uyarlanabilir kalite)", "preferences_quality_option_dash": "DASH (Uyarlanabilir Kalite)",
"preferences_quality_dash_option_auto": "Otomatik", "preferences_quality_dash_option_auto": "Otomatik",
"search_filters_features_option_purchased": "Satın alınan", "search_filters_features_option_purchased": "Satın Alınan",
"search_filters_features_option_three_sixty": "360°", "search_filters_features_option_three_sixty": "360°",
"videoinfo_watch_on_youTube": "YouTube'da izle", "videoinfo_watch_on_youTube": "YouTube'da İzle",
"download_subtitles": "Alt yazılar - `x` (.vtt)", "download_subtitles": "Alt Yazılar - `x` (.vtt)",
"preferences_save_player_pos_label": "Oynatma konumunu kaydet: ", "preferences_save_player_pos_label": "Oynatma Konumunu Kaydet: ",
"generic_views_count": "{{count}} görüntüleme", "generic_views_count": "{{count}} Görüntüleme",
"generic_views_count_plural": "{{count}} görüntüleme", "generic_views_count_plural": "{{count}} Görüntüleme",
"generic_subscribers_count": "{{count}} abone", "generic_subscribers_count": "{{count}} Abone",
"generic_subscribers_count_plural": "{{count}} abone", "generic_subscribers_count_plural": "{{count}} Abone",
"generic_subscriptions_count": "{{count}} abonelik", "generic_subscriptions_count": "{{count}} Abonelik",
"generic_subscriptions_count_plural": "{{count}} abonelik", "generic_subscriptions_count_plural": "{{count}} Abonelik",
"subscriptions_unseen_notifs_count": "{{count}} okunmamış bildirim", "subscriptions_unseen_notifs_count": "{{count}} Okunmamış Bildirim",
"subscriptions_unseen_notifs_count_plural": "{{count}} okunmamış bildirim", "subscriptions_unseen_notifs_count_plural": "{{count}} Okunmamış Bildirim",
"comments_points_count": "{{count}} puan", "comments_points_count": "{{count}} Puan",
"comments_points_count_plural": "{{count}} puan", "comments_points_count_plural": "{{count}} Puan",
"generic_count_hours": "{{count}} saat", "generic_count_hours": "{{count}} Saat",
"generic_count_hours_plural": "{{count}} saat", "generic_count_hours_plural": "{{count}} Saat",
"generic_count_minutes": "{{count}} dakika", "generic_count_minutes": "{{count}} Dakika",
"generic_count_minutes_plural": "{{count}} dakika", "generic_count_minutes_plural": "{{count}} Dakika",
"generic_count_seconds": "{{count}} saniye", "generic_count_seconds": "{{count}} Saniye",
"generic_count_seconds_plural": "{{count}} saniye", "generic_count_seconds_plural": "{{count}} Saniye",
"generic_playlists_count": "{{count}} oynatma listesi", "generic_playlists_count": "{{count}} Oynatma Listesi",
"generic_playlists_count_plural": "{{count}} oynatma listesi", "generic_playlists_count_plural": "{{count}} Oynatma Listesi",
"tokens_count": "{{count}} belirteç", "tokens_count": "{{count}} Belirteç",
"tokens_count_plural": "{{count}} belirteç", "tokens_count_plural": "{{count}} Belirteç",
"comments_view_x_replies": "{{count}} yanıtı görüntüle", "comments_view_x_replies": "{{count}} Yanıtı Görüntüle",
"comments_view_x_replies_plural": "{{count}} yanıtı görüntüle", "comments_view_x_replies_plural": "{{count}} Yanıtı Görüntüle",
"generic_count_years": "{{count}} yıl", "generic_count_years": "{{count}} Yıl",
"generic_count_years_plural": "{{count}} yıl", "generic_count_years_plural": "{{count}} Yıl",
"generic_count_months": "{{count}} ay", "generic_count_months": "{{count}} Ay",
"generic_count_months_plural": "{{count}} ay", "generic_count_months_plural": "{{count}} Ay",
"generic_count_days": "{{count}} gün", "generic_count_days": "{{count}} Gün",
"generic_count_days_plural": "{{count}} gün", "generic_count_days_plural": "{{count}} Gün",
"generic_videos_count": "{{count}} video", "generic_videos_count": "{{count}} Video",
"generic_videos_count_plural": "{{count}} video", "generic_videos_count_plural": "{{count}} Video",
"generic_count_weeks": "{{count}} hafta", "generic_count_weeks": "{{count}} Hafta",
"generic_count_weeks_plural": "{{count}} hafta", "generic_count_weeks_plural": "{{count}} Hafta",
"crash_page_you_found_a_bug": "Görünüşe göre Invidious'ta bir hata buldunuz!", "crash_page_you_found_a_bug": "Görünüşe göre Invidious'ta bir hata buldunuz!",
"crash_page_before_reporting": "Bir hatayı bildirmeden önce, şunları yaptığınızdan emin olun:", "crash_page_before_reporting": "Bir hatayı bildirmeden önce, şunları yaptığınızdan emin olun:",
"crash_page_refresh": "<a href=\"`x`\">sayfayı yenilemeye</a> çalıştınız", "crash_page_refresh": "<a href=\"`x`\">Sayfayı Yenilemeye</a> Çalıştınız",
"crash_page_switch_instance": "<a href=\"`x`\">başka bir örnek kullanmaya</a> çalıştınız", "crash_page_switch_instance": "<a href=\"`x`\">Başka Bir Örnek Kullanmaya</a> Çalıştınız",
"crash_page_read_the_faq": "<a href=\"`x`\">Sık Sorulan Soruları (SSS)</a> okudunuz", "crash_page_read_the_faq": "<a href=\"`x`\">Sık Sorulan Soruları (SSS)</a> Okudunuz",
"crash_page_search_issue": "<a href=\"`x`\">GitHub'daki sorunlarda</a> aradınız", "crash_page_search_issue": "<a href=\"`x`\">GitHub'daki Sorunlarda</a> Aradınız",
"crash_page_report_issue": "Yukarıdakilerin hiçbiri yardımcı olmadıysa, lütfen <a href=\"`x`\">GitHub'da yeni bir sorun açın</a> (tercihen İngilizce) ve mesajınıza aşağıdaki metni ekleyin (bu metni ÇEVİRMEYİN):", "crash_page_report_issue": "Yukarıdakilerin hiçbiri yardımcı olmadıysa, lütfen <a href=\"`x`\">GitHub'da yeni bir sorun açın</a> (Tercihen İngilizce) ve mesajınıza aşağıdaki metni ekleyin (Bu metni ÇEVİRMEYİN):",
"English (United Kingdom)": "İngilizce (Birleşik Krallık)", "English (United Kingdom)": "İngilizce (Birleşik Krallık)",
"Chinese": "Çince", "Chinese": "Çince",
"Interlingue": "İnterlingue", "Interlingue": "İnterlingue",
"Italian (auto-generated)": "İtalyanca (otomatik oluşturuldu)", "Italian (auto-generated)": "İtalyanca (Otomatik Oluşturuldu)",
"Japanese (auto-generated)": "Japonca (otomatik oluşturuldu)", "Japanese (auto-generated)": "Japonca (Otomatik Oluşturuldu)",
"Portuguese (Brazil)": "Portekizce (Brezilya)", "Portuguese (Brazil)": "Portekizce (Brezilya)",
"Russian (auto-generated)": "Rusça (otomatik oluşturuldu)", "Russian (auto-generated)": "Rusça (Otomatik Oluşturuldu)",
"Spanish (auto-generated)": "İspanyolca (otomatik oluşturuldu)", "Spanish (auto-generated)": "İspanyolca (Otomatik Oluşturuldu)",
"Spanish (Mexico)": "İspanyolca (Meksika)", "Spanish (Mexico)": "İspanyolca (Meksika)",
"English (United States)": "İngilizce (ABD)", "English (United States)": "İngilizce (ABD)",
"Cantonese (Hong Kong)": "Kantonca (Hong Kong)", "Cantonese (Hong Kong)": "Kantonca (Hong Kong)",
"Chinese (Taiwan)": "Çince (Tayvan)", "Chinese (Taiwan)": "Çince (Tayvan)",
"Dutch (auto-generated)": "Felemenkçe (otomatik oluşturuldu)", "Dutch (auto-generated)": "Felemenkçe (Otomatik Oluşturuldu)",
"Indonesian (auto-generated)": "Endonezyaca (otomatik oluşturuldu)", "Indonesian (auto-generated)": "Endonezyaca (Otomatik Oluşturuldu)",
"Chinese (Hong Kong)": "Çince (Hong Kong)", "Chinese (Hong Kong)": "Çince (Hong Kong)",
"French (auto-generated)": "Fransızca (otomatik oluşturuldu)", "French (auto-generated)": "Fransızca (Otomatik Oluşturuldu)",
"Korean (auto-generated)": "Korece (otomatik oluşturuldu)", "Korean (auto-generated)": "Korece (Otomatik Oluşturuldu)",
"Turkish (auto-generated)": "Türkçe (otomatik oluşturuldu)", "Turkish (auto-generated)": "Türkçe (Otomatik Oluşturuldu)",
"Chinese (China)": "Çince (Çin)", "Chinese (China)": "Çince (Çin)",
"German (auto-generated)": "Almanca (otomatik oluşturuldu)", "German (auto-generated)": "Almanca (Otomatik Oluşturuldu)",
"Portuguese (auto-generated)": "Portekizce (otomatik oluşturuldu)", "Portuguese (auto-generated)": "Portekizce (Otomatik Oluşturuldu)",
"Spanish (Spain)": "İspanyolca (İspanya)", "Spanish (Spain)": "İspanyolca (İspanya)",
"Vietnamese (auto-generated)": "Vietnamca (otomatik oluşturuldu)", "Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
"preferences_watch_history_label": "İzleme geçmişini etkinleştir: ", "preferences_watch_history_label": "İzleme Geçmişini Etkinleştir: ",
"search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.", "search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
"search_filters_type_option_all": "Herhangi bir tür", "search_filters_type_option_all": "Herhangi Bir Tür",
"search_filters_duration_option_none": "Herhangi bir süre", "search_filters_duration_option_none": "Herhangi Bir Süre",
"search_message_no_results": "Sonuç bulunamadı.", "search_message_no_results": "Sonuç bulunamadı.",
"search_filters_date_label": "Yükleme tarihi", "search_filters_date_label": "Yükleme Tarihi",
"search_filters_apply_button": "Seçili filtreleri uygula", "search_filters_apply_button": "Seçili Filtreleri Uygula",
"search_filters_date_option_none": "Herhangi bir tarih", "search_filters_date_option_none": "Herhangi Bir Tarih",
"search_filters_duration_option_medium": "Orta (4 - 20 dakika)", "search_filters_duration_option_medium": "Orta (4 - 20 Dakika)",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
"search_filters_title": "Filtreler", "search_filters_title": "Filtreler",
"search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin.", "search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin.",
"Popular enabled: ": "Popüler etkin: ", "Popular enabled: ": "Popüler Etkin: ",
"error_video_not_in_playlist": "İstenen video bu oynatma listesinde yok. <a href=\"`x`\">Oynatma listesi ana sayfası için buraya tıklayın.</a>" "error_video_not_in_playlist": "İstenen video bu oynatma listesinde yok. <a href=\"`x`\">Oynatma listesi ana sayfası için buraya tıklayın.</a>",
"channel_tab_channels_label": "Kanallar",
"channel_tab_shorts_label": "Kısa Çekimler",
"channel_tab_streams_label": "Canlı Yayınlar",
"channel_tab_playlists_label": "Oynatma Listeleri"
} }

ファイルの表示

@ -54,7 +54,7 @@
"preferences_continue_label": "Завжди вмикати наступне відео: ", "preferences_continue_label": "Завжди вмикати наступне відео: ",
"preferences_continue_autoplay_label": "Автовідтворення наступного відео: ", "preferences_continue_autoplay_label": "Автовідтворення наступного відео: ",
"preferences_listen_label": "Режим «тільки звук» як усталений: ", "preferences_listen_label": "Режим «тільки звук» як усталений: ",
"preferences_local_label": "Програвати відео через проксі? ", "preferences_local_label": "Відтворення відео через проксі: ",
"preferences_speed_label": "Усталена швидкість відео: ", "preferences_speed_label": "Усталена швидкість відео: ",
"preferences_quality_label": "Пріорітетна якість відео: ", "preferences_quality_label": "Пріорітетна якість відео: ",
"preferences_volume_label": "Гучність відео: ", "preferences_volume_label": "Гучність відео: ",
@ -63,13 +63,13 @@
"reddit": "Reddit", "reddit": "Reddit",
"preferences_captions_label": "Основна мова субтитрів: ", "preferences_captions_label": "Основна мова субтитрів: ",
"Fallback captions: ": "Запасна мова субтитрів: ", "Fallback captions: ": "Запасна мова субтитрів: ",
"preferences_related_videos_label": "Показувати схожі відео? ", "preferences_related_videos_label": "Показувати схожі відео: ",
"preferences_annotations_label": "Завжди показувати анотації? ", "preferences_annotations_label": "Завжди показувати анотації: ",
"preferences_category_visual": "Налаштування сайту", "preferences_category_visual": "Налаштування сайту",
"preferences_player_style_label": "Стиль програвача: ", "preferences_player_style_label": "Стиль програвача: ",
"Dark mode: ": "Темне оформлення: ", "Dark mode: ": "Темний режим: ",
"preferences_dark_mode_label": "Тема: ", "preferences_dark_mode_label": "Тема: ",
"dark": "темна", "dark": "Темна",
"light": "Світла", "light": "Світла",
"preferences_thin_mode_label": "Полегшене оформлення: ", "preferences_thin_mode_label": "Полегшене оформлення: ",
"preferences_category_subscription": "Налаштування підписок", "preferences_category_subscription": "Налаштування підписок",
@ -101,11 +101,11 @@
"preferences_category_admin": "Адміністраторські налаштування", "preferences_category_admin": "Адміністраторські налаштування",
"preferences_default_home_label": "Усталена домашня сторінка: ", "preferences_default_home_label": "Усталена домашня сторінка: ",
"preferences_feed_menu_label": "Меню потоку з відео: ", "preferences_feed_menu_label": "Меню потоку з відео: ",
"Top enabled: ": "Увімкнути топ відео? ", "Top enabled: ": "Увімкнути топ відео: ",
"CAPTCHA enabled: ": "Увімкнути капчу? ", "CAPTCHA enabled: ": "Увімкнути CAPTCHA: ",
"Login enabled: ": "Увімкнути авторизацію? ", "Login enabled: ": "Увімкнути вхід: ",
"Registration enabled: ": "Увімкнути реєстрацію? ", "Registration enabled: ": "Увімкнути реєстрацію: ",
"Report statistics: ": "Повідомляти статистику? ", "Report statistics: ": "Повідомляти статистику: ",
"Save preferences": "Зберегти налаштування", "Save preferences": "Зберегти налаштування",
"Subscription manager": "Менеджер підписок", "Subscription manager": "Менеджер підписок",
"Token manager": "Менеджер токенів", "Token manager": "Менеджер токенів",
@ -125,7 +125,7 @@
"Private": "Особистий", "Private": "Особистий",
"View all playlists": "Переглянути всі списки відтворення", "View all playlists": "Переглянути всі списки відтворення",
"Updated `x` ago": "Оновлено `x` тому", "Updated `x` ago": "Оновлено `x` тому",
"Delete playlist `x`?": "Видалити список відтворення \"x\"?", "Delete playlist `x`?": "Видалити список відтворення `x`?",
"Delete playlist": "Видалити список відтворення", "Delete playlist": "Видалити список відтворення",
"Create playlist": "Створити список відтворення", "Create playlist": "Створити список відтворення",
"Title": "Заголовок", "Title": "Заголовок",
@ -315,9 +315,9 @@
"`x` marked it with a ❤": "❤ цьому від каналу `x`", "`x` marked it with a ❤": "❤ цьому від каналу `x`",
"Audio mode": "Аудіорежим", "Audio mode": "Аудіорежим",
"Video mode": "Відеорежим", "Video mode": "Відеорежим",
"Videos": "Відео", "channel_tab_videos_label": "Відео",
"Playlists": "Плейлисти", "Playlists": "Плейлисти",
"Community": "Спільнота", "channel_tab_community_label": "Спільнота",
"Current version: ": "Поточна версія: ", "Current version: ": "Поточна версія: ",
"generic_views_count_0": "{{count}} перегляд", "generic_views_count_0": "{{count}} перегляд",
"generic_views_count_1": "{{count}} перегляди", "generic_views_count_1": "{{count}} перегляди",
@ -386,12 +386,12 @@
"Spanish (Mexico)": "Іспанська (Мексика)", "Spanish (Mexico)": "Іспанська (Мексика)",
"Spanish (Spain)": "Іспанська (Іспанія)", "Spanish (Spain)": "Іспанська (Іспанія)",
"next_steps_error_message_go_to_youtube": "Перейти до YouTube", "next_steps_error_message_go_to_youtube": "Перейти до YouTube",
"footer_donate_page": ожертвувати", "footer_donate_page": ідтримати",
"footer_documentation": "Документація", "footer_documentation": "Документація",
"footer_source_code": "Вихідний код", "footer_source_code": "Джерельний код",
"footer_original_source_code": "Оригінал вихідного коду", "footer_original_source_code": "Оригінал джерельного коду",
"footer_modfied_source_code": "Змінений вихідний код", "footer_modfied_source_code": "Змінений джерельний код",
"adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого вихідного коду", "adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого джерельного коду",
"none": "нема", "none": "нема",
"videoinfo_started_streaming_x_ago": "Трансляцію розпочато `x` тому", "videoinfo_started_streaming_x_ago": "Трансляцію розпочато `x` тому",
"crash_page_you_found_a_bug": "Схоже, ви знайшли ваду в Invidious!", "crash_page_you_found_a_bug": "Схоже, ви знайшли ваду в Invidious!",
@ -408,7 +408,7 @@
"next_steps_error_message": "Після чого спробуйте: ", "next_steps_error_message": "Після чого спробуйте: ",
"next_steps_error_message_refresh": "Оновити сторінку", "next_steps_error_message_refresh": "Оновити сторінку",
"Search": "Пошук", "Search": "Пошук",
"preferences_extend_desc_label": "Автоматично розширювати опис відео: ", "preferences_extend_desc_label": "Автоматично розгортати опис відео: ",
"preferences_category_misc": "Різноманітні параметри", "preferences_category_misc": "Різноманітні параметри",
"Show less": "Коротше", "Show less": "Коротше",
"preferences_quality_option_small": "Низька", "preferences_quality_option_small": "Низька",
@ -488,5 +488,9 @@
"search_filters_sort_option_rating": "Рейтингові", "search_filters_sort_option_rating": "Рейтингові",
"search_filters_sort_option_views": "Популярні", "search_filters_sort_option_views": "Популярні",
"Popular enabled: ": "Популярне ввімкнено: ", "Popular enabled: ": "Популярне ввімкнено: ",
"error_video_not_in_playlist": "Запитуваного відео в цьому списку відтворення не існує. <a href=\"`x`\">Клацніть тут, щоб переглянути домашню сторінку списку відтворення.</a>" "error_video_not_in_playlist": "Запитуваного відео в цьому списку відтворення не існує. <a href=\"`x`\">Клацніть тут, щоб переглянути домашню сторінку списку відтворення.</a>",
"channel_tab_shorts_label": "Shorts",
"channel_tab_streams_label": "Прямі трансляції",
"channel_tab_playlists_label": "Добірки",
"channel_tab_channels_label": "Канали"
} }

ファイルの表示

@ -311,9 +311,9 @@
"`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤", "`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤",
"Audio mode": "Chế độ âm thanh", "Audio mode": "Chế độ âm thanh",
"Video mode": "Chế độ quay", "Video mode": "Chế độ quay",
"Videos": "Video", "channel_tab_videos_label": "Video",
"Playlists": "Danh sách phát", "Playlists": "Danh sách phát",
"Community": "Cộng đồng", "channel_tab_community_label": "Cộng đồng",
"search_filters_sort_option_relevance": "liên quan", "search_filters_sort_option_relevance": "liên quan",
"search_filters_sort_option_rating": "Xếp hạng", "search_filters_sort_option_rating": "Xếp hạng",
"search_filters_sort_option_date": "ngày", "search_filters_sort_option_date": "ngày",

ファイルの表示

@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` 为此加 ❤", "`x` marked it with a ❤": "`x` 为此加 ❤",
"Audio mode": "音频模式", "Audio mode": "音频模式",
"Video mode": "视频模式", "Video mode": "视频模式",
"Videos": "视频", "channel_tab_videos_label": "视频",
"Playlists": "播放列表", "Playlists": "播放列表",
"Community": "社区", "channel_tab_community_label": "社区",
"search_filters_sort_option_relevance": "相关度", "search_filters_sort_option_relevance": "相关度",
"search_filters_sort_option_rating": "评分", "search_filters_sort_option_rating": "评分",
"search_filters_sort_option_date": "上传日期", "search_filters_sort_option_date": "上传日期",

ファイルの表示

@ -341,9 +341,9 @@
"`x` marked it with a ❤": "`x` 為此標記 ❤", "`x` marked it with a ❤": "`x` 為此標記 ❤",
"Audio mode": "音訊模式", "Audio mode": "音訊模式",
"Video mode": "視訊模式", "Video mode": "視訊模式",
"Videos": "影片", "channel_tab_videos_label": "影片",
"Playlists": "播放清單", "Playlists": "播放清單",
"Community": "社群", "channel_tab_community_label": "社群",
"search_filters_sort_option_relevance": "關聯", "search_filters_sort_option_relevance": "關聯",
"search_filters_sort_option_rating": "評分", "search_filters_sort_option_rating": "評分",
"search_filters_sort_option_date": "日期", "search_filters_sort_option_date": "日期",
@ -456,5 +456,9 @@
"search_filters_type_option_all": "任何類型", "search_filters_type_option_all": "任何類型",
"search_filters_date_option_none": "任何日期", "search_filters_date_option_none": "任何日期",
"Popular enabled: ": "已啟用人氣: ", "Popular enabled: ": "已啟用人氣: ",
"error_video_not_in_playlist": "此播放清單不存在請求的影片。<a href=\"`x`\">點擊此處檢視播放清單首頁。</a>" "error_video_not_in_playlist": "此播放清單不存在請求的影片。<a href=\"`x`\">點擊此處檢視播放清單首頁。</a>",
"channel_tab_shorts_label": "短片",
"channel_tab_playlists_label": "播放清單",
"channel_tab_channels_label": "頻道",
"channel_tab_streams_label": "直播"
} }

0
scripts/deploy-database.sh ノーマルファイル → 実行可能ファイル
ファイルの表示

2
scripts/fetch-player-dependencies.cr ノーマルファイル → 実行可能ファイル
ファイルの表示

@ -129,7 +129,7 @@ dependencies_to_install.each do |dep|
dep = "videojs.markers" if dep == "videojs-markers" dep = "videojs.markers" if dep == "videojs-markers"
if File.exists?("#{download_path}/package/dist/#{dep}.css") if File.exists?("#{download_path}/package/dist/#{dep}.css")
if minified && File.exists?("#{tmp_dir_path}/#{dep}/package/dist/#{dep}.min.css") if minified && File.exists?("#{download_path}/package/dist/#{dep}.min.css")
`mv #{download_path}/package/dist/#{dep}.min.css #{dest_path}/#{dep}.css` `mv #{download_path}/package/dist/#{dep}.min.css #{dest_path}/#{dep}.css`
else else
`mv #{download_path}/package/dist/#{dep}.css #{dest_path}/#{dep}.css` `mv #{download_path}/package/dist/#{dep}.css #{dest_path}/#{dep}.css`

0
scripts/install-dependencies.sh ノーマルファイル → 実行可能ファイル
ファイルの表示

ファイルの表示

@ -34,7 +34,7 @@ shards:
protodec: protodec:
git: https://github.com/iv-org/protodec.git git: https://github.com/iv-org/protodec.git
version: 0.1.4 version: 0.1.5
radix: radix:
git: https://github.com/luislavena/radix.git git: https://github.com/luislavena/radix.git

ファイルの表示

@ -24,7 +24,7 @@ dependencies:
version: ~> 0.6.1 version: ~> 0.6.1
protodec: protodec:
github: iv-org/protodec github: iv-org/protodec
version: ~> 0.1.4 version: ~> 0.1.5
lsquic: lsquic:
github: iv-org/lsquic.cr github: iv-org/lsquic.cr
version: ~> 2.18.1-2 version: ~> 2.18.1-2

ファイルの表示

@ -4,7 +4,7 @@ Spectator.describe Invidious::Hashtag do
it "parses richItemRenderer containers (test 1)" do it "parses richItemRenderer containers (test 1)" do
# Enable mock # Enable mock
test_content = load_mock("hashtag/martingarrix_page1") test_content = load_mock("hashtag/martingarrix_page1")
videos = extract_items(test_content) videos, _ = extract_items(test_content)
expect(typeof(videos)).to eq(Array(SearchItem)) expect(typeof(videos)).to eq(Array(SearchItem))
expect(videos.size).to eq(60) expect(videos.size).to eq(60)
@ -57,7 +57,7 @@ Spectator.describe Invidious::Hashtag do
it "parses richItemRenderer containers (test 2)" do it "parses richItemRenderer containers (test 2)" do
# Enable mock # Enable mock
test_content = load_mock("hashtag/martingarrix_page2") test_content = load_mock("hashtag/martingarrix_page2")
videos = extract_items(test_content) videos, _ = extract_items(test_content)
expect(typeof(videos)).to eq(Array(SearchItem)) expect(typeof(videos)).to eq(Array(SearchItem))
expect(videos.size).to eq(60) expect(videos.size).to eq(60)

ファイルの表示

@ -23,12 +23,6 @@ Spectator.describe "Helper" do
end end
end end
describe "#produce_channel_playlists_url" do
it "correctly produces a /browse_ajax URL with the given UCID and cursor" do
expect(produce_channel_playlists_url("UCCj956IF62FbT7Gouszaj9w", "AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW")).to eq("/browse_ajax?continuation=4qmFsgLNARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrABRWdsd2JHRjViR2x6ZEhNd0FqZ0JZQUZxQUxnQkFIcG1VVlZzVUdFeGF6VlNWa1ozWVZZNWJtVlhOSGhZTVVaNVVtNVdZVTFZU214VWFtZDRXREF4VG1KVmEzaFhWekZ6VVcxS2MyUjZhSEZPTUhCSlUxaFNSbEpyWXpGaFJHUjRXVEJ3VlZSdFVUQldlbXcwVGxaR01XRXhPVVJXYkc5M1RXcG9ibFozSUFFWUF3PT0%3D&gl=US&hl=en")
end
end
describe "#produce_comment_continuation" do describe "#produce_comment_continuation" do
it "correctly produces a continuation token for comments" do it "correctly produces a continuation token for comments" do
expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D") expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")

ファイルの表示

@ -34,6 +34,7 @@ require "protodec/utils"
require "./invidious/database/*" require "./invidious/database/*"
require "./invidious/database/migrations/*" require "./invidious/database/migrations/*"
require "./invidious/http_server/*"
require "./invidious/helpers/*" require "./invidious/helpers/*"
require "./invidious/yt_backend/*" require "./invidious/yt_backend/*"
require "./invidious/frontend/*" require "./invidious/frontend/*"
@ -48,6 +49,13 @@ require "./invidious/search/*"
require "./invidious/routes/**" require "./invidious/routes/**"
require "./invidious/jobs/**" require "./invidious/jobs/**"
# Declare the base namespace for invidious
module Invidious
end
# Simple alias to make code easier to read
alias IV = Invidious
CONFIG = Config.load CONFIG = Config.load
HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32)
@ -172,7 +180,7 @@ if CONFIG.popular_enabled
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
end end
CONNECTION_CHANNEL = Channel({Bool, Channel(PQ::Notification)}).new(32) CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url) Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new

ファイルの表示

@ -16,12 +16,6 @@ record AboutChannel,
tabs : Array(String), tabs : Array(String),
verified : Bool verified : Bool
record AboutRelatedChannel,
ucid : String,
author : String,
author_url : String,
author_thumbnail : String
def get_about_info(ucid, locale) : AboutChannel def get_about_info(ucid, locale) : AboutChannel
begin begin
# "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"} # "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"}
@ -100,34 +94,46 @@ def get_about_info(ucid, locale) : AboutChannel
total_views = 0_i64 total_views = 0_i64
joined = Time.unix(0) joined = Time.unix(0)
tabs = [] of String tab_names = [] of String
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?
if !tabs_json.nil? # Get the name of the tabs available on this channel
# Retrieve information from the tabs array. The index we are looking for varies between channels. tab_names = tabs_json.as_a.compact_map do |entry|
tabs_json.each do |node| name = entry.dig?("tabRenderer", "title").try &.as_s.downcase
# Try to find the about section which is located in only one of the tabs.
channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]?
.try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]?
.try &.[0]?.try &.["channelAboutFullMetadataRenderer"]?
if !channel_about_meta.nil? # This is a small fix to not add extra code on the HTML side
total_views = channel_about_meta["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D/, "").to_i64? || 0_i64 # I.e, the URL for the "live" tab is .../streams, so use "streams"
# everywhere for the sake of simplicity
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date. (name == "live") ? "streams" : name
joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, nd| acc + nd["text"].as_s } end
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
# Get the currently active tab ("About")
# Normal Auto-generated channels about_tab = extract_selected_tab(tabs_json)
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] # Try to find the about metadata section
if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) && channel_about_meta = about_tab.dig?(
(channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube" "content",
auto_generated = true "sectionListRenderer", "contents", 0,
end "itemSectionRenderer", "contents", 0,
end "channelAboutFullMetadataRenderer"
)
if !channel_about_meta.nil?
total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
joined = extract_text(channel_about_meta["joinedDateText"]?)
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
# Normal Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has
# ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
auto_generated = (
(channel_about_meta["primaryLinks"]?.try &.size) == 1 && \
extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube"
)
end end
tabs = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map(&.["tabRenderer"]["title"].as_s.downcase)
end end
sub_count = initdata sub_count = initdata
@ -148,46 +154,20 @@ def get_about_info(ucid, locale) : AboutChannel
joined: joined, joined: joined,
is_family_friendly: is_family_friendly, is_family_friendly: is_family_friendly,
allowed_regions: allowed_regions, allowed_regions: allowed_regions,
tabs: tabs, tabs: tab_names,
verified: author_verified || false, verified: author_verified || false,
) )
end end
def fetch_related_channels(about_channel : AboutChannel) : Array(AboutRelatedChannel) def fetch_related_channels(about_channel : AboutChannel, continuation : String? = nil) : {Array(SearchChannel), String?}
# params is {"2:string":"channels"} encoded if continuation.nil?
channels = YoutubeAPI.browse(browse_id: about_channel.ucid, params: "EghjaGFubmVscw%3D%3D") # params is {"2:string":"channels"} encoded
initial_data = YoutubeAPI.browse(browse_id: about_channel.ucid, params: "EghjaGFubmVscw%3D%3D")
tabs = channels.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs").try(&.as_a?) || [] of JSON::Any else
tab = tabs.find(&.dig?("tabRenderer", "title").try(&.as_s?).try(&.== "Channels")) initial_data = YoutubeAPI.browse(continuation)
return [] of AboutRelatedChannel if tab.nil?
items = tab.dig?(
"tabRenderer", "content",
"sectionListRenderer", "contents", 0,
"itemSectionRenderer", "contents", 0,
"gridRenderer", "items"
).try &.as_a?
related = [] of AboutRelatedChannel
return related if (items.nil? || items.empty?)
items.each do |item|
renderer = item["gridChannelRenderer"]?
next if !renderer
related_id = renderer.dig("channelId").as_s
related_title = renderer.dig("title", "simpleText").as_s
related_author_url = renderer.dig("navigationEndpoint", "browseEndpoint", "canonicalBaseUrl").as_s
related_author_thumbnail = HelperExtractors.get_thumbnails(renderer)
related << AboutRelatedChannel.new(
ucid: related_id,
author: related_title,
author_url: related_author_url,
author_thumbnail: related_author_thumbnail,
)
end end
return related items, continuation = extract_items(initial_data)
return items.select(SearchChannel), continuation
end end

ファイルの表示

@ -180,11 +180,16 @@ def fetch_channel(ucid, pull_all_videos : Bool)
LOGGER.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}") LOGGER.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}")
page = 1 channel = InvidiousChannel.new({
id: ucid,
author: author,
updated: Time.utc,
deleted: false,
subscribed: nil,
})
LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page") LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page")
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated) videos, continuation = IV::Channel::Tabs.get_videos(channel)
videos = extract_videos(initial_data, author, ucid)
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed")
rss.xpath_nodes("//feed/entry").each do |entry| rss.xpath_nodes("//feed/entry").each do |entry|
@ -197,7 +202,9 @@ def fetch_channel(ucid, pull_all_videos : Bool)
views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64? views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64?
views ||= 0_i64 views ||= 0_i64
channel_video = videos.select { |video| video.id == video_id }[0]? channel_video = videos
.select(SearchVideo)
.select(&.id.== video_id)[0]?
length_seconds = channel_video.try &.length_seconds length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0 length_seconds ||= 0
@ -228,58 +235,56 @@ def fetch_channel(ucid, pull_all_videos : Bool)
if was_insert if was_insert
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
Invidious::Database::Users.add_notification(video) if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
else else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
end end
end end
if pull_all_videos if pull_all_videos
page += 1
ids = [] of String
loop do loop do
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated) # Keep fetching videos using the continuation token retrieved earlier
videos = extract_videos(initial_data, author, ucid) videos, continuation = IV::Channel::Tabs.get_videos(channel, continuation: continuation)
count = videos.size count = 0
videos = videos.map { |video| ChannelVideo.new({ videos.select(SearchVideo).each do |video|
id: video.id, count += 1
title: video.title, video = ChannelVideo.new({
published: video.published, id: video.id,
updated: Time.utc, title: video.title,
ucid: video.ucid, published: video.published,
author: video.author, updated: Time.utc,
length_seconds: video.length_seconds, ucid: video.ucid,
live_now: video.live_now, author: video.author,
premiere_timestamp: video.premiere_timestamp, length_seconds: video.length_seconds,
views: video.views, live_now: video.live_now,
}) } premiere_timestamp: video.premiere_timestamp,
views: video.views,
videos.each do |video| })
ids << video.id
# We are notified of Red videos elsewhere (PubSub), which includes a correct published date, # We are notified of Red videos elsewhere (PubSub), which includes a correct published date,
# so since they don't provide a published date here we can safely ignore them. # so since they don't provide a published date here we can safely ignore them.
if Time.utc - video.published > 1.minute if Time.utc - video.published > 1.minute
was_insert = Invidious::Database::ChannelVideos.insert(video) was_insert = Invidious::Database::ChannelVideos.insert(video)
Invidious::Database::Users.add_notification(video) if was_insert if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
end
end end
end end
break if count < 25 break if count < 25
page += 1 sleep 500.milliseconds
end end
end end
channel = InvidiousChannel.new({ channel.updated = Time.utc
id: ucid,
author: author,
updated: Time.utc,
deleted: false,
subscribed: nil,
})
return channel return channel
end end

ファイルの表示

@ -69,7 +69,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
next if !post next if !post
content_html = post["contentText"]?.try { |t| parse_content(t) } || "" content_html = post["contentText"]?.try { |t| parse_content(t) } || ""
author = post["authorText"]?.try &.["simpleText"]? || "" author = post["authorText"]["runs"]?.try &.[0]?.try &.["text"]? || ""
json.object do json.object do
json.field "author", author json.field "author", author
@ -189,6 +189,32 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
# when .has_key?("pollRenderer") # when .has_key?("pollRenderer")
# attachment = attachment["pollRenderer"] # attachment = attachment["pollRenderer"]
# json.field "type", "poll" # json.field "type", "poll"
when .has_key?("postMultiImageRenderer")
attachment = attachment["postMultiImageRenderer"]
json.field "type", "multiImage"
json.field "images" do
json.array do
attachment["images"].as_a.each do |image|
json.array do
thumbnail = image["backstageImageRenderer"]["image"]["thumbnails"][0].as_h
width = thumbnail["width"].as_i
height = thumbnail["height"].as_i
aspect_ratio = (width.to_f / height.to_f)
url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640")
qualities = {320, 560, 640, 1280, 2000}
qualities.each do |quality|
json.object do
json.field "url", url.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", (quality / aspect_ratio).ceil.to_i
end
end
end
end
end
end
else else
json.field "type", "unknown" json.field "type", "unknown"
json.field "error", "Unrecognized attachment type." json.field "error", "Unrecognized attachment type."

ファイルの表示

@ -1,93 +1,28 @@
def fetch_channel_playlists(ucid, author, continuation, sort_by) def fetch_channel_playlists(ucid, author, continuation, sort_by)
if continuation if continuation
response_json = YoutubeAPI.browse(continuation) initial_data = YoutubeAPI.browse(continuation)
continuation_items = response_json["onResponseReceivedActions"]?
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]
return [] of SearchItem, nil if !continuation_items
items = [] of SearchItem
continuation_items.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item|
extract_item(item, author, ucid).try { |t| items << t }
}
continuation = continuation_items.as_a.last["continuationItemRenderer"]?
.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
else else
url = "/channel/#{ucid}/playlists?flow=list&view=1" params =
case sort_by
when "last", "last_added"
# Equivalent to "&sort=lad"
# {"2:string": "playlists", "3:varint": 4, "4:varint": 1, "6:varint": 1}
"EglwbGF5bGlzdHMYBCABMAE%3D"
when "oldest", "oldest_created"
# formerly "&sort=da"
# Not available anymore :c or maybe ??
# {"2:string": "playlists", "3:varint": 2, "4:varint": 1, "6:varint": 1}
"EglwbGF5bGlzdHMYAiABMAE%3D"
# {"2:string": "playlists", "3:varint": 1, "4:varint": 1, "6:varint": 1}
# "EglwbGF5bGlzdHMYASABMAE%3D"
when "newest", "newest_created"
# Formerly "&sort=dd"
# {"2:string": "playlists", "3:varint": 3, "4:varint": 1, "6:varint": 1}
"EglwbGF5bGlzdHMYAyABMAE%3D"
end
case sort_by initial_data = YoutubeAPI.browse(ucid, params: params || "")
when "last", "last_added"
#
when "oldest", "oldest_created"
url += "&sort=da"
when "newest", "newest_created"
url += "&sort=dd"
else nil # Ignore
end
response = YT_POOL.client &.get(url)
initial_data = extract_initial_data(response.body)
return [] of SearchItem, nil if !initial_data
items = extract_items(initial_data, author, ucid)
continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]?
end end
return items, continuation return extract_items(initial_data, author, ucid)
end
# ## NOTE: DEPRECATED
# Reason -> Unstable
# The Protobuf object must be provided with an id of the last playlist from the current "page"
# in order to fetch the next one accurately
# (if the id isn't included, entries shift around erratically between pages,
# leading to repetitions and skip overs)
#
# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user,
# it's better to stick to continuation tokens provided by the first request and onward
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "playlists",
"6:varint" => 2_i64,
"7:varint" => 1_i64,
"12:varint" => 1_i64,
"13:string" => "",
"23:varint" => 0_i64,
},
},
}
if cursor
cursor = Base64.urlsafe_encode(cursor, false) if !auto_generated
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = cursor
end
if auto_generated
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x32_i64
else
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 1_i64
case sort
when "oldest", "oldest_created"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 2_i64
when "newest", "newest_created"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 3_i64
when "last", "last_added"
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 4_i64
else nil # Ignore
end
end
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
object["80226972:embedded"].delete("3:base64")
continuation = object.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return "/browse_ajax?continuation=#{continuation}&gl=JP&hl=en"
end end

ファイルの表示

@ -16,6 +16,14 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
sort_by_numerical =
case sort_by
when "newest" then 1_i64
when "popular" then 2_i64
when "oldest" then 3_i64 # Broken as of 10/2022 :c
else 1_i64 # Fallback to "newest"
end
object_inner_1 = { object_inner_1 = {
"110:embedded" => { "110:embedded" => {
"3:embedded" => { "3:embedded" => {
@ -24,7 +32,7 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so
"1:string" => object_inner_2_encoded, "1:string" => object_inner_2_encoded,
"2:string" => "00000000-0000-0000-0000-000000000000", "2:string" => "00000000-0000-0000-0000-000000000000",
}, },
"3:varint" => 1_i64, "3:varint" => sort_by_numerical,
}, },
}, },
}, },
@ -52,34 +60,138 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so
return continuation return continuation
end end
def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest")
continuation = produce_channel_videos_continuation(ucid, page,
auto_generated: auto_generated, sort_by: sort_by, v2: true)
return YoutubeAPI.browse(continuation)
end
def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
videos = [] of SearchVideo
# 2.times do |i|
# initial_data = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
initial_data = get_channel_videos_response(ucid, 1, auto_generated: auto_generated, sort_by: sort_by)
videos = extract_videos(initial_data, author, ucid)
# end
return videos.size, videos
end
def get_latest_videos(ucid)
initial_data = get_channel_videos_response(ucid)
author = initial_data["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s
return extract_videos(initial_data, author, ucid)
end
# Used in bypass_captcha_job.cr # Used in bypass_captcha_job.cr
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2) continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2)
return "/browse_ajax?continuation=#{continuation}&gl=JP&hl=en" return "/browse_ajax?continuation=#{continuation}&gl=JP&hl=en"
end end
module Invidious::Channel::Tabs
extend self
# -------------------
# Regular videos
# -------------------
def make_initial_video_ctoken(ucid, sort_by) : String
return produce_channel_videos_continuation(ucid, sort_by: sort_by)
end
# Wrapper for AboutChannel, as we still need to call get_videos with
# an author name and ucid directly (e.g in RSS feeds).
# TODO: figure out how to get rid of that
def get_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
return get_videos(
channel.author, channel.ucid,
continuation: continuation, sort_by: sort_by
)
end
# Wrapper for InvidiousChannel, as we still need to call get_videos with
# an author name and ucid directly (e.g in RSS feeds).
# TODO: figure out how to get rid of that
def get_videos(channel : InvidiousChannel, *, continuation : String? = nil, sort_by = "newest")
return get_videos(
channel.author, channel.id,
continuation: continuation, sort_by: sort_by
)
end
def get_videos(author : String, ucid : String, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_video_ctoken(ucid, sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, author, ucid)
end
def get_60_videos(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
if continuation.nil?
# Fetch the first "page" of video
items, next_continuation = get_videos(channel, sort_by: sort_by)
else
# Fetch a "page" of videos using the given continuation token
items, next_continuation = get_videos(channel, continuation: continuation)
end
# If there is more to load, then load a second "page"
# and replace the previous continuation token
if !next_continuation.nil?
items_2, next_continuation = get_videos(channel, continuation: next_continuation)
items.concat items_2
end
return items, next_continuation
end
# -------------------
# Shorts
# -------------------
private def fetch_shorts_data(ucid : String, continuation : String? = nil)
if continuation.nil?
# EgZzaG9ydHPyBgUKA5oBAA%3D%3D is the protobuf object to load "shorts"
# TODO: try to extract the continuation tokens that allows other sorting options
return YoutubeAPI.browse(ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D")
else
return YoutubeAPI.browse(continuation: continuation)
end
end
def get_shorts(channel : AboutChannel, continuation : String? = nil)
initial_data = self.fetch_shorts_data(channel.ucid, continuation)
begin
# Try to parse the initial data fetched above
return extract_items(initial_data, channel.author, channel.ucid)
rescue ex : RetryOnceException
# Sometimes, for a completely unknown reason, the "reelItemRenderer"
# object is missing some critical information (it happens once in about
# 20 subsequent requests). Refreshing the page is required to properly
# show the "shorts" tab.
#
# In order to make the experience smoother for the user, we simulate
# said page refresh by fetching again the JSON. If that still doesn't
# work, we raise a BrokenTubeException, as something is really broken.
begin
initial_data = self.fetch_shorts_data(channel.ucid, continuation)
return extract_items(initial_data, channel.author, channel.ucid)
rescue ex : RetryOnceException
raise BrokenTubeException.new "reelPlayerHeaderSupportedRenderers"
end
end
end
# -------------------
# Livestreams
# -------------------
def get_livestreams(channel : AboutChannel, continuation : String? = nil)
if continuation.nil?
# EgdzdHJlYW1z8gYECgJ6AA%3D%3D is the protobuf object to load "streams"
initial_data = YoutubeAPI.browse(channel.ucid, params: "EgdzdHJlYW1z8gYECgJ6AA%3D%3D")
else
initial_data = YoutubeAPI.browse(continuation: continuation)
end
return extract_items(initial_data, channel.author, channel.ucid)
end
def get_60_livestreams(channel : AboutChannel, continuation : String? = nil)
if continuation.nil?
# Fetch the first "page" of streams
items, next_continuation = get_livestreams(channel)
else
# Fetch a "page" of streams using the given continuation token
items, next_continuation = get_livestreams(channel, continuation: continuation)
end
# If there is more to load, then load a second "page"
# and replace the previous continuation token
if !next_continuation.nil?
items_2, next_continuation = get_livestreams(channel, continuation: next_continuation)
items.concat items_2
end
return items, next_continuation
end
end

ファイルの表示

@ -110,6 +110,8 @@ class Config
property hsts : Bool? = true property hsts : Bool? = true
# Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local' # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
property disable_proxy : Bool? | Array(String)? = false property disable_proxy : Bool? | Array(String)? = false
# Enable the user notifications for all users
property enable_user_notifications : Bool = true
# URL to the modified source code to be easily AGPL compliant # URL to the modified source code to be easily AGPL compliant
# Will display in the footer, next to the main source code link # Will display in the footer, next to the main source code link

ファイルの表示

@ -154,6 +154,16 @@ module Invidious::Database::Users
# Update (misc) # Update (misc)
# ------------------- # -------------------
def feed_needs_update(video : ChannelVideo)
request = <<-SQL
UPDATE users
SET feed_needs_update = true
WHERE $1 = ANY(subscriptions)
SQL
PG_DB.exec(request, video.ucid)
end
def update_preferences(user : User) def update_preferences(user : User)
request = <<-SQL request = <<-SQL
UPDATE users UPDATE users

ファイルの表示

@ -33,3 +33,8 @@ end
class VideoNotAvailableException < Exception class VideoNotAvailableException < Exception
end end
# Exception used to indicate that the JSON response from YT is missing
# some important informations, and that the query should be sent again.
class RetryOnceException < Exception
end

44
src/invidious/frontend/channel_page.cr ノーマルファイル
ファイルの表示

@ -0,0 +1,44 @@
module Invidious::Frontend::ChannelPage
extend self
enum TabsAvailable
Videos
Shorts
Streams
Playlists
Community
Channels
end
def generate_tabs_links(locale : String, channel : AboutChannel, selected_tab : TabsAvailable)
return String.build(1500) do |str|
base_url = "/channel/#{channel.ucid}"
TabsAvailable.each do |tab|
# Ignore playlists, as it is not supported for auto-generated channels yet
next if (tab.playlists? && channel.auto_generated)
tab_name = tab.to_s.downcase
if channel.tabs.includes? tab_name
str << %(<div class="pure-u-1 pure-md-1-3">\n)
if tab == selected_tab
str << "\t<b>"
str << translate(locale, "channel_tab_#{tab_name}_label")
str << "</b>\n"
else
# Video tab doesn't have the last path component
url = tab.videos? ? base_url : "#{base_url}/#{tab_name}"
str << %(\t<a href=") << url << %(">)
str << translate(locale, "channel_tab_#{tab_name}_label")
str << "</a>\n"
end
str << "</div>"
end
end
end
end
end

ファイルの表示

@ -8,7 +8,8 @@ module Invidious::Hashtag
client_config = YoutubeAPI::ClientConfig.new(region: region) client_config = YoutubeAPI::ClientConfig.new(region: region)
response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config) response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config)
return extract_items(response) items, _ = extract_items(response)
return items
end end
def generate_continuation(hashtag : String, cursor : Int) def generate_continuation(hashtag : String, cursor : Int)

ファイルの表示

@ -20,7 +20,7 @@ module JSONFilter
/^\(|\(\(|\/\(/ /^\(|\(\(|\/\(/
end end
def self.parse_fields(fields_text : String) : Nil def self.parse_fields(fields_text : String, &) : Nil
if fields_text.empty? if fields_text.empty?
raise FieldsParser::ParseError.new "Fields is empty" raise FieldsParser::ParseError.new "Fields is empty"
end end
@ -42,7 +42,7 @@ module JSONFilter
parse_nest_groups(fields_text) { |nest_list| yield nest_list } parse_nest_groups(fields_text) { |nest_list| yield nest_list }
end end
def self.parse_single_nests(fields_text : String) : Nil def self.parse_single_nests(fields_text : String, &) : Nil
single_nests = remove_nest_groups(fields_text) single_nests = remove_nest_groups(fields_text)
if !single_nests.empty? if !single_nests.empty?
@ -60,7 +60,7 @@ module JSONFilter
end end
end end
def self.parse_nest_groups(fields_text : String) : Nil def self.parse_nest_groups(fields_text : String, &) : Nil
nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64) nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64)
bracket_pairs = get_bracket_pairs(fields_text, true) bracket_pairs = get_bracket_pairs(fields_text, true)

ファイルの表示

@ -265,4 +265,11 @@ class Category
end end
end end
struct Continuation
getter token
def initialize(@token : String)
end
end
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category

ファイルの表示

@ -162,7 +162,7 @@ def number_with_separator(number)
end end
def short_text_to_number(short_text : String) : Int64 def short_text_to_number(short_text : String) : Int64
matches = /(?<number>\d+(\.\d+)?)\s?(?<suffix>[mMkKbB])?/.match(short_text) matches = /(?<number>\d+(\.\d+)?)\s?(?<suffix>[mMkKbB]?)/.match(short_text)
number = matches.try &.["number"].to_f || 0.0 number = matches.try &.["number"].to_f || 0.0
case matches.try &.["suffix"].downcase case matches.try &.["suffix"].downcase
@ -259,7 +259,7 @@ def get_referer(env, fallback = "/", unroll = true)
end end
referer = referer.request_target referer = referer.request_target
referer = "/" + referer.gsub(/[^\/?@&%=\-_.0-9a-zA-Z]/, "").lstrip("/\\") referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\")
if referer == env.request.path if referer == env.request.path
referer = fallback referer = fallback

20
src/invidious/http_server/utils.cr ノーマルファイル
ファイルの表示

@ -0,0 +1,20 @@
module Invidious::HttpServer
module Utils
extend self
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
url = URI.parse(raw_url)
# Add some URL parameters
params = url.query_params
params["host"] = url.host.not_nil! # Should never be nil, in theory
params["region"] = region if !region.nil?
if absolute
return "#{HOST_URL}#{url.request_target}?#{params}"
else
return "#{url.request_target}?#{params}"
end
end
end
end

ファイルの表示

@ -1,12 +1,12 @@
class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
private getter connection_channel : Channel({Bool, Channel(PQ::Notification)}) private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)})
private getter pg_url : URI private getter pg_url : URI
def initialize(@connection_channel, @pg_url) def initialize(@connection_channel, @pg_url)
end end
def begin def begin
connections = [] of Channel(PQ::Notification) connections = [] of ::Channel(PQ::Notification)
PG.connect_listen(pg_url, "notifications") { |event| connections.each(&.send(event)) } PG.connect_listen(pg_url, "notifications") { |event| connections.each(&.send(event)) }

ファイルの表示

@ -8,7 +8,7 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob
max_fibers = CONFIG.channel_threads max_fibers = CONFIG.channel_threads
lim_fibers = max_fibers lim_fibers = max_fibers
active_fibers = 0 active_fibers = 0
active_channel = Channel(Bool).new active_channel = ::Channel(Bool).new
backoff = 2.minutes backoff = 2.minutes
loop do loop do

ファイルの表示

@ -7,7 +7,7 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
def begin def begin
max_fibers = CONFIG.feed_threads max_fibers = CONFIG.feed_threads
active_fibers = 0 active_fibers = 0
active_channel = Channel(Bool).new active_channel = ::Channel(Bool).new
loop do loop do
db.query("SELECT email FROM users WHERE feed_needs_update = true OR feed_needs_update IS NULL") do |rs| db.query("SELECT email FROM users WHERE feed_needs_update = true OR feed_needs_update IS NULL") do |rs|

ファイルの表示

@ -12,7 +12,7 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
end end
active_fibers = 0 active_fibers = 0
active_channel = Channel(Bool).new active_channel = ::Channel(Bool).new
loop do loop do
db.query_all("SELECT id FROM channels WHERE CURRENT_TIMESTAMP - subscribed > interval '4 days' OR subscribed IS NULL") do |rs| db.query_all("SELECT id FROM channels WHERE CURRENT_TIMESTAMP - subscribed > interval '4 days' OR subscribed IS NULL") do |rs|

ファイルの表示

@ -3,7 +3,7 @@ require "json"
module Invidious::JSONify::APIv1 module Invidious::JSONify::APIv1
extend self extend self
def video(video : Video, json : JSON::Builder, *, locale : String?) def video(video : Video, json : JSON::Builder, *, locale : String?, proxy : Bool = false)
json.object do json.object do
json.field "type", video.video_type json.field "type", video.video_type
@ -89,7 +89,14 @@ module Invidious::JSONify::APIv1
# Not available on MPEG-4 Timed Text (`text/mp4`) streams (livestreams only) # Not available on MPEG-4 Timed Text (`text/mp4`) streams (livestreams only)
json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]? json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]?
json.field "url", fmt["url"] if proxy
json.field "url", Invidious::HttpServer::Utils.proxy_video_url(
fmt["url"].to_s, absolute: true
)
else
json.field "url", fmt["url"]
end
json.field "itag", fmt["itag"].as_i.to_s json.field "itag", fmt["itag"].as_i.to_s
json.field "type", fmt["mimeType"] json.field "type", fmt["mimeType"]
json.field "clen", fmt["contentLength"]? || "-1" json.field "clen", fmt["contentLength"]? || "-1"

ファイルの表示

@ -203,7 +203,7 @@ module Invidious::Routes::Account
referer = get_referer(env) referer = get_referer(env)
if !user if !user
return env.redirect referer return env.redirect "/login?referer=#{URI.encode_path_segment(env.request.resource)}"
end end
user = user.as(User) user = user.as(User)

ファイルの表示

@ -29,7 +29,7 @@ module Invidious::Routes::API::Manifest
if local if local
uri = URI.parse(url) uri = URI.parse(url)
url = "#{uri.request_target}host/#{uri.host}/" url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
end end
"<BaseURL>#{url}</BaseURL>" "<BaseURL>#{url}</BaseURL>"
@ -42,7 +42,7 @@ module Invidious::Routes::API::Manifest
if local if local
adaptive_fmts.each do |fmt| adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
end end
end end

ファイルの表示

@ -1,13 +1,7 @@
module Invidious::Routes::API::V1::Channels module Invidious::Routes::API::V1::Channels
def self.home(env) # Macro to avoid duplicating some code below
locale = env.get("preferences").as(Preferences).locale # This sets the `channel` variable, or handles Exceptions.
private macro get_channel
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
sort_by = env.params.query["sort_by"]?.try &.downcase
sort_by ||= "newest"
begin begin
channel = get_about_info(ucid, locale) channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect rescue ex : ChannelRedirect
@ -18,17 +12,25 @@ module Invidious::Routes::API::V1::Channels
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
end
page = 1 def self.home(env)
if channel.auto_generated locale = env.get("preferences").as(Preferences).locale
videos = [] of SearchVideo ucid = env.params.url["ucid"]
count = 0
else env.response.content_type = "application/json"
begin
count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) # Use the private macro defined above.
rescue ex channel = nil # Make the compiler happy
return error_json(500, ex) get_channel()
end
# Retrieve "sort by" setting from URL parameters
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
begin
videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by)
rescue ex
return error_json(500, ex)
end end
JSON.build do |json| JSON.build do |json|
@ -100,31 +102,13 @@ module Invidious::Routes::API::V1::Channels
json.array do json.array do
# Fetch related channels # Fetch related channels
begin begin
related_channels = fetch_related_channels(channel) related_channels, _ = fetch_related_channels(channel)
rescue ex rescue ex
related_channels = [] of AboutRelatedChannel related_channels = [] of SearchChannel
end end
related_channels.each do |related_channel| related_channels.each do |related_channel|
json.object do related_channel.to_json(locale, json)
json.field "author", related_channel.author
json.field "authorId", related_channel.ucid
json.field "authorUrl", related_channel.author_url
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
qualities.each do |quality|
json.object do
json.field "url", related_channel.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end
end
end
end
end
end end
end end
end # relatedChannels end # relatedChannels
@ -134,61 +118,112 @@ module Invidious::Routes::API::V1::Channels
end end
def self.latest(env) def self.latest(env)
locale = env.get("preferences").as(Preferences).locale # Remove parameters that could affect this endpoint's behavior
env.params.query.delete("sort_by") if env.params.query.has_key?("sort_by")
env.params.query.delete("continuation") if env.params.query.has_key?("continuation")
env.response.content_type = "application/json" return self.videos(env)
ucid = env.params.url["ucid"]
begin
videos = get_latest_videos(ucid)
rescue ex
return error_json(500, ex)
end
JSON.build do |json|
json.array do
videos.each do |video|
video.to_json(locale, json)
end
end
end
end end
def self.videos(env) def self.videos(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
ucid = env.params.url["ucid"]
env.response.content_type = "application/json" env.response.content_type = "application/json"
ucid = env.params.url["ucid"] # Use the private macro defined above.
page = env.params.query["page"]?.try &.to_i? channel = nil # Make the compiler happy
page ||= 1 get_channel()
sort_by = env.params.query["sort"]?.try &.downcase
sort_by ||= env.params.query["sort_by"]?.try &.downcase # Retrieve some URL parameters
sort_by ||= "newest" sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]?
begin begin
channel = get_about_info(ucid, locale) videos, next_continuation = Channel::Tabs.get_60_videos(
rescue ex : ChannelRedirect channel, continuation: continuation, sort_by: sort_by
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id) )
return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id})
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
begin return JSON.build do |json|
count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) json.object do
rescue ex json.field "videos" do
return error_json(500, ex) json.array do
end videos.each &.to_json(locale, json)
end
JSON.build do |json|
json.array do
videos.each do |video|
video.to_json(locale, json)
end end
json.field "continuation", next_continuation if next_continuation
end
end
end
def self.shorts(env)
locale = env.get("preferences").as(Preferences).locale
ucid = env.params.url["ucid"]
env.response.content_type = "application/json"
# Use the private macro defined above.
channel = nil # Make the compiler happy
get_channel()
# Retrieve continuation from URL parameters
continuation = env.params.query["continuation"]?
begin
videos, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
)
rescue ex
return error_json(500, ex)
end
return JSON.build do |json|
json.object do
json.field "videos" do
json.array do
videos.each &.to_json(locale, json)
end
end
json.field "continuation", next_continuation if next_continuation
end
end
end
def self.streams(env)
locale = env.get("preferences").as(Preferences).locale
ucid = env.params.url["ucid"]
env.response.content_type = "application/json"
# Use the private macro defined above.
channel = nil # Make the compiler happy
get_channel()
# Retrieve continuation from URL parameters
continuation = env.params.query["continuation"]?
begin
videos, next_continuation = Channel::Tabs.get_60_livestreams(
channel, continuation: continuation
)
rescue ex
return error_json(500, ex)
end
return JSON.build do |json|
json.object do
json.field "videos" do
json.array do
videos.each &.to_json(locale, json)
end
end
json.field "continuation", next_continuation if next_continuation
end end
end end
end end
@ -204,16 +239,9 @@ module Invidious::Routes::API::V1::Channels
env.params.query["sort_by"]?.try &.downcase || env.params.query["sort_by"]?.try &.downcase ||
"last" "last"
begin # Use the macro defined above
channel = get_about_info(ucid, locale) channel = nil # Make the compiler happy
rescue ex : ChannelRedirect get_channel()
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
return error_json(302, "Channel is unavailable", {"authorId" => ex.channel_id})
rescue ex : NotFoundException
return error_json(404, ex)
rescue ex
return error_json(500, ex)
end
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
@ -255,6 +283,37 @@ module Invidious::Routes::API::V1::Channels
end end
end end
def self.channels(env)
locale = env.get("preferences").as(Preferences).locale
ucid = env.params.url["ucid"]
env.response.content_type = "application/json"
# Use the macro defined above
channel = nil # Make the compiler happy
get_channel()
continuation = env.params.query["continuation"]?
begin
items, next_continuation = fetch_related_channels(channel, continuation)
rescue ex
return error_json(500, ex)
end
JSON.build do |json|
json.object do
json.field "relatedChannels" do
json.array do
items.each &.to_json(locale, json)
end
end
json.field "continuation", next_continuation if next_continuation
end
end
end
def self.search(env) def self.search(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
region = env.params.query["region"]? region = env.params.query["region"]?

ファイルの表示

@ -6,6 +6,7 @@ module Invidious::Routes::API::V1::Videos
id = env.params.url["id"] id = env.params.url["id"]
region = env.params.query["region"]? region = env.params.query["region"]?
proxy = {"1", "true"}.any? &.== env.params.query["local"]?
begin begin
video = get_video(id, region: region) video = get_video(id, region: region)
@ -15,7 +16,9 @@ module Invidious::Routes::API::V1::Videos
return error_json(500, ex) return error_json(500, ex)
end end
video.to_json(locale, nil) return JSON.build do |json|
Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy)
end
end end
def self.captions(env) def self.captions(env)
@ -90,45 +93,50 @@ module Invidious::Routes::API::V1::Videos
# as well as some other markup that makes it cumbersome, so we try to fix that here # as well as some other markup that makes it cumbersome, so we try to fix that here
if caption.name.includes? "auto-generated" if caption.name.includes? "auto-generated"
caption_xml = YT_POOL.client &.get(url).body caption_xml = YT_POOL.client &.get(url).body
caption_xml = XML.parse(caption_xml)
webvtt = String.build do |str| if caption_xml.starts_with?("<?xml")
str << <<-END_VTT webvtt = caption.timedtext_to_vtt(caption_xml, tlang)
WEBVTT else
Kind: captions caption_xml = XML.parse(caption_xml)
Language: #{tlang || caption.language_code}
webvtt = String.build do |str|
str << <<-END_VTT
WEBVTT
Kind: captions
Language: #{tlang || caption.language_code}
END_VTT END_VTT
caption_nodes = caption_xml.xpath_nodes("//transcript/text") caption_nodes = caption_xml.xpath_nodes("//transcript/text")
caption_nodes.each_with_index do |node, i| caption_nodes.each_with_index do |node, i|
start_time = node["start"].to_f.seconds start_time = node["start"].to_f.seconds
duration = node["dur"]?.try &.to_f.seconds duration = node["dur"]?.try &.to_f.seconds
duration ||= start_time duration ||= start_time
if caption_nodes.size > i + 1 if caption_nodes.size > i + 1
end_time = caption_nodes[i + 1]["start"].to_f.seconds end_time = caption_nodes[i + 1]["start"].to_f.seconds
else else
end_time = start_time + duration end_time = start_time + duration
end
start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}"
end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}"
text = HTML.unescape(node.content)
text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
text = text.gsub(/<\/font>/, "")
if md = text.match(/(?<name>.*) : (?<text>.*)/)
text = "<v #{md["name"]}>#{md["text"]}</v>"
end
str << <<-END_CUE
#{start_time} --> #{end_time}
#{text}
END_CUE
end end
start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}"
end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}"
text = HTML.unescape(node.content)
text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
text = text.gsub(/<\/font>/, "")
if md = text.match(/(?<name>.*) : (?<text>.*)/)
text = "<v #{md["name"]}>#{md["text"]}</v>"
end
str << <<-END_CUE
#{start_time} --> #{end_time}
#{text}
END_CUE
end end
end end
else else
@ -138,7 +146,12 @@ module Invidious::Routes::API::V1::Videos
# #
# See: https://github.com/iv-org/invidious/issues/2391 # See: https://github.com/iv-org/invidious/issues/2391
webvtt = YT_POOL.client &.get("#{url}&format=vtt").body webvtt = YT_POOL.client &.get("#{url}&format=vtt").body
.gsub(/([0-9:.]{12} --> [0-9:.]{12}).+/, "\\1") if webvtt.starts_with?("<?xml")
webvtt = caption.timedtext_to_vtt(webvtt)
else
webvtt = YT_POOL.client &.get("#{url}&format=vtt").body
.gsub(/([0-9:.]{12} --> [0-9:.]{12}).+/, "\\1")
end
end end
if title = env.params.query["title"]? if title = env.params.query["title"]?

ファイルの表示

@ -7,21 +7,19 @@ module Invidious::Routes::Channels
def self.videos(env) def self.videos(env)
data = self.fetch_basic_information(env) data = self.fetch_basic_information(env)
if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
return data
end
locale, user, subscriptions, continuation, ucid, channel = data
page = env.params.query["page"]?.try &.to_i? locale, user, subscriptions, continuation, ucid, channel = data
page ||= 1
sort_by = env.params.query["sort_by"]?.try &.downcase sort_by = env.params.query["sort_by"]?.try &.downcase
if channel.auto_generated if channel.auto_generated
sort_options = {"last", "oldest", "newest"} sort_options = {"last", "oldest", "newest"}
sort_by ||= "last"
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items, next_continuation = fetch_channel_playlists(
channel.ucid, channel.author, continuation, (sort_by || "last")
)
items.uniq! do |item| items.uniq! do |item|
if item.responds_to?(:title) if item.responds_to?(:title)
item.title item.title
@ -33,34 +31,85 @@ module Invidious::Routes::Channels
items.each(&.author = "") items.each(&.author = "")
else else
sort_options = {"newest", "oldest", "popular"} sort_options = {"newest", "oldest", "popular"}
sort_by ||= "newest"
count, items = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) # Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_videos(
channel, continuation: continuation, sort_by: (sort_by || "newest")
)
end end
selected_tab = Frontend::ChannelPage::TabsAvailable::Videos
templated "channel"
end
def self.shorts(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data
if !channel.tabs.includes? "shorts"
return env.redirect "/channel/#{channel.ucid}"
end
# TODO: support sort option for shorts
sort_by = ""
sort_options = [] of String
# Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
)
selected_tab = Frontend::ChannelPage::TabsAvailable::Shorts
templated "channel"
end
def self.streams(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data
if !channel.tabs.includes? "streams"
return env.redirect "/channel/#{channel.ucid}"
end
# TODO: support sort option for livestreams
sort_by = ""
sort_options = [] of String
# Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_60_livestreams(
channel, continuation: continuation
)
selected_tab = Frontend::ChannelPage::TabsAvailable::Streams
templated "channel" templated "channel"
end end
def self.playlists(env) def self.playlists(env)
data = self.fetch_basic_information(env) data = self.fetch_basic_information(env)
if !data.is_a?(Tuple) return data if !data.is_a?(Tuple)
return data
end
locale, user, subscriptions, continuation, ucid, channel = data locale, user, subscriptions, continuation, ucid, channel = data
sort_options = {"last", "oldest", "newest"} sort_options = {"last", "oldest", "newest"}
sort_by = env.params.query["sort_by"]?.try &.downcase sort_by = env.params.query["sort_by"]?.try &.downcase
sort_by ||= "last"
if channel.auto_generated if channel.auto_generated
return env.redirect "/channel/#{channel.ucid}" return env.redirect "/channel/#{channel.ucid}"
end end
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items, next_continuation = fetch_channel_playlists(
channel.ucid, channel.author, continuation, (sort_by || "last")
)
items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) items = items.select(SearchPlaylist).map(&.as(SearchPlaylist))
items.each(&.author = "") items.each(&.author = "")
templated "playlists" selected_tab = Frontend::ChannelPage::TabsAvailable::Playlists
templated "channel"
end end
def self.community(env) def self.community(env)
@ -74,12 +123,15 @@ module Invidious::Routes::Channels
thin_mode = thin_mode == "true" thin_mode = thin_mode == "true"
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
# sort_by = env.params.query["sort_by"]?.try &.downcase
if !channel.tabs.includes? "community" if !channel.tabs.includes? "community"
return env.redirect "/channel/#{channel.ucid}" return env.redirect "/channel/#{channel.ucid}"
end end
# TODO: support sort options for community posts
sort_by = ""
sort_options = [] of String
begin begin
items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode)) items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode))
rescue ex : InfoException rescue ex : InfoException
@ -95,6 +147,26 @@ module Invidious::Routes::Channels
templated "community" templated "community"
end end
def self.channels(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data
if channel.auto_generated
return env.redirect "/channel/#{channel.ucid}"
end
items, next_continuation = fetch_related_channels(channel, continuation)
# Featured/related channels can't be sorted
sort_options = [] of String
sort_by = nil
selected_tab = Frontend::ChannelPage::TabsAvailable::Channels
templated "channel"
end
def self.about(env) def self.about(env)
data = self.fetch_basic_information(env) data = self.fetch_basic_information(env)
if !data.is_a?(Tuple) if !data.is_a?(Tuple)
@ -125,7 +197,7 @@ module Invidious::Routes::Channels
end end
selected_tab = env.request.path.split("/")[-1] selected_tab = env.request.path.split("/")[-1]
if ["home", "videos", "playlists", "community", "channels", "about"].includes? selected_tab if {"home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"}.includes? selected_tab
url = "/channel/#{ucid}/#{selected_tab}" url = "/channel/#{ucid}/#{selected_tab}"
else else
url = "/channel/#{ucid}" url = "/channel/#{ucid}"

ファイルの表示

@ -147,7 +147,7 @@ module Invidious::Routes::Embed
# PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) # PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email)
# end # end
if notifications && notifications.includes? id if CONFIG.enable_user_notifications && notifications && notifications.includes? id
Invidious::Database::Users.remove_notification(user.as(User), id) Invidious::Database::Users.remove_notification(user.as(User), id)
env.get("user").as(User).notifications.delete(id) env.get("user").as(User).notifications.delete(id)
notifications.delete(id) notifications.delete(id)

ファイルの表示

@ -96,12 +96,14 @@ module Invidious::Routes::Feeds
videos, notifications = get_subscription_feed(user, max_results, page) videos, notifications = get_subscription_feed(user, max_results, page)
# "updated" here is used for delivering new notifications, so if if CONFIG.enable_user_notifications
# we know a user has looked at their feed e.g. in the past 10 minutes, # "updated" here is used for delivering new notifications, so if
# they've already seen a video posted 20 minutes ago, and don't need # we know a user has looked at their feed e.g. in the past 10 minutes,
# to be notified. # they've already seen a video posted 20 minutes ago, and don't need
Invidious::Database::Users.clear_notifications(user) # to be notified.
user.notifications = [] of String Invidious::Database::Users.clear_notifications(user)
user.notifications = [] of String
end
env.set "user", user env.set "user", user
templated "feeds/subscriptions" templated "feeds/subscriptions"
@ -404,13 +406,15 @@ module Invidious::Routes::Feeds
video = get_video(id, force_refresh: true) video = get_video(id, force_refresh: true)
# Deliver notifications to `/api/v1/auth/notifications` if CONFIG.enable_user_notifications
payload = { # Deliver notifications to `/api/v1/auth/notifications`
"topic" => video.ucid, payload = {
"videoId" => video.id, "topic" => video.ucid,
"published" => published.to_unix, "videoId" => video.id,
}.to_json "published" => published.to_unix,
PG_DB.exec("NOTIFY notifications, E'#{payload}'") }.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
end
video = ChannelVideo.new({ video = ChannelVideo.new({
id: id, id: id,
@ -426,7 +430,13 @@ module Invidious::Routes::Feeds
}) })
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true) was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
Invidious::Database::Users.add_notification(video) if was_insert if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
end
end end
end end

ファイルの表示

@ -6,14 +6,14 @@ module Invidious::Routes::Login
user = env.get? "user" user = env.get? "user"
return env.redirect "/feed/subscriptions" if user referer = get_referer(env, "/feed/subscriptions")
return env.redirect referer if user
if !CONFIG.login_enabled if !CONFIG.login_enabled
return error_template(400, "Login has been disabled by administrator.") return error_template(400, "Login has been disabled by administrator.")
end end
referer = get_referer(env, "/feed/subscriptions")
email = nil email = nil
password = nil password = nil
captcha = nil captcha = nil

ファイルの表示

@ -35,6 +35,13 @@ module Invidious::Routes::VideoPlayback
end end
end end
# See: https://github.com/iv-org/invidious/issues/3302
range_header = env.request.headers["Range"]?
if range_header.nil?
range_for_head = query_params["range"]? || "0-640"
headers["Range"] = "bytes=#{range_for_head}"
end
client = make_client(URI.parse(host), region) client = make_client(URI.parse(host), region)
response = HTTP::Client::Response.new(500) response = HTTP::Client::Response.new(500)
error = "" error = ""
@ -70,6 +77,9 @@ module Invidious::Routes::VideoPlayback
end end
end end
# Remove the Range header added previously.
headers.delete("Range") if range_header.nil?
if response.status_code >= 400 if response.status_code >= 400
env.response.content_type = "text/plain" env.response.content_type = "text/plain"
haltf env, response.status_code haltf env, response.status_code
@ -91,14 +101,8 @@ module Invidious::Routes::VideoPlayback
env.response.headers["Access-Control-Allow-Origin"] = "*" env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = resp.headers["Location"]? if location = resp.headers["Location"]?
location = URI.parse(location) url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
location = "#{location.request_target}&host=#{location.host}" return env.redirect url
if region
location += "&region=#{region}"
end
return env.redirect location
end end
IO.copy(resp.body_io, env.response) IO.copy(resp.body_io, env.response)

ファイルの表示

@ -80,7 +80,7 @@ module Invidious::Routes::Watch
Invidious::Database::Users.mark_watched(user.as(User), id) Invidious::Database::Users.mark_watched(user.as(User), id)
end end
if notifications && notifications.includes? id if CONFIG.enable_user_notifications && notifications && notifications.includes? id
Invidious::Database::Users.remove_notification(user.as(User), id) Invidious::Database::Users.remove_notification(user.as(User), id)
env.get("user").as(User).notifications.delete(id) env.get("user").as(User).notifications.delete(id)
notifications.delete(id) notifications.delete(id)

ファイルの表示

@ -37,7 +37,9 @@ module Invidious::Routing
get "/feed/webhook/:token", Routes::Feeds, :push_notifications_get get "/feed/webhook/:token", Routes::Feeds, :push_notifications_get
post "/feed/webhook/:token", Routes::Feeds, :push_notifications_post post "/feed/webhook/:token", Routes::Feeds, :push_notifications_post
get "/modify_notifications", Routes::Notifications, :modify if CONFIG.enable_user_notifications
get "/modify_notifications", Routes::Notifications, :modify
end
{% end %} {% end %}
self.register_image_routes self.register_image_routes
@ -115,18 +117,23 @@ module Invidious::Routing
get "/channel/:ucid", Routes::Channels, :home get "/channel/:ucid", Routes::Channels, :home
get "/channel/:ucid/home", Routes::Channels, :home get "/channel/:ucid/home", Routes::Channels, :home
get "/channel/:ucid/videos", Routes::Channels, :videos get "/channel/:ucid/videos", Routes::Channels, :videos
get "/channel/:ucid/shorts", Routes::Channels, :shorts
get "/channel/:ucid/streams", Routes::Channels, :streams
get "/channel/:ucid/playlists", Routes::Channels, :playlists get "/channel/:ucid/playlists", Routes::Channels, :playlists
get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/community", Routes::Channels, :community
get "/channel/:ucid/channels", Routes::Channels, :channels
get "/channel/:ucid/about", Routes::Channels, :about get "/channel/:ucid/about", Routes::Channels, :about
get "/channel/:ucid/live", Routes::Channels, :live get "/channel/:ucid/live", Routes::Channels, :live
get "/user/:user/live", Routes::Channels, :live get "/user/:user/live", Routes::Channels, :live
get "/c/:user/live", Routes::Channels, :live get "/c/:user/live", Routes::Channels, :live
["", "/videos", "/playlists", "/community", "/about"].each do |path| {"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path|
# /c/LinusTechTips # /c/LinusTechTips
get "/c/:user#{path}", Routes::Channels, :brand_redirect get "/c/:user#{path}", Routes::Channels, :brand_redirect
# /user/linustechtips | Not always the same as /c/ # /user/linustechtips | Not always the same as /c/
get "/user/:user#{path}", Routes::Channels, :brand_redirect get "/user/:user#{path}", Routes::Channels, :brand_redirect
# /@LinusTechTips | Handle
get "/@:user#{path}", Routes::Channels, :brand_redirect
# /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
get "/attribution_link#{path}", Routes::Channels, :brand_redirect get "/attribution_link#{path}", Routes::Channels, :brand_redirect
# /profile?user=linustechtips # /profile?user=linustechtips
@ -220,6 +227,10 @@ module Invidious::Routing
# Channels # Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
{% for route in {"videos", "latest", "playlists", "community", "search"} %} {% for route in {"videos", "latest", "playlists", "community", "search"} %}
get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}} get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}}
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}} get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
@ -260,8 +271,10 @@ module Invidious::Routing
post "/api/v1/auth/tokens/register", {{namespace}}::Authenticated, :register_token post "/api/v1/auth/tokens/register", {{namespace}}::Authenticated, :register_token
post "/api/v1/auth/tokens/unregister", {{namespace}}::Authenticated, :unregister_token post "/api/v1/auth/tokens/unregister", {{namespace}}::Authenticated, :unregister_token
get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications if CONFIG.enable_user_notifications
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
end
# Misc # Misc
get "/api/v1/stats", {{namespace}}::Misc, :stats get "/api/v1/stats", {{namespace}}::Misc, :stats

ファイルの表示

@ -9,7 +9,8 @@ module Invidious::Search
client_config = YoutubeAPI::ClientConfig.new(region: query.region) client_config = YoutubeAPI::ClientConfig.new(region: query.region)
initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config) initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config)
return extract_items(initial_data) items, _ = extract_items(initial_data)
return items
end end
# Search a youtube channel # Search a youtube channel
@ -30,16 +31,7 @@ module Invidious::Search
continuation = produce_channel_search_continuation(ucid, query.text, query.page) continuation = produce_channel_search_continuation(ucid, query.text, query.page)
response_json = YoutubeAPI.browse(continuation) response_json = YoutubeAPI.browse(continuation)
continuation_items = response_json["onResponseReceivedActions"]? items, _ = extract_items(response_json, "", ucid)
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]
return [] of SearchItem if !continuation_items
items = [] of SearchItem
continuation_items.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each do |item|
extract_item(item["itemSectionRenderer"]["contents"].as_a[0]).try { |t| items << t }
end
return items return items
end end

ファイルの表示

@ -247,6 +247,12 @@ struct Video
info["reason"]?.try &.as_s info["reason"]?.try &.as_s
end end
def music : Array(VideoMusic)
info["music"].as_a.map { |music_json|
VideoMusic.new(music_json["album"].as_s, music_json["artist"].as_s, music_json["license"].as_s)
}
end
# Macros defining getters/setters for various types of data # Macros defining getters/setters for various types of data
private macro getset_string(name) private macro getset_string(name)

ファイルの表示

@ -31,6 +31,72 @@ module Invidious::Videos
return captions_list return captions_list
end end
def timedtext_to_vtt(timedtext : String, tlang = nil) : String
# In the future, we could just directly work with the url. This is more of a POC
cues = [] of XML::Node
tree = XML.parse(timedtext)
tree = tree.children.first
tree.children.each do |item|
if item.name == "body"
item.children.each do |cue|
if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n")
cues << cue
end
end
break
end
end
result = String.build do |result|
result << <<-END_VTT
WEBVTT
Kind: captions
Language: #{tlang || @language_code}
END_VTT
result << "\n\n"
cues.each_with_index do |node, i|
start_time = node["t"].to_f.milliseconds
duration = node["d"]?.try &.to_f.milliseconds
duration ||= start_time
if cues.size > i + 1
end_time = cues[i + 1]["t"].to_f.milliseconds
else
end_time = start_time + duration
end
# start_time
result << start_time.hours.to_s.rjust(2, '0')
result << ':' << start_time.minutes.to_s.rjust(2, '0')
result << ':' << start_time.seconds.to_s.rjust(2, '0')
result << '.' << start_time.milliseconds.to_s.rjust(3, '0')
result << " --> "
# end_time
result << end_time.hours.to_s.rjust(2, '0')
result << ':' << end_time.minutes.to_s.rjust(2, '0')
result << ':' << end_time.seconds.to_s.rjust(2, '0')
result << '.' << end_time.milliseconds.to_s.rjust(3, '0')
result << "\n"
node.children.each do |s|
result << s.content
end
result << "\n"
result << "\n"
end
end
return result
end
# List of all caption languages available on Youtube. # List of all caption languages available on Youtube.
LANGUAGES = { LANGUAGES = {
"", "",

12
src/invidious/videos/music.cr ノーマルファイル
ファイルの表示

@ -0,0 +1,12 @@
require "json"
struct VideoMusic
include JSON::Serializable
property album : String
property artist : String
property license : String
def initialize(@album : String, @artist : String, @license : String)
end
end

ファイルの表示

@ -66,8 +66,10 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("") reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
reason ||= player_response.dig("playabilityStatus", "reason").as_s reason ||= player_response.dig("playabilityStatus", "reason").as_s
# Stop here if video is not a scheduled livestream # Stop here if video is not a scheduled livestream or
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) # for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
return { return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new(reason), "reason" => JSON::Any.new(reason),
@ -309,6 +311,33 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
end end
end end
# Music section
music_list = [] of VideoMusic
music_desclist = player_response.dig?(
"engagementPanels", 1, "engagementPanelSectionListRenderer",
"content", "structuredDescriptionContentRenderer", "items", 2,
"videoDescriptionMusicSectionRenderer", "carouselLockups"
)
music_desclist.try &.as_a.each do |music_desc|
artist = nil
album = nil
music_license = nil
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc|
desc_title = extract_text(desc.dig?("infoRowRenderer", "title"))
if desc_title == "ARTIST"
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "ALBUM"
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "LICENSES"
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
end
end
music_list << VideoMusic.new(album.to_s, artist.to_s, music_license.to_s)
end
# Author infos # Author infos
author = video_details["author"]?.try &.as_s author = video_details["author"]?.try &.as_s
@ -359,6 +388,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
"genre" => JSON::Any.new(genre.try &.as_s || ""), "genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""),
"license" => JSON::Any.new(license.try &.as_s || ""), "license" => JSON::Any.new(license.try &.as_s || ""),
# Music section
"music" => JSON.parse(music_list.to_json),
# Author infos # Author infos
"author" => JSON::Any.new(author || ""), "author" => JSON::Any.new(author || ""),
"ucid" => JSON::Any.new(ucid || ""), "ucid" => JSON::Any.new(ucid || ""),

ファイルの表示

@ -1,8 +1,24 @@
<% ucid = channel.ucid %> <%-
<% author = HTML.escape(channel.author) %> ucid = channel.ucid
<% channel_profile_pic = URI.parse(channel.author_thumbnail).request_target %> author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
relative_url =
case selected_tab
when .shorts? then "/channel/#{ucid}/shorts"
when .streams? then "/channel/#{ucid}/streams"
when .playlists? then "/channel/#{ucid}/playlists"
when .channels? then "/channel/#{ucid}/channels"
else
"/channel/#{ucid}"
end
youtube_url = "https://www.youtube.com#{relative_url}"
redirect_url = Invidious::Frontend::Misc.redirect_url(env)
-%>
<% content_for "header" do %> <% content_for "header" do %>
<%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>"> <meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious"> <meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>"> <meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
@ -14,91 +30,14 @@
<meta name="twitter:title" content="<%= author %>"> <meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>"> <meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="/ggpht<%= channel_profile_pic %>"> <meta name="twitter:image" content="/ggpht<%= channel_profile_pic %>">
<link rel="alternate" href="https://www.youtube.com/channel/<%= ucid %>">
<title><%= author %> - Invidious</title>
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" /> <link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%>
<link rel="alternate" href="<%= youtube_url %>">
<title><%= author %> - Invidious</title>
<% end %> <% end %>
<% if channel.banner %> <%= rendered "components/channel_info" %>
<div class="h-box">
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
</div>
<div class="h-box">
<hr>
</div>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= channel_profile_pic %>">
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3">
<h3 style="text-align:right">
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1-3">
<a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
<div class="pure-u-1 pure-md-1-3">
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% else %>
<a href="https://redirect.invidious.io<%= env.request.path %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% end %>
</div>
<% if !channel.auto_generated %>
<div class="pure-u-1 pure-md-1-3">
<b><%= translate(locale, "Videos") %></b>
</div>
<% end %>
<div class="pure-u-1 pure-md-1-3">
<% if channel.auto_generated %>
<b><%= translate(locale, "Playlists") %></b>
<% else %>
<a href="/channel/<%= ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
<% end %>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if channel.tabs.includes? "community" %>
<a href="/channel/<%= ucid %>/community"><%= translate(locale, "Community") %></a>
<% end %>
</div>
</div>
<div class="pure-u-1-3"></div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
<% sort_options.each do |sort| %>
<div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %>
<b><%= translate(locale, sort) %></b>
<% else %>
<a href="/channel/<%= ucid %>?page=<%= page %>&sort_by=<%= sort %>">
<%= translate(locale, sort) %>
</a>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<div class="h-box"> <div class="h-box">
<hr> <hr>
@ -111,17 +50,10 @@
</div> </div>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5"> <div class="pure-u-1 pure-u-md-4-5"></div>
<% if page > 1 %>
<a href="/channel/<%= ucid %>?page=<%= page - 1 %><% if sort_by != "newest" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>">
<%= translate(locale, "Previous page") %>
</a>
<% end %>
</div>
<div class="pure-u-1 pure-u-lg-3-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
<% if count == 60 %> <% if next_continuation %>
<a href="/channel/<%= ucid %>?page=<%= page + 1 %><% if sort_by != "newest" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>"> <a href="<%= relative_url %>?continuation=<%= next_continuation %><% if sort_options.any? sort_by %>&sort_by=<%= sort_by %><% end %>">
<%= translate(locale, "Next page") %> <%= translate(locale, "Next page") %>
</a> </a>
<% end %> <% end %>

ファイルの表示

@ -1,71 +1,21 @@
<% ucid = channel.ucid %> <%-
<% author = HTML.escape(channel.author) %> ucid = channel.ucid
author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
relative_url = "/channel/#{ucid}/community"
youtube_url = "https://www.youtube.com#{relative_url}"
redirect_url = Invidious::Frontend::Misc.redirect_url(env)
selected_tab = Invidious::Frontend::ChannelPage::TabsAvailable::Community
-%>
<% content_for "header" do %> <% content_for "header" do %>
<link rel="alternate" href="<%= youtube_url %>">
<title><%= author %> - Invidious</title> <title><%= author %> - Invidious</title>
<% end %> <% end %>
<% if channel.banner %> <%= rendered "components/channel_info" %>
<div class="h-box">
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
</div>
<div class="h-box">
<hr>
</div>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3" style="text-align:right">
<h3 style="text-align:right">
<a href="/feed/channel/<%= channel.ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1-3">
<a href="https://www.youtube.com/channel/<%= channel.ucid %>/community"><%= translate(locale, "View channel on YouTube") %></a>
<div class="pure-u-1 pure-md-1-3">
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% else %>
<a href="https://redirect.invidious.io<%= env.request.resource %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% end %>
</div>
<% if !channel.auto_generated %>
<div class="pure-u-1 pure-md-1-3">
<a href="/channel/<%= channel.ucid %>"><%= translate(locale, "Videos") %></a>
</div>
<% end %>
<div class="pure-u-1 pure-md-1-3">
<a href="/channel/<%= channel.ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if channel.tabs.includes? "community" %>
<b><%= translate(locale, "Community") %></b>
<% end %>
</div>
</div>
<div class="pure-u-2-3"></div>
</div>
<div class="h-box"> <div class="h-box">
<hr> <hr>

ファイルの表示

@ -0,0 +1,60 @@
<% if channel.banner %>
<div class="h-box">
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
</div>
<div class="h-box">
<hr>
</div>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= channel_profile_pic %>">
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3">
<h3 style="text-align:right">
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1-2">
<div class="pure-u-1 pure-md-1-3">
<a href="<%= youtube_url %>"><%= translate(locale, "View channel on YouTube") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<a href="<%= redirect_url %>"><%= translate(locale, "Switch Invidious Instance") %></a>
</div>
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
</div>
<div class="pure-u-1-2">
<div class="pure-g" style="text-align:end">
<% sort_options.each do |sort| %>
<div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %>
<b><%= translate(locale, sort) %></b>
<% else %>
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= translate(locale, sort) %></a>
<% end %>
</div>
<% end %>
</div>
</div>
</div>

ファイルの表示

@ -23,6 +23,8 @@
</div> </div>
</div> </div>
<% if CONFIG.enable_user_notifications %>
<center> <center>
<%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %> <%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %>
</center> </center>
@ -39,6 +41,8 @@
<% end %> <% end %>
</div> </div>
<% end %>
<div class="h-box"> <div class="h-box">
<hr> <hr>
</div> </div>

ファイルの表示

@ -1,108 +0,0 @@
<% ucid = channel.ucid %>
<% author = HTML.escape(channel.author) %>
<% content_for "header" do %>
<title><%= author %> - Invidious</title>
<% end %>
<% if channel.banner %>
<div class="h-box">
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
</div>
<div class="h-box">
<hr>
</div>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3" style="text-align:right">
<h3 style="text-align:right">
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content if !channel.description_html.empty? %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-g pure-u-1-3">
<div class="pure-u-1 pure-md-1-3">
<a href="https://www.youtube.com/channel/<%= ucid %>/playlists"><%= translate(locale, "View channel on YouTube") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% else %>
<a href="https://redirect.invidious.io<%= env.request.resource %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% end %>
</div>
<div class="pure-u-1 pure-md-1-3">
<a href="/channel/<%= ucid %>"><%= translate(locale, "Videos") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if !channel.auto_generated %>
<b><%= translate(locale, "Playlists") %></b>
<% end %>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if channel.tabs.includes? "community" %>
<a href="/channel/<%= ucid %>/community"><%= translate(locale, "Community") %></a>
<% end %>
</div>
</div>
<div class="pure-u-1-3"></div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
<% {"last", "oldest", "newest"}.each do |sort| %>
<div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %>
<b><%= translate(locale, sort) %></b>
<% else %>
<a href="/channel/<%= ucid %>/playlists?sort_by=<%= sort %>">
<%= translate(locale, sort) %>
</a>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<div class="h-box">
<hr>
</div>
<div class="pure-g">
<% items.each do |item| %>
<%= rendered "components/item" %>
<% end %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-4-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
<% if continuation %>
<a href="/channel/<%= ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>">
<%= translate(locale, "Next page") %>
</a>
<% end %>
</div>
</div>

ファイルの表示

@ -55,7 +55,7 @@
<div class="pure-u-1-4"> <div class="pure-u-1-4">
<a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading"> <a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
<% notification_count = env.get("user").as(Invidious::User).notifications.size %> <% notification_count = env.get("user").as(Invidious::User).notifications.size %>
<% if notification_count > 0 %> <% if CONFIG.enable_user_notifications && notification_count > 0 %>
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i> <span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
<% else %> <% else %>
<i class="icon ion-ios-notifications-outline"></i> <i class="icon ion-ios-notifications-outline"></i>
@ -175,7 +175,9 @@
}.to_pretty_json }.to_pretty_json
%> %>
</script> </script>
<% if CONFIG.enable_user_notifications %>
<script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
<% end %>
<% end %> <% end %>
</body> </body>

ファイルの表示

@ -14,7 +14,7 @@
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube"> <label for="import_youtube">
<a rel="noopener" target="_blank" href="https://github.com/iv-org/documentation/blob/master/Export-YouTube-subscriptions.md"> <a rel="noopener" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
<%= translate(locale, "Import YouTube subscriptions") %> <%= translate(locale, "Import YouTube subscriptions") %>
</a> </a>
</label> </label>

ファイルの表示

@ -244,6 +244,7 @@
<input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>> <input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>>
</div> </div>
<% if CONFIG.enable_user_notifications %>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="notifications_only"><%= translate(locale, "preferences_notifications_only_label") %></label> <label for="notifications_only"><%= translate(locale, "preferences_notifications_only_label") %></label>
<input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>> <input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>>
@ -255,6 +256,7 @@
<a href="#" data-onclick="notification_requestPermission"><%= translate(locale, "Enable web notifications") %></a> <a href="#" data-onclick="notification_requestPermission"><%= translate(locale, "Enable web notifications") %></a>
</div> </div>
<% end %> <% end %>
<% end %>
<% end %> <% end %>
<% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %> <% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %>

変更されたファイルが多すぎるため、一部のファイルは表示されません さらに表示