Merge branch 'master' of github.com:iv-org/invidious
このコミットが含まれているのは:
コミット
eac9e6aaa4
|
@ -490,8 +490,9 @@ hr {
|
|||
}
|
||||
|
||||
/* Description Expansion Styling*/
|
||||
#descexpansionbutton {
|
||||
display: none
|
||||
#descexpansionbutton,
|
||||
#music-desc-expansion {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#descexpansionbutton ~ div {
|
||||
|
@ -509,6 +510,11 @@ hr {
|
|||
margin-top: 20px;
|
||||
}
|
||||
|
||||
label[for="descexpansionbutton"]:hover,
|
||||
label[for="music-desc-expansion"]:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Bidi (bidirectional text) support */
|
||||
h1,
|
||||
h2,
|
||||
|
@ -517,14 +523,38 @@ h4,
|
|||
h5,
|
||||
p,
|
||||
#descriptionWrapper,
|
||||
#description-box {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: start;
|
||||
#description-box,
|
||||
#music-description-box {
|
||||
unicode-bidi: plaintext;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
#descriptionWrapper {
|
||||
max-width: 600px;
|
||||
white-space: pre-wrap;
|
||||
max-width: 600px;
|
||||
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 */
|
||||
|
|
|
@ -295,6 +295,17 @@ https_only: false
|
|||
##
|
||||
#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
|
||||
|
|
|
@ -43,7 +43,7 @@ RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
|
|||
|
||||
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache librsvg ttf-opensans
|
||||
RUN apk add --no-cache librsvg ttf-opensans tini
|
||||
WORKDIR /invidious
|
||||
RUN addgroup -g 1000 -S invidious && \
|
||||
adduser -u 1000 -S invidious -G invidious
|
||||
|
@ -58,4 +58,5 @@ RUN chmod o+rX -R ./assets ./config ./locales
|
|||
|
||||
EXPOSE 3000
|
||||
USER invidious
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD [ "/invidious/invidious" ]
|
||||
|
|
|
@ -42,7 +42,7 @@ RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
|
|||
fi
|
||||
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache librsvg ttf-opensans
|
||||
RUN apk add --no-cache librsvg ttf-opensans tini
|
||||
WORKDIR /invidious
|
||||
RUN addgroup -g 1000 -S invidious && \
|
||||
adduser -u 1000 -S invidious -G invidious
|
||||
|
@ -57,4 +57,5 @@ RUN chmod o+rX -R ./assets ./config ./locales
|
|||
|
||||
EXPOSE 3000
|
||||
USER invidious
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD [ "/invidious/invidious" ]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami/
|
||||
version: 11.1.3
|
||||
digest: sha256:79061645472b6fb342d45e8e5b3aacd018ef5067193e46a060bccdc99fe7f6e1
|
||||
generated: "2022-03-02T05:57:20.081432389+13:00"
|
||||
version: 12.1.9
|
||||
digest: sha256:71ff342a6c0a98bece3d7fe199983afb2113f8db65a3e3819de875af2c45add7
|
||||
generated: "2023-01-20T20:42:32.757707004Z"
|
||||
|
|
|
@ -17,6 +17,6 @@ maintainers:
|
|||
email: mail@leonklingele.de
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: ~11.1.3
|
||||
version: ~12.1.6
|
||||
repository: "https://charts.bitnami.com/bitnami/"
|
||||
engine: gotpl
|
||||
|
|
|
@ -34,6 +34,8 @@ securityContext:
|
|||
|
||||
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
||||
postgresql:
|
||||
image:
|
||||
tag: 13
|
||||
auth:
|
||||
username: kemal
|
||||
password: kemal
|
||||
|
|
136
locales/ar.json
136
locales/ar.json
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"LIVE": "مُباشِر",
|
||||
"Shared `x` ago": "تمَّ رفع المقطع المرئيّ مُنذ `x`",
|
||||
"Shared `x` ago": "تمَّ الرفع مُنذ `x`",
|
||||
"Unsubscribe": "إلغاء الاشتراك",
|
||||
"Subscribe": "الإشتراك",
|
||||
"View channel on YouTube": "زيارة القناة على موقع يوتيوب",
|
||||
"View playlist on YouTube": "عرض قائمة التشغيل على اليوتيوب",
|
||||
"newest": "الأجدد",
|
||||
"Subscribe": "الاشتراك",
|
||||
"View channel on YouTube": "زيارة القناة على يوتيوب",
|
||||
"View playlist on YouTube": "عرض قائمة التشغيل على يوتيوب",
|
||||
"newest": "الأحدث",
|
||||
"oldest": "الأقدم",
|
||||
"popular": "الأكثر شعبية",
|
||||
"last": "الأخيرة",
|
||||
|
@ -96,8 +96,8 @@
|
|||
"`x` is live": "`x` في بث مباشر",
|
||||
"preferences_category_data": "إعدادات التفضيلات",
|
||||
"Clear watch history": "حذف سجل المشاهدة",
|
||||
"Import/export data": "إضافة\\استخراج البيانات",
|
||||
"Change password": "غير كلمة السر",
|
||||
"Import/export data": "إستيراد و تصدير البيانات",
|
||||
"Change password": "تغير كلمة السر",
|
||||
"Manage subscriptions": "إدارة الاشتراكات",
|
||||
"Manage tokens": "إدارة الرموز",
|
||||
"Watch history": "سجل المشاهدة",
|
||||
|
@ -137,7 +137,7 @@
|
|||
"Title": "العنوان",
|
||||
"Playlist privacy": "إعدادات الخصوصية",
|
||||
"Editing playlist `x`": "تعديل قائمة التشغيل `x`",
|
||||
"Show more": "إظهار المزيد",
|
||||
"Show more": "عرض المزيد",
|
||||
"Show less": "عرض اقل",
|
||||
"Watch on YouTube": "مشاهدة الفيديو على اليوتيوب",
|
||||
"Switch Invidious Instance": "تبديل المثيل Invidious",
|
||||
|
@ -147,20 +147,20 @@
|
|||
"License: ": "التراخيص: ",
|
||||
"Family friendly? ": "محتوى عائلي؟ ",
|
||||
"Wilson score: ": "درجة ويلسون: ",
|
||||
"Engagement: ": "نسبة المشاركة: ",
|
||||
"Engagement: ": "نسبة التفاعل: ",
|
||||
"Whitelisted regions: ": "الدول المسموح فيها هذا الفيديو: ",
|
||||
"Blacklisted regions: ": "الدول المحظور فيها هذا الفيديو: ",
|
||||
"Shared `x`": "شارك منذ `x`",
|
||||
"Shared `x`": "تمت المشاركة في `x`",
|
||||
"Premieres in `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.": "أهلًا! يبدو أن جافاسكريبت معطلٌ لديك. اضغط هنا لعرض التعليقات، وَضَع في اعتبارك أنها ستأخذ وقتًا أطول للتحميل.",
|
||||
"View YouTube comments": "عرض تعليقات اليوتيوب",
|
||||
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع Reddit",
|
||||
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات",
|
||||
"": "عرض `x` تعليقات"
|
||||
},
|
||||
"View Reddit comments": "عرض تعليقات ريدإت Reddit",
|
||||
"View Reddit comments": "عرض تعليقات ريديت",
|
||||
"Hide replies": "إخفاء الردود",
|
||||
"Show replies": "عرض الردود",
|
||||
"Incorrect password": "كلمة السر غير صحيحة",
|
||||
|
@ -182,20 +182,20 @@
|
|||
"channel:`x`": "قناة:`x`",
|
||||
"Deleted or invalid channel": "قناة ممسوحة او غير صالحة",
|
||||
"This channel does not exist.": "هذه القناة غير موجودة.",
|
||||
"Could not get channel info.": "لم يستطع الحصول على معلومات القناة.",
|
||||
"Could not fetch comments": "لم يتمكن من إحضار التعليقات",
|
||||
"Could not get channel info.": "لم يتمكن الحصول على معلومات القناة.",
|
||||
"Could not fetch comments": "لا يتمكن إحضار التعليقات",
|
||||
"`x` ago": "`x` منذ",
|
||||
"Load more": "عرض المزيد",
|
||||
"Load more": "تحميل المزيد",
|
||||
"Could not create mix.": "تعذر إنشاء مزيج.",
|
||||
"Empty playlist": "قائمة التشغيل فارغة",
|
||||
"Not a playlist.": "قائمة التشغيل غير صالحة.",
|
||||
"Playlist does not exist.": "قائمة التشغيل غير موجودة.",
|
||||
"Could not pull trending pages.": "لم يستطع عرض الصفحات الراجئة.",
|
||||
"Hidden field \"challenge\" is a required field": "مكان مخفي \"تحدي\" مكان مطلوب",
|
||||
"Hidden field \"token\" is a required field": "مكان مخفي \"رمز\" مكان مطلوب",
|
||||
"Erroneous challenge": "تحدي غير صالح",
|
||||
"Could not pull trending pages.": "لا يتمكن عرض الصفحات الراجئة.",
|
||||
"Hidden field \"challenge\" is a required field": "الحقل المخفي \"تحدي\" حقل مطلوب",
|
||||
"Hidden field \"token\" is a required field": "الحقل المخفي \"رمز\" حقل مطلوب",
|
||||
"Erroneous challenge": "تحدي خاطئ",
|
||||
"Erroneous token": "رمز مميز خاطئ",
|
||||
"No such user": "مستخدم غير صالح",
|
||||
"No such user": "مستخدم غير موجود",
|
||||
"Token is expired, please try again": "الرمز منتهى الصلاحية، الرجاء المحاولة مرة اخرى",
|
||||
"English": "إنجليزي",
|
||||
"English (auto-generated)": "إنجليزي (تم إنشائه تلقائيًا)",
|
||||
|
@ -325,15 +325,15 @@
|
|||
"`x` marked it with a ❤": "`x` أعجب بهذا",
|
||||
"Audio mode": "الوضع الصوتي",
|
||||
"Video mode": "وضع الفيديو",
|
||||
"Videos": "الفيديوهات",
|
||||
"channel_tab_videos_label": "الفيديوهات",
|
||||
"Playlists": "قوائم التشغيل",
|
||||
"Community": "المجتمع",
|
||||
"search_filters_sort_option_relevance": "ملاؤم",
|
||||
"channel_tab_community_label": "المجتمع",
|
||||
"search_filters_sort_option_relevance": "ملائمة",
|
||||
"search_filters_sort_option_rating": "تقييم",
|
||||
"search_filters_sort_option_date": "التاريخ",
|
||||
"search_filters_sort_option_views": "مشاهدات",
|
||||
"search_filters_type_label": "نوع المحتوى",
|
||||
"search_filters_duration_label": "المدة الزمنية",
|
||||
"search_filters_duration_label": "المدة",
|
||||
"search_filters_features_label": "الميزات",
|
||||
"search_filters_sort_label": "فرز",
|
||||
"search_filters_date_option_hour": "آخر ساعة",
|
||||
|
@ -351,8 +351,8 @@
|
|||
"search_filters_features_option_c_commons": "المشاع الإبداعي",
|
||||
"search_filters_features_option_three_d": "ثلاثي الأبعاد",
|
||||
"search_filters_features_option_live": "مباشر",
|
||||
"search_filters_features_option_four_k": "4k",
|
||||
"search_filters_features_option_location": "الأماكن",
|
||||
"search_filters_features_option_four_k": "4K",
|
||||
"search_filters_features_option_location": "المكان",
|
||||
"search_filters_features_option_hdr": "وضع التباين العالي",
|
||||
"Current version: ": "الإصدار الحالي: ",
|
||||
"next_steps_error_message": "بعد ذلك يجب أن تحاول: ",
|
||||
|
@ -360,10 +360,10 @@
|
|||
"next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب",
|
||||
"search_filters_duration_option_short": "قصير (< 4 دقائق)",
|
||||
"search_filters_duration_option_long": "طويل (> 20 دقيقة)",
|
||||
"footer_source_code": "شفرة المصدر",
|
||||
"footer_original_source_code": "كود المصدر الأصلي",
|
||||
"footer_modfied_source_code": "شفرة المصدر المعدلة",
|
||||
"adminprefs_modified_source_code_url_label": "URL إلى مستودع التعليمات البرمجية المصدرية المعدلة",
|
||||
"footer_source_code": "الكود المصدر",
|
||||
"footer_original_source_code": "الكود المصدر الأصلي",
|
||||
"footer_modfied_source_code": "الكود المصدر المعدل",
|
||||
"adminprefs_modified_source_code_url_label": "URL إلى مستودع الكود المصدر المعدل",
|
||||
"footer_documentation": "التوثيق",
|
||||
"footer_donate_page": "تبرّع",
|
||||
"preferences_region_label": "بلد المحتوى: ",
|
||||
|
@ -398,31 +398,31 @@
|
|||
"invidious": "الخيالي",
|
||||
"preferences_save_player_pos_label": "حفظ موضع التشغيل: ",
|
||||
"crash_page_you_found_a_bug": "يبدو أنك قد وجدت خطأً برمجيًّا في Invidious!",
|
||||
"generic_videos_count_0": "لا فيديوهات",
|
||||
"generic_videos_count_0": "لا يوجد فيديوهات",
|
||||
"generic_videos_count_1": "فيديو واحد",
|
||||
"generic_videos_count_2": "فيديوهين",
|
||||
"generic_videos_count_3": "{{count}} فيديوهات",
|
||||
"generic_videos_count_4": "{{count}} فيديو",
|
||||
"generic_videos_count_5": "{{count}} فيديو",
|
||||
"generic_subscribers_count_0": "لا مشتركين",
|
||||
"generic_subscribers_count_0": "لا يوجد مشترك",
|
||||
"generic_subscribers_count_1": "مشترك واحد",
|
||||
"generic_subscribers_count_2": "مشتركان",
|
||||
"generic_subscribers_count_3": "{{count}} مشتركين",
|
||||
"generic_subscribers_count_4": "{{count}} مشترك",
|
||||
"generic_subscribers_count_5": "{{count}} مشترك",
|
||||
"generic_views_count_0": "لا مشاهدات",
|
||||
"generic_views_count_0": "لا يوجد مشاهدة",
|
||||
"generic_views_count_1": "مشاهدة واحدة",
|
||||
"generic_views_count_2": "مشاهدتان",
|
||||
"generic_views_count_3": "{{count}} مشاهدات",
|
||||
"generic_views_count_4": "{{count}} مشاهدة",
|
||||
"generic_views_count_5": "{{count}} مشاهدة",
|
||||
"generic_subscriptions_count_0": "لا اشتراكات",
|
||||
"generic_subscriptions_count_0": "لا يوجد اشتراك",
|
||||
"generic_subscriptions_count_1": "اشتراك واحد",
|
||||
"generic_subscriptions_count_2": "اشتراكان",
|
||||
"generic_subscriptions_count_3": "{{count}} اشتراكات",
|
||||
"generic_subscriptions_count_4": "{{count}} اشتراك",
|
||||
"generic_subscriptions_count_5": "{{count}} اشتراك",
|
||||
"generic_playlists_count_0": "لا قوائم تشغيل",
|
||||
"generic_playlists_count_0": "لا يوجد قوائم تشغيل",
|
||||
"generic_playlists_count_1": "قائمة تشغيل واحدة",
|
||||
"generic_playlists_count_2": "قائمتا تشغيل",
|
||||
"generic_playlists_count_3": "{{count}} قوائم تشغيل",
|
||||
|
@ -463,10 +463,10 @@
|
|||
"search_message_change_filters_or_query": "حاول توسيع استعلام البحث و / أو تغيير عوامل التصفية.",
|
||||
"search_filters_date_label": "تاريخ الرفع",
|
||||
"generic_count_weeks_0": "{{count}} أسبوع",
|
||||
"generic_count_weeks_1": "{{count}} أسبوع",
|
||||
"generic_count_weeks_2": "{{count}} أسبوع",
|
||||
"generic_count_weeks_3": "{{count}} أسبوع",
|
||||
"generic_count_weeks_4": "{{count}} أسابيع",
|
||||
"generic_count_weeks_1": "أسبوع واحد",
|
||||
"generic_count_weeks_2": "أسبوعين",
|
||||
"generic_count_weeks_3": "{{count}} أسابيع",
|
||||
"generic_count_weeks_4": "{{count}} أسبوع",
|
||||
"generic_count_weeks_5": "{{count}} أسبوع",
|
||||
"Popular enabled: ": "تم تمكين الشعبية: ",
|
||||
"search_filters_duration_option_medium": "متوسط (4-20 دقيقة)",
|
||||
|
@ -474,16 +474,16 @@
|
|||
"search_filters_type_option_all": "أي نوع",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"generic_count_minutes_0": "{{count}} دقيقة",
|
||||
"generic_count_minutes_1": "{{count}} دقيقة",
|
||||
"generic_count_minutes_2": "{{count}} دقيقة",
|
||||
"generic_count_minutes_3": "{{count}} دقيقة",
|
||||
"generic_count_minutes_4": "{{count}} دقائق",
|
||||
"generic_count_minutes_1": "دقيقة واحدة",
|
||||
"generic_count_minutes_2": "دقيقتين",
|
||||
"generic_count_minutes_3": "{{count}} دقائق",
|
||||
"generic_count_minutes_4": "{{count}} دقيقة",
|
||||
"generic_count_minutes_5": "{{count}} دقيقة",
|
||||
"generic_count_hours_0": "{{count}} ساعة",
|
||||
"generic_count_hours_1": "{{count}} ساعة",
|
||||
"generic_count_hours_2": "{{count}} ساعة",
|
||||
"generic_count_hours_3": "{{count}} ساعة",
|
||||
"generic_count_hours_4": "{{count}} ساعات",
|
||||
"generic_count_hours_1": "ساعة واحدة",
|
||||
"generic_count_hours_2": "ساعتين",
|
||||
"generic_count_hours_3": "{{count}} ساعات",
|
||||
"generic_count_hours_4": "{{count}} ساعة",
|
||||
"generic_count_hours_5": "{{count}} ساعة",
|
||||
"comments_view_x_replies_0": "عرض رد {{count}}",
|
||||
"comments_view_x_replies_1": "عرض رد {{count}}",
|
||||
|
@ -493,10 +493,10 @@
|
|||
"comments_view_x_replies_5": "عرض رد {{count}}",
|
||||
"search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
|
||||
"comments_points_count_0": "{{count}} نقطة",
|
||||
"comments_points_count_1": "{{count}} نقطة",
|
||||
"comments_points_count_2": "{{count}} نقطة",
|
||||
"comments_points_count_3": "{{count}} نقطة",
|
||||
"comments_points_count_4": "{{count}} نقاط",
|
||||
"comments_points_count_1": "نقطة واحدة",
|
||||
"comments_points_count_2": "نقطتان",
|
||||
"comments_points_count_3": "{{count}} نقط",
|
||||
"comments_points_count_4": "{{count}} نقطة",
|
||||
"comments_points_count_5": "{{count}} نقطة",
|
||||
"generic_count_years_0": "{{count}} السنة",
|
||||
"generic_count_years_1": "{{count}} السنة",
|
||||
|
@ -512,17 +512,17 @@
|
|||
"tokens_count_5": "الرمز المميز {{count}}",
|
||||
"search_filters_apply_button": "تطبيق الفلاتر المحددة",
|
||||
"search_filters_duration_option_none": "أي مدة",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}} إشعار غير مرئي",
|
||||
"subscriptions_unseen_notifs_count_1": "{{count}} إشعار غير مرئي",
|
||||
"subscriptions_unseen_notifs_count_2": "{{count}} إشعار غير مرئي",
|
||||
"subscriptions_unseen_notifs_count_3": "{{count}} إشعار غير مرئي",
|
||||
"subscriptions_unseen_notifs_count_4": "{{count}} إشعارات غير مرئية",
|
||||
"subscriptions_unseen_notifs_count_5": "{{count}} إشعار غير مرئي",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}} إشعار جديد",
|
||||
"subscriptions_unseen_notifs_count_1": "إشعار واحد جديد",
|
||||
"subscriptions_unseen_notifs_count_2": "إشعارين جديدين",
|
||||
"subscriptions_unseen_notifs_count_3": "{{count}} إشعارات جديدة",
|
||||
"subscriptions_unseen_notifs_count_4": "{{count}} إشعارا جديد",
|
||||
"subscriptions_unseen_notifs_count_5": "{{count}} إشعار جديد",
|
||||
"generic_count_days_0": "{{count}} يوم",
|
||||
"generic_count_days_1": "{{count}} يوم",
|
||||
"generic_count_days_2": "{{count}} يوم",
|
||||
"generic_count_days_3": "{{count}} يوم",
|
||||
"generic_count_days_4": "{{count}} أيام",
|
||||
"generic_count_days_1": "يوم واحد",
|
||||
"generic_count_days_2": "يومين",
|
||||
"generic_count_days_3": "{{count}} أيام",
|
||||
"generic_count_days_4": "{{count}} يوم",
|
||||
"generic_count_days_5": "{{count}} يوم",
|
||||
"generic_count_months_0": "{{count}} شهر",
|
||||
"generic_count_months_1": "{{count}} شهر",
|
||||
|
@ -531,10 +531,14 @@
|
|||
"generic_count_months_4": "{{count}} شهور",
|
||||
"generic_count_months_5": "{{count}} شهر",
|
||||
"generic_count_seconds_0": "{{count}} ثانية",
|
||||
"generic_count_seconds_1": "{{count}} ثانية",
|
||||
"generic_count_seconds_2": "{{count}} ثانية",
|
||||
"generic_count_seconds_3": "{{count}} ثانية",
|
||||
"generic_count_seconds_4": "{{count}} ثوانٍ",
|
||||
"generic_count_seconds_1": "ثانية واحدة",
|
||||
"generic_count_seconds_2": "ثانيتين",
|
||||
"generic_count_seconds_3": "{{count}} ثوانٍ",
|
||||
"generic_count_seconds_4": "{{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",
|
||||
"Download": "Descarrega",
|
||||
"Download as: ": "Descarrega com: ",
|
||||
"Videos": "Vídeos",
|
||||
"channel_tab_videos_label": "Vídeos",
|
||||
"search_filters_type_label": "Tipus",
|
||||
"search_filters_duration_label": "Duració",
|
||||
"search_filters_sort_label": "Ordena per",
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
"reddit": "Reddit",
|
||||
"preferences_captions_label": "Výchozí 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_extend_desc_label": "Rozšířit automaticky popis u videa: ",
|
||||
"preferences_category_visual": "Nastavení vzhledu",
|
||||
|
@ -260,8 +260,8 @@
|
|||
"`x` marked it with a ❤": "`x` to označil(a) se ❤",
|
||||
"Audio mode": "Audiový režim",
|
||||
"Video mode": "Videový režim",
|
||||
"Videos": "Videa",
|
||||
"Community": "Komunita",
|
||||
"channel_tab_videos_label": "Videa",
|
||||
"channel_tab_community_label": "Komunita",
|
||||
"search_filters_sort_option_rating": "Hodnocení",
|
||||
"search_filters_sort_option_date": "Datum nahrání",
|
||||
"search_filters_sort_option_views": "Počet zhlédnutí",
|
||||
|
@ -488,5 +488,9 @@
|
|||
"search_filters_sort_option_relevance": "Relevantnost",
|
||||
"search_filters_apply_button": "Použít vybrané filtry",
|
||||
"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",
|
||||
"Czech": "Tjekkisk",
|
||||
"Danish": "Dansk",
|
||||
"Community": "Samfund",
|
||||
"channel_tab_community_label": "Samfund",
|
||||
"Afrikaans": "Afrikansk",
|
||||
"Portuguese": "Portugisisk",
|
||||
"Ukrainian": "Ukrainsk",
|
||||
|
@ -267,7 +267,7 @@
|
|||
"search_filters_sort_option_rating": "Bedømmelse",
|
||||
"Yoruba": "Yoruba",
|
||||
"Erroneous token": "Fejlagtig token",
|
||||
"Videos": "Videoer",
|
||||
"channel_tab_videos_label": "Videoer",
|
||||
"search_filters_type_option_show": "Vis",
|
||||
"Luxembourgish": "Luxemboursk",
|
||||
"Vietnamese": "Vietnamesisk",
|
||||
|
|
|
@ -325,9 +325,9 @@
|
|||
"`x` marked it with a ❤": "`x` markierte es mit einem ❤",
|
||||
"Audio mode": "Audiomodus",
|
||||
"Video mode": "Videomodus",
|
||||
"Videos": "Videos",
|
||||
"channel_tab_videos_label": "Videos",
|
||||
"Playlists": "Wiedergabelisten",
|
||||
"Community": "Gemeinschaft",
|
||||
"channel_tab_community_label": "Gemeinschaft",
|
||||
"search_filters_sort_option_relevance": "Relevanz",
|
||||
"search_filters_sort_option_rating": "Bewertung",
|
||||
"search_filters_sort_option_date": "Datum",
|
||||
|
@ -471,5 +471,6 @@
|
|||
"search_filters_apply_button": "Ausgewählte Filter anwenden",
|
||||
"search_filters_duration_option_none": "Beliebige Länge",
|
||||
"search_filters_date_label": "Upload-Datum",
|
||||
"search_filters_date_option_none": "Beliebiges Datum"
|
||||
"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` έβαλε ❤",
|
||||
"Audio mode": "Λειτουργία ήχου",
|
||||
"Video mode": "Λειτουργία βίντεο",
|
||||
"Videos": "Βίντεο",
|
||||
"channel_tab_videos_label": "Βίντεο",
|
||||
"Playlists": "Λίστες Αναπαραγωγής",
|
||||
"Community": "Κοινότητα",
|
||||
"channel_tab_community_label": "Κοινότητα",
|
||||
"Current version: ": "Τρέχουσα έκδοση: ",
|
||||
"generic_playlists_count": "{{count}} λίστα αναπαραγωγής",
|
||||
"generic_playlists_count_plural": "{{count}} λίστες αναπαραγωγής",
|
||||
|
|
|
@ -188,6 +188,9 @@
|
|||
"Engagement: ": "Engagement: ",
|
||||
"Whitelisted regions: ": "Whitelisted regions: ",
|
||||
"Blacklisted regions: ": "Blacklisted regions: ",
|
||||
"Music in this video": "Music in this video",
|
||||
"Artist: ": "Artist: ",
|
||||
"Album: ": "Album: ",
|
||||
"Shared `x`": "Shared `x`",
|
||||
"Premieres in `x`": "Premieres in `x`",
|
||||
"Premieres `x`": "Premieres `x`",
|
||||
|
@ -404,9 +407,7 @@
|
|||
"`x` marked it with a ❤": "`x` marked it with a ❤",
|
||||
"Audio mode": "Audio mode",
|
||||
"Video mode": "Video mode",
|
||||
"Videos": "Videos",
|
||||
"Playlists": "Playlists",
|
||||
"Community": "Community",
|
||||
"search_filters_title": "Filters",
|
||||
"search_filters_date_label": "Upload 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_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):",
|
||||
"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",
|
||||
"View channel on YouTube": "Vidu kanalon en JuTubo",
|
||||
"View playlist on YouTube": "Vidu ludliston en JuTubo",
|
||||
"newest": "pli novaj",
|
||||
"oldest": "pli malnovaj",
|
||||
"newest": "plej novaj",
|
||||
"oldest": "plej malnovaj",
|
||||
"popular": "popularaj",
|
||||
"last": "lasta",
|
||||
"Next page": "Sekva paĝo",
|
||||
|
@ -325,9 +325,9 @@
|
|||
"`x` marked it with a ❤": "`x` markis ĝin per ❤",
|
||||
"Audio mode": "Aŭda reĝimo",
|
||||
"Video mode": "Videa reĝimo",
|
||||
"Videos": "Filmetoj",
|
||||
"channel_tab_videos_label": "Videoj",
|
||||
"Playlists": "Ludlistoj",
|
||||
"Community": "Komunumo",
|
||||
"channel_tab_community_label": "Komunumo",
|
||||
"search_filters_sort_option_relevance": "rilateco",
|
||||
"search_filters_sort_option_rating": "takso",
|
||||
"search_filters_sort_option_date": "dato",
|
||||
|
@ -472,5 +472,9 @@
|
|||
"generic_subscribers_count_plural": "{{count}} abonantoj",
|
||||
"generic_count_months": "{{count}} monato",
|
||||
"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 ❤",
|
||||
"Audio mode": "Modo de audio",
|
||||
"Video mode": "Modo de vídeo",
|
||||
"Videos": "Vídeos",
|
||||
"channel_tab_videos_label": "Vídeos",
|
||||
"Playlists": "Listas de reproducción",
|
||||
"Community": "Comunidad",
|
||||
"channel_tab_community_label": "Comunidad",
|
||||
"search_filters_sort_option_relevance": "relevancia",
|
||||
"search_filters_sort_option_rating": "valoración",
|
||||
"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_filters_duration_option_medium": "Medio (4 - 20 minutes)",
|
||||
"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",
|
||||
"Javanese": "Jaava",
|
||||
"Lithuanian": "Leedu",
|
||||
"Videos": "Videod",
|
||||
"Community": "Kogukond",
|
||||
"channel_tab_videos_label": "Videod",
|
||||
"channel_tab_community_label": "Kogukond",
|
||||
"CAPTCHA is a required field": "CAPTCHA on kohustuslik väli",
|
||||
"comments_points_count": "{{count}} punkt",
|
||||
"comments_points_count_plural": "{{count}} punkti",
|
||||
|
|
|
@ -341,9 +341,9 @@
|
|||
"`x` marked it with a ❤": "`x` نشان گذاری شده با یک ❤",
|
||||
"Audio mode": "حالت صدا",
|
||||
"Video mode": "حالت ویدیو",
|
||||
"Videos": "ویدیو ها",
|
||||
"channel_tab_videos_label": "ویدیو ها",
|
||||
"Playlists": "سیاهههای پخش",
|
||||
"Community": "اجتماع",
|
||||
"channel_tab_community_label": "اجتماع",
|
||||
"search_filters_sort_option_relevance": "مرتبط بودن",
|
||||
"search_filters_sort_option_rating": "امتیاز",
|
||||
"search_filters_sort_option_date": "تاریخ بارگذاری",
|
||||
|
@ -411,5 +411,18 @@
|
|||
"search_filters_duration_option_long": "بلند (> 20 دقیقه)",
|
||||
"adminprefs_modified_source_code_url_label": "URL مخزن کد منبع ویریش شده",
|
||||
"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ä",
|
||||
"Audio mode": "Äänitila",
|
||||
"Video mode": "Videotila",
|
||||
"Videos": "Videot",
|
||||
"channel_tab_videos_label": "Videot",
|
||||
"Playlists": "Soittolistat",
|
||||
"Community": "Yhteisö",
|
||||
"channel_tab_community_label": "Yhteisö",
|
||||
"search_filters_sort_option_relevance": "Osuvuus",
|
||||
"search_filters_sort_option_rating": "Arvostelu",
|
||||
"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_filters_date_option_none": "Milloin tahansa",
|
||||
"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 ❤",
|
||||
"Audio mode": "Mode audio",
|
||||
"Video mode": "Mode vidéo",
|
||||
"Videos": "Vidéos",
|
||||
"channel_tab_videos_label": "Vidéos",
|
||||
"Playlists": "Listes de lecture",
|
||||
"Community": "Communauté",
|
||||
"channel_tab_community_label": "Communauté",
|
||||
"search_filters_sort_option_relevance": "Pertinence",
|
||||
"search_filters_sort_option_rating": "Notation",
|
||||
"search_filters_sort_option_date": "Date d'ajout",
|
||||
|
@ -472,5 +472,9 @@
|
|||
"search_filters_date_label": "Date d'ajout",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"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`",
|
||||
"Audio mode": "Audio mode",
|
||||
"Video mode": "Video mode",
|
||||
"Videos": "סרטונים",
|
||||
"channel_tab_videos_label": "סרטונים",
|
||||
"Playlists": "פלייליסטים",
|
||||
"Community": "קהילה",
|
||||
"channel_tab_community_label": "קהילה",
|
||||
"search_filters_sort_option_relevance": "רלוונטיות",
|
||||
"search_filters_sort_option_rating": "דירוג",
|
||||
"search_filters_sort_option_date": "תאריך העלאה",
|
||||
|
|
|
@ -401,12 +401,12 @@
|
|||
"(edited)": "(संपादित)",
|
||||
"YouTube comment permalink": "YouTube पर टिप्पणी की स्थायी कड़ी",
|
||||
"permalink": "स्थायी कड़ी",
|
||||
"Videos": "वीडियो",
|
||||
"channel_tab_videos_label": "वीडियो",
|
||||
"`x` marked it with a ❤": "`x` ने इसे एक ❤ से चिह्नित किया",
|
||||
"Audio mode": "ऑडियो मोड",
|
||||
"Playlists": "प्लेलिस्ट्स",
|
||||
"Video mode": "वीडियो मोड",
|
||||
"Community": "समुदाय",
|
||||
"channel_tab_community_label": "समुदाय",
|
||||
"search_filters_title": "फ़िल्टर",
|
||||
"search_filters_date_label": "अपलोड करने का समय",
|
||||
"search_filters_date_option_none": "कोई भी समय",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"View playlist on YouTube": "Prikaži zbirku na YouTubeu",
|
||||
"newest": "najnovije",
|
||||
"oldest": "najstarije",
|
||||
"popular": "popularni",
|
||||
"last": "zadnji",
|
||||
"popular": "popularne",
|
||||
"last": "zadnje",
|
||||
"Next page": "Sljedeća stranica",
|
||||
"Previous page": "Prethodna stranica",
|
||||
"Clear watch history?": "Izbrisati povijest gledanja?",
|
||||
|
@ -43,9 +43,9 @@
|
|||
"Time (h:mm:ss):": "Vrijeme (h:mm:ss):",
|
||||
"Text CAPTCHA": "Tekstualni CAPTCHA",
|
||||
"Image CAPTCHA": "Slikovni CAPTCHA",
|
||||
"Sign In": "Prijava",
|
||||
"Sign In": "Prijavi se",
|
||||
"Register": "Registriraj se",
|
||||
"E-mail": "E-mail",
|
||||
"E-mail": "E-mail adresa",
|
||||
"Google verification code": "Googleov potvrdni kod",
|
||||
"Preferences": "Postavke",
|
||||
"preferences_category_player": "Postavke playera",
|
||||
|
@ -325,9 +325,9 @@
|
|||
"`x` marked it with a ❤": "Označeno sa ❤ od `x`",
|
||||
"Audio mode": "Audio modus",
|
||||
"Video mode": "Videomodus",
|
||||
"Videos": "Videa",
|
||||
"channel_tab_videos_label": "Videa",
|
||||
"Playlists": "Zbirke",
|
||||
"Community": "Zajednica",
|
||||
"channel_tab_community_label": "Zajednica",
|
||||
"search_filters_sort_option_relevance": "Značaj",
|
||||
"search_filters_sort_option_rating": "Ocjena",
|
||||
"search_filters_sort_option_date": "Datum prijenosa",
|
||||
|
@ -488,5 +488,9 @@
|
|||
"search_filters_apply_button": "Primijeni odabrane filtre",
|
||||
"search_filters_type_option_all": "Bilo koja vrsta",
|
||||
"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",
|
||||
"Audio mode": "Csak hanggal",
|
||||
"Video mode": "Hanggal és képpel",
|
||||
"Videos": "Videói",
|
||||
"channel_tab_videos_label": "Videói",
|
||||
"Playlists": "Lejátszási listái",
|
||||
"Community": "Közösség",
|
||||
"channel_tab_community_label": "Közösség",
|
||||
"Current version: ": "Jelenlegi verzió: ",
|
||||
"preferences_quality_option_medium": "Közepes",
|
||||
"preferences_quality_dash_option_auto": "Automatikus",
|
||||
|
@ -470,5 +470,7 @@
|
|||
"search_filters_duration_option_none": "Mindegy",
|
||||
"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_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 ❤",
|
||||
"Audio mode": "Mode audio",
|
||||
"Video mode": "Mode video",
|
||||
"Videos": "Video",
|
||||
"channel_tab_videos_label": "Video",
|
||||
"Playlists": "Daftar putar",
|
||||
"Community": "Komunitas",
|
||||
"channel_tab_community_label": "Komunitas",
|
||||
"search_filters_sort_option_relevance": "Relevansi",
|
||||
"search_filters_sort_option_rating": "Penilaian",
|
||||
"search_filters_sort_option_date": "Tanggal Unggah",
|
||||
|
|
|
@ -315,9 +315,9 @@
|
|||
"`x` marked it with a ❤": "`x` merkti það með ❤",
|
||||
"Audio mode": "Hljóð ham",
|
||||
"Video mode": "Myndband ham",
|
||||
"Videos": "Myndbönd",
|
||||
"channel_tab_videos_label": "Myndbönd",
|
||||
"Playlists": "Spilunarlistar",
|
||||
"Community": "Samfélag",
|
||||
"channel_tab_community_label": "Samfélag",
|
||||
"Current version: ": "Núverandi útgáfa: ",
|
||||
"preferences_watch_history_label": "Virkja áhorfssögu: "
|
||||
}
|
||||
|
|
|
@ -290,7 +290,7 @@
|
|||
"Southern Sotho": "Sotho del Sud",
|
||||
"Spanish": "Spagnolo",
|
||||
"Spanish (Latin America)": "Spagnolo (America latina)",
|
||||
"Sundanese": "Sudanese",
|
||||
"Sundanese": "Sundanese",
|
||||
"Swahili": "Swahili",
|
||||
"Swedish": "Svedese",
|
||||
"Tajik": "Tagico",
|
||||
|
@ -344,9 +344,8 @@
|
|||
"`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤",
|
||||
"Audio mode": "Modalità audio",
|
||||
"Video mode": "Modalità video",
|
||||
"Videos": "Video",
|
||||
"channel_tab_videos_label": "Video",
|
||||
"Playlists": "Playlist",
|
||||
"Community": "Comunità",
|
||||
"search_filters_sort_option_relevance": "Pertinenza",
|
||||
"search_filters_sort_option_rating": "Valutazione",
|
||||
"search_filters_sort_option_date": "Data di caricamento",
|
||||
|
@ -472,5 +471,10 @@
|
|||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_apply_button": "Applica filtri selezionati",
|
||||
"crash_page_refresh": "provato a <a href=\"`x`\">ricaricare la pagina</a>",
|
||||
"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` が❤を込めてマークしました",
|
||||
"Audio mode": "オーディオモード",
|
||||
"Video mode": "ビデオモード",
|
||||
"Videos": "動画",
|
||||
"channel_tab_videos_label": "動画",
|
||||
"Playlists": "プレイリスト",
|
||||
"Community": "コミュニティ",
|
||||
"channel_tab_community_label": "コミュニティ",
|
||||
"search_filters_sort_option_relevance": "関連",
|
||||
"search_filters_sort_option_rating": "評価",
|
||||
"search_filters_sort_option_date": "時刻",
|
||||
|
@ -403,7 +403,7 @@
|
|||
"none": "なし",
|
||||
"download_subtitles": "字幕 - `x` (.vtt)",
|
||||
"search_filters_features_option_purchased": "購入済み",
|
||||
"preferences_quality_option_dash": "DASH (適切な品質)",
|
||||
"preferences_quality_option_dash": "DASH (適応品質)",
|
||||
"preferences_quality_dash_option_worst": "最悪",
|
||||
"preferences_quality_dash_option_best": "最高",
|
||||
"videoinfo_started_streaming_x_ago": "`x`分前に配信を開始",
|
||||
|
@ -438,5 +438,20 @@
|
|||
"search_message_no_results": "一致する検索結果はありませんでした",
|
||||
"English (United States)": "英語 (アメリカ)",
|
||||
"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_max_results_label": "피드에 표시된 동영상 수: ",
|
||||
"Redirect homepage to feed: ": "피드로 홈페이지 리디렉션: ",
|
||||
"preferences_annotations_subscribed_label": "구독한 채널에 기본적으로 특수효과를 표시하시겠습니까? ",
|
||||
"preferences_annotations_subscribed_label": "구독한 채널에 기본으로 주석 표시: ",
|
||||
"preferences_category_subscription": "구독 설정",
|
||||
"preferences_automatic_instance_redirect_label": "자동 인스턴스 리디렉션 (redirect.invidious.io로 대체): ",
|
||||
"preferences_thin_mode_label": "단순 모드: ",
|
||||
|
@ -11,7 +11,7 @@
|
|||
"preferences_dark_mode_label": "테마: ",
|
||||
"Dark mode: ": "다크 모드: ",
|
||||
"preferences_player_style_label": "플레이어 스타일: ",
|
||||
"preferences_category_visual": "시각 설정",
|
||||
"preferences_category_visual": "환경 설정",
|
||||
"preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ",
|
||||
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ",
|
||||
"preferences_annotations_label": "기본으로 주석 표시: ",
|
||||
|
@ -25,8 +25,8 @@
|
|||
"preferences_quality_label": "선호하는 비디오 품질: ",
|
||||
"preferences_speed_label": "기본 속도: ",
|
||||
"preferences_local_label": "비디오를 프록시: ",
|
||||
"preferences_listen_label": "라디오 모드 활성화: ",
|
||||
"preferences_continue_autoplay_label": "다음 동영상 자동재생 ",
|
||||
"preferences_listen_label": "라디오 모드: ",
|
||||
"preferences_continue_autoplay_label": "다음 동영상 자동재생: ",
|
||||
"preferences_continue_label": "다음 동영상으로 이동: ",
|
||||
"preferences_autoplay_label": "자동재생: ",
|
||||
"preferences_video_loop_label": "항상 반복: ",
|
||||
|
@ -37,8 +37,8 @@
|
|||
"Register": "회원가입",
|
||||
"Sign In": "로그인",
|
||||
"preferences_category_misc": "기타 설정",
|
||||
"Image CAPTCHA": "이미지 CAPTCHA",
|
||||
"Text CAPTCHA": "텍스트 CAPTCHA",
|
||||
"Image CAPTCHA": "이미지 캡차",
|
||||
"Text CAPTCHA": "텍스트 캡차",
|
||||
"Time (h:mm:ss):": "시각 (h:mm:ss):",
|
||||
"Password": "비밀번호",
|
||||
"User ID": "사용자 ID",
|
||||
|
@ -50,15 +50,15 @@
|
|||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||
"History": "역사",
|
||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||
"Export data as JSON": "데이터를 JSON으로 내보내기",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "구독을 OPML로 내보내기 (NewPipe 및 FreeTube 용)",
|
||||
"Export subscriptions as OPML": "구독을 OPML로 내보내기",
|
||||
"Export data as JSON": "JSON으로 데이터 내보내기",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
||||
"Export subscriptions as OPML": "OPML로 구독 내보내기",
|
||||
"Export": "내보내기",
|
||||
"Import NewPipe data (.zip)": "NewPipe 데이터 가져오기 (.zip)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe 구독을 가져오기 (.json)",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube 구독 가져오기 (.db)",
|
||||
"Import NewPipe data (.zip)": "뉴파이프 데이터 가져오기 (.zip)",
|
||||
"Import NewPipe subscriptions (.json)": "뉴파이프 구독 가져오기 (.json)",
|
||||
"Import FreeTube subscriptions (.db)": "프리튜브 구독 가져오기 (.db)",
|
||||
"Import YouTube subscriptions": "유튜브 구독 가져오기",
|
||||
"Import Invidious data": "인비디어스 JSON 데이터 가져오기",
|
||||
"Import Invidious data": "인비디어스 데이터 가져오기 (.json)",
|
||||
"Import": "가져오기",
|
||||
"Import and Export Data": "데이터 가져오기 및 내보내기",
|
||||
"No": "아니요",
|
||||
|
@ -150,9 +150,9 @@
|
|||
"Subscription manager": "구독 관리자",
|
||||
"Save preferences": "설정 저장",
|
||||
"Report statistics: ": "통계 보고: ",
|
||||
"Registration enabled: ": "등록 활성화: ",
|
||||
"Registration enabled: ": "회원가입 활성화: ",
|
||||
"Login enabled: ": "로그인 활성화: ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA 활성화: ",
|
||||
"CAPTCHA enabled: ": "캡차 활성화: ",
|
||||
"Top enabled: ": "Top 활성화: ",
|
||||
"preferences_show_nick_label": "상단에 닉네임 표시: ",
|
||||
"preferences_feed_menu_label": "피드 메뉴: ",
|
||||
|
@ -187,8 +187,8 @@
|
|||
"Polish": "폴란드어",
|
||||
"Persian": "페르시아어",
|
||||
"Pashto": "파슈토어",
|
||||
"Nyanja": "체와어",
|
||||
"Norwegian Bokmål": "보크몰",
|
||||
"Nyanja": "냔자어",
|
||||
"Norwegian Bokmål": "노르웨이 부크몰어",
|
||||
"Nepali": "네팔어",
|
||||
"Mongolian": "몽골어",
|
||||
"Marathi": "마라티어",
|
||||
|
@ -284,10 +284,10 @@
|
|||
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
|
||||
"Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요",
|
||||
"Wrong username or password": "잘못된 사용자 이름 또는 비밀번호",
|
||||
"Password is a required field": "비밀번호는 필수 필드입니다",
|
||||
"User ID is a required field": "사용자 ID는 필수 필드입니다",
|
||||
"CAPTCHA is a required field": "CAPTCHA는 필수 필드입니다",
|
||||
"Erroneous CAPTCHA": "잘못된 CAPTCHA",
|
||||
"Password is a required field": "비밀번호는 필수 입력란입니다",
|
||||
"User ID is a required field": "사용자 ID는 필수 입력란입니다",
|
||||
"CAPTCHA is a required field": "캡차는 필수 입력란입니다",
|
||||
"Erroneous CAPTCHA": "잘못된 캡차",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.",
|
||||
"Blacklisted regions: ": "차단된 지역: ",
|
||||
"Playlists": "재생목록",
|
||||
|
@ -297,7 +297,7 @@
|
|||
"Empty playlist": "재생목록 비어 있음",
|
||||
"Show annotations": "주석 보이기",
|
||||
"Hide annotations": "주석 숨기기",
|
||||
"Switch Invidious Instance": "Invidious 인스턴스 변경",
|
||||
"Switch Invidious Instance": "인비디어스 인스턴스 변경",
|
||||
"Spanish": "스페인어",
|
||||
"Southern Sotho": "소토어",
|
||||
"Somali": "소말리어",
|
||||
|
@ -347,8 +347,8 @@
|
|||
"search_filters_sort_option_date": "업로드 날짜",
|
||||
"search_filters_sort_option_rating": "평점",
|
||||
"search_filters_sort_option_relevance": "관련성",
|
||||
"Community": "커뮤니티",
|
||||
"Videos": "동영상",
|
||||
"channel_tab_community_label": "커뮤니티",
|
||||
"channel_tab_videos_label": "동영상",
|
||||
"Video mode": "비디오 모드",
|
||||
"Audio mode": "오디오 모드",
|
||||
"permalink": "퍼머링크",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL",
|
||||
"search_filters_title": "필터",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"Popular enabled: ": "인기 급상승 활성화: ",
|
||||
"Popular enabled: ": "인기 활성화: ",
|
||||
"Dutch (auto-generated)": "네덜란드어 (자동 생성됨)",
|
||||
"Chinese (Hong Kong)": "중국어 (홍콩)",
|
||||
"Chinese (Taiwan)": "중국어 (대만)",
|
||||
|
@ -415,7 +415,7 @@
|
|||
"Spanish (auto-generated)": "스페인어 (자동 생성됨)",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"preferences_quality_dash_option_worst": "최저",
|
||||
"preferences_watch_history_label": "시청 기록 활성화: ",
|
||||
"preferences_watch_history_label": "시청 기록 저장: ",
|
||||
"invidious": "인비디어스",
|
||||
"preferences_quality_option_small": "낮음",
|
||||
"preferences_quality_dash_option_auto": "자동",
|
||||
|
@ -439,10 +439,10 @@
|
|||
"footer_donate_page": "기부하기",
|
||||
"preferences_quality_option_dash": "DASH (다양한 화질)",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"preferences_save_player_pos_label": "이어서 보기 활성화: ",
|
||||
"preferences_save_player_pos_label": "이어서 보기: ",
|
||||
"none": "없음",
|
||||
"videoinfo_started_streaming_x_ago": "`x` 전에 스트리밍을 시작했습니다",
|
||||
"crash_page_you_found_a_bug": "Invidious에서 버그를 찾은 것 같습니다!",
|
||||
"crash_page_you_found_a_bug": "인비디어스에서 버그를 찾은 것 같습니다!",
|
||||
"download_subtitles": "자막 - `x`(.vtt)",
|
||||
"user_saved_playlists": "`x`개의 저장된 재생목록",
|
||||
"crash_page_before_reporting": "버그를 보고하기 전에 다음 사항이 있는지 확인합니다:",
|
||||
|
@ -456,5 +456,9 @@
|
|||
"crash_page_report_issue": "위의 방법 중 어느 것도 도움이 되지 않았다면, <a href=\"`x`\">깃허브에서 새 이슈를 열고</a>(가능하면 영어로) 메시지에 다음 텍스트를 포함하세요(해당 텍스트를 번역하지 마십시오):",
|
||||
"videoinfo_youTube_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 ❤",
|
||||
"Audio mode": "Garso rėžimas",
|
||||
"Video mode": "Vaizdo rėžimas",
|
||||
"Videos": "Vaizdo įrašai",
|
||||
"channel_tab_videos_label": "Vaizdo įrašai",
|
||||
"Playlists": "Grojaraiščiai",
|
||||
"Community": "Bendruomenė",
|
||||
"channel_tab_community_label": "Bendruomenė",
|
||||
"search_filters_sort_option_relevance": "Aktualumas",
|
||||
"search_filters_sort_option_rating": "Reitingas",
|
||||
"search_filters_sort_option_date": "Įkėlimo data",
|
||||
|
|
|
@ -325,9 +325,9 @@
|
|||
"`x` marked it with a ❤": "`x` levnet et ❤",
|
||||
"Audio mode": "Lydmodus",
|
||||
"Video mode": "Video-modus",
|
||||
"Videos": "Videoer",
|
||||
"channel_tab_videos_label": "Videoer",
|
||||
"Playlists": "Spillelister",
|
||||
"Community": "Gemenskap",
|
||||
"channel_tab_community_label": "Gemenskap",
|
||||
"search_filters_sort_option_relevance": "relevans",
|
||||
"search_filters_sort_option_rating": "vurdering",
|
||||
"search_filters_sort_option_date": "dato",
|
||||
|
|
|
@ -320,9 +320,9 @@
|
|||
"`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤",
|
||||
"Audio mode": "Audiomodus",
|
||||
"Video mode": "Videomodus",
|
||||
"Videos": "Video's",
|
||||
"channel_tab_videos_label": "Video's",
|
||||
"Playlists": "Afspeellijsten",
|
||||
"Community": "Gemeenschap",
|
||||
"channel_tab_community_label": "Gemeenschap",
|
||||
"search_filters_sort_option_relevance": "relevantie",
|
||||
"search_filters_sort_option_rating": "beoordeling",
|
||||
"search_filters_sort_option_date": "datum",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -324,9 +324,9 @@
|
|||
"`x` marked it with a ❤": "`x` oznaczonych ❤",
|
||||
"Audio mode": "Tryb audio",
|
||||
"Video mode": "Tryb wideo",
|
||||
"Videos": "Filmy",
|
||||
"channel_tab_videos_label": "Wideo",
|
||||
"Playlists": "Playlisty",
|
||||
"Community": "Społeczność",
|
||||
"channel_tab_community_label": "Społeczność",
|
||||
"search_filters_sort_option_relevance": "Trafność",
|
||||
"search_filters_sort_option_rating": "Ocena",
|
||||
"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_filters_type_option_all": "Dowolny typ",
|
||||
"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 ❤",
|
||||
"Audio mode": "Modo de áudio",
|
||||
"Video mode": "Modo de vídeo",
|
||||
"Videos": "Vídeos",
|
||||
"channel_tab_videos_label": "Vídeos",
|
||||
"Playlists": "Listas de reprodução",
|
||||
"Community": "Comunidade",
|
||||
"channel_tab_community_label": "Comunidade",
|
||||
"search_filters_sort_option_relevance": "relevância",
|
||||
"search_filters_sort_option_rating": "avaliação",
|
||||
"search_filters_sort_option_date": "data",
|
||||
|
@ -471,5 +471,6 @@
|
|||
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
||||
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
|
||||
"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": "Importar",
|
||||
"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 NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
|
||||
"Export": "Exportar",
|
||||
"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 data as JSON": "Exportar dados do Invidious como JSON",
|
||||
"Export data as JSON": "Exportar dados Invidious como JSON",
|
||||
"Delete account?": "Eliminar conta?",
|
||||
"History": "Histórico",
|
||||
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
|
||||
|
@ -341,9 +341,9 @@
|
|||
"`x` marked it with a ❤": "`x` foi marcado como ❤",
|
||||
"Audio mode": "Modo de áudio",
|
||||
"Video mode": "Modo de vídeo",
|
||||
"Videos": "Vídeos",
|
||||
"channel_tab_videos_label": "Vídeos",
|
||||
"Playlists": "Listas de reprodução",
|
||||
"Community": "Comunidade",
|
||||
"channel_tab_community_label": "Comunidade",
|
||||
"search_filters_sort_option_relevance": "Relevância",
|
||||
"search_filters_sort_option_rating": "Avaliação",
|
||||
"search_filters_sort_option_date": "Data de envio",
|
||||
|
@ -379,24 +379,24 @@
|
|||
"generic_videos_count_plural": "{{count}} vídeos",
|
||||
"generic_playlists_count": "{{count}} lista de reprodução",
|
||||
"generic_playlists_count_plural": "{{count}} listas de reprodução",
|
||||
"generic_subscriptions_count": "{{count}} subscrição",
|
||||
"generic_subscriptions_count_plural": "{{count}} subscrições",
|
||||
"generic_subscriptions_count": "{{count}} inscrição",
|
||||
"generic_subscriptions_count_plural": "{{count}} inscrições",
|
||||
"generic_views_count": "{{count}} visualização",
|
||||
"generic_views_count_plural": "{{count}} visualizações",
|
||||
"generic_subscribers_count": "{{count}} subscritor",
|
||||
"generic_subscribers_count_plural": "{{count}} subscritores",
|
||||
"generic_subscribers_count": "{{count}} inscrito",
|
||||
"generic_subscribers_count_plural": "{{count}} inscritos",
|
||||
"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",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} notificação por ver",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações por ver",
|
||||
"Popular enabled: ": "Página \"Popular\" ativada: ",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} notificação não vista",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas",
|
||||
"Popular enabled: ": "Página \"popular\" ativada: ",
|
||||
"search_message_no_results": "Nenhum resultado encontrado.",
|
||||
"preferences_quality_dash_option_auto": "Automática",
|
||||
"preferences_region_label": "País para o conteúdo: ",
|
||||
"preferences_quality_dash_option_auto": "Automático",
|
||||
"preferences_region_label": "País do conteúdo: ",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"preferences_watch_history_label": "Ativar histórico de visualizações ",
|
||||
"preferences_watch_history_label": "Ativar histórico de reprodução: ",
|
||||
"preferences_quality_dash_option_best": "Melhor",
|
||||
"preferences_quality_dash_option_worst": "Pior",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
|
@ -404,13 +404,13 @@
|
|||
"preferences_quality_option_hd720": "HD720",
|
||||
"preferences_quality_option_dash": "DASH (qualidade adaptativa)",
|
||||
"preferences_quality_option_medium": "Média",
|
||||
"preferences_quality_option_small": "Pequena",
|
||||
"preferences_quality_option_small": "Baixa",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"Video unavailable": "Vídeo indisponível",
|
||||
"Russian (auto-generated)": "Russo (geradas automaticamente)",
|
||||
"Video unavailable": "Vídeo não disponível",
|
||||
"Russian (auto-generated)": "Russo (gerado automaticamente)",
|
||||
"comments_view_x_replies": "Ver {{count}} resposta",
|
||||
"comments_view_x_replies_plural": "Ver {{count}} respostas",
|
||||
"comments_points_count": "{{count}} ponto",
|
||||
|
@ -418,18 +418,18 @@
|
|||
"English (United Kingdom)": "Inglês (Reino Unido)",
|
||||
"Chinese (Hong Kong)": "Chinês (Hong Kong)",
|
||||
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
||||
"Dutch (auto-generated)": "Holandês (geradas automaticamente)",
|
||||
"French (auto-generated)": "Francês (geradas automaticamente)",
|
||||
"German (auto-generated)": "Alemão (geradas automaticamente)",
|
||||
"Indonesian (auto-generated)": "Indonésio (geradas automaticamente)",
|
||||
"Interlingue": "Interlingue",
|
||||
"Italian (auto-generated)": "Italiano (geradas automaticamente)",
|
||||
"Japanese (auto-generated)": "Japonês (geradas automaticamente)",
|
||||
"Korean (auto-generated)": "Coreano (geradas automaticamente)",
|
||||
"Portuguese (auto-generated)": "Português (geradas automaticamente)",
|
||||
"Dutch (auto-generated)": "Holandês (gerado automaticamente)",
|
||||
"French (auto-generated)": "Francês (gerado automaticamente)",
|
||||
"German (auto-generated)": "Alemão (gerado automaticamente)",
|
||||
"Indonesian (auto-generated)": "Indonésio (gerado automaticamente)",
|
||||
"Interlingue": "Interlíngua",
|
||||
"Italian (auto-generated)": "Italiano (gerado automaticamente)",
|
||||
"Japanese (auto-generated)": "Japonês (gerado automaticamente)",
|
||||
"Korean (auto-generated)": "Coreano (gerado automaticamente)",
|
||||
"Portuguese (auto-generated)": "Português (gerado automaticamente)",
|
||||
"Portuguese (Brazil)": "Português (Brasil)",
|
||||
"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_duration_option_none": "Qualquer duração",
|
||||
"search_filters_duration_option_short": "Curto (< 4 minutos)",
|
||||
|
@ -438,29 +438,39 @@
|
|||
"search_filters_features_option_purchased": "Comprado",
|
||||
"search_filters_apply_button": "Aplicar filtros selecionados",
|
||||
"videoinfo_watch_on_youTube": "Ver no YouTube",
|
||||
"videoinfo_youTube_embed_link": "Embutir",
|
||||
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte modificado",
|
||||
"videoinfo_invidious_embed_link": "Ligação embutida",
|
||||
"videoinfo_youTube_embed_link": "Incorporar",
|
||||
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
|
||||
"videoinfo_invidious_embed_link": "Incorporar hiperligação",
|
||||
"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)",
|
||||
"user_created_playlists": "`x` listas de reprodução criadas",
|
||||
"user_saved_playlists": "`x` listas de reprodução guardadas",
|
||||
"preferences_save_player_pos_label": "Guardar posição de reprodução: ",
|
||||
"Turkish (auto-generated)": "Turco (geradas automaticamente)",
|
||||
"preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
|
||||
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
||||
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
||||
"Chinese (China)": "Chinês (China)",
|
||||
"Spanish (auto-generated)": "Espanhol (geradas automaticamente)",
|
||||
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
|
||||
"Spanish (Mexico)": "Espanhol (México)",
|
||||
"English (United States)": "Inglês (Estados Unidos)",
|
||||
"footer_donate_page": "Doar",
|
||||
"footer_documentation": "Documentação",
|
||||
"footer_source_code": "Código-fonte",
|
||||
"footer_original_source_code": "Código-fonte original",
|
||||
"footer_modfied_source_code": "Código-fonte modificado",
|
||||
"footer_modfied_source_code": "Código-fonte alterado",
|
||||
"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_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",
|
||||
"last": "últimos",
|
||||
"Current version: ": "Versão atual: ",
|
||||
"Community": "Comunidade",
|
||||
"channel_tab_community_label": "Comunidade",
|
||||
"Playlists": "Listas de reprodução",
|
||||
"Videos": "Vídeos",
|
||||
"channel_tab_videos_label": "Vídeos",
|
||||
"Video mode": "Modo de vídeo",
|
||||
"Audio mode": "Modo de áudio",
|
||||
"`x` marked it with a ❤": "`x` foi marcado como ❤",
|
||||
|
|
|
@ -315,9 +315,9 @@
|
|||
"`x` marked it with a ❤": "`x` l-a marcat cu o ❤",
|
||||
"Audio mode": "Mod audio",
|
||||
"Video mode": "Mod video",
|
||||
"Videos": "Videoclipuri",
|
||||
"channel_tab_videos_label": "Videoclipuri",
|
||||
"Playlists": "Liste de redare",
|
||||
"Community": "Comunitate",
|
||||
"channel_tab_community_label": "Comunitate",
|
||||
"Current version: ": "Versiunea actuală: ",
|
||||
"crash_page_read_the_faq": "citit lista <a href=\"`x`\">Întrebărilor Frecvente (FAQ)</a>",
|
||||
"generic_count_days_0": "{{count}} zi",
|
||||
|
|
|
@ -325,9 +325,9 @@
|
|||
"`x` marked it with a ❤": "❤ от автора канала \"`x`\"",
|
||||
"Audio mode": "Аудио режим",
|
||||
"Video mode": "Видео режим",
|
||||
"Videos": "Видео",
|
||||
"channel_tab_videos_label": "Видео",
|
||||
"Playlists": "Плейлисты",
|
||||
"Community": "Сообщество",
|
||||
"channel_tab_community_label": "Сообщество",
|
||||
"search_filters_sort_option_relevance": "по актуальности",
|
||||
"search_filters_sort_option_rating": "по рейтингу",
|
||||
"search_filters_sort_option_date": "по дате загрузки",
|
||||
|
|
|
@ -222,7 +222,7 @@
|
|||
"About": "O aplikaciji",
|
||||
"%A %B %-d, %Y": "%A %-d %B %Y",
|
||||
"Audio mode": "Avdio način",
|
||||
"Videos": "Videoposnetki",
|
||||
"channel_tab_videos_label": "Videoposnetki",
|
||||
"search_filters_date_label": "Datum nalaganja",
|
||||
"search_filters_date_option_today": "Danes",
|
||||
"search_filters_date_option_week": "Ta teden",
|
||||
|
@ -455,7 +455,7 @@
|
|||
"Download": "Prenesi",
|
||||
"permalink": "stalna povezava",
|
||||
"`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°",
|
||||
"Video mode": "Video način",
|
||||
"search_filters_features_option_c_commons": "Creative Commons",
|
||||
|
|
|
@ -259,10 +259,10 @@
|
|||
"YouTube comment permalink": "Permalidhje komenti YouTube",
|
||||
"Audio mode": "Mënyrë për audion",
|
||||
"Playlists": "Luajlista",
|
||||
"Community": "Bashkësi",
|
||||
"channel_tab_community_label": "Bashkësi",
|
||||
"search_filters_sort_option_relevance": "Rëndësi",
|
||||
"Video mode": "Mënyrë video",
|
||||
"Videos": "Video",
|
||||
"channel_tab_videos_label": "Video",
|
||||
"search_filters_sort_option_rating": "Vlerësim",
|
||||
"search_filters_sort_option_date": "Datë ngarkimi",
|
||||
"search_filters_sort_option_views": "Numër parjesh",
|
||||
|
@ -446,6 +446,22 @@
|
|||
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
|
||||
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
|
||||
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
|
||||
"Shared `x`": "Ndau me të tjerë `x`",
|
||||
"search_filters_title": "Filtra"
|
||||
"Shared `x`": "Ndarë me të tjerë më `x`",
|
||||
"search_filters_title": "Filtra",
|
||||
"Popular enabled: ": "Me populloret të aktivizuara: ",
|
||||
"error_video_not_in_playlist": "Videoja e kërkuar s’ekziston 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": "S’u gjetën përfundime.",
|
||||
"Could not pull trending pages.": "S’u 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_locale_label": "Jezik: ",
|
||||
"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",
|
||||
"Fallback captions: ": "Titl u slučaju da glavni nije dostupan: ",
|
||||
"Private": "Privatno",
|
||||
|
@ -289,7 +289,7 @@
|
|||
"Erroneous token": "Pogrešan žeton",
|
||||
"Czech": "Češki",
|
||||
"Latin": "Latinski",
|
||||
"Videos": "Video klipovi",
|
||||
"channel_tab_videos_label": "Video klipovi",
|
||||
"search_filters_features_option_four_k": "4К",
|
||||
"footer_donate_page": "Doniraj",
|
||||
"English": "Engleski",
|
||||
|
|
|
@ -245,7 +245,7 @@
|
|||
"(edited)": "(измењено)",
|
||||
"`x` marked it with a ❤": "`x` је означио/ла ово са ❤",
|
||||
"Audio mode": "Аудио мод",
|
||||
"Videos": "Видео клипови",
|
||||
"channel_tab_videos_label": "Видео клипови",
|
||||
"search_filters_sort_option_views": "Број прегледа",
|
||||
"search_filters_features_label": "Карактеристике",
|
||||
"search_filters_date_option_today": "Данас",
|
||||
|
@ -298,7 +298,7 @@
|
|||
"Ukrainian": "Украјински",
|
||||
"permalink": "трајна веза",
|
||||
"Pashto": "Паштунски",
|
||||
"Community": "Заједница",
|
||||
"channel_tab_community_label": "Заједница",
|
||||
"Sindhi": "Синди",
|
||||
"Could not fetch comments": "Узимање коментара није успело",
|
||||
"Bangla": "Бангла/Бенгалски",
|
||||
|
|
|
@ -323,9 +323,9 @@
|
|||
"`x` marked it with a ❤": "`x` lämnade ett ❤",
|
||||
"Audio mode": "Ljudläge",
|
||||
"Video mode": "Videoläge",
|
||||
"Videos": "Videor",
|
||||
"channel_tab_videos_label": "Videor",
|
||||
"Playlists": "Spellistor",
|
||||
"Community": "Gemenskap",
|
||||
"channel_tab_community_label": "Gemenskap",
|
||||
"search_filters_sort_option_relevance": "Relevans",
|
||||
"search_filters_sort_option_rating": "Rankning",
|
||||
"search_filters_sort_option_date": "Datum",
|
||||
|
|
564
locales/tr.json
564
locales/tr.json
|
@ -1,126 +1,126 @@
|
|||
{
|
||||
"LIVE": "CANLI",
|
||||
"Shared `x` ago": "`x` önce paylaşıldı",
|
||||
"Unsubscribe": "Abonelikten çık",
|
||||
"Subscribe": "Abone ol",
|
||||
"View channel on YouTube": "Kanalı YouTube'da görüntüle",
|
||||
"View playlist on YouTube": "Oynatma listesini YouTube'da görüntüle",
|
||||
"newest": "en yeni",
|
||||
"oldest": "en eski",
|
||||
"popular": "popüler",
|
||||
"last": "son",
|
||||
"Next page": "Sonraki sayfa",
|
||||
"Previous page": "Önceki sayfa",
|
||||
"Shared `x` ago": "`x` Önce Paylaşıldı",
|
||||
"Unsubscribe": "Abonelikten Çık",
|
||||
"Subscribe": "Abone Ol",
|
||||
"View channel on YouTube": "Kanalı YouTube'da Görüntüle",
|
||||
"View playlist on YouTube": "Oynatma Listesini YouTube'da Görüntüle",
|
||||
"newest": "En Yeni",
|
||||
"oldest": "En Eski",
|
||||
"popular": "Popüler",
|
||||
"last": "Son",
|
||||
"Next page": "Sonraki Sayfa",
|
||||
"Previous page": "Önceki Sayfa",
|
||||
"Clear watch history?": "İzleme geçmişi temizlensin mi?",
|
||||
"New password": "Yeni parola",
|
||||
"New passwords must match": "Yeni parolalar eşleşmek zorunda",
|
||||
"Cannot change password for Google accounts": "Google hesapları için parola değiştirilemez",
|
||||
"New password": "Yeni Parola",
|
||||
"New passwords must match": "Yeni Parolalar Eşleşmek Zorunda",
|
||||
"Cannot change password for Google accounts": "Google Hesapları İçin Parola Değiştirilemez",
|
||||
"Authorize token?": "Belirteç yetkilendirilsin mi?",
|
||||
"Authorize token for `x`?": "`x` için belirteç yetkilendirilsin mi?",
|
||||
"Yes": "Evet",
|
||||
"No": "Hayır",
|
||||
"Import and Export Data": "Verileri İçe ve Dışa Aktar",
|
||||
"Import": "İçe aktar",
|
||||
"Import Invidious data": "İnvidious JSON verilerini içe aktar",
|
||||
"Import YouTube subscriptions": "YouTube/OPML aboneliklerini içe aktar",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube aboneliklerini içe aktar (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe aboneliklerini içe aktar (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipe verilerini içe aktar (.zip)",
|
||||
"Export": "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 data as JSON": "Invidious verilerini JSON olarak dışa aktar",
|
||||
"Import": "İçe Aktar",
|
||||
"Import Invidious data": "Invidious JSON Verilerini İçe Aktar",
|
||||
"Import YouTube subscriptions": "YouTube/OPML Aboneliklerini İçe Aktar",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube Aboneliklerini İçe Aktar (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe Aboneliklerini İçe Aktar (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipe Verilerini İçe Aktar (.zip)",
|
||||
"Export": "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 İçin)",
|
||||
"Export data as JSON": "İnvidious Verilerini JSON Olarak Dışa Aktar",
|
||||
"Delete account?": "Hesap silinsin mi?",
|
||||
"History": "Geçmiş",
|
||||
"An alternative front-end to YouTube": "YouTube için alternatif bir ön-yüz",
|
||||
"JavaScript license information": "JavaScript lisans bilgileri",
|
||||
"source": "kaynak",
|
||||
"Log in": "Oturum aç",
|
||||
"Log in/register": "Oturum aç/kayıt ol",
|
||||
"Log in with Google": "Google ile oturum aç",
|
||||
"User ID": "Kullanıcı kimliği",
|
||||
"An alternative front-end to YouTube": "YouTube İçin Alternatif Bir Ön-Yüz",
|
||||
"JavaScript license information": "JavaScript Lisans Bilgileri",
|
||||
"source": "Kaynak",
|
||||
"Log in": "Oturum Aç",
|
||||
"Log in/register": "Oturum Aç/Kayıt Ol",
|
||||
"Log in with Google": "Google İle Oturum Aç",
|
||||
"User ID": "Kullanıcı Kimliği",
|
||||
"Password": "Parola",
|
||||
"Time (h:mm:ss):": "Zaman (h:mm:ss):",
|
||||
"Text CAPTCHA": "Metin CAPTCHA",
|
||||
"Image CAPTCHA": "Resim CAPTCHA",
|
||||
"Sign In": "Oturum Aç",
|
||||
"Register": "Kayıt Ol",
|
||||
"E-mail": "E-posta",
|
||||
"Google verification code": "Google doğrulama kodu",
|
||||
"E-mail": "E-Posta",
|
||||
"Google verification code": "Google Doğrulama Kodu",
|
||||
"Preferences": "Tercihler",
|
||||
"preferences_category_player": "Oynatıcı tercihleri",
|
||||
"preferences_video_loop_label": "Sürekli döngü: ",
|
||||
"preferences_autoplay_label": "Otomatik oynat: ",
|
||||
"preferences_continue_label": "Öntanımlı olarak sonrakini oynat: ",
|
||||
"preferences_continue_autoplay_label": "Sonraki videoyu otomatik oynat: ",
|
||||
"preferences_listen_label": "Öntanımlı olarak dinle: ",
|
||||
"preferences_local_label": "Videoları proxy'le: ",
|
||||
"preferences_speed_label": "Öntanımlı hız: ",
|
||||
"preferences_quality_label": "Tercih edilen video kalitesi: ",
|
||||
"preferences_volume_label": "Oynatıcı ses seviyesi: ",
|
||||
"preferences_comments_label": "Öntanımlı yorumlar: ",
|
||||
"preferences_category_player": "Oynatıcı Tercihleri",
|
||||
"preferences_video_loop_label": "Sürekli Döngü: ",
|
||||
"preferences_autoplay_label": "Otomatik Oynat: ",
|
||||
"preferences_continue_label": "Öntanımlı Olarak Sonrakini Oynat: ",
|
||||
"preferences_continue_autoplay_label": "Sonraki Videoyu Otomatik Oynat: ",
|
||||
"preferences_listen_label": "Öntanımlı Olarak Dinle: ",
|
||||
"preferences_local_label": "Videolara Proxy Uygula: ",
|
||||
"preferences_speed_label": "Öntanımlı Hız: ",
|
||||
"preferences_quality_label": "Tercih Edilen Video Kalitesi: ",
|
||||
"preferences_volume_label": "Oynatıcı Ses Seviyesi: ",
|
||||
"preferences_comments_label": "Öntanımlı Yorumlar: ",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "Reddit",
|
||||
"preferences_captions_label": "Öntanımlı altyazılar: ",
|
||||
"Fallback captions: ": "Yedek altyazılar: ",
|
||||
"preferences_related_videos_label": "İlgili videoları göster: ",
|
||||
"preferences_annotations_label": "Öntanımlı olarak ek açıklamaları göster: ",
|
||||
"preferences_extend_desc_label": "Video açıklamasını otomatik olarak genişlet: ",
|
||||
"preferences_vr_mode_label": "Etkileşimli 360 derece videolar (WebGL gerektirir): ",
|
||||
"preferences_category_visual": "Görsel tercihler",
|
||||
"preferences_player_style_label": "Oynatıcı biçimi: ",
|
||||
"Dark mode: ": "Karanlık mod: ",
|
||||
"preferences_captions_label": "Öntanımlı Altyazılar: ",
|
||||
"Fallback captions: ": "Yedek Altyazılar: ",
|
||||
"preferences_related_videos_label": "İlgili Videoları Göster: ",
|
||||
"preferences_annotations_label": "Öntanımlı Olarak Ek Açıklamaları Göster: ",
|
||||
"preferences_extend_desc_label": "Video Açıklamasını Otomatik Olarak Genişlet: ",
|
||||
"preferences_vr_mode_label": "Etkileşimli 360 Derece Videolar (WebGL Gerektirir): ",
|
||||
"preferences_category_visual": "Görsel Tercihler",
|
||||
"preferences_player_style_label": "Oynatıcı Biçimi: ",
|
||||
"Dark mode: ": "Koyu Mod: ",
|
||||
"preferences_dark_mode_label": "Tema: ",
|
||||
"dark": "karanlık",
|
||||
"light": "aydınlık",
|
||||
"preferences_thin_mode_label": "İnce mod: ",
|
||||
"preferences_category_misc": "Çeşitli tercihler",
|
||||
"preferences_automatic_instance_redirect_label": "Otomatik örnek yeniden yönlendirmesi (yedek: redirect.invidious.io): ",
|
||||
"preferences_category_subscription": "Abonelik tercihleri",
|
||||
"preferences_annotations_subscribed_label": "Abone olunan kanallar için ek açıklamaları öntanımlı olarak göster: ",
|
||||
"Redirect homepage to feed: ": "Ana sayfayı akışa yönlendir: ",
|
||||
"preferences_max_results_label": "Akışta gösterilen video sayısı: ",
|
||||
"preferences_sort_label": "Videoları sıralama kriteri: ",
|
||||
"published": "yayınlandı",
|
||||
"published - reverse": "yayınlandı - ters",
|
||||
"alphabetically": "alfabetik olarak",
|
||||
"alphabetically - reverse": "alfabetik olarak - ters",
|
||||
"channel name": "kanal adı",
|
||||
"channel name - reverse": "kanal adı - ters",
|
||||
"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: ",
|
||||
"preferences_unseen_only_label": "Sadece izlenmemişleri göster: ",
|
||||
"preferences_notifications_only_label": "Sadece bildirimleri göster (eğer varsa): ",
|
||||
"Enable web notifications": "Ağ bildirimlerini etkinleştir",
|
||||
"`x` uploaded a video": "`x` bir video yükledi",
|
||||
"`x` is live": "`x` canlı yayında",
|
||||
"preferences_category_data": "Veri tercihleri",
|
||||
"Clear watch history": "İzleme geçmişini temizle",
|
||||
"Import/export data": "Verileri içe/dışa aktar",
|
||||
"Change password": "Parolayı değiştir",
|
||||
"Manage subscriptions": "Abonelikleri yönet",
|
||||
"Manage tokens": "Belirteçleri yönet",
|
||||
"Watch history": "İzleme geçmişi",
|
||||
"Delete account": "Hesap silme",
|
||||
"preferences_category_admin": "Yönetici tercihleri",
|
||||
"preferences_default_home_label": "Öntanımlı ana sayfa: ",
|
||||
"preferences_feed_menu_label": "Akış menüsü: ",
|
||||
"preferences_show_nick_label": "Takma adı üstte göster: ",
|
||||
"Top enabled: ": "Top etkin: ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA etkin: ",
|
||||
"Login enabled: ": "Oturum açma etkin: ",
|
||||
"Registration enabled: ": "Kayıt olma etkin: ",
|
||||
"Report statistics: ": "Rapor istatistikleri: ",
|
||||
"Save preferences": "Tercihleri kaydet",
|
||||
"Subscription manager": "Abonelik yöneticisi",
|
||||
"Token manager": "Belirteç yöneticisi",
|
||||
"dark": "Koyu",
|
||||
"light": "Açık",
|
||||
"preferences_thin_mode_label": "İnce Mod: ",
|
||||
"preferences_category_misc": "Çeşitli Tercihler",
|
||||
"preferences_automatic_instance_redirect_label": "Otomatik Örnek Yeniden Yönlendirmesi (Yedek: redirect.invidious.io): ",
|
||||
"preferences_category_subscription": "Abonelik Tercihleri",
|
||||
"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: ",
|
||||
"preferences_max_results_label": "Akışta Gösterilen Video Sayısı: ",
|
||||
"preferences_sort_label": "Videoları Sıralama Kriteri: ",
|
||||
"published": "Yayınlandı",
|
||||
"published - reverse": "Yayınlandı - Ters",
|
||||
"alphabetically": "Alfabetik Olarak",
|
||||
"alphabetically - reverse": "Alfabetik Olarak - Ters",
|
||||
"channel name": "Kanal Adı",
|
||||
"channel name - reverse": "Kanal Adı - Ters",
|
||||
"Only show latest video from channel: ": "Sadece Kanaldaki En Son Videoyu Göster: ",
|
||||
"Only show latest unwatched video from channel: ": "Sadece Kanaldaki En Son İzlenmemiş Videoyu Göster: ",
|
||||
"preferences_unseen_only_label": "Sadece İzlenmemişleri Göster: ",
|
||||
"preferences_notifications_only_label": "Sadece Bildirimleri Göster (Eğer Varsa): ",
|
||||
"Enable web notifications": "Ağ Bildirimlerini Etkinleştir",
|
||||
"`x` uploaded a video": "`x` Bir Video Yükledi",
|
||||
"`x` is live": "`x` Canlı Yayında",
|
||||
"preferences_category_data": "Veri Tercihleri",
|
||||
"Clear watch history": "İzleme Geçmişini Temizle",
|
||||
"Import/export data": "Verileri İçe/Dışa Aktar",
|
||||
"Change password": "Parolayı Değiştir",
|
||||
"Manage subscriptions": "Abonelikleri Yönet",
|
||||
"Manage tokens": "Belirteçleri Yönet",
|
||||
"Watch history": "İzleme Geçmişi",
|
||||
"Delete account": "Hesap Silme",
|
||||
"preferences_category_admin": "Yönetici Tercihleri",
|
||||
"preferences_default_home_label": "Öntanımlı Ana Sayfa: ",
|
||||
"preferences_feed_menu_label": "Akış Menüsü: ",
|
||||
"preferences_show_nick_label": "Takma Adı Üstte Göster: ",
|
||||
"Top enabled: ": "Top Etkin: ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA Etkin: ",
|
||||
"Login enabled: ": "Oturum Açma Etkin: ",
|
||||
"Registration enabled: ": "Kayıt Olma Etkin: ",
|
||||
"Report statistics: ": "Rapor İstatistikleri: ",
|
||||
"Save preferences": "Tercihleri Kaydet",
|
||||
"Subscription manager": "Abonelik Yöneticisi",
|
||||
"Token manager": "Belirteç Yöneticisi",
|
||||
"Token": "Belirteç",
|
||||
"Import/export": "İçe/dışa aktar",
|
||||
"unsubscribe": "abonelikten çık",
|
||||
"revoke": "geri al",
|
||||
"Import/export": "İçe/Dışa Aktar",
|
||||
"unsubscribe": "Abonelikten Çık",
|
||||
"revoke": "Geri Al",
|
||||
"Subscriptions": "Abonelikler",
|
||||
"search": "ara",
|
||||
"Log out": "Çıkış yap",
|
||||
"search": "Ara",
|
||||
"Log out": "Çıkış Yap",
|
||||
"Released under the AGPLv3 on Github.": "GitHub'da AGPLv3 altında yayınlandı.",
|
||||
"Source available here.": "Kaynak kodları burada bulunabilir.",
|
||||
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
|
||||
|
@ -129,76 +129,76 @@
|
|||
"Public": "Genel",
|
||||
"Unlisted": "Listelenmemiş",
|
||||
"Private": "Özel",
|
||||
"View all playlists": "Tüm oynatma listelerini görüntüle",
|
||||
"Updated `x` ago": "`x` önce güncellendi",
|
||||
"View all playlists": "Tüm Oynatma Listelerini Görüntüle",
|
||||
"Updated `x` ago": "`x` Önce Güncellendi",
|
||||
"Delete playlist `x`?": "`x` oynatma listesi silinsin mi?",
|
||||
"Delete playlist": "Oynatma listesini sil",
|
||||
"Create playlist": "Oynatma listesi oluştur",
|
||||
"Delete playlist": "Oynatma Listesini Sil",
|
||||
"Create playlist": "Oynatma Listesi Oluştur",
|
||||
"Title": "Başlık",
|
||||
"Playlist privacy": "Oynatma listesi gizliliği",
|
||||
"Editing playlist `x`": "`x` oynatma listesi düzenleniyor",
|
||||
"Show more": "Daha fazla göster",
|
||||
"Show less": "Daha az göster",
|
||||
"Watch on YouTube": "YouTube'da izle",
|
||||
"Playlist privacy": "Oynatma Listesi Gizliliği",
|
||||
"Editing playlist `x`": "`x` Oynatma Listesi Düzenleniyor",
|
||||
"Show more": "Daha Fazla Göster",
|
||||
"Show less": "Daha Az Göster",
|
||||
"Watch on YouTube": "YouTube'da İzle",
|
||||
"Switch Invidious Instance": "Invidious Örneğini Değiştir",
|
||||
"Hide annotations": "Ek açıklamaları gizle",
|
||||
"Show annotations": "Ek açıklamaları göster",
|
||||
"Hide annotations": "Ek Açıklamaları Gizle",
|
||||
"Show annotations": "Ek Açıklamaları Göster",
|
||||
"Genre: ": "Tür: ",
|
||||
"License: ": "Lisans: ",
|
||||
"Family friendly? ": "Aile için uygun mu? ",
|
||||
"Wilson score: ": "Wilson puanı: ",
|
||||
"Engagement: ": "İzleyenlerin oy verme oranı: ",
|
||||
"Whitelisted regions: ": "Beyaz listeye alınan bölgeler: ",
|
||||
"Blacklisted regions: ": "Kara listeye alınan bölgeler: ",
|
||||
"Shared `x`": "`x` paylaşıldı",
|
||||
"Premieres in `x`": "`x`içinde ilk gösterim",
|
||||
"Premieres `x`": "`x` ilk gösterim",
|
||||
"Wilson score: ": "Wilson Puanı: ",
|
||||
"Engagement: ": "İzleyenlerin Oy Verme Oranı: ",
|
||||
"Whitelisted regions: ": "Beyaz Listeye Alınan Bölgeler: ",
|
||||
"Blacklisted regions: ": "Kara Listeye Alınan Bölgeler: ",
|
||||
"Shared `x`": "`x` Paylaşıldı",
|
||||
"Premieres in `x`": "`x`İçinde İlk 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.",
|
||||
"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 YouTube comments": "YouTube Yorumlarını Görüntüle",
|
||||
"View more comments on Reddit": "Reddit'te Daha Fazla Yorum Görüntüle",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` yorumu görüntüle",
|
||||
"": "`x` yorumu görüntüle"
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` Yorumu Görüntüle",
|
||||
"": "`x` Yorumu Görüntüle"
|
||||
},
|
||||
"View Reddit comments": "Reddit yorumlarını görüntüle",
|
||||
"Hide replies": "Cevapları gizle",
|
||||
"Show replies": "Cevapları göster",
|
||||
"Incorrect password": "Yanlış parola",
|
||||
"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.",
|
||||
"Invalid TFA code": "Geçersiz TFA kodu",
|
||||
"View Reddit comments": "Reddit Yorumlarını Görüntüle",
|
||||
"Hide replies": "Cevapları Gizle",
|
||||
"Show replies": "Cevapları Göster",
|
||||
"Incorrect password": "Yanlış Parola",
|
||||
"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 (Kimlik Doğrulayıcı ya da SMS) açık olduğundan emin olun.",
|
||||
"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.",
|
||||
"Wrong answer": "Yanlış cevap",
|
||||
"Wrong answer": "Yanlış Cevap",
|
||||
"Erroneous CAPTCHA": "Hatalı CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA 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",
|
||||
"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",
|
||||
"Password cannot be empty": "Parola boş olamaz",
|
||||
"Password cannot be longer than 55 characters": "Parola 55 karakterden uzun olamaz",
|
||||
"Please log in": "Lütfen oturum açın",
|
||||
"Invidious Private Feed for `x`": "`x` için İnvidious Özel Akışı",
|
||||
"channel:`x`": "kanal:`x`",
|
||||
"Deleted or invalid channel": "Silinmiş ya da geçersiz kanal",
|
||||
"CAPTCHA is a required field": "CAPTCHA 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",
|
||||
"Wrong username or password": "Yanlış Kullanıcı Adı ya da Parola",
|
||||
"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 longer than 55 characters": "Parola 55 Karakterden Uzun Olamaz",
|
||||
"Please log in": "Lütfen Oturum Açın",
|
||||
"Invidious Private Feed for `x`": "`x` İçin Invidious Özel Akışı",
|
||||
"channel:`x`": "Kanal:`x`",
|
||||
"Deleted or invalid channel": "Silinmiş ya da Geçersiz Kanal",
|
||||
"This channel does not exist.": "Bu kanal mevcut değil.",
|
||||
"Could not get channel info.": "Kanal bilgisi alınamadı.",
|
||||
"Could not fetch comments": "Yorumlar alınamadı",
|
||||
"`x` ago": "`x` önce",
|
||||
"Load more": "Daha fazla yükle",
|
||||
"Could not fetch comments": "Yorumlar Alınamadı",
|
||||
"`x` ago": "`x` Önce",
|
||||
"Load more": "Daha Fazla Yükle",
|
||||
"Could not create mix.": "Mix oluşturulamadı.",
|
||||
"Empty playlist": "Boş oynatma listesi",
|
||||
"Empty playlist": "Boş Oynatma Listesi",
|
||||
"Not a playlist.": "Oynatma listesi değil.",
|
||||
"Playlist does not exist.": "Oynatma listesi mevcut değil.",
|
||||
"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 \"token\" is a required field": "\"belirteç\" gizli alanı zorunlu bir alandır",
|
||||
"Erroneous challenge": "Hatalı challenge",
|
||||
"Erroneous token": "Hatalı belirteç",
|
||||
"No such user": "Böyle bir kullanıcı yok",
|
||||
"Token is expired, please try again": "Belirtecin süresi doldu, lütfen tekrar deneyin",
|
||||
"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",
|
||||
"Erroneous challenge": "Hatalı Challenge",
|
||||
"Erroneous token": "Hatalı Belirteç",
|
||||
"No such user": "Böyle Bir Kullanıcı Yok",
|
||||
"Token is expired, please try again": "Belirtecin Süresi Doldu, Lütfen Tekrar Deneyin",
|
||||
"English": "İngilizce",
|
||||
"English (auto-generated)": "İngilizce (otomatik oluşturuldu)",
|
||||
"English (auto-generated)": "İngilizce (Otomatik Oluşturuldu)",
|
||||
"Afrikaans": "Afrikanca",
|
||||
"Albanian": "Arnavutça",
|
||||
"Amharic": "Amharca",
|
||||
|
@ -230,9 +230,9 @@
|
|||
"German": "Almanca",
|
||||
"Greek": "Yunanca",
|
||||
"Gujarati": "Guceratça",
|
||||
"Haitian Creole": "Haiti Creole dili",
|
||||
"Haitian Creole": "Haiti Creole Dili",
|
||||
"Hausa": "Hausaca",
|
||||
"Hawaiian": "Hawaii dili",
|
||||
"Hawaiian": "Hawaii Dili",
|
||||
"Hebrew": "İbranice",
|
||||
"Hindi": "Hintçe",
|
||||
"Hmong": "Hmong",
|
||||
|
@ -244,7 +244,7 @@
|
|||
"Italian": "İtalyanca",
|
||||
"Japanese": "Japonca",
|
||||
"Javanese": "Cava dili",
|
||||
"Kannada": "Kannada dili",
|
||||
"Kannada": "Kannada Dili",
|
||||
"Kazakh": "Kazakça",
|
||||
"Khmer": "Kmerce",
|
||||
"Korean": "Korece",
|
||||
|
@ -258,10 +258,10 @@
|
|||
"Macedonian": "Makedonca",
|
||||
"Malagasy": "Malgaşça",
|
||||
"Malay": "Malayca",
|
||||
"Malayalam": "Malayalam dili",
|
||||
"Malayalam": "Malayalam Dili",
|
||||
"Maltese": "Maltaca",
|
||||
"Maori": "Maori dili",
|
||||
"Marathi": "Marati dili",
|
||||
"Maori": "Maori Dili",
|
||||
"Marathi": "Marati Dili",
|
||||
"Mongolian": "Moğolca",
|
||||
"Nepali": "Nepalce",
|
||||
"Norwegian Bokmål": "Norveççe Bokmål",
|
||||
|
@ -270,19 +270,19 @@
|
|||
"Persian": "Farsça",
|
||||
"Polish": "Lehçe",
|
||||
"Portuguese": "Portekizce",
|
||||
"Punjabi": "Pencap dili",
|
||||
"Punjabi": "Pencap Dili",
|
||||
"Romanian": "Rumence",
|
||||
"Russian": "Rusça",
|
||||
"Samoan": "Samoa dili",
|
||||
"Samoan": "Samoa Dili",
|
||||
"Scottish Gaelic": "İskoç Galcesi",
|
||||
"Serbian": "Sırpça",
|
||||
"Shona": "Şona dili",
|
||||
"Shona": "Şona Dili",
|
||||
"Sindhi": "Sintçe",
|
||||
"Sinhala": "Seylanca",
|
||||
"Slovak": "Slovakça",
|
||||
"Slovenian": "Slovence",
|
||||
"Somali": "Somalice",
|
||||
"Southern Sotho": "Güney Sotho dili",
|
||||
"Southern Sotho": "Güney Sotho Dili",
|
||||
"Spanish": "İspanyolca",
|
||||
"Spanish (Latin America)": "İspanyolca (Latin Amerika)",
|
||||
"Sundanese": "Sundaca",
|
||||
|
@ -290,7 +290,7 @@
|
|||
"Swedish": "İsveççe",
|
||||
"Tajik": "Tacikçe",
|
||||
"Tamil": "Tamilce",
|
||||
"Telugu": "Telugu dili",
|
||||
"Telugu": "Telugu Dili",
|
||||
"Thai": "Tayca",
|
||||
"Turkish": "Türkçe",
|
||||
"Ukrainian": "Ukraynaca",
|
||||
|
@ -299,178 +299,182 @@
|
|||
"Vietnamese": "Vietnamca",
|
||||
"Welsh": "Galce",
|
||||
"Western Frisian": "Batı Frizcesi",
|
||||
"Xhosa": "Xhosa dili",
|
||||
"Xhosa": "Xhosa Dili",
|
||||
"Yiddish": "Yiddiş",
|
||||
"Yoruba": "Yoruba dili",
|
||||
"Yoruba": "Yoruba Dili",
|
||||
"Zulu": "Zuluca",
|
||||
"Fallback comments: ": "Yedek yorumlar: ",
|
||||
"Fallback comments: ": "Yedek Yorumlar: ",
|
||||
"Popular": "Popüler",
|
||||
"Search": "Ara",
|
||||
"Top": "Enler",
|
||||
"About": "Hakkında",
|
||||
"Rating: ": "Değerlendirme: ",
|
||||
"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ı",
|
||||
"Music": "Müzik",
|
||||
"Gaming": "Oyun",
|
||||
"News": "Haberler",
|
||||
"Movies": "Filmler",
|
||||
"Download": "İndir",
|
||||
"Download as: ": "Şu şekilde indir: ",
|
||||
"Download as: ": "Şu Şekilde İndir: ",
|
||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||
"(edited)": "(düzenlendi)",
|
||||
"YouTube comment permalink": "YouTube yorumu kalıcı linki",
|
||||
"permalink": "kalıcı link",
|
||||
"`x` marked it with a ❤": "`x` ❤ ile işaretledi",
|
||||
"Audio mode": "Ses modu",
|
||||
"Video mode": "Video modu",
|
||||
"Videos": "Videolar",
|
||||
"Playlists": "Oynatma listeleri",
|
||||
"Community": "Topluluk",
|
||||
"(edited)": "(Düzenlendi)",
|
||||
"YouTube comment permalink": "YouTube Yorumu Kalıcı Linki",
|
||||
"permalink": "Kalıcı Link",
|
||||
"`x` marked it with a ❤": "`x` ❤ İle İşaretledi",
|
||||
"Audio mode": "Ses Modu",
|
||||
"Video mode": "Video Modu",
|
||||
"channel_tab_videos_label": "Videolar",
|
||||
"Playlists": "Oynatma Listeleri",
|
||||
"channel_tab_community_label": "Topluluk",
|
||||
"search_filters_sort_option_relevance": "İlgi",
|
||||
"search_filters_sort_option_rating": "Değerlendirme",
|
||||
"search_filters_sort_option_date": "Yükleme tarihi",
|
||||
"search_filters_sort_option_views": "Görüntüleme sayısı",
|
||||
"search_filters_sort_option_date": "Yükleme Tarihi",
|
||||
"search_filters_sort_option_views": "Görüntüleme Sayısı",
|
||||
"search_filters_type_label": "Tür",
|
||||
"search_filters_duration_label": "Süre",
|
||||
"search_filters_features_label": "Özellikler",
|
||||
"search_filters_sort_label": "Sıralama Ölçütü",
|
||||
"search_filters_date_option_hour": "Son Saat",
|
||||
"search_filters_date_option_today": "Bugün",
|
||||
"search_filters_date_option_week": "Bu hafta",
|
||||
"search_filters_date_option_month": "Bu ay",
|
||||
"search_filters_date_option_year": "Bu yıl",
|
||||
"search_filters_date_option_week": "Bu Hafta",
|
||||
"search_filters_date_option_month": "Bu Ay",
|
||||
"search_filters_date_option_year": "Bu Yıl",
|
||||
"search_filters_type_option_video": "Video",
|
||||
"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_show": "Gösteri",
|
||||
"search_filters_features_option_hd": "HD",
|
||||
"search_filters_features_option_subtitles": "Alt yazılar",
|
||||
"search_filters_features_option_c_commons": "Creative Commons",
|
||||
"search_filters_features_option_three_d": "3B",
|
||||
"search_filters_features_option_subtitles": "Alt Yazılar",
|
||||
"search_filters_features_option_c_commons": "Yaratıcı",
|
||||
"search_filters_features_option_three_d": "3D",
|
||||
"search_filters_features_option_live": "Canlı",
|
||||
"search_filters_features_option_four_k": "4K",
|
||||
"search_filters_features_option_location": "Konum",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"Current version: ": "Şu anki sürüm: ",
|
||||
"next_steps_error_message": "Bundan sonra şunları denemelisiniz: ",
|
||||
"Current version: ": "Şu Anki Sürüm: ",
|
||||
"next_steps_error_message": "Bundan Sonra Şunları Denemelisiniz: ",
|
||||
"next_steps_error_message_refresh": "Yenile",
|
||||
"next_steps_error_message_go_to_youtube": "YouTube'a git",
|
||||
"search_filters_duration_option_short": "Kısa (4 dakikadan az)",
|
||||
"search_filters_duration_option_long": "Uzun (20 dakikadan fazla)",
|
||||
"next_steps_error_message_go_to_youtube": "YouTube'a Git",
|
||||
"search_filters_duration_option_short": "Kısa (4 Dakikadan Az)",
|
||||
"search_filters_duration_option_long": "Uzun (20 Dakikadan Fazla)",
|
||||
"footer_documentation": "Belgelendirme",
|
||||
"footer_source_code": "Kaynak kodları",
|
||||
"footer_original_source_code": "Orijinal 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",
|
||||
"footer_donate_page": "Bağış yap",
|
||||
"preferences_region_label": "İçerik ülkesi: ",
|
||||
"preferences_quality_dash_label": "Tercih edilen DASH video kalitesi: ",
|
||||
"footer_source_code": "Kaynak Kodları",
|
||||
"footer_original_source_code": "Orijinal 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",
|
||||
"footer_donate_page": "Bağış Yap",
|
||||
"preferences_region_label": "İçerik Ülkesi: ",
|
||||
"preferences_quality_dash_label": "Tercih Edilen DASH Video Kalitesi: ",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"preferences_quality_dash_option_best": "En iyi",
|
||||
"preferences_quality_dash_option_worst": "En kötü",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
"preferences_quality_dash_option_best": "En İyi",
|
||||
"preferences_quality_dash_option_worst": "En Kötü",
|
||||
"preferences_quality_dash_option_4320p": "4320P",
|
||||
"preferences_quality_dash_option_2160p": "2160P",
|
||||
"preferences_quality_dash_option_480p": "480P",
|
||||
"preferences_quality_dash_option_360p": "360P",
|
||||
"preferences_quality_dash_option_240p": "240P",
|
||||
"preferences_quality_dash_option_144p": "144P",
|
||||
"invidious": "Invidious",
|
||||
"none": "yok",
|
||||
"videoinfo_started_streaming_x_ago": "`x` önce yayına başladı",
|
||||
"videoinfo_youTube_embed_link": "Göm",
|
||||
"videoinfo_invidious_embed_link": "Bağlantıyı Göm",
|
||||
"user_created_playlists": "`x` oluşturulan oynatma listeleri",
|
||||
"user_saved_playlists": "`x` kaydedilen oynatma listeleri",
|
||||
"none": "Yok",
|
||||
"videoinfo_started_streaming_x_ago": "`x` Önce Yayına Başladı",
|
||||
"videoinfo_youTube_embed_link": "Entegre Et",
|
||||
"videoinfo_invidious_embed_link": "Bağlantıyı Entegre Et",
|
||||
"user_created_playlists": "`x` Oluşturulan Oynatma Listeleri",
|
||||
"user_saved_playlists": "`x` Kaydedilen Oynatma Listeleri",
|
||||
"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_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"Video unavailable": "Video kullanılamıyor",
|
||||
"preferences_quality_option_dash": "DASH (uyarlanabilir kalite)",
|
||||
"preferences_quality_dash_option_1440p": "1440P",
|
||||
"preferences_quality_dash_option_1080p": "1080P",
|
||||
"Video unavailable": "Video Kullanılamıyor",
|
||||
"preferences_quality_option_dash": "DASH (Uyarlanabilir Kalite)",
|
||||
"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°",
|
||||
"videoinfo_watch_on_youTube": "YouTube'da izle",
|
||||
"download_subtitles": "Alt yazılar - `x` (.vtt)",
|
||||
"preferences_save_player_pos_label": "Oynatma konumunu kaydet: ",
|
||||
"generic_views_count": "{{count}} görüntüleme",
|
||||
"generic_views_count_plural": "{{count}} görüntüleme",
|
||||
"generic_subscribers_count": "{{count}} abone",
|
||||
"generic_subscribers_count_plural": "{{count}} abone",
|
||||
"generic_subscriptions_count": "{{count}} abonelik",
|
||||
"generic_subscriptions_count_plural": "{{count}} abonelik",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} okunmamış bildirim",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} okunmamış bildirim",
|
||||
"comments_points_count": "{{count}} puan",
|
||||
"comments_points_count_plural": "{{count}} puan",
|
||||
"generic_count_hours": "{{count}} saat",
|
||||
"generic_count_hours_plural": "{{count}} saat",
|
||||
"generic_count_minutes": "{{count}} dakika",
|
||||
"generic_count_minutes_plural": "{{count}} dakika",
|
||||
"generic_count_seconds": "{{count}} saniye",
|
||||
"generic_count_seconds_plural": "{{count}} saniye",
|
||||
"generic_playlists_count": "{{count}} oynatma listesi",
|
||||
"generic_playlists_count_plural": "{{count}} oynatma listesi",
|
||||
"tokens_count": "{{count}} belirteç",
|
||||
"tokens_count_plural": "{{count}} belirteç",
|
||||
"comments_view_x_replies": "{{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_plural": "{{count}} yıl",
|
||||
"generic_count_months": "{{count}} ay",
|
||||
"generic_count_months_plural": "{{count}} ay",
|
||||
"generic_count_days": "{{count}} gün",
|
||||
"generic_count_days_plural": "{{count}} gün",
|
||||
"generic_videos_count": "{{count}} video",
|
||||
"generic_videos_count_plural": "{{count}} video",
|
||||
"generic_count_weeks": "{{count}} hafta",
|
||||
"generic_count_weeks_plural": "{{count}} hafta",
|
||||
"videoinfo_watch_on_youTube": "YouTube'da İzle",
|
||||
"download_subtitles": "Alt Yazılar - `x` (.vtt)",
|
||||
"preferences_save_player_pos_label": "Oynatma Konumunu Kaydet: ",
|
||||
"generic_views_count": "{{count}} Görüntüleme",
|
||||
"generic_views_count_plural": "{{count}} Görüntüleme",
|
||||
"generic_subscribers_count": "{{count}} Abone",
|
||||
"generic_subscribers_count_plural": "{{count}} Abone",
|
||||
"generic_subscriptions_count": "{{count}} Abonelik",
|
||||
"generic_subscriptions_count_plural": "{{count}} Abonelik",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} Okunmamış Bildirim",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} Okunmamış Bildirim",
|
||||
"comments_points_count": "{{count}} Puan",
|
||||
"comments_points_count_plural": "{{count}} Puan",
|
||||
"generic_count_hours": "{{count}} Saat",
|
||||
"generic_count_hours_plural": "{{count}} Saat",
|
||||
"generic_count_minutes": "{{count}} Dakika",
|
||||
"generic_count_minutes_plural": "{{count}} Dakika",
|
||||
"generic_count_seconds": "{{count}} Saniye",
|
||||
"generic_count_seconds_plural": "{{count}} Saniye",
|
||||
"generic_playlists_count": "{{count}} Oynatma Listesi",
|
||||
"generic_playlists_count_plural": "{{count}} Oynatma Listesi",
|
||||
"tokens_count": "{{count}} Belirteç",
|
||||
"tokens_count_plural": "{{count}} Belirteç",
|
||||
"comments_view_x_replies": "{{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_plural": "{{count}} Yıl",
|
||||
"generic_count_months": "{{count}} Ay",
|
||||
"generic_count_months_plural": "{{count}} Ay",
|
||||
"generic_count_days": "{{count}} Gün",
|
||||
"generic_count_days_plural": "{{count}} Gün",
|
||||
"generic_videos_count": "{{count}} Video",
|
||||
"generic_videos_count_plural": "{{count}} Video",
|
||||
"generic_count_weeks": "{{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_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_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_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_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_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_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)",
|
||||
"Chinese": "Çince",
|
||||
"Interlingue": "İnterlingue",
|
||||
"Italian (auto-generated)": "İtalyanca (otomatik oluşturuldu)",
|
||||
"Japanese (auto-generated)": "Japonca (otomatik oluşturuldu)",
|
||||
"Italian (auto-generated)": "İtalyanca (Otomatik Oluşturuldu)",
|
||||
"Japanese (auto-generated)": "Japonca (Otomatik Oluşturuldu)",
|
||||
"Portuguese (Brazil)": "Portekizce (Brezilya)",
|
||||
"Russian (auto-generated)": "Rusça (otomatik oluşturuldu)",
|
||||
"Spanish (auto-generated)": "İspanyolca (otomatik oluşturuldu)",
|
||||
"Russian (auto-generated)": "Rusça (Otomatik Oluşturuldu)",
|
||||
"Spanish (auto-generated)": "İspanyolca (Otomatik Oluşturuldu)",
|
||||
"Spanish (Mexico)": "İspanyolca (Meksika)",
|
||||
"English (United States)": "İngilizce (ABD)",
|
||||
"Cantonese (Hong Kong)": "Kantonca (Hong Kong)",
|
||||
"Chinese (Taiwan)": "Çince (Tayvan)",
|
||||
"Dutch (auto-generated)": "Felemenkçe (otomatik oluşturuldu)",
|
||||
"Indonesian (auto-generated)": "Endonezyaca (otomatik oluşturuldu)",
|
||||
"Dutch (auto-generated)": "Felemenkçe (Otomatik Oluşturuldu)",
|
||||
"Indonesian (auto-generated)": "Endonezyaca (Otomatik Oluşturuldu)",
|
||||
"Chinese (Hong Kong)": "Çince (Hong Kong)",
|
||||
"French (auto-generated)": "Fransızca (otomatik oluşturuldu)",
|
||||
"Korean (auto-generated)": "Korece (otomatik oluşturuldu)",
|
||||
"Turkish (auto-generated)": "Türkçe (otomatik oluşturuldu)",
|
||||
"French (auto-generated)": "Fransızca (Otomatik Oluşturuldu)",
|
||||
"Korean (auto-generated)": "Korece (Otomatik Oluşturuldu)",
|
||||
"Turkish (auto-generated)": "Türkçe (Otomatik Oluşturuldu)",
|
||||
"Chinese (China)": "Çince (Çin)",
|
||||
"German (auto-generated)": "Almanca (otomatik oluşturuldu)",
|
||||
"Portuguese (auto-generated)": "Portekizce (otomatik oluşturuldu)",
|
||||
"German (auto-generated)": "Almanca (Otomatik Oluşturuldu)",
|
||||
"Portuguese (auto-generated)": "Portekizce (Otomatik Oluşturuldu)",
|
||||
"Spanish (Spain)": "İspanyolca (İspanya)",
|
||||
"Vietnamese (auto-generated)": "Vietnamca (otomatik oluşturuldu)",
|
||||
"preferences_watch_history_label": "İzleme geçmişini etkinleştir: ",
|
||||
"Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
|
||||
"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_filters_type_option_all": "Herhangi bir tür",
|
||||
"search_filters_duration_option_none": "Herhangi bir süre",
|
||||
"search_filters_type_option_all": "Herhangi Bir Tür",
|
||||
"search_filters_duration_option_none": "Herhangi Bir Süre",
|
||||
"search_message_no_results": "Sonuç bulunamadı.",
|
||||
"search_filters_date_label": "Yükleme tarihi",
|
||||
"search_filters_apply_button": "Seçili filtreleri uygula",
|
||||
"search_filters_date_option_none": "Herhangi bir tarih",
|
||||
"search_filters_duration_option_medium": "Orta (4 - 20 dakika)",
|
||||
"search_filters_date_label": "Yükleme Tarihi",
|
||||
"search_filters_apply_button": "Seçili Filtreleri Uygula",
|
||||
"search_filters_date_option_none": "Herhangi Bir Tarih",
|
||||
"search_filters_duration_option_medium": "Orta (4 - 20 Dakika)",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_title": "Filtreler",
|
||||
"search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin.",
|
||||
"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>"
|
||||
"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>",
|
||||
"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_autoplay_label": "Автовідтворення наступного відео: ",
|
||||
"preferences_listen_label": "Режим «тільки звук» як усталений: ",
|
||||
"preferences_local_label": "Програвати відео через проксі? ",
|
||||
"preferences_local_label": "Відтворення відео через проксі: ",
|
||||
"preferences_speed_label": "Усталена швидкість відео: ",
|
||||
"preferences_quality_label": "Пріорітетна якість відео: ",
|
||||
"preferences_volume_label": "Гучність відео: ",
|
||||
|
@ -63,13 +63,13 @@
|
|||
"reddit": "Reddit",
|
||||
"preferences_captions_label": "Основна мова субтитрів: ",
|
||||
"Fallback captions: ": "Запасна мова субтитрів: ",
|
||||
"preferences_related_videos_label": "Показувати схожі відео? ",
|
||||
"preferences_annotations_label": "Завжди показувати анотації? ",
|
||||
"preferences_related_videos_label": "Показувати схожі відео: ",
|
||||
"preferences_annotations_label": "Завжди показувати анотації: ",
|
||||
"preferences_category_visual": "Налаштування сайту",
|
||||
"preferences_player_style_label": "Стиль програвача: ",
|
||||
"Dark mode: ": "Темне оформлення: ",
|
||||
"Dark mode: ": "Темний режим: ",
|
||||
"preferences_dark_mode_label": "Тема: ",
|
||||
"dark": "темна",
|
||||
"dark": "Темна",
|
||||
"light": "Світла",
|
||||
"preferences_thin_mode_label": "Полегшене оформлення: ",
|
||||
"preferences_category_subscription": "Налаштування підписок",
|
||||
|
@ -101,11 +101,11 @@
|
|||
"preferences_category_admin": "Адміністраторські налаштування",
|
||||
"preferences_default_home_label": "Усталена домашня сторінка: ",
|
||||
"preferences_feed_menu_label": "Меню потоку з відео: ",
|
||||
"Top enabled: ": "Увімкнути топ відео? ",
|
||||
"CAPTCHA enabled: ": "Увімкнути капчу? ",
|
||||
"Login enabled: ": "Увімкнути авторизацію? ",
|
||||
"Registration enabled: ": "Увімкнути реєстрацію? ",
|
||||
"Report statistics: ": "Повідомляти статистику? ",
|
||||
"Top enabled: ": "Увімкнути топ відео: ",
|
||||
"CAPTCHA enabled: ": "Увімкнути CAPTCHA: ",
|
||||
"Login enabled: ": "Увімкнути вхід: ",
|
||||
"Registration enabled: ": "Увімкнути реєстрацію: ",
|
||||
"Report statistics: ": "Повідомляти статистику: ",
|
||||
"Save preferences": "Зберегти налаштування",
|
||||
"Subscription manager": "Менеджер підписок",
|
||||
"Token manager": "Менеджер токенів",
|
||||
|
@ -125,7 +125,7 @@
|
|||
"Private": "Особистий",
|
||||
"View all playlists": "Переглянути всі списки відтворення",
|
||||
"Updated `x` ago": "Оновлено `x` тому",
|
||||
"Delete playlist `x`?": "Видалити список відтворення \"x\"?",
|
||||
"Delete playlist `x`?": "Видалити список відтворення `x`?",
|
||||
"Delete playlist": "Видалити список відтворення",
|
||||
"Create playlist": "Створити список відтворення",
|
||||
"Title": "Заголовок",
|
||||
|
@ -315,9 +315,9 @@
|
|||
"`x` marked it with a ❤": "❤ цьому від каналу `x`",
|
||||
"Audio mode": "Аудіорежим",
|
||||
"Video mode": "Відеорежим",
|
||||
"Videos": "Відео",
|
||||
"channel_tab_videos_label": "Відео",
|
||||
"Playlists": "Плейлисти",
|
||||
"Community": "Спільнота",
|
||||
"channel_tab_community_label": "Спільнота",
|
||||
"Current version: ": "Поточна версія: ",
|
||||
"generic_views_count_0": "{{count}} перегляд",
|
||||
"generic_views_count_1": "{{count}} перегляди",
|
||||
|
@ -386,12 +386,12 @@
|
|||
"Spanish (Mexico)": "Іспанська (Мексика)",
|
||||
"Spanish (Spain)": "Іспанська (Іспанія)",
|
||||
"next_steps_error_message_go_to_youtube": "Перейти до YouTube",
|
||||
"footer_donate_page": "Пожертвувати",
|
||||
"footer_donate_page": "Підтримати",
|
||||
"footer_documentation": "Документація",
|
||||
"footer_source_code": "Вихідний код",
|
||||
"footer_original_source_code": "Оригінал вихідного коду",
|
||||
"footer_modfied_source_code": "Змінений вихідний код",
|
||||
"adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого вихідного коду",
|
||||
"footer_source_code": "Джерельний код",
|
||||
"footer_original_source_code": "Оригінал джерельного коду",
|
||||
"footer_modfied_source_code": "Змінений джерельний код",
|
||||
"adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого джерельного коду",
|
||||
"none": "нема",
|
||||
"videoinfo_started_streaming_x_ago": "Трансляцію розпочато `x` тому",
|
||||
"crash_page_you_found_a_bug": "Схоже, ви знайшли ваду в Invidious!",
|
||||
|
@ -408,7 +408,7 @@
|
|||
"next_steps_error_message": "Після чого спробуйте: ",
|
||||
"next_steps_error_message_refresh": "Оновити сторінку",
|
||||
"Search": "Пошук",
|
||||
"preferences_extend_desc_label": "Автоматично розширювати опис відео: ",
|
||||
"preferences_extend_desc_label": "Автоматично розгортати опис відео: ",
|
||||
"preferences_category_misc": "Різноманітні параметри",
|
||||
"Show less": "Коротше",
|
||||
"preferences_quality_option_small": "Низька",
|
||||
|
@ -488,5 +488,9 @@
|
|||
"search_filters_sort_option_rating": "Рейтингові",
|
||||
"search_filters_sort_option_views": "Популярні",
|
||||
"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 ❤",
|
||||
"Audio mode": "Chế độ âm thanh",
|
||||
"Video mode": "Chế độ quay",
|
||||
"Videos": "Video",
|
||||
"channel_tab_videos_label": "Video",
|
||||
"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_rating": "Xếp hạng",
|
||||
"search_filters_sort_option_date": "ngày",
|
||||
|
|
|
@ -341,9 +341,9 @@
|
|||
"`x` marked it with a ❤": "`x` 为此加 ❤",
|
||||
"Audio mode": "音频模式",
|
||||
"Video mode": "视频模式",
|
||||
"Videos": "视频",
|
||||
"channel_tab_videos_label": "视频",
|
||||
"Playlists": "播放列表",
|
||||
"Community": "社区",
|
||||
"channel_tab_community_label": "社区",
|
||||
"search_filters_sort_option_relevance": "相关度",
|
||||
"search_filters_sort_option_rating": "评分",
|
||||
"search_filters_sort_option_date": "上传日期",
|
||||
|
|
|
@ -341,9 +341,9 @@
|
|||
"`x` marked it with a ❤": "`x` 為此標記 ❤",
|
||||
"Audio mode": "音訊模式",
|
||||
"Video mode": "視訊模式",
|
||||
"Videos": "影片",
|
||||
"channel_tab_videos_label": "影片",
|
||||
"Playlists": "播放清單",
|
||||
"Community": "社群",
|
||||
"channel_tab_community_label": "社群",
|
||||
"search_filters_sort_option_relevance": "關聯",
|
||||
"search_filters_sort_option_rating": "評分",
|
||||
"search_filters_sort_option_date": "日期",
|
||||
|
@ -456,5 +456,9 @@
|
|||
"search_filters_type_option_all": "任何類型",
|
||||
"search_filters_date_option_none": "任何日期",
|
||||
"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": "直播"
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ dependencies_to_install.each do |dep|
|
|||
dep = "videojs.markers" if dep == "videojs-markers"
|
||||
|
||||
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`
|
||||
else
|
||||
`mv #{download_path}/package/dist/#{dep}.css #{dest_path}/#{dep}.css`
|
||||
|
|
|
@ -34,7 +34,7 @@ shards:
|
|||
|
||||
protodec:
|
||||
git: https://github.com/iv-org/protodec.git
|
||||
version: 0.1.4
|
||||
version: 0.1.5
|
||||
|
||||
radix:
|
||||
git: https://github.com/luislavena/radix.git
|
||||
|
|
|
@ -24,7 +24,7 @@ dependencies:
|
|||
version: ~> 0.6.1
|
||||
protodec:
|
||||
github: iv-org/protodec
|
||||
version: ~> 0.1.4
|
||||
version: ~> 0.1.5
|
||||
lsquic:
|
||||
github: iv-org/lsquic.cr
|
||||
version: ~> 2.18.1-2
|
||||
|
|
|
@ -4,7 +4,7 @@ Spectator.describe Invidious::Hashtag do
|
|||
it "parses richItemRenderer containers (test 1)" do
|
||||
# Enable mock
|
||||
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(videos.size).to eq(60)
|
||||
|
@ -57,7 +57,7 @@ Spectator.describe Invidious::Hashtag do
|
|||
it "parses richItemRenderer containers (test 2)" do
|
||||
# Enable mock
|
||||
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(videos.size).to eq(60)
|
||||
|
|
|
@ -23,12 +23,6 @@ Spectator.describe "Helper" do
|
|||
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
|
||||
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")
|
||||
|
|
|
@ -34,6 +34,7 @@ require "protodec/utils"
|
|||
|
||||
require "./invidious/database/*"
|
||||
require "./invidious/database/migrations/*"
|
||||
require "./invidious/http_server/*"
|
||||
require "./invidious/helpers/*"
|
||||
require "./invidious/yt_backend/*"
|
||||
require "./invidious/frontend/*"
|
||||
|
@ -48,6 +49,13 @@ require "./invidious/search/*"
|
|||
require "./invidious/routes/**"
|
||||
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
|
||||
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)
|
||||
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::ClearExpiredItemsJob.new
|
||||
|
|
|
@ -16,12 +16,6 @@ record AboutChannel,
|
|||
tabs : Array(String),
|
||||
verified : Bool
|
||||
|
||||
record AboutRelatedChannel,
|
||||
ucid : String,
|
||||
author : String,
|
||||
author_url : String,
|
||||
author_thumbnail : String
|
||||
|
||||
def get_about_info(ucid, locale) : AboutChannel
|
||||
begin
|
||||
# "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
|
||||
joined = Time.unix(0)
|
||||
|
||||
tabs = [] of String
|
||||
tab_names = [] of String
|
||||
|
||||
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
|
||||
if !tabs_json.nil?
|
||||
# Retrieve information from the tabs array. The index we are looking for varies between channels.
|
||||
tabs_json.each do |node|
|
||||
# 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 tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?
|
||||
# Get the name of the tabs available on this channel
|
||||
tab_names = tabs_json.as_a.compact_map do |entry|
|
||||
name = entry.dig?("tabRenderer", "title").try &.as_s.downcase
|
||||
|
||||
if !channel_about_meta.nil?
|
||||
total_views = channel_about_meta["viewCountText"]?.try &.["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 = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, nd| acc + nd["text"].as_s }
|
||||
.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"]
|
||||
if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) &&
|
||||
(channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube"
|
||||
auto_generated = true
|
||||
end
|
||||
end
|
||||
# This is a small fix to not add extra code on the HTML side
|
||||
# I.e, the URL for the "live" tab is .../streams, so use "streams"
|
||||
# everywhere for the sake of simplicity
|
||||
(name == "live") ? "streams" : name
|
||||
end
|
||||
|
||||
# Get the currently active tab ("About")
|
||||
about_tab = extract_selected_tab(tabs_json)
|
||||
|
||||
# Try to find the about metadata section
|
||||
channel_about_meta = about_tab.dig?(
|
||||
"content",
|
||||
"sectionListRenderer", "contents", 0,
|
||||
"itemSectionRenderer", "contents", 0,
|
||||
"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
|
||||
tabs = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map(&.["tabRenderer"]["title"].as_s.downcase)
|
||||
end
|
||||
|
||||
sub_count = initdata
|
||||
|
@ -148,46 +154,20 @@ def get_about_info(ucid, locale) : AboutChannel
|
|||
joined: joined,
|
||||
is_family_friendly: is_family_friendly,
|
||||
allowed_regions: allowed_regions,
|
||||
tabs: tabs,
|
||||
tabs: tab_names,
|
||||
verified: author_verified || false,
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_related_channels(about_channel : AboutChannel) : Array(AboutRelatedChannel)
|
||||
# params is {"2:string":"channels"} encoded
|
||||
channels = YoutubeAPI.browse(browse_id: about_channel.ucid, params: "EghjaGFubmVscw%3D%3D")
|
||||
|
||||
tabs = channels.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs").try(&.as_a?) || [] of JSON::Any
|
||||
tab = tabs.find(&.dig?("tabRenderer", "title").try(&.as_s?).try(&.== "Channels"))
|
||||
|
||||
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,
|
||||
)
|
||||
def fetch_related_channels(about_channel : AboutChannel, continuation : String? = nil) : {Array(SearchChannel), String?}
|
||||
if continuation.nil?
|
||||
# params is {"2:string":"channels"} encoded
|
||||
initial_data = YoutubeAPI.browse(browse_id: about_channel.ucid, params: "EghjaGFubmVscw%3D%3D")
|
||||
else
|
||||
initial_data = YoutubeAPI.browse(continuation)
|
||||
end
|
||||
|
||||
return related
|
||||
items, continuation = extract_items(initial_data)
|
||||
|
||||
return items.select(SearchChannel), continuation
|
||||
end
|
||||
|
|
|
@ -180,11 +180,16 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
|||
|
||||
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")
|
||||
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
|
||||
videos = extract_videos(initial_data, author, ucid)
|
||||
videos, continuation = IV::Channel::Tabs.get_videos(channel)
|
||||
|
||||
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed")
|
||||
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 ||= 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 ||= 0
|
||||
|
@ -228,58 +235,56 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
|||
|
||||
if was_insert
|
||||
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
|
||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
||||
end
|
||||
end
|
||||
|
||||
if pull_all_videos
|
||||
page += 1
|
||||
|
||||
ids = [] of String
|
||||
|
||||
loop do
|
||||
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
|
||||
videos = extract_videos(initial_data, author, ucid)
|
||||
# Keep fetching videos using the continuation token retrieved earlier
|
||||
videos, continuation = IV::Channel::Tabs.get_videos(channel, continuation: continuation)
|
||||
|
||||
count = videos.size
|
||||
videos = videos.map { |video| ChannelVideo.new({
|
||||
id: video.id,
|
||||
title: video.title,
|
||||
published: video.published,
|
||||
updated: Time.utc,
|
||||
ucid: video.ucid,
|
||||
author: video.author,
|
||||
length_seconds: video.length_seconds,
|
||||
live_now: video.live_now,
|
||||
premiere_timestamp: video.premiere_timestamp,
|
||||
views: video.views,
|
||||
}) }
|
||||
|
||||
videos.each do |video|
|
||||
ids << video.id
|
||||
count = 0
|
||||
videos.select(SearchVideo).each do |video|
|
||||
count += 1
|
||||
video = ChannelVideo.new({
|
||||
id: video.id,
|
||||
title: video.title,
|
||||
published: video.published,
|
||||
updated: Time.utc,
|
||||
ucid: video.ucid,
|
||||
author: video.author,
|
||||
length_seconds: video.length_seconds,
|
||||
live_now: video.live_now,
|
||||
premiere_timestamp: video.premiere_timestamp,
|
||||
views: video.views,
|
||||
})
|
||||
|
||||
# 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.
|
||||
if Time.utc - video.published > 1.minute
|
||||
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
|
||||
|
||||
break if count < 25
|
||||
page += 1
|
||||
sleep 500.milliseconds
|
||||
end
|
||||
end
|
||||
|
||||
channel = InvidiousChannel.new({
|
||||
id: ucid,
|
||||
author: author,
|
||||
updated: Time.utc,
|
||||
deleted: false,
|
||||
subscribed: nil,
|
||||
})
|
||||
|
||||
channel.updated = Time.utc
|
||||
return channel
|
||||
end
|
||||
|
|
|
@ -69,7 +69,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
|||
next if !post
|
||||
|
||||
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.field "author", author
|
||||
|
@ -189,6 +189,32 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
|||
# when .has_key?("pollRenderer")
|
||||
# attachment = attachment["pollRenderer"]
|
||||
# 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
|
||||
json.field "type", "unknown"
|
||||
json.field "error", "Unrecognized attachment type."
|
||||
|
|
|
@ -1,93 +1,28 @@
|
|||
def fetch_channel_playlists(ucid, author, continuation, sort_by)
|
||||
if continuation
|
||||
response_json = 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
|
||||
initial_data = YoutubeAPI.browse(continuation)
|
||||
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
|
||||
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"]?
|
||||
initial_data = YoutubeAPI.browse(ucid, params: params || "")
|
||||
end
|
||||
|
||||
return items, continuation
|
||||
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"
|
||||
return extract_items(initial_data, author, ucid)
|
||||
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| 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 = {
|
||||
"110: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,
|
||||
"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
|
||||
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
|
||||
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)
|
||||
return "/browse_ajax?continuation=#{continuation}&gl=JP&hl=en"
|
||||
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
|
||||
# Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
|
||||
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
|
||||
# Will display in the footer, next to the main source code link
|
||||
|
|
|
@ -154,6 +154,16 @@ module Invidious::Database::Users
|
|||
# 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)
|
||||
request = <<-SQL
|
||||
UPDATE users
|
||||
|
|
|
@ -33,3 +33,8 @@ end
|
|||
|
||||
class VideoNotAvailableException < Exception
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config)
|
||||
|
||||
return extract_items(response)
|
||||
items, _ = extract_items(response)
|
||||
return items
|
||||
end
|
||||
|
||||
def generate_continuation(hashtag : String, cursor : Int)
|
||||
|
|
|
@ -20,7 +20,7 @@ module JSONFilter
|
|||
/^\(|\(\(|\/\(/
|
||||
end
|
||||
|
||||
def self.parse_fields(fields_text : String) : Nil
|
||||
def self.parse_fields(fields_text : String, &) : Nil
|
||||
if fields_text.empty?
|
||||
raise FieldsParser::ParseError.new "Fields is empty"
|
||||
end
|
||||
|
@ -42,7 +42,7 @@ module JSONFilter
|
|||
parse_nest_groups(fields_text) { |nest_list| yield nest_list }
|
||||
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)
|
||||
|
||||
if !single_nests.empty?
|
||||
|
@ -60,7 +60,7 @@ module JSONFilter
|
|||
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)
|
||||
bracket_pairs = get_bracket_pairs(fields_text, true)
|
||||
|
||||
|
|
|
@ -265,4 +265,11 @@ class Category
|
|||
end
|
||||
end
|
||||
|
||||
struct Continuation
|
||||
getter token
|
||||
|
||||
def initialize(@token : String)
|
||||
end
|
||||
end
|
||||
|
||||
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category
|
||||
|
|
|
@ -162,7 +162,7 @@ def number_with_separator(number)
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
case matches.try &.["suffix"].downcase
|
||||
|
@ -259,7 +259,7 @@ def get_referer(env, fallback = "/", unroll = true)
|
|||
end
|
||||
|
||||
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
|
||||
referer = fallback
|
||||
|
|
|
@ -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
|
||||
private getter connection_channel : Channel({Bool, Channel(PQ::Notification)})
|
||||
private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)})
|
||||
private getter pg_url : URI
|
||||
|
||||
def initialize(@connection_channel, @pg_url)
|
||||
end
|
||||
|
||||
def begin
|
||||
connections = [] of Channel(PQ::Notification)
|
||||
connections = [] of ::Channel(PQ::Notification)
|
||||
|
||||
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
|
||||
lim_fibers = max_fibers
|
||||
active_fibers = 0
|
||||
active_channel = Channel(Bool).new
|
||||
active_channel = ::Channel(Bool).new
|
||||
backoff = 2.minutes
|
||||
|
||||
loop do
|
||||
|
|
|
@ -7,7 +7,7 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
|
|||
def begin
|
||||
max_fibers = CONFIG.feed_threads
|
||||
active_fibers = 0
|
||||
active_channel = Channel(Bool).new
|
||||
active_channel = ::Channel(Bool).new
|
||||
|
||||
loop do
|
||||
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
|
||||
|
||||
active_fibers = 0
|
||||
active_channel = Channel(Bool).new
|
||||
active_channel = ::Channel(Bool).new
|
||||
|
||||
loop do
|
||||
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
|
||||
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.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)
|
||||
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 "type", fmt["mimeType"]
|
||||
json.field "clen", fmt["contentLength"]? || "-1"
|
||||
|
|
|
@ -203,7 +203,7 @@ module Invidious::Routes::Account
|
|||
referer = get_referer(env)
|
||||
|
||||
if !user
|
||||
return env.redirect referer
|
||||
return env.redirect "/login?referer=#{URI.encode_path_segment(env.request.resource)}"
|
||||
end
|
||||
|
||||
user = user.as(User)
|
||||
|
|
|
@ -29,7 +29,7 @@ module Invidious::Routes::API::Manifest
|
|||
|
||||
if local
|
||||
uri = URI.parse(url)
|
||||
url = "#{uri.request_target}host/#{uri.host}/"
|
||||
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
|
||||
end
|
||||
|
||||
"<BaseURL>#{url}</BaseURL>"
|
||||
|
@ -42,7 +42,7 @@ module Invidious::Routes::API::Manifest
|
|||
|
||||
if local
|
||||
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
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
module Invidious::Routes::API::V1::Channels
|
||||
def self.home(env)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
ucid = env.params.url["ucid"]
|
||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||
sort_by ||= "newest"
|
||||
|
||||
# Macro to avoid duplicating some code below
|
||||
# This sets the `channel` variable, or handles Exceptions.
|
||||
private macro get_channel
|
||||
begin
|
||||
channel = get_about_info(ucid, locale)
|
||||
rescue ex : ChannelRedirect
|
||||
|
@ -18,17 +12,25 @@ module Invidious::Routes::API::V1::Channels
|
|||
rescue ex
|
||||
return error_json(500, ex)
|
||||
end
|
||||
end
|
||||
|
||||
page = 1
|
||||
if channel.auto_generated
|
||||
videos = [] of SearchVideo
|
||||
count = 0
|
||||
else
|
||||
begin
|
||||
count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
|
||||
rescue ex
|
||||
return error_json(500, ex)
|
||||
end
|
||||
def self.home(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 "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
|
||||
|
||||
JSON.build do |json|
|
||||
|
@ -100,31 +102,13 @@ module Invidious::Routes::API::V1::Channels
|
|||
json.array do
|
||||
# Fetch related channels
|
||||
begin
|
||||
related_channels = fetch_related_channels(channel)
|
||||
related_channels, _ = fetch_related_channels(channel)
|
||||
rescue ex
|
||||
related_channels = [] of AboutRelatedChannel
|
||||
related_channels = [] of SearchChannel
|
||||
end
|
||||
|
||||
related_channels.each do |related_channel|
|
||||
json.object do
|
||||
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
|
||||
related_channel.to_json(locale, json)
|
||||
end
|
||||
end
|
||||
end # relatedChannels
|
||||
|
@ -134,61 +118,112 @@ module Invidious::Routes::API::V1::Channels
|
|||
end
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
return self.videos(env)
|
||||
end
|
||||
|
||||
def self.videos(env)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
ucid = env.params.url["ucid"]
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
ucid = env.params.url["ucid"]
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
sort_by = env.params.query["sort"]?.try &.downcase
|
||||
sort_by ||= env.params.query["sort_by"]?.try &.downcase
|
||||
sort_by ||= "newest"
|
||||
# Use the private macro defined above.
|
||||
channel = nil # Make the compiler happy
|
||||
get_channel()
|
||||
|
||||
# Retrieve some URL parameters
|
||||
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
|
||||
continuation = env.params.query["continuation"]?
|
||||
|
||||
begin
|
||||
channel = get_about_info(ucid, locale)
|
||||
rescue ex : ChannelRedirect
|
||||
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)
|
||||
videos, next_continuation = Channel::Tabs.get_60_videos(
|
||||
channel, continuation: continuation, sort_by: sort_by
|
||||
)
|
||||
rescue ex
|
||||
return error_json(500, ex)
|
||||
end
|
||||
|
||||
begin
|
||||
count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
|
||||
rescue ex
|
||||
return error_json(500, ex)
|
||||
end
|
||||
|
||||
JSON.build do |json|
|
||||
json.array do
|
||||
videos.each do |video|
|
||||
video.to_json(locale, json)
|
||||
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.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
|
||||
|
@ -204,16 +239,9 @@ module Invidious::Routes::API::V1::Channels
|
|||
env.params.query["sort_by"]?.try &.downcase ||
|
||||
"last"
|
||||
|
||||
begin
|
||||
channel = get_about_info(ucid, locale)
|
||||
rescue ex : ChannelRedirect
|
||||
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
|
||||
# Use the macro defined above
|
||||
channel = nil # Make the compiler happy
|
||||
get_channel()
|
||||
|
||||
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
|
||||
|
||||
|
@ -255,6 +283,37 @@ module Invidious::Routes::API::V1::Channels
|
|||
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)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
region = env.params.query["region"]?
|
||||
|
|
|
@ -6,6 +6,7 @@ module Invidious::Routes::API::V1::Videos
|
|||
|
||||
id = env.params.url["id"]
|
||||
region = env.params.query["region"]?
|
||||
proxy = {"1", "true"}.any? &.== env.params.query["local"]?
|
||||
|
||||
begin
|
||||
video = get_video(id, region: region)
|
||||
|
@ -15,7 +16,9 @@ module Invidious::Routes::API::V1::Videos
|
|||
return error_json(500, ex)
|
||||
end
|
||||
|
||||
video.to_json(locale, nil)
|
||||
return JSON.build do |json|
|
||||
Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
if caption.name.includes? "auto-generated"
|
||||
caption_xml = YT_POOL.client &.get(url).body
|
||||
caption_xml = XML.parse(caption_xml)
|
||||
|
||||
webvtt = String.build do |str|
|
||||
str << <<-END_VTT
|
||||
WEBVTT
|
||||
Kind: captions
|
||||
Language: #{tlang || caption.language_code}
|
||||
if caption_xml.starts_with?("<?xml")
|
||||
webvtt = caption.timedtext_to_vtt(caption_xml, tlang)
|
||||
else
|
||||
caption_xml = XML.parse(caption_xml)
|
||||
|
||||
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.each_with_index do |node, i|
|
||||
start_time = node["start"].to_f.seconds
|
||||
duration = node["dur"]?.try &.to_f.seconds
|
||||
duration ||= start_time
|
||||
caption_nodes = caption_xml.xpath_nodes("//transcript/text")
|
||||
caption_nodes.each_with_index do |node, i|
|
||||
start_time = node["start"].to_f.seconds
|
||||
duration = node["dur"]?.try &.to_f.seconds
|
||||
duration ||= start_time
|
||||
|
||||
if caption_nodes.size > i + 1
|
||||
end_time = caption_nodes[i + 1]["start"].to_f.seconds
|
||||
else
|
||||
end_time = start_time + duration
|
||||
if caption_nodes.size > i + 1
|
||||
end_time = caption_nodes[i + 1]["start"].to_f.seconds
|
||||
else
|
||||
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
|
||||
|
||||
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
|
||||
else
|
||||
|
@ -138,7 +146,12 @@ module Invidious::Routes::API::V1::Videos
|
|||
#
|
||||
# See: https://github.com/iv-org/invidious/issues/2391
|
||||
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
|
||||
|
||||
if title = env.params.query["title"]?
|
||||
|
|
|
@ -7,21 +7,19 @@ module Invidious::Routes::Channels
|
|||
|
||||
def self.videos(env)
|
||||
data = self.fetch_basic_information(env)
|
||||
if !data.is_a?(Tuple)
|
||||
return data
|
||||
end
|
||||
locale, user, subscriptions, continuation, ucid, channel = data
|
||||
return data if !data.is_a?(Tuple)
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i?
|
||||
page ||= 1
|
||||
locale, user, subscriptions, continuation, ucid, channel = data
|
||||
|
||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||
|
||||
if channel.auto_generated
|
||||
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|
|
||||
if item.responds_to?(:title)
|
||||
item.title
|
||||
|
@ -33,34 +31,85 @@ module Invidious::Routes::Channels
|
|||
items.each(&.author = "")
|
||||
else
|
||||
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
|
||||
|
||||
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"
|
||||
end
|
||||
|
||||
def self.playlists(env)
|
||||
data = self.fetch_basic_information(env)
|
||||
if !data.is_a?(Tuple)
|
||||
return data
|
||||
end
|
||||
return data if !data.is_a?(Tuple)
|
||||
|
||||
locale, user, subscriptions, continuation, ucid, channel = data
|
||||
|
||||
sort_options = {"last", "oldest", "newest"}
|
||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||
sort_by ||= "last"
|
||||
|
||||
if channel.auto_generated
|
||||
return env.redirect "/channel/#{channel.ucid}"
|
||||
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.each(&.author = "")
|
||||
|
||||
templated "playlists"
|
||||
selected_tab = Frontend::ChannelPage::TabsAvailable::Playlists
|
||||
templated "channel"
|
||||
end
|
||||
|
||||
def self.community(env)
|
||||
|
@ -74,12 +123,15 @@ module Invidious::Routes::Channels
|
|||
thin_mode = thin_mode == "true"
|
||||
|
||||
continuation = env.params.query["continuation"]?
|
||||
# sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||
|
||||
if !channel.tabs.includes? "community"
|
||||
return env.redirect "/channel/#{channel.ucid}"
|
||||
end
|
||||
|
||||
# TODO: support sort options for community posts
|
||||
sort_by = ""
|
||||
sort_options = [] of String
|
||||
|
||||
begin
|
||||
items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode))
|
||||
rescue ex : InfoException
|
||||
|
@ -95,6 +147,26 @@ module Invidious::Routes::Channels
|
|||
templated "community"
|
||||
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)
|
||||
data = self.fetch_basic_information(env)
|
||||
if !data.is_a?(Tuple)
|
||||
|
@ -125,7 +197,7 @@ module Invidious::Routes::Channels
|
|||
end
|
||||
|
||||
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}"
|
||||
else
|
||||
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)
|
||||
# end
|
||||
|
||||
if notifications && notifications.includes? id
|
||||
if CONFIG.enable_user_notifications && notifications && notifications.includes? id
|
||||
Invidious::Database::Users.remove_notification(user.as(User), id)
|
||||
env.get("user").as(User).notifications.delete(id)
|
||||
notifications.delete(id)
|
||||
|
|
|
@ -96,12 +96,14 @@ module Invidious::Routes::Feeds
|
|||
|
||||
videos, notifications = get_subscription_feed(user, max_results, page)
|
||||
|
||||
# "updated" here is used for delivering new notifications, so if
|
||||
# we know a user has looked at their feed e.g. in the past 10 minutes,
|
||||
# they've already seen a video posted 20 minutes ago, and don't need
|
||||
# to be notified.
|
||||
Invidious::Database::Users.clear_notifications(user)
|
||||
user.notifications = [] of String
|
||||
if CONFIG.enable_user_notifications
|
||||
# "updated" here is used for delivering new notifications, so if
|
||||
# we know a user has looked at their feed e.g. in the past 10 minutes,
|
||||
# they've already seen a video posted 20 minutes ago, and don't need
|
||||
# to be notified.
|
||||
Invidious::Database::Users.clear_notifications(user)
|
||||
user.notifications = [] of String
|
||||
end
|
||||
env.set "user", user
|
||||
|
||||
templated "feeds/subscriptions"
|
||||
|
@ -404,13 +406,15 @@ module Invidious::Routes::Feeds
|
|||
|
||||
video = get_video(id, force_refresh: true)
|
||||
|
||||
# Deliver notifications to `/api/v1/auth/notifications`
|
||||
payload = {
|
||||
"topic" => video.ucid,
|
||||
"videoId" => video.id,
|
||||
"published" => published.to_unix,
|
||||
}.to_json
|
||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
||||
if CONFIG.enable_user_notifications
|
||||
# Deliver notifications to `/api/v1/auth/notifications`
|
||||
payload = {
|
||||
"topic" => video.ucid,
|
||||
"videoId" => video.id,
|
||||
"published" => published.to_unix,
|
||||
}.to_json
|
||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
||||
end
|
||||
|
||||
video = ChannelVideo.new({
|
||||
id: id,
|
||||
|
@ -426,7 +430,13 @@ module Invidious::Routes::Feeds
|
|||
})
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -6,14 +6,14 @@ module Invidious::Routes::Login
|
|||
|
||||
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
|
||||
return error_template(400, "Login has been disabled by administrator.")
|
||||
end
|
||||
|
||||
referer = get_referer(env, "/feed/subscriptions")
|
||||
|
||||
email = nil
|
||||
password = nil
|
||||
captcha = nil
|
||||
|
|
|
@ -35,6 +35,13 @@ module Invidious::Routes::VideoPlayback
|
|||
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)
|
||||
response = HTTP::Client::Response.new(500)
|
||||
error = ""
|
||||
|
@ -70,6 +77,9 @@ module Invidious::Routes::VideoPlayback
|
|||
end
|
||||
end
|
||||
|
||||
# Remove the Range header added previously.
|
||||
headers.delete("Range") if range_header.nil?
|
||||
|
||||
if response.status_code >= 400
|
||||
env.response.content_type = "text/plain"
|
||||
haltf env, response.status_code
|
||||
|
@ -91,14 +101,8 @@ module Invidious::Routes::VideoPlayback
|
|||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
if location = resp.headers["Location"]?
|
||||
location = URI.parse(location)
|
||||
location = "#{location.request_target}&host=#{location.host}"
|
||||
|
||||
if region
|
||||
location += "®ion=#{region}"
|
||||
end
|
||||
|
||||
return env.redirect location
|
||||
url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
|
||||
return env.redirect url
|
||||
end
|
||||
|
||||
IO.copy(resp.body_io, env.response)
|
||||
|
|
|
@ -80,7 +80,7 @@ module Invidious::Routes::Watch
|
|||
Invidious::Database::Users.mark_watched(user.as(User), id)
|
||||
end
|
||||
|
||||
if notifications && notifications.includes? id
|
||||
if CONFIG.enable_user_notifications && notifications && notifications.includes? id
|
||||
Invidious::Database::Users.remove_notification(user.as(User), id)
|
||||
env.get("user").as(User).notifications.delete(id)
|
||||
notifications.delete(id)
|
||||
|
|
|
@ -37,7 +37,9 @@ module Invidious::Routing
|
|||
get "/feed/webhook/:token", Routes::Feeds, :push_notifications_get
|
||||
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 %}
|
||||
|
||||
self.register_image_routes
|
||||
|
@ -115,18 +117,23 @@ module Invidious::Routing
|
|||
get "/channel/:ucid", Routes::Channels, :home
|
||||
get "/channel/:ucid/home", Routes::Channels, :home
|
||||
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/community", Routes::Channels, :community
|
||||
get "/channel/:ucid/channels", Routes::Channels, :channels
|
||||
get "/channel/:ucid/about", Routes::Channels, :about
|
||||
get "/channel/:ucid/live", Routes::Channels, :live
|
||||
get "/user/: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
|
||||
get "/c/:user#{path}", Routes::Channels, :brand_redirect
|
||||
# /user/linustechtips | Not always the same as /c/
|
||||
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
|
||||
get "/attribution_link#{path}", Routes::Channels, :brand_redirect
|
||||
# /profile?user=linustechtips
|
||||
|
@ -220,6 +227,10 @@ module Invidious::Routing
|
|||
|
||||
# Channels
|
||||
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"} %}
|
||||
get "/api/v1/channels/#{{{route}}}/:ucid", {{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/unregister", {{namespace}}::Authenticated, :unregister_token
|
||||
|
||||
get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
|
||||
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
|
||||
if CONFIG.enable_user_notifications
|
||||
get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
|
||||
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
|
||||
end
|
||||
|
||||
# Misc
|
||||
get "/api/v1/stats", {{namespace}}::Misc, :stats
|
||||
|
|
|
@ -9,7 +9,8 @@ module Invidious::Search
|
|||
client_config = YoutubeAPI::ClientConfig.new(region: query.region)
|
||||
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
|
||||
|
||||
# Search a youtube channel
|
||||
|
@ -30,16 +31,7 @@ module Invidious::Search
|
|||
continuation = produce_channel_search_continuation(ucid, query.text, query.page)
|
||||
response_json = YoutubeAPI.browse(continuation)
|
||||
|
||||
continuation_items = response_json["onResponseReceivedActions"]?
|
||||
.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
|
||||
|
||||
items, _ = extract_items(response_json, "", ucid)
|
||||
return items
|
||||
end
|
||||
|
||||
|
|
|
@ -247,6 +247,12 @@ struct Video
|
|||
info["reason"]?.try &.as_s
|
||||
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
|
||||
|
||||
private macro getset_string(name)
|
||||
|
|
|
@ -31,6 +31,72 @@ module Invidious::Videos
|
|||
return captions_list
|
||||
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.
|
||||
LANGUAGES = {
|
||||
"",
|
||||
|
|
|
@ -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 ||= player_response.dig("playabilityStatus", "reason").as_s
|
||||
|
||||
# Stop here if video is not a scheduled livestream
|
||||
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
|
||||
# Stop here if video is not a scheduled livestream or
|
||||
# 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 {
|
||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||
"reason" => JSON::Any.new(reason),
|
||||
|
@ -309,6 +311,33 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
|||
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 = 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 || ""),
|
||||
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""),
|
||||
"license" => JSON::Any.new(license.try &.as_s || ""),
|
||||
# Music section
|
||||
"music" => JSON.parse(music_list.to_json),
|
||||
# Author infos
|
||||
"author" => JSON::Any.new(author || ""),
|
||||
"ucid" => JSON::Any.new(ucid || ""),
|
||||
|
|
|
@ -1,8 +1,24 @@
|
|||
<% ucid = channel.ucid %>
|
||||
<% author = HTML.escape(channel.author) %>
|
||||
<% channel_profile_pic = URI.parse(channel.author_thumbnail).request_target %>
|
||||
<%-
|
||||
ucid = channel.ucid
|
||||
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 %>
|
||||
<%- if selected_tab.videos? -%>
|
||||
<meta name="description" content="<%= channel.description %>">
|
||||
<meta property="og:site_name" content="Invidious">
|
||||
<meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
|
||||
|
@ -14,91 +30,14 @@
|
|||
<meta name="twitter:title" content="<%= author %>">
|
||||
<meta name="twitter:description" content="<%= channel.description %>">
|
||||
<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 %>" />
|
||||
<%- end -%>
|
||||
|
||||
<link rel="alternate" href="<%= youtube_url %>">
|
||||
<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<%= channel_profile_pic %>">
|
||||
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <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>
|
||||
<%= rendered "components/channel_info" %>
|
||||
|
||||
<div class="h-box">
|
||||
<hr>
|
||||
|
@ -111,17 +50,10 @@
|
|||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-lg-1-5">
|
||||
<% 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-md-4-5"></div>
|
||||
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
|
||||
<% if count == 60 %>
|
||||
<a href="/channel/<%= ucid %>?page=<%= page + 1 %><% if sort_by != "newest" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>">
|
||||
<% if next_continuation %>
|
||||
<a href="<%= relative_url %>?continuation=<%= next_continuation %><% if sort_options.any? sort_by %>&sort_by=<%= sort_by %><% end %>">
|
||||
<%= translate(locale, "Next page") %>
|
||||
</a>
|
||||
<% 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 %>
|
||||
<link rel="alternate" href="<%= youtube_url %>">
|
||||
<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 %> <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>
|
||||
<%= rendered "components/channel_info" %>
|
||||
|
||||
<div class="h-box">
|
||||
<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 %> <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>
|
||||
|
||||
<% if CONFIG.enable_user_notifications %>
|
||||
|
||||
<center>
|
||||
<%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %>
|
||||
</center>
|
||||
|
@ -39,6 +41,8 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<hr>
|
||||
</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 %> <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">
|
||||
<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 %>
|
||||
<% 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>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-notifications-outline"></i>
|
||||
|
@ -175,7 +175,9 @@
|
|||
}.to_pretty_json
|
||||
%>
|
||||
</script>
|
||||
<% if CONFIG.enable_user_notifications %>
|
||||
<script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<div class="pure-control-group">
|
||||
<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") %>
|
||||
</a>
|
||||
</label>
|
||||
|
|
|
@ -244,6 +244,7 @@
|
|||
<input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<% if CONFIG.enable_user_notifications %>
|
||||
<div class="pure-control-group">
|
||||
<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 %>>
|
||||
|
@ -255,6 +256,7 @@
|
|||
<a href="#" data-onclick="notification_requestPermission"><%= translate(locale, "Enable web notifications") %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %>
|
||||
|
|
変更されたファイルが多すぎるため、一部のファイルは表示されません さらに表示
読み込み中…
新しいイシューから参照