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

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

ファイルの表示

@ -490,8 +490,9 @@ hr {
}
/* Description Expansion Styling*/
#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

ファイルの表示

@ -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",

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

@ -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 sekziston në këtë luajlistë. <a href=\"`x`\">Klikoni këtu për faqen hyrëse të luajlistës.</a>",
"search_message_use_another_instance": " Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.",
"search_filters_date_label": "Datë ngarkimi",
"preferences_watch_history_label": "Aktivizo historik parjesh: ",
"Top enabled: ": "Me kryesueset të aktivizuara: ",
"preferences_video_loop_label": "Përsërite gjithmonë: ",
"search_message_no_results": "Su gjetën përfundime.",
"Could not pull trending pages.": "Su morën dot faqet në modë.",
"search_filters_date_option_none": "Çfarëdo date",
"search_message_change_filters_or_query": "Provoni të zgjeroni kërkesën tuaj të kërkimit dhe/ose të ndryshoni filtrat.",
"search_filters_type_option_all": "Çfarëdo lloji",
"search_filters_duration_option_none": "Çfarëdo kohëzgjatjeje",
"search_filters_duration_option_medium": "Mesatare (4 - 20 minuta)",
"search_filters_features_option_vr180": "VR180",
"search_filters_apply_button": "Apliko filtrat e përzgjedhur"
}

ファイルの表示

@ -257,7 +257,7 @@
"preferences_volume_label": "Jačina zvuka: ",
"preferences_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",

ファイルの表示

@ -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 ı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 ı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": "ı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 ıklamaları gizle",
"Show annotations": "Ek ıklamaları göster",
"Hide annotations": "Ek ıklamaları Gizle",
"Show annotations": "Ek ı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": "直播"
}

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

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

@ -129,7 +129,7 @@ dependencies_to_install.each do |dep|
dep = "videojs.markers" if dep == "videojs-markers"
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`

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

ファイルの表示

@ -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

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

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

ファイルの表示

@ -8,7 +8,8 @@ module Invidious::Hashtag
client_config = YoutubeAPI::ClientConfig.new(region: region)
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

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

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

ファイルの表示

@ -1,12 +1,12 @@
class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
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 += "&region=#{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 = {
"",

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

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

ファイルの表示

@ -66,8 +66,10 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
reason ||= 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 %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3">
<h3 style="text-align:right">
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1-3">
<a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
<div class="pure-u-1 pure-md-1-3">
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% else %>
<a href="https://redirect.invidious.io<%= env.request.path %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% end %>
</div>
<% if !channel.auto_generated %>
<div class="pure-u-1 pure-md-1-3">
<b><%= translate(locale, "Videos") %></b>
</div>
<% end %>
<div class="pure-u-1 pure-md-1-3">
<% if channel.auto_generated %>
<b><%= translate(locale, "Playlists") %></b>
<% else %>
<a href="/channel/<%= ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
<% end %>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if channel.tabs.includes? "community" %>
<a href="/channel/<%= ucid %>/community"><%= translate(locale, "Community") %></a>
<% end %>
</div>
</div>
<div class="pure-u-1-3"></div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
<% sort_options.each do |sort| %>
<div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %>
<b><%= translate(locale, sort) %></b>
<% else %>
<a href="/channel/<%= ucid %>?page=<%= page %>&sort_by=<%= sort %>">
<%= translate(locale, sort) %>
</a>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<%= 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 %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3" style="text-align:right">
<h3 style="text-align:right">
<a href="/feed/channel/<%= channel.ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1-3">
<a href="https://www.youtube.com/channel/<%= channel.ucid %>/community"><%= translate(locale, "View channel on YouTube") %></a>
<div class="pure-u-1 pure-md-1-3">
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% else %>
<a href="https://redirect.invidious.io<%= env.request.resource %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% end %>
</div>
<% if !channel.auto_generated %>
<div class="pure-u-1 pure-md-1-3">
<a href="/channel/<%= channel.ucid %>"><%= translate(locale, "Videos") %></a>
</div>
<% end %>
<div class="pure-u-1 pure-md-1-3">
<a href="/channel/<%= channel.ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if channel.tabs.includes? "community" %>
<b><%= translate(locale, "Community") %></b>
<% end %>
</div>
</div>
<div class="pure-u-2-3"></div>
</div>
<%= 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 %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3">
<h3 style="text-align:right">
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1-2">
<div class="pure-u-1 pure-md-1-3">
<a href="<%= youtube_url %>"><%= translate(locale, "View channel on YouTube") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<a href="<%= redirect_url %>"><%= translate(locale, "Switch Invidious Instance") %></a>
</div>
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
</div>
<div class="pure-u-1-2">
<div class="pure-g" style="text-align:end">
<% sort_options.each do |sort| %>
<div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %>
<b><%= translate(locale, sort) %></b>
<% else %>
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= translate(locale, sort) %></a>
<% end %>
</div>
<% end %>
</div>
</div>
</div>

ファイルの表示

@ -23,6 +23,8 @@
</div>
</div>
<% 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 %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3" style="text-align:right">
<h3 style="text-align:right">
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<div class="h-box">
<div id="descriptionWrapper">
<p><span style="white-space:pre-wrap"><%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content if !channel.description_html.empty? %></span></p>
</div>
</div>
<div class="h-box">
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>
<div class="pure-g h-box">
<div class="pure-g pure-u-1-3">
<div class="pure-u-1 pure-md-1-3">
<a href="https://www.youtube.com/channel/<%= ucid %>/playlists"><%= translate(locale, "View channel on YouTube") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% else %>
<a href="https://redirect.invidious.io<%= env.request.resource %>"><%= translate(locale, "Switch Invidious Instance") %></a>
<% end %>
</div>
<div class="pure-u-1 pure-md-1-3">
<a href="/channel/<%= ucid %>"><%= translate(locale, "Videos") %></a>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if !channel.auto_generated %>
<b><%= translate(locale, "Playlists") %></b>
<% end %>
</div>
<div class="pure-u-1 pure-md-1-3">
<% if channel.tabs.includes? "community" %>
<a href="/channel/<%= ucid %>/community"><%= translate(locale, "Community") %></a>
<% end %>
</div>
</div>
<div class="pure-u-1-3"></div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
<% {"last", "oldest", "newest"}.each do |sort| %>
<div class="pure-u-1 pure-md-1-3">
<% if sort_by == sort %>
<b><%= translate(locale, sort) %></b>
<% else %>
<a href="/channel/<%= ucid %>/playlists?sort_by=<%= sort %>">
<%= translate(locale, sort) %>
</a>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<div class="h-box">
<hr>
</div>
<div class="pure-g">
<% items.each do |item| %>
<%= rendered "components/item" %>
<% end %>
</div>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-4-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
<% if continuation %>
<a href="/channel/<%= ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>">
<%= translate(locale, "Next page") %>
</a>
<% end %>
</div>
</div>

ファイルの表示

@ -55,7 +55,7 @@
<div class="pure-u-1-4">
<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 %>

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