diff --git a/.ameba.yml b/.ameba.yml index 247705e8..96cbc8f0 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -77,10 +77,6 @@ Metrics/CyclomaticComplexity: # process_video_params(query, preferences) => [20/10] - src/invidious/videos.cr - # produce_search_params(page, sort, ...) => [29/10] - # process_search_query(query, page, ...) => [14/10] - - src/invidious/search.cr - #src/invidious/playlists.cr:327:5 diff --git a/assets/css/default.css b/assets/css/default.css index 8b2b3578..49069c92 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -15,6 +15,11 @@ body { background-color: rgb(255, 0, 0, 0.5); } +.underlined { + border-bottom: 1px solid; + margin-bottom: 20px; +} + .channel-profile > * { font-size: 1.17em; font-weight: bold; @@ -475,30 +480,6 @@ body.dark-theme { } } -#filters { - display: inline; - margin-top: 15px; -} - -#filters > div { - display: inline-block; -} - -#filters > summary { - display: block; - margin-bottom: 15px; -} - -#filters > summary::before { - content: "[ + ]"; - font-size: 1.5em; -} - -#filters[open] > summary::before { - content: "[ - ]"; - font-size: 1.5em; -} - /*With commit d9528f5 all contents of the page is now within a flexbox. However, the hr element is rendered improperly within one. See https://stackoverflow.com/a/34372979 for more info */ diff --git a/assets/css/search.css b/assets/css/search.css new file mode 100644 index 00000000..a5996362 --- /dev/null +++ b/assets/css/search.css @@ -0,0 +1,117 @@ +summary { + /* This should hide the marker */ + display: block; + + font-size: 1.17em; + font-weight: bold; + margin: 0 auto 10px auto; +} + +summary::-webkit-details-marker, +summary::marker { display: none; } + +summary:before { + border-radius: 5px; + content: "[ + ]"; + margin: -2px 10px 0 10px; + padding: 1px 0 3px 0; + text-align: center; + width: 40px; +} + +details[open] > summary:before { content: "[ ‒ ]"; } + + +#filters-box { + padding: 10px 20px 20px 10px; + margin: 10px 15px; +} +#filters-flex { + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-items: flex-start; + align-content: flex-start; + justify-content: flex-start; +} + + +fieldset, legend { + display: contents !important; + border: none !important; + margin: 0 !important; + padding: 0 !important; +} + + +.filter-column { + display: inline-block; + display: inline-flex; + width: max-content; + min-width: max-content; + max-width: 16em; + margin: 15px; + flex-grow: 2; + flex-basis: auto; + flex-direction: column; +} +.filter-name, .filter-options { + display: block; + padding: 5px 10px; + margin: 0; + text-align: start; +} + +.filter-options div { margin: 6px 0; } +.filter-options div * { vertical-align: middle; } +.filter-options label { margin: 0 10px; } + + +#filters-apply { text-align: end; } + +/* Error message */ + +.no-results-error { + text-align: center; + line-height: 180%; + font-size: 110%; + padding: 15px 15px 125px 15px; +} + +/* Responsive rules */ + +@media only screen and (max-width: 800px) { + summary { font-size: 1.30em; } + #filters-box { + margin: 10px 0 0 0; + padding: 0; + } + #filters-apply { + text-align: center; + padding: 15px; + } +} + +/* Light theme */ + +.light-theme #filters-box { + background: #dfdfdf; +} + +@media (prefers-color-scheme: light) { + .no-theme #filters-box { + background: #dfdfdf; + } +} + +/* Dark theme */ + +.dark-theme #filters-box { + background: #373737; +} + +@media (prefers-color-scheme: dark) { + .no-theme #filters-box { + background: #373737; + } +} diff --git a/locales/ar.json b/locales/ar.json index 10ef200b..73b01a48 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -329,39 +329,39 @@ "Videos": "الفيديوهات", "Playlists": "قوائم التشغيل", "Community": "المجتمع", - "relevance": "ملاؤم", - "rating": "تقييم", - "date": "التاريخ", - "views": "مشاهدات", - "content_type": "نوع المحتوى", - "duration": "المدة الزمنية", - "features": "الميزات", - "sort": "فرز", - "hour": "آخر ساعة", - "today": "اليوم", - "week": "هذا الأسبوع", - "month": "هذا الشهر", - "year": "هذه السنة", - "video": "فيديو", - "channel": "قناة", - "playlist": "قائمة التشغيل", - "movie": "فيلم", - "show": "عرض", - "hd": "عالية الدقة", - "subtitles": "ترجمات", - "creative_commons": "المشاع الإبداعي", - "3d": "ثلاثي الأبعاد", - "live": "مباشر", - "4k": "4k", - "location": "الأماكن", - "hdr": "وضع التباين العالي", - "filter": "معامل الفرز", + "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_features_label": "الميزات", + "search_filters_sort_label": "فرز", + "search_filters_date_option_hour": "آخر ساعة", + "search_filters_date_option_today": "اليوم", + "search_filters_date_option_week": "هذا الأسبوع", + "search_filters_date_option_month": "هذا الشهر", + "search_filters_date_option_year": "هذه السنة", + "search_filters_type_option_video": "فيديو", + "search_filters_type_option_channel": "قناة", + "search_filters_type_option_playlist": "قائمة التشغيل", + "search_filters_type_option_movie": "فيلم", + "search_filters_type_option_show": "عرض", + "search_filters_features_option_hd": "عالية الدقة", + "search_filters_features_option_subtitles": "ترجمات", + "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_hdr": "وضع التباين العالي", + "search_filters_label": "معامل الفرز", "Current version: ": "الإصدار الحالي: ", "next_steps_error_message": "بعد ذلك يجب أن تحاول: ", "next_steps_error_message_refresh": "تحديث", "next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب", - "short": "قصير (< 4 دقائق)", - "long": "طويل (> 20 دقيقة)", + "search_filters_duration_option_short": "قصير (< 4 دقائق)", + "search_filters_duration_option_long": "طويل (> 20 دقيقة)", "footer_source_code": "شفرة المصدر", "footer_original_source_code": "كود المصدر الأصلي", "footer_modfied_source_code": "شفرة المصدر المعدلة", @@ -386,7 +386,7 @@ "preferences_quality_dash_option_360p": "360p", "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", - "purchased": "تم شراؤها", + "search_filters_features_option_purchased": "تم شراؤها", "none": "لاشيء", "videoinfo_started_streaming_x_ago": "بدأ البث منذ `x`", "videoinfo_watch_on_youTube": "مشاهدة على يوتيوب", @@ -395,7 +395,7 @@ "user_created_playlists": "'x' إنشاء قوائم التشغيل", "user_saved_playlists": "قوائم التشغيل المحفوظة 'x'", "Video unavailable": "الفيديو غير متوفر", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "download_subtitles": "ترجمات - 'x' (.vtt)", "invidious": "الخيالي", "preferences_save_player_pos_label": "حفظ موضع التشغيل: ", diff --git a/locales/ca.json b/locales/ca.json index 1fa7cc1f..741414d2 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -52,16 +52,16 @@ "Download": "Descarrega", "Download as: ": "Descarrega com: ", "Videos": "Vídeos", - "content_type": "Tipus", - "duration": "Duració", - "sort": "Ordena per", - "week": "Aquesta setmana", - "month": "Aquest mes", - "year": "Aquest any", - "video": "Vídeo", - "channel": "Canal", - "short": "Curt (< 4 minuts)", - "long": "Llarg (> 20 minuts)", + "search_filters_type_label": "Tipus", + "search_filters_duration_label": "Duració", + "search_filters_sort_label": "Ordena per", + "search_filters_date_option_week": "Aquesta setmana", + "search_filters_date_option_month": "Aquest mes", + "search_filters_date_option_year": "Aquest any", + "search_filters_type_option_video": "Vídeo", + "search_filters_type_option_channel": "Canal", + "search_filters_duration_option_short": "Curt (< 4 minuts)", + "search_filters_duration_option_long": "Llarg (> 20 minuts)", "Current version: ": "Versió actual: ", "Malay": "Malai", "Persian": "Persa", @@ -93,11 +93,11 @@ "Spanish": "Castellà", "Vietnamese": "Vietnamita", "News": "Notícies", - "show": "Mostra", + "search_filters_type_option_show": "Mostra", "footer_documentation": "Documentació", "Thai": "Tailandès", "Music": "Música", - "relevance": "Rellevància", - "hour": "Última hora", - "today": "Avui" + "search_filters_sort_option_relevance": "Rellevància", + "search_filters_date_option_hour": "Última hora", + "search_filters_date_option_today": "Avui" } diff --git a/locales/cs.json b/locales/cs.json index 0dbfe14f..f8af17d2 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -262,29 +262,29 @@ "Video mode": "Videový režim", "Videos": "Videa", "Community": "Komunita", - "rating": "Hodnocení", - "date": "Datum zveřejnění", - "views": "Počet zhlédnutí", - "duration": "Délka", - "hour": "Před hodinou", - "today": "Dnes", - "week": "Tento týden", - "month": "Tento měsíc", - "year": "Tento rok", - "video": "Video", - "channel": "Kanál", - "playlist": "Playlist", - "movie": "Film", - "show": "Show", - "hd": "HD", - "subtitles": "Titulky", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Živě", - "4k": "4K", - "location": "Umístění", - "hdr": "HDR", - "filter": "Filtr", + "search_filters_sort_option_rating": "hodnocení", + "search_filters_sort_option_date": "datum", + "search_filters_sort_option_views": "zhlédnutí", + "search_filters_duration_label": "délka", + "search_filters_date_option_hour": "hodina", + "search_filters_date_option_today": "dnes", + "search_filters_date_option_week": "týden", + "search_filters_date_option_month": "měsíc", + "search_filters_date_option_year": "rok", + "search_filters_type_option_video": "video", + "search_filters_type_option_channel": "kanál", + "search_filters_type_option_playlist": "playlist", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "zobrazit", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "titulky", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "živě", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "umístění", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Filtr", "generic_count_days_0": "{{count}} dnem", "generic_count_days_1": "{{count}} dny", "generic_count_days_2": "{{count}} dny", diff --git a/locales/da.json b/locales/da.json index 92e4e9f9..c0678283 100644 --- a/locales/da.json +++ b/locales/da.json @@ -202,7 +202,7 @@ "Hidden field \"challenge\" is a required field": "Det skjulte felt \"challenge\" er et påkrævet felt", "Albanian": "Albansk", "preferences_quality_dash_label": "Fortrukket DASH video kvalitet: ", - "live": "Direkte", + "search_filters_features_option_live": "Direkte", "Lao": "Lao-tse", "Filipino": "Filippinsk", "Greek": "Græsk", @@ -213,23 +213,23 @@ "preferences_locale_label": "Sprog: ", "News": "Nyheder", "permalink": "permalink", - "date": "Upload dato", - "features": "Funktioner", - "filter": "Filter", + "search_filters_sort_option_date": "Upload dato", + "search_filters_features_label": "Funktioner", + "search_filters_label": "Filter", "Khmer": "Khmer", "Finnish": "Finsk", - "week": "Denne uge", + "search_filters_date_option_week": "Denne uge", "Korean": "Koreansk", "Telugu": "Telugu", "Malayalam": "Malayalam", "View as playlist": "Se som spilleliste", "Hungarian": "Ungarsk", "Welsh": "Walisisk", - "subtitles": "Undertekster/CC", + "search_filters_features_option_subtitles": "Undertekster/CC", "Bosnian": "Bosnisk", "Yiddish": "Jiddisch", "Belarusian": "Belarussisk", - "today": "I dag", + "search_filters_date_option_today": "I dag", "Shona": "Shona", "Slovenian": "Slovensk", "Gaming": "Gaming", @@ -248,33 +248,33 @@ "footer_modfied_source_code": "Modificeret Kildekode", "Released under the AGPLv3 on Github.": "Udgivet under AGPLv3 på Github.", "Tajik": "Tadsjikisk", - "month": "Denne måned", + "search_filters_date_option_month": "Denne måned", "Hebrew": "Hebraisk", "Kannada": "Kannada", "Current version: ": "Nuværende version: ", "Amharic": "Amharisk", "Swedish": "Svensk", "Corsican": "Korsikansk", - "movie": "Film", + "search_filters_type_option_movie": "Film", "Could not pull trending pages.": "Kunne ikke hente trending sider.", "English": "Engelsk", - "hd": "HD", + "search_filters_features_option_hd": "HD", "Hausa": "Islandsk", - "year": "Dette år", + "search_filters_date_option_year": "Dette år", "Japanese": "Japansk", - "content_type": "Type", + "search_filters_type_label": "Type", "Icelandic": "Islandsk", "Basque": "Baskisk", - "rating": "Bedømmelse", + "search_filters_sort_option_rating": "Bedømmelse", "Yoruba": "Yoruba", "Erroneous token": "Fejlagtig token", "Videos": "Videoer", - "show": "Vis", + "search_filters_type_option_show": "Vis", "Luxembourgish": "Luxemboursk", "Vietnamese": "Vietnamesisk", "Latvian": "Lettisk", "Indonesian": "Indonesisk", - "duration": "Varighed", + "search_filters_duration_label": "Varighed", "footer_original_source_code": "Original kildekode", "Search": "Søg", "Serbian": "Serbisk", @@ -289,8 +289,8 @@ "Rating: ": "Bedømmelse: ", "Movies": "Film", "YouTube comment permalink": "Youtube kommentarer permalink", - "location": "Lokation", - "hdr": "HDR", + "search_filters_features_option_location": "Lokation", + "search_filters_features_option_hdr": "HDR", "Cebuano": "Cebuano (Sugbuanon)", "Nyanja": "Nyanja", "Chinese (Simplified)": "Kinesisk (forenklet)", @@ -306,11 +306,11 @@ "German": "Tysk", "Maori": "Maori", "Slovak": "Slovakisk", - "relevance": "Relevans", - "hour": "Sidste time", - "playlist": "Spilleliste", - "long": "Lang (> 20 minutter)", - "creative_commons": "Creative Commons", + "search_filters_sort_option_relevance": "Relevans", + "search_filters_date_option_hour": "Sidste time", + "search_filters_type_option_playlist": "Spilleliste", + "search_filters_duration_option_long": "Lang (> 20 minutter)", + "search_filters_features_option_c_commons": "Creative Commons", "Marathi": "Marathi", "Sindhi": "Sindhi", "preferences_category_misc": "Diverse indstillinger", @@ -327,8 +327,8 @@ "Western Frisian": "Vestfrisisk", "Top": "Top", "Music": "Musik", - "views": "Antal visninger", - "sort": "Sorter efter", + "search_filters_sort_option_views": "Antal visninger", + "search_filters_sort_label": "Sorter efter", "Zulu": "Zulu", "Invidious Private Feed for `x`": "Invidious Privat Feed til `x`", "English (auto-generated)": "Engelsk (autogenereret)", @@ -359,16 +359,16 @@ "Scottish Gaelic": "Skotsk Gælisk", "Default": "Standard", "Video mode": "Videotilstand", - "short": "Kort (< 4 minutter)", + "search_filters_duration_option_short": "Kort (< 4 minutter)", "Hidden field \"token\" is a required field": "Det skjulte felt \"token\" er et påkrævet felt", "Azerbaijani": "Aserbajdsjansk", "Georgian": "Georgisk", "Italian": "Italiensk", "Audio mode": "Lydtilstand", - "video": "Video", - "channel": "Kanal", - "3d": "3D", - "4k": "4K", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_four_k": "4K", "Hmong": "Hmong", "preferences_quality_option_medium": "Medium", "preferences_quality_option_small": "Lille", @@ -381,8 +381,8 @@ "preferences_quality_dash_option_360p": "360p", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", - "purchased": "Købt", - "360": "360°", + "search_filters_features_option_purchased": "Købt", + "search_filters_features_option_three_sixty": "360°", "none": "ingen", "videoinfo_started_streaming_x_ago": "Streamen blev startet for `x`siden", "videoinfo_watch_on_youTube": "Se på YouTube", diff --git a/locales/de.json b/locales/de.json index 665810a4..d1b3af23 100644 --- a/locales/de.json +++ b/locales/de.json @@ -329,45 +329,45 @@ "Videos": "Videos", "Playlists": "Wiedergabelisten", "Community": "Gemeinschaft", - "relevance": "Relevanz", - "rating": "Bewertung", - "date": "Datum", - "views": "Aufrufe", - "content_type": "Inhaltstyp", - "duration": "Dauer", - "features": "Eigenschaften", - "sort": "sortieren", - "hour": "Letzte Stunde", - "today": "Heute", - "week": "Diese Woche", - "month": "Diesen Monat", - "year": "Dieses Jahr", - "video": "Video", - "channel": "Kanal", - "playlist": "Wiedergabeliste", - "movie": "Film", - "show": "anzeigen", - "hd": "HD", - "subtitles": "Untertitel / CC", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Live", - "4k": "4K", - "location": "Standort", - "hdr": "HDR", - "filter": "Filtern", + "search_filters_sort_option_relevance": "Relevanz", + "search_filters_sort_option_rating": "Bewertung", + "search_filters_sort_option_date": "Datum", + "search_filters_sort_option_views": "Aufrufe", + "search_filters_type_label": "Inhaltstyp", + "search_filters_duration_label": "Dauer", + "search_filters_features_label": "Eigenschaften", + "search_filters_sort_label": "sortieren", + "search_filters_date_option_hour": "Letzte Stunde", + "search_filters_date_option_today": "Heute", + "search_filters_date_option_week": "Diese Woche", + "search_filters_date_option_month": "Diesen Monat", + "search_filters_date_option_year": "Dieses Jahr", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "search_filters_type_option_playlist": "Wiedergabeliste", + "search_filters_type_option_movie": "Film", + "search_filters_type_option_show": "Anzeigen", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Untertitel / CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Standort", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Filtern", "Current version: ": "Aktuelle Version: ", "next_steps_error_message": "Danach folgendes versuchen: ", "next_steps_error_message_refresh": "Aktualisieren", "next_steps_error_message_go_to_youtube": "Zu YouTube gehen", "footer_donate_page": "Spende", - "long": "Lang (> 20 Minuten)", + "search_filters_duration_option_long": "Lang (> 20 Minuten)", "footer_original_source_code": "Original Quellcode", "footer_modfied_source_code": "Modifizierter Quellcode", "footer_documentation": "Dokumentation", "footer_source_code": "Quellcode", "adminprefs_modified_source_code_url_label": "URL zum Repositorie des modifizierten Quellcodes", - "short": "Kurz (< 4 Minuten)", + "search_filters_duration_option_short": "Kurz (< 4 Minuten)", "preferences_region_label": "Land der Inhalte: ", "preferences_quality_option_dash": "DASH (automatische Qualität)", "preferences_quality_option_hd720": "HD720", @@ -389,12 +389,12 @@ "user_created_playlists": "`x` Wiedergabelisten erstellt", "user_saved_playlists": "`x` Wiedergabelisten gespeichert", "preferences_save_player_pos_label": "Aktuelle Position im Video speichern: ", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "preferences_quality_dash_option_best": "Höchste", "preferences_quality_dash_option_worst": "Niedrigste", "preferences_quality_dash_option_1440p": "1440p", "videoinfo_youTube_embed_link": "Eingebettet", - "purchased": "Gekauft", + "search_filters_features_option_purchased": "Gekauft", "none": "keine", "videoinfo_started_streaming_x_ago": "Stream begann vor `x`", "videoinfo_watch_on_youTube": "Auf YouTube ansehen", diff --git a/locales/el.json b/locales/el.json index 24e42153..52e83a1e 100644 --- a/locales/el.json +++ b/locales/el.json @@ -373,51 +373,51 @@ "preferences_region_label": "Χώρα περιεχομένου: ", "preferences_category_misc": "Διάφορες προτιμήσεις", "Show more": "Εμφάνιση περισσότερων", - "today": "Σήμερα", - "360": "360°", + "search_filters_date_option_today": "Σήμερα", + "search_filters_features_option_three_sixty": "360°", "videoinfo_started_streaming_x_ago": "Ξεκίνησε η ροή `x` πριν από", "videoinfo_watch_on_youTube": "Παρακολουθήστε στο YouTube", "download_subtitles": "Υπότιτλοι - `x` (.vtt)", "user_created_playlists": "`x` δημιουργημένες λίστες αναπαραγωγής", "user_saved_playlists": "`x` αποθηκευμένες λίστες αναπαραγωγής", - "rating": "Αξιολόγηση", - "relevance": "Συνάφεια", - "purchased": "Αγορασμένο", - "date": "Ημερομηνία μεταφόρτωσης", - "content_type": "Τύπος", - "duration": "Διάρκεια", - "week": "Αυτή την εβδομάδα", - "year": "Φέτος", - "channel": "Κανάλι", - "playlist": "Λίστα αναπαραγωγής", - "long": "Μεγάλο (> 20 λεπτά)", - "hd": "HD", - "location": "Τοποθεσία", - "3d": "3D", + "search_filters_sort_option_rating": "Αξιολόγηση", + "search_filters_sort_option_relevance": "Συνάφεια", + "search_filters_features_option_purchased": "Αγορασμένο", + "search_filters_sort_option_date": "Ημερομηνία μεταφόρτωσης", + "search_filters_type_label": "Τύπος", + "search_filters_duration_label": "Διάρκεια", + "search_filters_date_option_week": "Αυτή την εβδομάδα", + "search_filters_date_option_year": "Φέτος", + "search_filters_type_option_channel": "Κανάλι", + "search_filters_type_option_playlist": "Λίστα αναπαραγωγής", + "search_filters_duration_option_long": "Μεγάλο (> 20 λεπτά)", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_location": "Τοποθεσία", + "search_filters_features_option_three_d": "3D", "next_steps_error_message": "Μετά από αυτό θα πρέπει να προσπαθήσετε να: ", "next_steps_error_message_go_to_youtube": "Μεταβείτε στο YouTube", "footer_donate_page": "Δωρεά", "footer_original_source_code": "Πρωτότυπος πηγαίος κώδικας", "preferences_show_nick_label": "Εμφάνιση ψευδώνυμου στην κορυφή: ", - "hour": "Τελευταία ώρα", + "search_filters_date_option_hour": "Τελευταία ώρα", "adminprefs_modified_source_code_url_label": "URL σε αποθετήριο τροποποιημένου πηγαίου κώδικα", - "subtitles": "Υπότιτλοι/CC", - "month": "Αυτόν τον μήνα", + "search_filters_features_option_subtitles": "Υπότιτλοι/CC", + "search_filters_date_option_month": "Αυτόν τον μήνα", "Released under the AGPLv3 on Github.": "Κυκλοφορεί υπό την AGPLv3 στο Github.", - "sort": "Ταξινόμηση κατά", - "filter": "Φίλτρο", - "movie": "Ταινία", + "search_filters_sort_label": "Ταξινόμηση κατά", + "search_filters_label": "Φίλτρο", + "search_filters_type_option_movie": "Ταινία", "footer_modfied_source_code": "Τροποποιημένος πηγαίος κώδικας", - "features": "Χαρακτηριστικά", - "4k": "4K", + "search_filters_features_label": "Χαρακτηριστικά", + "search_filters_features_option_four_k": "4K", "footer_documentation": "Τεκμηρίωση", - "short": "Σύντομο (< 4 λεπτά)", + "search_filters_duration_option_short": "Σύντομο (< 4 λεπτά)", "next_steps_error_message_refresh": "Ανανέωση", - "video": "Βίντεο", - "live": "Ζωντανά", - "creative_commons": "Creative Commons", + "search_filters_type_option_video": "Βίντεο", + "search_filters_features_option_live": "Ζωντανά", + "search_filters_features_option_c_commons": "Creative Commons", "Search": "Αναζήτηση", - "hdr": "HDR", + "search_filters_features_option_hdr": "HDR", "preferences_extend_desc_label": "Αυτόματη επέκταση της περιγραφής του βίντεο: ", "preferences_vr_mode_label": "Διαδραστικά βίντεο 360 μοιρών (απαιτεί WebGL): ", "Show less": "Εμφάνιση λιγότερων", @@ -448,6 +448,6 @@ "none": "κανένα", "videoinfo_youTube_embed_link": "Ενσωμάτωση", "videoinfo_invidious_embed_link": "Σύνδεσμος Ενσωμάτωσης", - "show": "Μπάρα προόδου διαβάσματος", + "search_filters_type_option_show": "Μπάρα προόδου διαβάσματος", "preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: " } diff --git a/locales/en-US.json b/locales/en-US.json index a78d8062..58098929 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -175,7 +175,9 @@ "Show less": "Show less", "Watch on YouTube": "Watch on YouTube", "Switch Invidious Instance": "Switch Invidious Instance", - "Broken? Try another Invidious Instance": "Broken? Try another Invidious Instance", + "search_message_no_results": "No results found.", + "search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.", + "search_message_use_another_instance": " You can also search on another instance.", "Hide annotations": "Hide annotations", "Show annotations": "Show annotations", "Genre: ": "Genre: ", @@ -404,37 +406,44 @@ "Videos": "Videos", "Playlists": "Playlists", "Community": "Community", - "relevance": "Relevance", - "rating": "Rating", - "date": "Upload date", - "views": "View count", - "content_type": "Type", - "duration": "Duration", - "features": "Features", - "sort": "Sort By", - "hour": "Last Hour", - "today": "Today", - "week": "This week", - "month": "This month", - "year": "This year", - "video": "Video", - "channel": "Channel", - "playlist": "Playlist", - "movie": "Movie", - "show": "Show", - "short": "Short (< 4 minutes)", - "long": "Long (> 20 minutes)", - "hd": "HD", - "subtitles": "Subtitles/CC", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Live", - "4k": "4K", - "location": "Location", - "hdr": "HDR", - "purchased": "Purchased", - "360": "360°", - "filter": "Filter", + "search_filters_title": "Filters", + "search_filters_date_label": "Upload date", + "search_filters_date_option_none": "Any date", + "search_filters_date_option_hour": "Last Hour", + "search_filters_date_option_today": "Today", + "search_filters_date_option_week": "This week", + "search_filters_date_option_month": "This month", + "search_filters_date_option_year": "This year", + "search_filters_type_label": "Type", + "search_filters_type_option_all": "Any type", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Channel", + "search_filters_type_option_playlist": "Playlist", + "search_filters_type_option_movie": "Movie", + "search_filters_type_option_show": "Show", + "search_filters_duration_label": "Duration", + "search_filters_duration_option_none": "Any duration", + "search_filters_duration_option_short": "Short (< 4 minutes)", + "search_filters_duration_option_medium": "Medium (4 - 20 minutes)", + "search_filters_duration_option_long": "Long (> 20 minutes)", + "search_filters_features_label": "Features", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Subtitles/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_sixty": "360°", + "search_filters_features_option_vr180": "VR180", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_hdr": "HDR", + "search_filters_features_option_location": "Location", + "search_filters_features_option_purchased": "Purchased", + "search_filters_sort_label": "Sort By", + "search_filters_sort_option_relevance": "Relevance", + "search_filters_sort_option_rating": "Rating", + "search_filters_sort_option_date": "Upload Date", + "search_filters_sort_option_views": "View count", + "search_filters_apply_button": "Apply selected filters", "Current version: ": "Current version: ", "next_steps_error_message": "After which you should try to: ", "next_steps_error_message_refresh": "Refresh", diff --git a/locales/eo.json b/locales/eo.json index e7a8453e..b12d7ff0 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -329,39 +329,39 @@ "Videos": "Filmetoj", "Playlists": "Ludlistoj", "Community": "Komunumo", - "relevance": "rilateco", - "rating": "takso", - "date": "dato", - "views": "vidoj", - "content_type": "enhavtipo", - "duration": "daŭro", - "features": "trajtoj", - "sort": "ordigi", - "hour": "horo", - "today": "hodiaŭ", - "week": "semajno", - "month": "monato", - "year": "jaro", - "video": "filmeto", - "channel": "kanalo", - "playlist": "ludlisto", - "movie": "filmo", - "show": "spektaĵo", - "hd": "altdistingiva", - "subtitles": "subtekstoj", - "creative_commons": "Krea Komunaĵo", - "3d": "3D", - "live": "nuna", - "4k": "4k", - "location": "loko", - "hdr": "granddinamikgama", - "filter": "filtri", + "search_filters_sort_option_relevance": "rilateco", + "search_filters_sort_option_rating": "takso", + "search_filters_sort_option_date": "dato", + "search_filters_sort_option_views": "vidoj", + "search_filters_type_label": "enhavtipo", + "search_filters_duration_label": "daŭro", + "search_filters_features_label": "trajtoj", + "search_filters_sort_label": "ordigi", + "search_filters_date_option_hour": "horo", + "search_filters_date_option_today": "hodiaŭ", + "search_filters_date_option_week": "semajno", + "search_filters_date_option_month": "monato", + "search_filters_date_option_year": "jaro", + "search_filters_type_option_video": "filmeto", + "search_filters_type_option_channel": "kanalo", + "search_filters_type_option_playlist": "ludlisto", + "search_filters_type_option_movie": "filmo", + "search_filters_type_option_show": "spektaĵo", + "search_filters_features_option_hd": "altdistingiva", + "search_filters_features_option_subtitles": "subtekstoj", + "search_filters_features_option_c_commons": "Krea Komunaĵo", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "nuna", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "loko", + "search_filters_features_option_hdr": "granddinamikgama", + "search_filters_label": "filtri", "Current version: ": "Nuna versio: ", "next_steps_error_message": "Poste, vi provu: ", "next_steps_error_message_refresh": "Reŝargi", "next_steps_error_message_go_to_youtube": "Iri al JuTubo", - "long": "Longa (> 20 minutos)", - "short": "Mallonga (< 4 minutos)", + "search_filters_duration_option_long": "Longa (> 20 minutos)", + "search_filters_duration_option_short": "Mallonga (< 4 minutos)", "footer_documentation": "Dokumentaro", "footer_source_code": "Fontkodo", "adminprefs_modified_source_code_url_label": "URL al modifita deponejo de fontkodo", diff --git a/locales/es.json b/locales/es.json index 689cb310..c1f2726a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -329,39 +329,39 @@ "Videos": "Vídeos", "Playlists": "Listas de reproducción", "Community": "Comunidad", - "relevance": "relevancia", - "rating": "valoración", - "date": "fecha", - "views": "visualizaciones", - "content_type": "content_type", - "duration": "duración", - "features": "funcionalidades", - "sort": "ordenar", - "hour": "hora", - "today": "hoy", - "week": "semana", - "month": "mes", - "year": "año", - "video": "vídeo", - "channel": "canal", - "playlist": "lista de reproducción", - "movie": "película", - "show": "programa", - "hd": "hd", - "subtitles": "subtítulos", - "creative_commons": "creative_commons", - "3d": "3d", - "live": "directo", - "4k": "4k", - "location": "ubicación", - "hdr": "hdr", - "filter": "filtro", + "search_filters_sort_option_relevance": "relevancia", + "search_filters_sort_option_rating": "valoración", + "search_filters_sort_option_date": "fecha", + "search_filters_sort_option_views": "visualizaciones", + "search_filters_type_label": "content_type", + "search_filters_duration_label": "duración", + "search_filters_features_label": "funcionalidades", + "search_filters_sort_label": "ordenar", + "search_filters_date_option_hour": "hora", + "search_filters_date_option_today": "hoy", + "search_filters_date_option_week": "semana", + "search_filters_date_option_month": "mes", + "search_filters_date_option_year": "año", + "search_filters_type_option_video": "vídeo", + "search_filters_type_option_channel": "canal", + "search_filters_type_option_playlist": "lista de reproducción", + "search_filters_type_option_movie": "película", + "search_filters_type_option_show": "programa", + "search_filters_features_option_hd": "hd", + "search_filters_features_option_subtitles": "subtítulos", + "search_filters_features_option_c_commons": "creative_commons", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "directo", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "ubicación", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "filtro", "Current version: ": "Versión actual: ", "next_steps_error_message": "Después de lo cual deberías intentar: ", "next_steps_error_message_refresh": "Recargar la página", "next_steps_error_message_go_to_youtube": "Ir a YouTube", - "short": "Corto (< 4 minutos)", - "long": "Largo (> 20 minutos)", + "search_filters_duration_option_short": "Corto (< 4 minutos)", + "search_filters_duration_option_long": "Largo (> 20 minutos)", "footer_documentation": "Documentación", "footer_original_source_code": "Código fuente original", "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado", @@ -395,8 +395,8 @@ "preferences_quality_dash_option_worst": "La peor", "videoinfo_invidious_embed_link": "Enlace para Insertar", "preferences_quality_dash_option_1080p": "1080p", - "purchased": "Comprado", - "360": "360°", + "search_filters_features_option_purchased": "Comprado", + "search_filters_features_option_three_sixty": "360°", "videoinfo_watch_on_youTube": "Ver en YouTube", "preferences_save_player_pos_label": "Guardar posición de reproducción: ", "generic_views_count": "{{count}} visualización", diff --git a/locales/fa.json b/locales/fa.json index 48b5a17d..26f1b220 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -345,33 +345,33 @@ "Videos": "ویدیو ها", "Playlists": "سیاهه‌های پخش", "Community": "اجتماع", - "relevance": "مرتبط بودن", - "rating": "امتیاز", - "date": "تاریخ بارگذاری", - "views": "تعداد بازدید", - "content_type": "نوع", - "duration": "مدت", - "features": "ویژگی‌ها", - "sort": "به ترتیب", - "hour": "یک ساعت گذشته", - "today": "امروز", - "week": "این هفته", - "month": "این ماه", - "year": "امسال", - "video": "ویدئو", - "channel": "کانال", - "playlist": "سیاههٔ پخش", - "movie": "فیلم", - "show": "نمایش", - "hd": "HD", - "subtitles": "زیرنویس", - "creative_commons": "کریتیو کامونز", - "3d": "سه‌بعدی", - "live": "زنده", - "4k": "4K", - "location": "مکان", - "hdr": "HDR", - "filter": "پالایه", + "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_features_label": "ویژگی‌ها", + "search_filters_sort_label": "به ترتیب", + "search_filters_date_option_hour": "یک ساعت گذشته", + "search_filters_date_option_today": "امروز", + "search_filters_date_option_week": "این هفته", + "search_filters_date_option_month": "این ماه", + "search_filters_date_option_year": "امسال", + "search_filters_type_option_video": "ویدئو", + "search_filters_type_option_channel": "کانال", + "search_filters_type_option_playlist": "سیاههٔ پخش", + "search_filters_type_option_movie": "فیلم", + "search_filters_type_option_show": "نمایش", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "زیرنویس", + "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_hdr": "HDR", + "search_filters_label": "پالایه", "Current version: ": "نسخه فعلی: ", "next_steps_error_message": "اکنون بایستی یکی از این موارد را امتحان کنید: ", "next_steps_error_message_refresh": "تازه‌سازی", @@ -393,7 +393,7 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", "invidious": "اینویدیوس", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "footer_donate_page": "کمک مالی", "footer_source_code": "کد منبع", "footer_modfied_source_code": "کد منبع ویرایش شده", @@ -405,12 +405,12 @@ "download_subtitles": "زیرنویس‌ها - `x` (.vtt)", "Video unavailable": "ویدئو دردسترس نیست", "preferences_save_player_pos_label": "ذخیره زمان کنونی ویدئو: ", - "purchased": "خریداری شده", + "search_filters_features_option_purchased": "خریداری شده", "preferences_quality_dash_label": "کیفیت ترجیحی ویدئو DASH: ", "preferences_region_label": "کشور محتوا: ", "footer_documentation": "مستندات", "footer_original_source_code": "کد منبع اصلی", - "long": "بلند (> 20 دقیقه)", + "search_filters_duration_option_long": "بلند (> 20 دقیقه)", "adminprefs_modified_source_code_url_label": "URL مخزن کد منبع ویریش شده", - "short": "کوتاه (< 4 دقیقه)" + "search_filters_duration_option_short": "کوتاه (< 4 دقیقه)" } diff --git a/locales/fi.json b/locales/fi.json index 5bc27a9c..5aa7c8b7 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -328,33 +328,33 @@ "Videos": "Videot", "Playlists": "Soittolistat", "Community": "Yhteisö", - "relevance": "Osuvuus", - "rating": "Arvostelu", - "date": "Latauspäivämäärä", - "views": "Katselukerrat", - "content_type": "Tyyppi", - "duration": "Kesto", - "features": "Ominaisuudet", - "sort": "Luokittele", - "hour": "Viimeisin tunti", - "today": "Tänään", - "week": "Tämä viikko", - "month": "Tämä kuukausi", - "year": "Tämä vuosi", - "video": "Video", - "channel": "Kanava", - "playlist": "Soittolista", - "movie": "Elokuva", - "show": "Ohjelma", - "hd": "HD", - "subtitles": "Tekstitys/CC", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Suora lähetys", - "4k": "4K", - "location": "Sijainti", - "hdr": "HDR", - "filter": "Suodatin", + "search_filters_sort_option_relevance": "Osuvuus", + "search_filters_sort_option_rating": "Arvostelu", + "search_filters_sort_option_date": "Latauspäivämäärä", + "search_filters_sort_option_views": "Katselukerrat", + "search_filters_type_label": "Tyyppi", + "search_filters_duration_label": "Kesto", + "search_filters_features_label": "Ominaisuudet", + "search_filters_sort_label": "Luokittele", + "search_filters_date_option_hour": "Viimeisin tunti", + "search_filters_date_option_today": "Tänään", + "search_filters_date_option_week": "Tämä viikko", + "search_filters_date_option_month": "Tämä kuukausi", + "search_filters_date_option_year": "Tämä vuosi", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanava", + "search_filters_type_option_playlist": "Soittolista", + "search_filters_type_option_movie": "Elokuva", + "search_filters_type_option_show": "Ohjelma", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Tekstitys/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Suora lähetys", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Sijainti", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Suodatin", "Current version: ": "Tämänhetkinen versio: ", "next_steps_error_message": "Sinun tulisi kokeilla seuraavia: ", "next_steps_error_message_refresh": "Päivitä", @@ -423,8 +423,8 @@ "preferences_quality_dash_label": "Haluttava DASH-videolaatu: ", "generic_count_years": "{{count}} vuosi", "generic_count_years_plural": "{{count}} vuotta", - "purchased": "Ostettu", - "360": "360°", + "search_filters_features_option_purchased": "Ostettu", + "search_filters_features_option_three_sixty": "360°", "videoinfo_watch_on_youTube": "Katso YouTubessa", "none": "ei mikään", "videoinfo_started_streaming_x_ago": "Striimaaminen aloitettu `x` sitten", @@ -433,8 +433,8 @@ "footer_source_code": "Lähdekoodi", "adminprefs_modified_source_code_url_label": "URL muokattuun lähdekoodirepositoryyn", "Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssin alla GitHubissa.", - "short": "Lyhyt (< 4 minuuttia)", - "long": "Pitkä (> 20 minuuttia)", + "search_filters_duration_option_short": "Lyhyt (< 4 minuuttia)", + "search_filters_duration_option_long": "Pitkä (> 20 minuuttia)", "footer_documentation": "Dokumentaatio", "footer_original_source_code": "Alkuperäinen lähdekoodi", "footer_modfied_source_code": "Muokattu lähdekoodi", diff --git a/locales/fr.json b/locales/fr.json index 96103580..3c83ec1b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -361,33 +361,33 @@ "Videos": "Vidéos", "Playlists": "Listes de lecture", "Community": "Communauté", - "relevance": "pertinence", - "rating": "évaluation", - "date": "date", - "views": "nombre de vues", - "content_type": "type", - "duration": "durée", - "features": "fonctionnalités", - "sort": "Trier par", - "hour": "dernière heure", - "today": "aujourd'hui", - "week": "semaine", - "month": "mois", - "year": "année", - "video": "vidéo", - "channel": "chaîne", - "playlist": "liste de lecture", - "movie": "film", - "show": "émission", - "hd": "HD", - "subtitles": "sous-titres / CC", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "en direct", - "4k": "4K", - "location": "emplacement", - "hdr": "HDR", - "filter": "filtrer", + "search_filters_sort_option_relevance": "pertinence", + "search_filters_sort_option_rating": "évaluation", + "search_filters_sort_option_date": "date", + "search_filters_sort_option_views": "nombre de vues", + "search_filters_type_label": "type", + "search_filters_duration_label": "durée", + "search_filters_features_label": "fonctionnalités", + "search_filters_sort_label": "Trier par", + "search_filters_date_option_hour": "dernière heure", + "search_filters_date_option_today": "aujourd'hui", + "search_filters_date_option_week": "semaine", + "search_filters_date_option_month": "mois", + "search_filters_date_option_year": "année", + "search_filters_type_option_video": "vidéo", + "search_filters_type_option_channel": "chaîne", + "search_filters_type_option_playlist": "liste de lecture", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "émission", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "sous-titres / CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "en direct", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "emplacement", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "filtrer", "Current version: ": "Version actuelle : ", "next_steps_error_message": "Vous pouvez essayer de : ", "next_steps_error_message_refresh": "Rafraîchir la page", @@ -397,8 +397,8 @@ "preferences_region_label": "Pays du contenu : ", "footer_donate_page": "Faire un don", "footer_modfied_source_code": "Code source modifié", - "short": "Courte (< 4 minutes)", - "long": "Longue (> 20 minutes)", + "search_filters_duration_option_short": "Courte (< 4 minutes)", + "search_filters_duration_option_long": "Longue (> 20 minutes)", "adminprefs_modified_source_code_url_label": "URL du dépôt du code source modifié", "footer_documentation": "Documentation", "footer_original_source_code": "Code source original", @@ -415,12 +415,12 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "none": "aucun", "videoinfo_started_streaming_x_ago": "En stream depuis `x`", "videoinfo_watch_on_youTube": "Regarder sur YouTube", "videoinfo_youTube_embed_link": "Intégrer", - "purchased": "Acheter", + "search_filters_features_option_purchased": "Acheter", "videoinfo_invidious_embed_link": "Lien intégré", "download_subtitles": "Sous-titres - `x` (.vtt)", "user_saved_playlists": "`x` listes de lecture sauvegardées", diff --git a/locales/he.json b/locales/he.json index 2c9258b9..fc75b953 100644 --- a/locales/he.json +++ b/locales/he.json @@ -274,32 +274,32 @@ "Videos": "סרטונים", "Playlists": "פלייליסטים", "Community": "קהילה", - "relevance": "רלוונטיות", - "rating": "דירוג", - "date": "תאריך העלאה", - "views": "מספר צפיות", - "content_type": "סוג", - "duration": "משך זמן", - "features": "תכונות", - "sort": "מיון לפי", - "hour": "השעה האחרונה", - "today": "היום", - "week": "השבוע", - "month": "החודש", - "year": "השנה", - "video": "סרטון", - "channel": "ערוץ", - "playlist": "פלייליסט", - "movie": "סרט", - "show": "תכנית טלוויזיה", - "hd": "HD", - "subtitles": "כתוביות", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Live", - "4k": "4K", - "location": "מיקום", - "hdr": "HDR", - "filter": "סינון", + "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_features_label": "תכונות", + "search_filters_sort_label": "מיון לפי", + "search_filters_date_option_hour": "השעה האחרונה", + "search_filters_date_option_today": "היום", + "search_filters_date_option_week": "השבוע", + "search_filters_date_option_month": "החודש", + "search_filters_date_option_year": "השנה", + "search_filters_type_option_video": "סרטון", + "search_filters_type_option_channel": "ערוץ", + "search_filters_type_option_playlist": "פלייליסט", + "search_filters_type_option_movie": "סרט", + "search_filters_type_option_show": "תכנית טלוויזיה", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "כתוביות", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "מיקום", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "סינון", "Current version: ": "הגרסה הנוכחית: " } diff --git a/locales/hr.json b/locales/hr.json index 688368d2..1a204fd6 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -329,41 +329,41 @@ "Videos": "Videa", "Playlists": "Zbirke", "Community": "Zajednica", - "relevance": "značaj", - "rating": "ocjena", - "date": "datum", - "views": "prikazi", - "content_type": "vrsta_sadržaja", - "duration": "trajanje", - "features": "funkcije", - "sort": "redoslijed", - "hour": "sat", - "today": "danas", - "week": "tjedan", - "month": "mjesec", - "year": "godina", - "video": "video", - "channel": "kanal", - "playlist": "Zbirka", - "movie": "film", - "show": "emisija", - "hd": "hd", - "subtitles": "titlovi", - "creative_commons": "creative_commons", - "3d": "3d", - "live": "uživo", - "4k": "4k", - "location": "lokacija", - "hdr": "hdr", - "filter": "filtar", + "search_filters_sort_option_relevance": "značaj", + "search_filters_sort_option_rating": "ocjena", + "search_filters_sort_option_date": "datum", + "search_filters_sort_option_views": "prikazi", + "search_filters_type_label": "vrsta_sadržaja", + "search_filters_duration_label": "trajanje", + "search_filters_features_label": "funkcije", + "search_filters_sort_label": "redoslijed", + "search_filters_date_option_hour": "sat", + "search_filters_date_option_today": "danas", + "search_filters_date_option_week": "tjedan", + "search_filters_date_option_month": "mjesec", + "search_filters_date_option_year": "godina", + "search_filters_type_option_video": "video", + "search_filters_type_option_channel": "kanal", + "search_filters_type_option_playlist": "Zbirka", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "emisija", + "search_filters_features_option_hd": "hd", + "search_filters_features_option_subtitles": "titlovi", + "search_filters_features_option_c_commons": "creative_commons", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "uživo", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "lokacija", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "filtar", "Current version: ": "Trenutačna verzija: ", "next_steps_error_message": "Nakon toga bi trebali pokušati sljedeće: ", "next_steps_error_message_refresh": "Aktualiziraj stranicu", "next_steps_error_message_go_to_youtube": "Idi na YouTube", "footer_donate_page": "Doniraj", "adminprefs_modified_source_code_url_label": "URL do repozitorija izmijenjenog izvornog koda", - "short": "Kratki (< 4 minute)", - "long": "Dugi (> 20 minute)", + "search_filters_duration_option_short": "Kratki (< 4 minute)", + "search_filters_duration_option_long": "Dugi (> 20 minute)", "footer_source_code": "Izvorni kod", "footer_modfied_source_code": "Izmijenjeni izvorni kod", "footer_documentation": "Dokumentacija", @@ -382,8 +382,8 @@ "preferences_quality_dash_option_240p": "240 p", "preferences_quality_dash_option_144p": "144 p", "invidious": "Invidious", - "purchased": "Kupljeno", - "360": "360 °", + "search_filters_features_option_purchased": "Kupljeno", + "search_filters_features_option_three_sixty": "360 °", "none": "bez", "videoinfo_youTube_embed_link": "Ugradi", "user_created_playlists": "`x` stvorene zbirke", diff --git a/locales/hu-HU.json b/locales/hu-HU.json index d1948a47..1c1d9598 100644 --- a/locales/hu-HU.json +++ b/locales/hu-HU.json @@ -365,9 +365,9 @@ "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", "videoinfo_started_streaming_x_ago": "`x` ezelőtt kezdte streamelni", - "views": "Mennyien látták", - "purchased": "Megvásárolva", - "360": "360°-os", + "search_filters_sort_option_views": "Mennyien látták", + "search_filters_features_option_purchased": "Megvásárolva", + "search_filters_features_option_three_sixty": "360°-os", "footer_original_source_code": "Eredeti forráskód", "none": "egyik sem", "videoinfo_watch_on_youTube": "YouTube-on megnézni", @@ -382,14 +382,14 @@ "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_label": "DASH-videó minősége: ", "preferences_quality_option_small": "Rossz", - "date": "Feltöltés dátuma", + "search_filters_sort_option_date": "Feltöltés dátuma", "Video unavailable": "A videó nem érhető el", "preferences_save_player_pos_label": "A videó folytatása onnan, ahol félbe lett hagyva: ", "preferences_show_nick_label": "Becenév mutatása felül: ", "Released under the AGPLv3 on Github.": "AGPLv3 licenc alapján a GitHubon", - "3d": "3D-ben", - "live": "Élőben", - "filter": "Szűrők", + "search_filters_features_option_three_d": "3D-ben", + "search_filters_features_option_live": "Élőben", + "search_filters_label": "Szűrők", "next_steps_error_message_refresh": "Újratöltés", "footer_donate_page": "Adakozás", "footer_source_code": "Forráskód", @@ -397,40 +397,40 @@ "adminprefs_modified_source_code_url_label": "A módosított forráskód repositoryjának URL-je:", "preferences_automatic_instance_redirect_label": "Váltáskor másik Invidious oldal automatikus betöltése (redirect.invidious.io töltődik, ha nem működne): ", "preferences_region_label": "Ország tartalmainak mutatása: ", - "relevance": "Relevancia", - "rating": "Pontszám", - "content_type": "Típus", - "today": "Mai napon", - "channel": "Csatorna", - "video": "Videó", - "playlist": "Lejátszási lista", - "creative_commons": "Creative Commons", - "features": "Jellemzők", - "sort": "Rendezés módja", + "search_filters_sort_option_relevance": "Relevancia", + "search_filters_sort_option_rating": "Pontszám", + "search_filters_type_label": "Típus", + "search_filters_date_option_today": "Mai napon", + "search_filters_type_option_channel": "Csatorna", + "search_filters_type_option_video": "Videó", + "search_filters_type_option_playlist": "Lejátszási lista", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_label": "Jellemzők", + "search_filters_sort_label": "Rendezés módja", "preferences_category_misc": "További beállítások", "%A %B %-d, %Y": "%Y. %B %-d %A", - "long": "Hosszú (20 percnél hosszabb)", - "year": "Ebben az évben", - "hour": "Az elmúlt órában", - "movie": "Film", - "hdr": "HDR", + "search_filters_duration_option_long": "Hosszú (20 percnél hosszabb)", + "search_filters_date_option_year": "Ebben az évben", + "search_filters_date_option_hour": "Az elmúlt órában", + "search_filters_type_option_movie": "Film", + "search_filters_features_option_hdr": "HDR", "Broken? Try another Invidious Instance": "Nem működik? Próbáld meg egy másik Invidious oldallal.", - "duration": "Játékidő", + "search_filters_duration_label": "Játékidő", "next_steps_error_message": "Az alábbi lehetőségek állnak rendelkezésre: ", "Xhosa": "xhosza", "Switch Invidious Instance": "Váltás másik Invidious-oldalra", "Urdu": "urdu", - "week": "Ezen a héten", + "search_filters_date_option_week": "Ezen a héten", "Invalid TFA code": "A kétlépéses hitelesítés kódja nem megfelelő", "footer_documentation": "Dokumentáció", - "hd": "HD", + "search_filters_features_option_hd": "HD", "next_steps_error_message_go_to_youtube": "Ugrás a YouTube-ra", - "show": "Műsor", - "4k": "4K", - "short": "Rövid (4 percnél nem több)", - "month": "Ebben a hónapban", - "subtitles": "Felirattal", - "location": "Közelben", + "search_filters_type_option_show": "Műsor", + "search_filters_features_option_four_k": "4K", + "search_filters_duration_option_short": "Rövid (4 percnél nem több)", + "search_filters_date_option_month": "Ebben a hónapban", + "search_filters_features_option_subtitles": "Felirattal", + "search_filters_features_option_location": "Közelben", "crash_page_you_found_a_bug": "Úgy néz ki, találtál egy hibát az Invidiousban.", "crash_page_before_reporting": "Mielőtt jelentenéd a hibát:", "crash_page_read_the_faq": "olvasd el a Gyakran Ismételt Kérdéseket (GYIK)", diff --git a/locales/id.json b/locales/id.json index 778c4de2..a56792c8 100644 --- a/locales/id.json +++ b/locales/id.json @@ -345,33 +345,33 @@ "Videos": "Video", "Playlists": "Daftar putar", "Community": "Komunitas", - "relevance": "Relevansi", - "rating": "Penilaian", - "date": "Tanggal unggah", - "views": "Jumlah ditonton", - "content_type": "Tipe", - "duration": "Durasi", - "features": "Fitur", - "sort": "Urut Berdasarkan", - "hour": "Jam Terakhir", - "today": "Hari Ini", - "week": "Pekan Ini", - "month": "Bulan Ini", - "year": "Tahun Ini", - "video": "Video", - "channel": "Kanal", - "playlist": "Daftar Putar", - "movie": "Film", - "show": "Pertunjukan/Acara", - "hd": "HD", - "subtitles": "Takarir", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Siaran Langsung", - "4k": "4K", - "location": "Lokasi", - "hdr": "HDR", - "filter": "Saring", + "search_filters_sort_option_relevance": "Relevansi", + "search_filters_sort_option_rating": "Penilaian", + "search_filters_sort_option_date": "Tanggal unggah", + "search_filters_sort_option_views": "Jumlah ditonton", + "search_filters_type_label": "Tipe", + "search_filters_duration_label": "Durasi", + "search_filters_features_label": "Fitur", + "search_filters_sort_label": "Urut Berdasarkan", + "search_filters_date_option_hour": "Jam Terakhir", + "search_filters_date_option_today": "Hari Ini", + "search_filters_date_option_week": "Pekan Ini", + "search_filters_date_option_month": "Bulan Ini", + "search_filters_date_option_year": "Tahun Ini", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "search_filters_type_option_playlist": "Daftar Putar", + "search_filters_type_option_movie": "Film", + "search_filters_type_option_show": "Pertunjukan/Acara", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Takarir", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Siaran Langsung", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Lokasi", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Saring", "Current version: ": "Versi saat ini: ", "next_steps_error_message": "Setelah itu Anda harus mencoba: ", "next_steps_error_message_refresh": "Segarkan", @@ -380,8 +380,8 @@ "adminprefs_modified_source_code_url_label": "URL ke repositori kode sumber yang dimodifikasi", "footer_source_code": "Kode sumber", "footer_original_source_code": "Kode sumber yang asli", - "short": "Pendek (< 4 menit)", - "long": "Panjang (> 20 menit)", + "search_filters_duration_option_short": "Pendek (< 4 menit)", + "search_filters_duration_option_long": "Panjang (> 20 menit)", "footer_modfied_source_code": "Kode sumber yang dimodifikasi", "footer_documentation": "Dokumentasi", "preferences_region_label": "Konten dari negara: ", @@ -398,8 +398,8 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", - "purchased": "Dibeli", - "360": "360°", + "search_filters_features_option_purchased": "Dibeli", + "search_filters_features_option_three_sixty": "360°", "none": "tidak ada", "videoinfo_watch_on_youTube": "Tonton di YouTube", "videoinfo_youTube_embed_link": "Tersemat", diff --git a/locales/it.json b/locales/it.json index 1f34d65c..ce4843b5 100644 --- a/locales/it.json +++ b/locales/it.json @@ -347,32 +347,32 @@ "Videos": "Video", "Playlists": "Playlist", "Community": "Comunità", - "relevance": "Pertinenza", - "rating": "Valutazione", - "date": "Data di caricamento", - "views": "Numero di visualizzazioni", - "content_type": "Tipo", - "duration": "Durata", - "features": "Caratteristiche", - "sort": "Ordina per", - "hour": "Ultima ora", - "today": "Oggi", - "week": "Questa settimana", - "month": "Questo mese", - "year": "Quest'anno", - "video": "Video", - "channel": "Canale", - "playlist": "Playlist", - "movie": "Film", - "hd": "AD", - "subtitles": "Sottotitoli / CC", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "In diretta", - "4k": "4K", - "location": "Posizione", - "hdr": "HDR", - "filter": "Filtra", + "search_filters_sort_option_relevance": "Pertinenza", + "search_filters_sort_option_rating": "Valutazione", + "search_filters_sort_option_date": "Data di caricamento", + "search_filters_sort_option_views": "Numero di visualizzazioni", + "search_filters_type_label": "Tipo", + "search_filters_duration_label": "Durata", + "search_filters_features_label": "Caratteristiche", + "search_filters_sort_label": "Ordina per", + "search_filters_date_option_hour": "Ultima ora", + "search_filters_date_option_today": "Oggi", + "search_filters_date_option_week": "Questa settimana", + "search_filters_date_option_month": "Questo mese", + "search_filters_date_option_year": "Quest'anno", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Canale", + "search_filters_type_option_playlist": "Playlist", + "search_filters_type_option_movie": "Film", + "search_filters_features_option_hd": "AD", + "search_filters_features_option_subtitles": "Sottotitoli / CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "In diretta", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Posizione", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Filtra", "Current version: ": "Versione attuale: ", "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_360p": "360p", @@ -382,7 +382,7 @@ "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_4320p": "4320p", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "preferences_quality_dash_option_144p": "144p", "Released under the AGPLv3 on Github.": "Rilasciato su Github con licenza AGPLv3.", "preferences_quality_option_medium": "Media", diff --git a/locales/ja.json b/locales/ja.json index 9708c0ea..977efd53 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -345,44 +345,44 @@ "Videos": "動画", "Playlists": "プレイリスト", "Community": "コミュニティ", - "relevance": "関連", - "rating": "評価", - "date": "時刻", - "views": "再生回数", - "content_type": "コンテンツの種類", - "duration": "再生時間", - "features": "機能", - "sort": "順番", - "hour": "1時間前", - "today": "今日", - "week": "今週", - "month": "今月", - "year": "今年", - "video": "動画", - "channel": "チャンネル", - "playlist": "再生リスト", - "movie": "映画", - "show": "番組", - "hd": "HD", - "subtitles": "字幕", - "creative_commons": "クリエイティブ・コモンズ", - "3d": "3D", - "live": "生配信", - "4k": "4K", - "location": "場所", - "hdr": "HDR", - "filter": "フィルタ", + "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_features_label": "機能", + "search_filters_sort_label": "順番", + "search_filters_date_option_hour": "1時間前", + "search_filters_date_option_today": "今日", + "search_filters_date_option_week": "今週", + "search_filters_date_option_month": "今月", + "search_filters_date_option_year": "今年", + "search_filters_type_option_video": "動画", + "search_filters_type_option_channel": "チャンネル", + "search_filters_type_option_playlist": "再生リスト", + "search_filters_type_option_movie": "映画", + "search_filters_type_option_show": "番組", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "字幕", + "search_filters_features_option_c_commons": "クリエイティブ・コモンズ", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "生配信", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "場所", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "フィルタ", "Current version: ": "現在のバージョン: ", "next_steps_error_message": "下記のものを試して下さい: ", "next_steps_error_message_refresh": "再読込", "next_steps_error_message_go_to_youtube": "YouTubeへ", - "short": "4 分未満", + "search_filters_duration_option_short": "4 分未満", "footer_documentation": "文書", "footer_source_code": "ソースコード", "footer_original_source_code": "ソースコード(元)", "footer_modfied_source_code": "ソースコード(編集)", "adminprefs_modified_source_code_url_label": "編集したソースコードのレポジトリーURL", - "long": "20 分以上", + "search_filters_duration_option_long": "20 分以上", "preferences_region_label": "地域: ", "footer_donate_page": "寄付する", "preferences_quality_dash_label": "優先するDash画質 : ", @@ -404,7 +404,7 @@ "videoinfo_invidious_embed_link": "埋め込みリンク", "none": "なし", "download_subtitles": "字幕 - `x` (.vtt)", - "purchased": "購入済み", + "search_filters_features_option_purchased": "購入済み", "preferences_quality_option_dash": "DASH (適切な品質)", "preferences_quality_dash_option_worst": "最悪", "preferences_quality_dash_option_best": "最高", diff --git a/locales/ko.json b/locales/ko.json index a579fe56..be630f2c 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -86,7 +86,7 @@ "generic_playlists_count_0": "{{count}} 재생목록", "generic_subscribers_count_0": "{{count}} 구독자", "generic_subscriptions_count_0": "{{count}} 구독", - "playlist": "재생목록", + "search_filters_type_option_playlist": "재생목록", "Korean": "한국어", "Japanese": "일본어", "Greek": "그리스어", @@ -195,16 +195,16 @@ "Maori": "마오리어", "Maltese": "몰타어", "Wrong answer": "잘못된 답변", - "live": "실시간", - "3d": "3D", - "location": "지역", - "4k": "4K", - "filter": "필터", - "hdr": "HDR", + "search_filters_features_option_live": "실시간", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_location": "지역", + "search_filters_features_option_four_k": "4K", + "search_filters_label": "필터", + "search_filters_features_option_hdr": "HDR", "Current version: ": "현재 버전: ", "next_steps_error_message_refresh": "새로 고침", "next_steps_error_message_go_to_youtube": "YouTube로 가기", - "subtitles": "자막", + "search_filters_features_option_subtitles": "자막", "`x` marked it with a ❤": "`x`님의 ❤", "Download as: ": "다음으로 다운로드: ", "Download": "다운로드", @@ -219,7 +219,7 @@ "Latvian": "라트비아어", "Latin": "라틴어", "Lao": "라오어", - "channel": "채널", + "search_filters_type_option_channel": "채널", "Kyrgyz": "키르기스어", "Kurdish": "쿠르드어", "Khmer": "크메르어", @@ -279,7 +279,7 @@ "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "JavaScript가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.", "Shared `x`": "공유된 `x`", "Whitelisted regions: ": "차단되지 않은 지역: ", - "views": "조회수", + "search_filters_sort_option_views": "조회수", "Please log in": "로그인하세요", "Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다", "Password cannot be empty": "비밀번호는 비워둘 수 없습니다", @@ -343,12 +343,12 @@ "Premieres `x`": "최초 공개 `x`", "Premieres in `x`": "`x` 에 최초 공개", "next_steps_error_message": "다음 방법을 시도해 보세요: ", - "creative_commons": "크리에이티브 커먼즈", - "duration": "길이", - "content_type": "구분", - "date": "업로드 날짜", - "rating": "평점", - "relevance": "관련성", + "search_filters_features_option_c_commons": "크리에이티브 커먼즈", + "search_filters_duration_label": "길이", + "search_filters_type_label": "구분", + "search_filters_sort_option_date": "업로드 날짜", + "search_filters_sort_option_rating": "평점", + "search_filters_sort_option_relevance": "관련성", "Community": "커뮤니티", "Videos": "동영상", "Video mode": "비디오 모드", @@ -365,19 +365,19 @@ "Rating: ": "평점: ", "About": "정보", "Top": "최고", - "hd": "HD", - "show": "쇼", - "movie": "영화", - "video": "동영상", - "year": "올해", - "month": "이번 달", - "week": "이번 주", - "today": "오늘", - "hour": "지난 1시간", - "sort": "정렬기준", - "features": "기능별", - "short": "4분 미만", - "long": "20분 초과", + "search_filters_features_option_hd": "HD", + "search_filters_type_option_show": "쇼", + "search_filters_type_option_movie": "영화", + "search_filters_type_option_video": "동영상", + "search_filters_date_option_year": "올해", + "search_filters_date_option_month": "이번 달", + "search_filters_date_option_week": "이번 주", + "search_filters_date_option_today": "오늘", + "search_filters_date_option_hour": "지난 1시간", + "search_filters_sort_label": "정렬기준", + "search_filters_features_label": "기능별", + "search_filters_duration_option_short": "4분 미만", + "search_filters_duration_option_long": "20분 초과", "footer_documentation": "문서", "footer_source_code": "소스 코드", "footer_original_source_code": "원본 소스 코드", diff --git a/locales/lt.json b/locales/lt.json index a5cee472..873f10d7 100644 --- a/locales/lt.json +++ b/locales/lt.json @@ -329,39 +329,39 @@ "Videos": "Vaizdo įrašai", "Playlists": "Grojaraiščiai", "Community": "Bendruomenė", - "relevance": "Aktualumas", - "rating": "Reitingas", - "date": "Įkėlimo data", - "views": "Peržiūrų skaičius", - "content_type": "Tipas", - "duration": "Trukmė", - "features": "Funkcijos", - "sort": "Rūšiuoti pagal", - "hour": "Per paskutinę valandą", - "today": "Šiandien", - "week": "Šią savaitę", - "month": "Šį mėnesį", - "year": "Šiais metais", - "video": "Vaizdo įrašas", - "channel": "Kanalas", - "playlist": "Grojaraštis", - "movie": "Filmas", - "show": "Serialas", - "hd": "HD", - "subtitles": "Subtitrai/CC", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Tiesiogiai", - "4k": "4K", - "location": "Vietovė", - "hdr": "HDR", - "filter": "Filtras", + "search_filters_sort_option_relevance": "Aktualumas", + "search_filters_sort_option_rating": "Reitingas", + "search_filters_sort_option_date": "Įkėlimo data", + "search_filters_sort_option_views": "Peržiūrų skaičius", + "search_filters_type_label": "Tipas", + "search_filters_duration_label": "Trukmė", + "search_filters_features_label": "Funkcijos", + "search_filters_sort_label": "Rūšiuoti pagal", + "search_filters_date_option_hour": "Per paskutinę valandą", + "search_filters_date_option_today": "Šiandien", + "search_filters_date_option_week": "Šią savaitę", + "search_filters_date_option_month": "Šį mėnesį", + "search_filters_date_option_year": "Šiais metais", + "search_filters_type_option_video": "Vaizdo įrašas", + "search_filters_type_option_channel": "Kanalas", + "search_filters_type_option_playlist": "Grojaraštis", + "search_filters_type_option_movie": "Filmas", + "search_filters_type_option_show": "Serialas", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Subtitrai/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Tiesiogiai", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Vietovė", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Filtras", "Current version: ": "Dabartinė versija: ", "next_steps_error_message": "Po to turėtumėte pabandyti: ", "next_steps_error_message_refresh": "Atnaujinti", "next_steps_error_message_go_to_youtube": "Eiti į YouTube", - "short": "Trumpas (< 4 minučių)", - "long": "Ilgas (> 20 minučių)", + "search_filters_duration_option_short": "Trumpas (< 4 minučių)", + "search_filters_duration_option_long": "Ilgas (> 20 minučių)", "footer_documentation": "Dokumentacija", "footer_source_code": "Pirminis kodas", "footer_original_source_code": "Pradinis pirminis kodas", diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 8864efee..2c6006be 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -329,40 +329,40 @@ "Videos": "Videoer", "Playlists": "Spillelister", "Community": "Gemenskap", - "relevance": "relevans", - "rating": "vurdering", - "date": "dato", - "views": "visninger", - "content_type": "innholdstype", - "duration": "varighet", - "features": "funksjoner", - "sort": "sorter", - "hour": "time", - "today": "i dag", - "week": "uke", - "month": "måned", - "year": "år", - "video": "video", - "channel": "kanal", - "playlist": "spilleliste", - "movie": "film", - "show": "vis", - "hd": "HD", - "subtitles": "undertekster", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "direkte", - "4k": "4k", - "location": "sted", - "hdr": "HDR", - "filter": "filtrer", + "search_filters_sort_option_relevance": "relevans", + "search_filters_sort_option_rating": "vurdering", + "search_filters_sort_option_date": "dato", + "search_filters_sort_option_views": "visninger", + "search_filters_type_label": "innholdstype", + "search_filters_duration_label": "varighet", + "search_filters_features_label": "funksjoner", + "search_filters_sort_label": "sorter", + "search_filters_date_option_hour": "time", + "search_filters_date_option_today": "i dag", + "search_filters_date_option_week": "uke", + "search_filters_date_option_month": "måned", + "search_filters_date_option_year": "år", + "search_filters_type_option_video": "video", + "search_filters_type_option_channel": "kanal", + "search_filters_type_option_playlist": "spilleliste", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "vis", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "undertekster", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "direkte", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "sted", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "filtrer", "Current version: ": "Gjeldende versjon: ", "next_steps_error_message": "Etterpå bør du prøve dette: ", "next_steps_error_message_refresh": "Gjenoppfrisk", "next_steps_error_message_go_to_youtube": "Gå til YouTube", - "long": "Lang (> 20 minutter)", + "search_filters_duration_option_long": "Lang (> 20 minutter)", "footer_donate_page": "Doner", - "short": "Kort (< 4 minutter)", + "search_filters_duration_option_short": "Kort (< 4 minutter)", "footer_documentation": "Dokumentasjon", "footer_source_code": "Kildekode", "footer_original_source_code": "Opprinnelig kildekode", @@ -384,8 +384,8 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", - "purchased": "Kjøpt", - "360": "360°", + "search_filters_features_option_purchased": "Kjøpt", + "search_filters_features_option_three_sixty": "360°", "none": "intet", "videoinfo_watch_on_youTube": "Se på YouTube", "videoinfo_youTube_embed_link": "Bak inn", diff --git a/locales/nl.json b/locales/nl.json index d148d872..77c6246d 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -323,33 +323,33 @@ "Videos": "Video's", "Playlists": "Afspeellijsten", "Community": "Gemeenschap", - "relevance": "relevantie", - "rating": "beoordeling", - "date": "datum", - "views": "keren bekeken", - "content_type": "Type inhoud", - "duration": "duur", - "features": "eigenschappen", - "sort": "sorteren", - "hour": "uur", - "today": "vandaag", - "week": "week", - "month": "maand", - "year": "jaar", - "video": "video", - "channel": "kanaal", - "playlist": "afspeellijst", - "movie": "film", - "show": "show", - "hd": "HD", - "subtitles": "ondertitels", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Live", - "4k": "4K", - "location": "locatie", - "hdr": "HDR", - "filter": "verfijnen", + "search_filters_sort_option_relevance": "relevantie", + "search_filters_sort_option_rating": "beoordeling", + "search_filters_sort_option_date": "datum", + "search_filters_sort_option_views": "keren bekeken", + "search_filters_type_label": "Type inhoud", + "search_filters_duration_label": "duur", + "search_filters_features_label": "eigenschappen", + "search_filters_sort_label": "sorteren", + "search_filters_date_option_hour": "uur", + "search_filters_date_option_today": "vandaag", + "search_filters_date_option_week": "week", + "search_filters_date_option_month": "maand", + "search_filters_date_option_year": "jaar", + "search_filters_type_option_video": "video", + "search_filters_type_option_channel": "kanaal", + "search_filters_type_option_playlist": "afspeellijst", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "show", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "ondertitels", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "locatie", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "verfijnen", "Current version: ": "Huidige versie: ", "Switch Invidious Instance": "Schakel tussen de Invidious Instanties", "preferences_automatic_instance_redirect_label": "Automatische instantie-omleiding (terugval naar redirect.invidious.io): ", @@ -358,7 +358,7 @@ "preferences_category_misc": "Diverse voorkeuren", "preferences_show_nick_label": "Toon bijnaam bovenaan: ", "Released under the AGPLv3 on Github.": "Uitgebracht onder de AGPLv3 op Github.", - "short": "Kort (<4 minuten)", + "search_filters_duration_option_short": "Kort (<4 minuten)", "next_steps_error_message_refresh": "Vernieuwen", "next_steps_error_message_go_to_youtube": "Ga naar YouTube", "footer_donate_page": "Doneren", @@ -369,7 +369,7 @@ "Broken? Try another Invidious Instance": "Kapot? Probeer een andere Invidious Instantie", "next_steps_error_message": "Waarna u moet proberen om: ", "footer_source_code": "Bron-code", - "long": "Lang (> 20 minuten)", + "search_filters_duration_option_long": "Lang (> 20 minuten)", "preferences_quality_option_dash": "DASH (adaptieve kwaliteit)", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_medium": "Gemiddeld", @@ -397,6 +397,6 @@ "Video unavailable": "Video onbeschikbaar", "preferences_save_player_pos_label": "Huidig afspeeltijdstip opslaan: ", "none": "geen", - "purchased": "Gekocht", - "360": "360º" + "search_filters_features_option_purchased": "Gekocht", + "search_filters_features_option_three_sixty": "360º" } diff --git a/locales/pl.json b/locales/pl.json index 0f4e0927..642da259 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -328,33 +328,33 @@ "Videos": "Filmy", "Playlists": "Playlisty", "Community": "Społeczność", - "relevance": "Trafność", - "rating": "Ocena", - "date": "data", - "views": "Liczba wyświetleń", - "content_type": "Typ", - "duration": "Długość", - "features": "Funkcje", - "sort": "sortuj", - "hour": "godzina", - "today": "dzisiaj", - "week": "tydzień", - "month": "miesiąc", - "year": "rok", - "video": "Film", - "channel": "kanał", - "playlist": "playlista", - "movie": "film", - "show": "pokaż", - "hd": "hd", - "subtitles": "napisy", - "creative_commons": "creative_commons", - "3d": "3d", - "live": "Na żywo", - "4k": "4k", - "location": "Lokalizacja", - "hdr": "hdr", - "filter": "filtr", + "search_filters_sort_option_relevance": "Trafność", + "search_filters_sort_option_rating": "Ocena", + "search_filters_sort_option_date": "data", + "search_filters_sort_option_views": "Liczba wyświetleń", + "search_filters_type_label": "Typ", + "search_filters_duration_label": "Długość", + "search_filters_features_label": "Funkcje", + "search_filters_sort_label": "sortuj", + "search_filters_date_option_hour": "godzina", + "search_filters_date_option_today": "dzisiaj", + "search_filters_date_option_week": "tydzień", + "search_filters_date_option_month": "miesiąc", + "search_filters_date_option_year": "rok", + "search_filters_type_option_video": "Film", + "search_filters_type_option_channel": "kanał", + "search_filters_type_option_playlist": "playlista", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "pokaż", + "search_filters_features_option_hd": "hd", + "search_filters_features_option_subtitles": "napisy", + "search_filters_features_option_c_commons": "creative_commons", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "Na żywo", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "Lokalizacja", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "filtr", "Current version: ": "Aktualna wersja: ", "next_steps_error_message": "Po czym powinien*ś spróbować: ", "next_steps_error_message_refresh": "Odśwież", @@ -432,8 +432,8 @@ "preferences_quality_dash_label": "Preferowana jakość filmu DASH: ", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_2160p": "2160p", - "purchased": "Zakupione", - "360": "360°", + "search_filters_features_option_purchased": "Zakupione", + "search_filters_features_option_three_sixty": "360°", "footer_donate_page": "Dotacja", "none": "żadne", "videoinfo_started_streaming_x_ago": "Transmisja rozpoczęta `x` temu", @@ -447,8 +447,8 @@ "preferences_save_player_pos_label": "Zapisz pozycję odtwarzania: ", "preferences_region_label": "Region zawartości: ", "Released under the AGPLv3 on Github.": "Wydany na licencji AGPLv3 na Github.", - "short": "Krótkie (< 4 minutes)", - "long": "Długie (> 20 minutes)", + "search_filters_duration_option_short": "Krótkie (< 4 minutes)", + "search_filters_duration_option_long": "Długie (> 20 minutes)", "footer_documentation": "Dokumentacja", "footer_source_code": "Kod źródłowy", "footer_modfied_source_code": "Zmodyfikowany Kod źródłowy", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 71a232c7..c0b12b2d 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -345,41 +345,41 @@ "Videos": "Vídeos", "Playlists": "Listas de reprodução", "Community": "Comunidade", - "relevance": "relevância", - "rating": "avaliação", - "date": "data", - "views": "visualizações", - "content_type": "content_type", - "duration": "duração", - "features": "recursos", - "sort": "ordenar", - "hour": "hora", - "today": "hoje", - "week": "semana", - "month": "mês", - "year": "ano", - "video": "vídeo", - "channel": "Canal", - "playlist": "playlist", - "movie": "filme", - "show": "show", - "hd": "hd", - "subtitles": "legendas", - "creative_commons": "creative_commons", - "3d": "3d", - "live": "ao vivo", - "4k": "4k", - "location": "localização", - "hdr": "hdr", - "filter": "filtro", + "search_filters_sort_option_relevance": "relevância", + "search_filters_sort_option_rating": "avaliação", + "search_filters_sort_option_date": "data", + "search_filters_sort_option_views": "visualizações", + "search_filters_type_label": "content_type", + "search_filters_duration_label": "duração", + "search_filters_features_label": "recursos", + "search_filters_sort_label": "ordenar", + "search_filters_date_option_hour": "hora", + "search_filters_date_option_today": "hoje", + "search_filters_date_option_week": "semana", + "search_filters_date_option_month": "mês", + "search_filters_date_option_year": "ano", + "search_filters_type_option_video": "vídeo", + "search_filters_type_option_channel": "Canal", + "search_filters_type_option_playlist": "playlist", + "search_filters_type_option_movie": "filme", + "search_filters_type_option_show": "show", + "search_filters_features_option_hd": "hd", + "search_filters_features_option_subtitles": "legendas", + "search_filters_features_option_c_commons": "creative_commons", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "ao vivo", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "localização", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "filtro", "Current version: ": "Versão atual: ", "next_steps_error_message": "Depois disso, você deve tentar: ", "next_steps_error_message_refresh": "Atualizar", "next_steps_error_message_go_to_youtube": "Ir para o YouTube", "footer_donate_page": "Doe", "adminprefs_modified_source_code_url_label": "URL para repositório de código fonte modificado", - "long": "Longo (> 20 minutos)", - "short": "Curto (< 4 minutos)", + "search_filters_duration_option_long": "Longo (> 20 minutos)", + "search_filters_duration_option_short": "Curto (< 4 minutos)", "footer_documentation": "Documentação", "footer_source_code": "Código fonte", "footer_original_source_code": "Código fonte original", @@ -404,7 +404,7 @@ "crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se você:", "preferences_save_player_pos_label": "Salvar a posição de reprodução: ", - "purchased": "Comprado", + "search_filters_features_option_purchased": "Comprado", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", "crash_page_search_issue": "procurou por um erro existente no Github", @@ -428,7 +428,7 @@ "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", "preferences_quality_option_medium": "Médio", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "none": "none", "videoinfo_watch_on_youTube": "Assistir no YouTube", "videoinfo_youTube_embed_link": "Embutir", diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 4dba553e..ca819c67 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -345,33 +345,33 @@ "Videos": "Vídeos", "Playlists": "Listas de reprodução", "Community": "Comunidade", - "relevance": "Relevância", - "rating": "Avaliação", - "date": "Data de envio", - "views": "Visualizações", - "content_type": "Tipo", - "duration": "Duração", - "features": "Funcionalidades", - "sort": "Ordenar por", - "hour": "Última hora", - "today": "Hoje", - "week": "Esta semana", - "month": "Este mês", - "year": "Este ano", - "video": "Vídeo", - "channel": "Canal", - "playlist": "Lista de reprodução", - "movie": "Filme", - "show": "Espetáculo", - "hd": "HD", - "subtitles": "Legendas", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Em direto", - "4k": "4K", - "location": "Localização", - "hdr": "HDR", - "filter": "Filtro", + "search_filters_sort_option_relevance": "Relevância", + "search_filters_sort_option_rating": "Avaliação", + "search_filters_sort_option_date": "Data de envio", + "search_filters_sort_option_views": "Visualizações", + "search_filters_type_label": "Tipo", + "search_filters_duration_label": "Duração", + "search_filters_features_label": "Funcionalidades", + "search_filters_sort_label": "Ordenar por", + "search_filters_date_option_hour": "Última hora", + "search_filters_date_option_today": "Hoje", + "search_filters_date_option_week": "Esta semana", + "search_filters_date_option_month": "Este mês", + "search_filters_date_option_year": "Este ano", + "search_filters_type_option_video": "Vídeo", + "search_filters_type_option_channel": "Canal", + "search_filters_type_option_playlist": "Lista de reprodução", + "search_filters_type_option_movie": "Filme", + "search_filters_type_option_show": "Espetáculo", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Legendas", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Em direto", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Localização", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Filtro", "Current version: ": "Versão atual: ", "next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message_refresh": "Atualizar", diff --git a/locales/pt.json b/locales/pt.json index 0a352f79..9956357d 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,9 +1,9 @@ { - "show": "Espetáculo", - "views": "Visualizações", - "date": "Data de envio", - "rating": "Avaliação", - "relevance": "Relevância", + "search_filters_type_option_show": "Espetáculo", + "search_filters_sort_option_views": "Visualizações", + "search_filters_sort_option_date": "Data de envio", + "search_filters_sort_option_rating": "Avaliação", + "search_filters_sort_option_relevance": "Relevância", "Broken? Try another Invidious Instance": "Falhou? Tente outra Instância do Invidious", "Switch Invidious Instance": "Mudar a instância do Invidious", "Show less": "Mostrar menos", @@ -17,28 +17,28 @@ "next_steps_error_message_go_to_youtube": "Ir ao YouTube", "next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message_refresh": "Atualizar", - "filter": "Filtro", - "hdr": "HDR", - "location": "Localização", - "4k": "4K", - "live": "Em direto", - "3d": "3D", - "creative_commons": "Creative Commons", - "subtitles": "Legendas", - "hd": "HD", - "movie": "Filme", - "playlist": "Lista de reprodução", - "channel": "Canal", - "video": "Vídeo", - "year": "Este ano", - "month": "Este mês", - "week": "Esta semana", - "today": "Hoje", - "hour": "Última hora", - "sort": "Ordenar por", - "features": "Funcionalidades", - "duration": "Duração", - "content_type": "Tipo", + "search_filters_label": "Filtro", + "search_filters_features_option_hdr": "HDR", + "search_filters_features_option_location": "Localização", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_live": "Em direto", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_subtitles": "Legendas", + "search_filters_features_option_hd": "HD", + "search_filters_type_option_movie": "Filme", + "search_filters_type_option_playlist": "Lista de reprodução", + "search_filters_type_option_channel": "Canal", + "search_filters_type_option_video": "Vídeo", + "search_filters_date_option_year": "Este ano", + "search_filters_date_option_month": "Este mês", + "search_filters_date_option_week": "Esta semana", + "search_filters_date_option_today": "Hoje", + "search_filters_date_option_hour": "Última hora", + "search_filters_sort_label": "Ordenar por", + "search_filters_features_label": "Funcionalidades", + "search_filters_duration_label": "Duração", + "search_filters_type_label": "Tipo", "permalink": "hiperligação permanente", "YouTube comment permalink": "Hiperligação permanente do comentário no YouTube", "Download as: ": "Descarregar como: ", @@ -376,8 +376,8 @@ "Unsubscribe": "Anular subscrição", "Shared `x` ago": "Partilhado `x` atrás", "LIVE": "Em direto", - "short": "Curto (< 4 minutos)", - "long": "Longo (> 20 minutos)", + "search_filters_duration_option_short": "Curto (< 4 minutos)", + "search_filters_duration_option_long": "Longo (> 20 minutos)", "footer_source_code": "Código-fonte", "footer_original_source_code": "Código-fonte original", "adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado", @@ -397,8 +397,8 @@ "preferences_quality_dash_option_360p": "360p", "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", - "purchased": "Comprado", - "360": "360°", + "search_filters_features_option_purchased": "Comprado", + "search_filters_features_option_three_sixty": "360°", "videoinfo_invidious_embed_link": "Incorporar hiperligação", "Video unavailable": "Vídeo não disponível", "invidious": "Invidious", diff --git a/locales/ru.json b/locales/ru.json index c223bcf8..cc6b87b7 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -329,39 +329,39 @@ "Videos": "Видео", "Playlists": "Плейлисты", "Community": "Сообщество", - "relevance": "Актуальность", - "rating": "Рейтинг", - "date": "Дата загрузки", - "views": "Просмотры", - "content_type": "Тип", - "duration": "Длительность", - "features": "Функции", - "sort": "Сортировать по", - "hour": "Последний час", - "today": "Сегодня", - "week": "Эта неделя", - "month": "Этот месяц", - "year": "Этот год", - "video": "Видео", - "channel": "Канал", - "playlist": "Плейлист", - "movie": "Фильм", - "show": "Показать", - "hd": "HD", - "subtitles": "Субтитры", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Прямой эфир", - "4k": "4K", - "location": "Местоположение", - "hdr": "HDR", - "filter": "Фильтр", + "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_features_label": "Функции", + "search_filters_sort_label": "Сортировать по", + "search_filters_date_option_hour": "Последний час", + "search_filters_date_option_today": "Сегодня", + "search_filters_date_option_week": "Эта неделя", + "search_filters_date_option_month": "Этот месяц", + "search_filters_date_option_year": "Этот год", + "search_filters_type_option_video": "Видео", + "search_filters_type_option_channel": "Канал", + "search_filters_type_option_playlist": "Плейлист", + "search_filters_type_option_movie": "Фильм", + "search_filters_type_option_show": "Показать", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Субтитры", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Прямой эфир", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Местоположение", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Фильтр", "Current version: ": "Текущая версия: ", "next_steps_error_message": "После чего следует попробовать: ", "next_steps_error_message_refresh": "Обновить", "next_steps_error_message_go_to_youtube": "Перейти на YouTube", - "short": "Короткие (< 4 минут)", - "long": "Длинные (> 20 минут)", + "search_filters_duration_option_short": "Короткие (< 4 минут)", + "search_filters_duration_option_long": "Длинные (> 20 минут)", "preferences_quality_dash_option_best": "Наилучшее", "generic_count_weeks_0": "{{count}} неделя", "generic_count_weeks_1": "{{count}} недели", @@ -437,7 +437,7 @@ "generic_count_seconds_0": "{{count}} секунда", "generic_count_seconds_1": "{{count}} секунды", "generic_count_seconds_2": "{{count}} секунд", - "purchased": "Приобретено", + "search_filters_features_option_purchased": "Приобретено", "videoinfo_started_streaming_x_ago": "Трансляция началась `x` назад", "crash_page_switch_instance": "пробовали использовать другое зеркало", "crash_page_read_the_faq": "прочли Частые Вопросы (ЧаВо)", @@ -473,7 +473,7 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", - "360": "360°", + "search_filters_features_option_three_sixty": "360°", "Video unavailable": "Видео недоступно", "preferences_save_player_pos_label": "Запоминать позицию: ", "preferences_region_label": "Страна: ", diff --git a/locales/sq.json b/locales/sq.json index 3e2a3fb1..34daf206 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -26,11 +26,11 @@ "Tamil": "Tamilisht", "Telugu": "Telugu", "Vietnamese": "Vietnamisht", - "creative_commons": "Creative Commons", - "3d": "3D", - "live": "Drejtpërsëdrejti", - "4k": "4K", - "location": "Vendndodhja", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Drejtpërsëdrejti", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Vendndodhja", "videoinfo_watch_on_youTube": "Shiheni në YouTube", "videoinfo_youTube_embed_link": "Trupëzojeni", "videoinfo_invidious_embed_link": "Lidhje Trupëzimi", @@ -261,32 +261,32 @@ "Audio mode": "Mënyrë për audion", "Playlists": "Luajlista", "Community": "Bashkësi", - "relevance": "Rëndësi", + "search_filters_sort_option_relevance": "Rëndësi", "Video mode": "Mënyrë video", "Videos": "Video", - "rating": "Vlerësim", - "date": "Datë ngarkimi", - "views": "Numër parjesh", - "content_type": "Lloj", - "duration": "Kohëzgjatje", - "features": "Veçori", - "sort": "Renditi Sipas", - "hour": "Orën e Fundit", - "today": "Sot", - "long": "E gjatë (> 20 minuta)", - "hd": "HD", - "subtitles": "Titra/CC", - "hdr": "HDR", - "week": "Këtë javë", - "month": "Këtë muaj", - "year": "Këtë vit", - "video": "Video", - "channel": "Kanal", - "playlist": "Luajlistë", - "movie": "Film", - "show": "Shfaqe", - "short": "E shkurtër (< 4 minuta)", - "purchased": "Të blera", + "search_filters_sort_option_rating": "Vlerësim", + "search_filters_sort_option_date": "Datë ngarkimi", + "search_filters_sort_option_views": "Numër parjesh", + "search_filters_type_label": "Lloj", + "search_filters_duration_label": "Kohëzgjatje", + "search_filters_features_label": "Veçori", + "search_filters_sort_label": "Renditi Sipas", + "search_filters_date_option_hour": "Orën e Fundit", + "search_filters_date_option_today": "Sot", + "search_filters_duration_option_long": "E gjatë (> 20 minuta)", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Titra/CC", + "search_filters_features_option_hdr": "HDR", + "search_filters_date_option_week": "Këtë javë", + "search_filters_date_option_month": "Këtë muaj", + "search_filters_date_option_year": "Këtë vit", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "search_filters_type_option_playlist": "Luajlistë", + "search_filters_type_option_movie": "Film", + "search_filters_type_option_show": "Shfaqe", + "search_filters_duration_option_short": "E shkurtër (< 4 minuta)", + "search_filters_features_option_purchased": "Të blera", "footer_modfied_source_code": "Kod Burim i ndryshuar", "adminprefs_modified_source_code_url_label": "URL e depos së ndryshuar të kodit burim", "none": "asnjë", @@ -370,8 +370,8 @@ "Mongolian": "Mongolisht", "Nepali": "Nepaleze", "Norwegian Bokmål": "Norvegjishte Bokmål", - "360": "360°", - "filter": "Filtroji", + "search_filters_features_option_three_sixty": "360°", + "search_filters_label": "Filtroji", "Current version: ": "Versioni i tanishëm: ", "next_steps_error_message": "Pas të cilës duhet të provoni të: ", "next_steps_error_message_refresh": "Rifreskoje", diff --git a/locales/sr.json b/locales/sr.json index 40e53231..2b0fbe98 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -131,26 +131,26 @@ "YouTube comment permalink": "YouTube komentar trajna veza", "Audio mode": "Audio mod", "Playlists": "Plej liste", - "relevance": "Relevantnost", - "rating": "Ocene", - "date": "Datum otpremanja", - "views": "Broj pregleda", + "search_filters_sort_option_relevance": "Relevantnost", + "search_filters_sort_option_rating": "Ocene", + "search_filters_sort_option_date": "Datum otpremanja", + "search_filters_sort_option_views": "Broj pregleda", "`x` marked it with a ❤": "`x` je označio/la ovo sa ❤", - "duration": "Trajanje", - "features": "Karakteristike", - "hour": "Poslednji sat", - "week": "Ove sedmice", - "month": "Ovaj mesec", - "year": "Ove godine", - "video": "Video", - "playlist": "Plej lista", - "movie": "Film", - "long": "Dugo (> 20 minuta)", - "hd": "HD", - "creative_commons": "Creative Commons (Licenca)", - "3d": "3D", - "hdr": "Video Visoke Rezolucije", - "filter": "Filter", + "search_filters_duration_label": "Trajanje", + "search_filters_features_label": "Karakteristike", + "search_filters_date_option_hour": "Poslednji sat", + "search_filters_date_option_week": "Ove sedmice", + "search_filters_date_option_month": "Ovaj mesec", + "search_filters_date_option_year": "Ove godine", + "search_filters_type_option_video": "Video", + "search_filters_type_option_playlist": "Plej lista", + "search_filters_type_option_movie": "Film", + "search_filters_duration_option_long": "Dugo (> 20 minuta)", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_c_commons": "Creative Commons (Licenca)", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_hdr": "Video Visoke Rezolucije", + "search_filters_label": "Filter", "next_steps_error_message": "Nakon čega bi trebali probati: ", "next_steps_error_message_go_to_youtube": "Idi na YouTube", "footer_documentation": "Dokumentacija", @@ -225,13 +225,13 @@ "preferences_category_visual": "Vizuelne preference", "preferences_captions_label": "Podrazumevani titl: ", "Music": "Muzika", - "content_type": "Tip", + "search_filters_type_label": "Tip", "Broken? Try another Invidious Instance": "Ne funkcioniše ispravno? Probajte drugu Invidious instancu", "Tamil": "Tamilski", "Save preferences": "Sačuvaj podešavanja", "Only show latest unwatched video from channel: ": "Prikaži samo poslednje video klipove koji nisu pogledani sa kanala: ", "Xhosa": "Kosa (Jezik)", - "channel": "Kanal", + "search_filters_type_option_channel": "Kanal", "Hungarian": "Mađarski", "Maori": "Maori (Jezik)", "Manage subscriptions": "Upravljaj zapisima", @@ -243,7 +243,7 @@ "preferences_default_home_label": "Podrazumevana početna stranica: ", "Serbian": "Srpski", "License: ": "Licenca: ", - "live": "Uživo", + "search_filters_features_option_live": "Uživo", "Report statistics: ": "Izveštavaj o statistici: ", "Only show latest video from channel: ": "Prikazuj poslednje video klipove samo sa kanala: ", "channel name - reverse": "ime kanala - obrnuto", @@ -266,12 +266,12 @@ "alphabetically": "po alfabetu", "No such user": "Nepostojeći korisnik", "Subscriptions": "Praćenja", - "today": "Danas", + "search_filters_date_option_today": "Danas", "Finnish": "Finski", "Lao": "Laoski", "Login enabled: ": "Prijava omogućena: ", "Shona": "Šona", - "location": "Lokacija", + "search_filters_features_option_location": "Lokacija", "Load more": "Učitaj više", "Released under the AGPLv3 on Github.": "Izbačeno pod licencom AGPLv3 na Github-u.", "Slovenian": "Slovenački", @@ -292,7 +292,7 @@ "Czech": "Češki", "Latin": "Latinski", "Videos": "Video klipovi", - "4k": "4К", + "search_filters_features_option_four_k": "4К", "footer_donate_page": "Doniraj", "English": "Engleski", "Arabic": "Arapski", @@ -310,7 +310,7 @@ "Swahili": "Svahili", "Yiddish": "Jidiš", "Zulu": "Zulu", - "subtitles": "Titl/Prevod", + "search_filters_features_option_subtitles": "Titl/Prevod", "Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 karaktera", "This channel does not exist.": "Ovaj kanal ne postoji.", "Belarusian": "Beloruski", @@ -329,9 +329,9 @@ "Clear watch history": "Obriši istoriju gledanja", "preferences_category_admin": "Administratorska podešavanja", "published": "objavljeno", - "sort": "Poredaj prema", - "show": "Emisija", - "short": "Kratko (< 4 minute)", + "search_filters_sort_label": "Poredaj prema", + "search_filters_type_option_show": "Emisija", + "search_filters_duration_option_short": "Kratko (< 4 minute)", "Current version: ": "Trenutna verzija: ", "Top enabled: ": "Vrh omogućen: ", "Public": "Javno", diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 40c50674..7a9f47d6 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -182,14 +182,14 @@ "Georgian": "Грузијски", "Greek": "Грчки", "Hausa": "Хауса", - "video": "Видео", - "playlist": "Плеј листа", - "movie": "Филм", - "long": "Дуго (> 20 минута)", - "creative_commons": "Creative Commons (Лиценца)", - "live": "Уживо", - "location": "Локација", - "filter": "Филтер", + "search_filters_type_option_video": "Видео", + "search_filters_type_option_playlist": "Плеј листа", + "search_filters_type_option_movie": "Филм", + "search_filters_duration_option_long": "Дуго (> 20 минута)", + "search_filters_features_option_c_commons": "Creative Commons (Лиценца)", + "search_filters_features_option_live": "Уживо", + "search_filters_features_option_location": "Локација", + "search_filters_label": "Филтер", "next_steps_error_message": "Након чега би требали пробати: ", "footer_donate_page": "Донирај", "footer_documentation": "Документација", @@ -247,9 +247,9 @@ "`x` marked it with a ❤": "`x` је означио/ла ово са ❤", "Audio mode": "Аудио мод", "Videos": "Видео клипови", - "views": "Број прегледа", - "features": "Карактеристике", - "today": "Данас", + "search_filters_sort_option_views": "Број прегледа", + "search_filters_features_label": "Карактеристике", + "search_filters_date_option_today": "Данас", "%A %B %-d, %Y": "%A %B %-d, %Y", "preferences_locale_label": "Језик: ", "Persian": "Перзијски", @@ -257,7 +257,7 @@ "": "Прикажи `x` коментара", "([^.,0-9]|^)1([^.,0-9]|$)": "Прикажи `x` коментар" }, - "channel": "Канал", + "search_filters_type_option_channel": "Канал", "Haitian Creole": "Хаићански Креолски", "Armenian": "Јерменски", "next_steps_error_message_go_to_youtube": "Иди на YouTube", @@ -265,10 +265,10 @@ "preferences_vr_mode_label": "Интерактивни видео клипови у 360 степени: ", "Switch Invidious Instance": "Промени Invidious инстанцу", "Portuguese": "Португалски", - "week": "Ове седмице", - "show": "Емисија", + "search_filters_date_option_week": "Ове седмице", + "search_filters_type_option_show": "Емисија", "Fallback comments: ": "Коментари у случају отказивања: ", - "hdr": "Видео Високе Резолуције", + "search_filters_features_option_hdr": "Видео Високе Резолуције", "About": "О програму", "Kazakh": "Казашки", "Shared `x`": "Подељено `x`", @@ -277,7 +277,7 @@ "Erroneous challenge": "Погрешан изазов", "Danish": "Дански", "Could not get channel info.": "Узимање података о каналу није успело.", - "hd": "HD", + "search_filters_features_option_hd": "HD", "Slovenian": "Словеначки", "Load more": "Учитај више", "German": "Немачки", @@ -288,12 +288,12 @@ "Southern Sotho": "Јужни Сото", "Popular": "Популарно", "Gujarati": "Гуџарати", - "year": "Ове године", + "search_filters_date_option_year": "Ове године", "Irish": "Ирски", "YouTube comment permalink": "YouTube коментар трајна веза", "Malagasy": "Малгашки", "Token is expired, please try again": "Жетон је истекао, молимо вас да покушате поново", - "short": "Кратко (< 4 минуте)", + "search_filters_duration_option_short": "Кратко (< 4 минуте)", "Samoan": "Самоански", "Tamil": "Тамилски", "Ukrainian": "Украјински", @@ -307,20 +307,20 @@ "Lithuanian": "Литвански", "Icelandic": "Исландски", "Thai": "Тајски", - "month": "Овај месец", - "content_type": "Тип", - "hour": "Последњи сат", + "search_filters_date_option_month": "Овај месец", + "search_filters_type_label": "Тип", + "search_filters_date_option_hour": "Последњи сат", "Spanish": "Шпански", - "date": "Датум отпремања", + "search_filters_sort_option_date": "Датум отпремања", "View as playlist": "Погледај као плеј листу", - "relevance": "Релевантност", + "search_filters_sort_option_relevance": "Релевантност", "Estonian": "Естонски", "Sinhala": "Синхалешки", "Corsican": "Корзикански", "Filipino": "Филипино", "Gaming": "Игрице", "Movies": "Филмови", - "rating": "Оцене", + "search_filters_sort_option_rating": "Оцене", "Top enabled: ": "Врх омогућен: ", "Released under the AGPLv3 on Github.": "Избачено под лиценцом AGPLv3 на Github-у.", "Afrikaans": "Африканс", @@ -340,9 +340,9 @@ "Swedish": "Шведски", "Music": "Музика", "Download as: ": "Преузми као: ", - "duration": "Трајање", - "sort": "Поредај према", - "subtitles": "Титл/Превод", + "search_filters_duration_label": "Трајање", + "search_filters_sort_label": "Поредај према", + "search_filters_features_option_subtitles": "Титл/Превод", "preferences_extend_desc_label": "Аутоматски прикажи цео опис видеа: ", "Show less": "Прикажи мање", "Broken? Try another Invidious Instance": "Не функционише исправно? Пробајте другу Invidious инстанцу", @@ -359,8 +359,8 @@ "Top": "Врх", "Video mode": "Видео мод", "footer_source_code": "Изворна Кода", - "3d": "3D", - "4k": "4K", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_four_k": "4K", "Erroneous CAPTCHA": "Погрешна CAPTCHA", "`x` ago": "пре `x`", "Arabic": "Арапски", diff --git a/locales/sv-SE.json b/locales/sv-SE.json index ab0d0773..de5ec6f7 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -327,39 +327,39 @@ "Videos": "Videor", "Playlists": "Spellistor", "Community": "Gemenskap", - "relevance": "Relevans", - "rating": "Rankning", - "date": "datum", - "views": "visningar", - "content_type": "Typ", - "duration": "Varaktighet", - "features": "Funktioner", - "sort": "Sortera efter", - "hour": "timme", - "today": "idag", - "week": "vecka", - "month": "månad", - "year": "år", - "video": "video", - "channel": "kanal", - "playlist": "spellista", - "movie": "film", - "show": "tv-serie", - "hd": "hd", - "subtitles": "undertexter", - "creative_commons": "creative_commons", - "3d": "3d", - "live": "live", - "4k": "4k", - "location": "plats", - "hdr": "hdr", - "filter": "Filter", + "search_filters_sort_option_relevance": "Relevans", + "search_filters_sort_option_rating": "Rankning", + "search_filters_sort_option_date": "Datum", + "search_filters_sort_option_views": "Visningar", + "search_filters_type_label": "Typ", + "search_filters_duration_label": "Varaktighet", + "search_filters_features_label": "Funktioner", + "search_filters_sort_label": "Sortera efter", + "search_filters_date_option_hour": "timme", + "search_filters_date_option_today": "idag", + "search_filters_date_option_week": "vecka", + "search_filters_date_option_month": "månad", + "search_filters_date_option_year": "år", + "search_filters_type_option_video": "video", + "search_filters_type_option_channel": "kanal", + "search_filters_type_option_playlist": "spellista", + "search_filters_type_option_movie": "film", + "search_filters_type_option_show": "tv-serie", + "search_filters_features_option_hd": "hd", + "search_filters_features_option_subtitles": "undertexter", + "search_filters_features_option_c_commons": "creative_commons", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "live", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "plats", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "Filter", "Current version: ": "Nuvarande version: ", "next_steps_error_message_refresh": "Uppdatera", "next_steps_error_message_go_to_youtube": "Gå till Youtube", "Released under the AGPLv3 on Github.": "Publicerad under AGPLv3 på Github.", "footer_source_code": "Källkod", - "long": "Lång (> 20 minuter)", + "search_filters_duration_option_long": "Lång (> 20 minuter)", "footer_documentation": "Dokumentation", - "short": "Kort (< 4 minuter)" + "search_filters_duration_option_short": "Kort (< 4 minuter)" } diff --git a/locales/tr.json b/locales/tr.json index 094728fa..3412598f 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -329,39 +329,39 @@ "Videos": "Videolar", "Playlists": "Oynatma listeleri", "Community": "Topluluk", - "relevance": "İlgi", - "rating": "Değerlendirme", - "date": "Yükleme tarihi", - "views": "Görüntüleme sayısı", - "content_type": "Tür", - "duration": "Süre", - "features": "Özellikler", - "sort": "Sıralama Ölçütü", - "hour": "Son Saat", - "today": "Bugün", - "week": "Bu hafta", - "month": "Bu ay", - "year": "Bu yıl", - "video": "Video", - "channel": "Kanal", - "playlist": "Oynatma listesi", - "movie": "Film", - "show": "Gösteri", - "hd": "HD", - "subtitles": "Alt yazılar", - "creative_commons": "Creative Commons", - "3d": "3B", - "live": "Canlı", - "4k": "4K", - "location": "Konum", - "hdr": "HDR", - "filter": "Filtrele", + "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_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_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "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_live": "Canlı", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Konum", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "Filtrele", "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", - "short": "Kısa (4 dakikadan az)", - "long": "Uzun (20 dakikadan fazla)", + "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ı", @@ -394,8 +394,8 @@ "Video unavailable": "Video kullanılamıyor", "preferences_quality_option_dash": "DASH (uyarlanabilir kalite)", "preferences_quality_dash_option_auto": "Otomatik", - "purchased": "Satın alınan", - "360": "360°", + "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: ", diff --git a/locales/vi.json b/locales/vi.json index a8550686..3112ef4a 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -315,32 +315,32 @@ "Videos": "Video", "Playlists": "Danh sách phát", "Community": "Cộng đồng", - "relevance": "liên quan", - "rating": "Xếp hạng", - "date": "ngày", - "views": "lượt xem", - "content_type": "content_type", - "duration": "thời lượng", - "features": "đặc trưng", - "sort": "sắp xếp", - "hour": "giờ", - "today": "hôm nay", - "week": "tuần", - "month": "tháng", - "year": "năm", - "video": "video", - "channel": "kênh", - "playlist": "danh sách phát", - "movie": "bộ phim", - "show": "chỉ", - "hd": "hd", - "subtitles": "phụ đề", - "creative_commons": "Commons sáng tạo", - "3d": "3d", - "live": "trực tiếp", - "4k": "4k", - "location": "vị trí", - "hdr": "hdr", - "filter": "bộ lọc", + "search_filters_sort_option_relevance": "liên quan", + "search_filters_sort_option_rating": "Xếp hạng", + "search_filters_sort_option_date": "ngày", + "search_filters_sort_option_views": "lượt xem", + "search_filters_type_label": "content_type", + "search_filters_duration_label": "thời lượng", + "search_filters_features_label": "đặc trưng", + "search_filters_sort_label": "sắp xếp", + "search_filters_date_option_hour": "giờ", + "search_filters_date_option_today": "hôm nay", + "search_filters_date_option_week": "tuần", + "search_filters_date_option_month": "tháng", + "search_filters_date_option_year": "năm", + "search_filters_type_option_video": "video", + "search_filters_type_option_channel": "kênh", + "search_filters_type_option_playlist": "danh sách phát", + "search_filters_type_option_movie": "bộ phim", + "search_filters_type_option_show": "chỉ", + "search_filters_features_option_hd": "hd", + "search_filters_features_option_subtitles": "phụ đề", + "search_filters_features_option_c_commons": "Commons sáng tạo", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "trực tiếp", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "vị trí", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "bộ lọc", "Current version: ": "Phiên bản hiện tại: " } diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 4b760dd3..13e6c00e 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -345,39 +345,39 @@ "Videos": "视频", "Playlists": "播放列表", "Community": "社区", - "relevance": "相关度", - "rating": "评分", - "date": "上传日期", - "views": "观看次数", - "content_type": "类型", - "duration": "持续时间", - "features": "功能", - "sort": "排序依据", - "hour": "上个小时", - "today": "今日", - "week": "本周", - "month": "本月", - "year": "今年", - "video": "视频", - "channel": "频道", - "playlist": "播放列表", - "movie": "电影", - "show": "真人秀", - "hd": "高清", - "subtitles": "字幕", - "creative_commons": "creative_commons 许可", - "3d": "3d", - "live": "直播", - "4k": "4k", - "location": "位置", - "hdr": "hdr", - "filter": "过滤器", + "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_features_label": "功能", + "search_filters_sort_label": "排序依据", + "search_filters_date_option_hour": "上个小时", + "search_filters_date_option_today": "今日", + "search_filters_date_option_week": "本周", + "search_filters_date_option_month": "本月", + "search_filters_date_option_year": "今年", + "search_filters_type_option_video": "视频", + "search_filters_type_option_channel": "频道", + "search_filters_type_option_playlist": "播放列表", + "search_filters_type_option_movie": "电影", + "search_filters_type_option_show": "真人秀", + "search_filters_features_option_hd": "高清", + "search_filters_features_option_subtitles": "字幕", + "search_filters_features_option_c_commons": "creative_commons 许可", + "search_filters_features_option_three_d": "3d", + "search_filters_features_option_live": "直播", + "search_filters_features_option_four_k": "4k", + "search_filters_features_option_location": "位置", + "search_filters_features_option_hdr": "hdr", + "search_filters_label": "过滤器", "Current version: ": "当前版本: ", "next_steps_error_message": "在此之后你应尝试: ", "next_steps_error_message_refresh": "刷新", "next_steps_error_message_go_to_youtube": "转到 YouTube", - "short": "短(少于4分钟)", - "long": "长(多于 20 分钟)", + "search_filters_duration_option_short": "短(少于4分钟)", + "search_filters_duration_option_long": "长(多于 20 分钟)", "footer_documentation": "文档", "footer_source_code": "源代码", "footer_modfied_source_code": "修改的源代码", @@ -418,8 +418,8 @@ "user_created_playlists": "`x` 创建了播放列表", "user_saved_playlists": "`x` 保存了播放列表", "Video unavailable": "视频不可用", - "purchased": "已购买", - "360": "360°", + "search_filters_features_option_purchased": "已购买", + "search_filters_features_option_three_sixty": "360°", "none": "无", "preferences_save_player_pos_label": "保存播放位置: ", "Spanish (Mexico)": "西班牙语 (墨西哥)", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 84bd1dae..189dba18 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -345,39 +345,39 @@ "Videos": "影片", "Playlists": "播放清單", "Community": "社群", - "relevance": "關聯", - "rating": "評分", - "date": "日期", - "views": "檢視", - "content_type": "內容類型", - "duration": "時長", - "features": "特色", - "sort": "排序", - "hour": "小時", - "today": "今天", - "week": "週", - "month": "月", - "year": "年", - "video": "影片", - "channel": "頻道", - "playlist": "播放清單", - "movie": "電影", - "show": "秀", - "hd": "HD", - "subtitles": "字幕", - "creative_commons": "創用 CC", - "3d": "3D", - "live": "直播", - "4k": "4K", - "location": "位置", - "hdr": "HDR", - "filter": "篩選條件", + "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_features_label": "特色", + "search_filters_sort_label": "排序", + "search_filters_date_option_hour": "小時", + "search_filters_date_option_today": "今天", + "search_filters_date_option_week": "週", + "search_filters_date_option_month": "月", + "search_filters_date_option_year": "年", + "search_filters_type_option_video": "影片", + "search_filters_type_option_channel": "頻道", + "search_filters_type_option_playlist": "播放清單", + "search_filters_type_option_movie": "電影", + "search_filters_type_option_show": "秀", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "字幕", + "search_filters_features_option_c_commons": "創用 CC", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "直播", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "位置", + "search_filters_features_option_hdr": "HDR", + "search_filters_label": "篩選條件", "Current version: ": "目前版本: ", "next_steps_error_message": "之後您應該嘗試: ", "next_steps_error_message_refresh": "重新整理", "next_steps_error_message_go_to_youtube": "到 YouTube", - "short": "短(小於4分鐘)", - "long": "長(多於20分鐘)", + "search_filters_duration_option_short": "短(小於4分鐘)", + "search_filters_duration_option_long": "長(多於20分鐘)", "footer_documentation": "文件", "footer_source_code": "原始碼", "footer_original_source_code": "原本的原始碼", @@ -398,8 +398,8 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", - "purchased": "已購買", - "360": "360°", + "search_filters_features_option_purchased": "已購買", + "search_filters_features_option_three_sixty": "360°", "none": "無", "videoinfo_started_streaming_x_ago": "`x` 前開始串流", "videoinfo_watch_on_youTube": "在 YouTube 上觀看", diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr index b2436989..5ecebef3 100644 --- a/spec/invidious/helpers_spec.cr +++ b/spec/invidious/helpers_spec.cr @@ -29,20 +29,6 @@ Spectator.describe "Helper" do end end - describe "#produce_search_params" do - it "correctly produces token for searching with specified filters" do - expect(produce_search_params).to eq("CAASAhABSAA%3D") - - expect(produce_search_params(sort: "upload_date", content_type: "video")).to eq("CAISAhABSAA%3D") - - expect(produce_search_params(content_type: "playlist")).to eq("CAASAhADSAA%3D") - - expect(produce_search_params(sort: "date", content_type: "video", features: ["hd", "cc", "purchased", "hdr"])).to eq("CAISCxABIAEwAUgByAEBSAA%3D") - - expect(produce_search_params(content_type: "channel")).to eq("CAASAhACSAA%3D") - 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") diff --git a/spec/invidious/search/iv_filters_spec.cr b/spec/invidious/search/iv_filters_spec.cr new file mode 100644 index 00000000..b0897a63 --- /dev/null +++ b/spec/invidious/search/iv_filters_spec.cr @@ -0,0 +1,371 @@ +require "../../../src/invidious/search/filters" + +require "http/params" +require "spectator" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +FEATURES_TEXT = { + Invidious::Search::Filters::Features::Live => "live", + Invidious::Search::Filters::Features::FourK => "4k", + Invidious::Search::Filters::Features::HD => "hd", + Invidious::Search::Filters::Features::Subtitles => "subtitles", + Invidious::Search::Filters::Features::CCommons => "commons", + Invidious::Search::Filters::Features::ThreeSixty => "360", + Invidious::Search::Filters::Features::VR180 => "vr180", + Invidious::Search::Filters::Features::ThreeD => "3d", + Invidious::Search::Filters::Features::HDR => "hdr", + Invidious::Search::Filters::Features::Location => "location", + Invidious::Search::Filters::Features::Purchased => "purchased", +} + +Spectator.describe Invidious::Search::Filters do + # ------------------- + # Decode (legacy) + # ------------------- + + describe "#from_legacy_filters" do + it "Decodes channel: filter" do + query = "test channel:UC123456 request" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("UC123456") + expect(qury).to eq("test request") + expect(subs).to be_false + end + + it "Decodes user: filter" do + query = "user:LinusTechTips broke something (again)" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("LinusTechTips") + expect(qury).to eq("broke something (again)") + expect(subs).to be_false + end + + it "Decodes type: filter" do + Invidious::Search::Filters::Type.each do |value| + query = "Eiffel 65 - Blue [1 Hour] type:#{value}" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(type: value)) + expect(chan).to eq("") + expect(qury).to eq("Eiffel 65 - Blue [1 Hour]") + expect(subs).to be_false + end + end + + it "Decodes content_type: filter" do + Invidious::Search::Filters::Type.each do |value| + query = "I like to watch content_type:#{value}" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(type: value)) + expect(chan).to eq("") + expect(qury).to eq("I like to watch") + expect(subs).to be_false + end + end + + it "Decodes date: filter" do + Invidious::Search::Filters::Date.each do |value| + query = "This date:#{value} is old!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(date: value)) + expect(chan).to eq("") + expect(qury).to eq("This is old!") + expect(subs).to be_false + end + end + + it "Decodes duration: filter" do + Invidious::Search::Filters::Duration.each do |value| + query = "This duration:#{value} is old!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(duration: value)) + expect(chan).to eq("") + expect(qury).to eq("This is old!") + expect(subs).to be_false + end + end + + it "Decodes feature: filter" do + Invidious::Search::Filters::Features.each do |value| + string = FEATURES_TEXT[value] + query = "I like my precious feature:#{string} ^^" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(features: value)) + expect(chan).to eq("") + expect(qury).to eq("I like my precious ^^") + expect(subs).to be_false + end + end + + it "Decodes features: filter" do + query = "This search has many features:vr180,cc,hdr :o" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons) + + expect(fltr).to eq(described_class.new(features: features)) + expect(chan).to eq("") + expect(qury).to eq("This search has many :o") + expect(subs).to be_false + end + + it "Decodes sort: filter" do + Invidious::Search::Filters::Sort.each do |value| + query = "Computer? sort:#{value} my files!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new(sort: value)) + expect(chan).to eq("") + expect(qury).to eq("Computer? my files!") + expect(subs).to be_false + end + end + + it "Decodes subscriptions: filter" do + query = "enable subscriptions:true" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("") + expect(qury).to eq("enable") + expect(subs).to be_true + end + + it "Ignores junk data" do + query = "duration:I sort:like type:cleaning features:stuff date:up!" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("") + expect(qury).to eq("") + expect(subs).to be_false + end + + it "Keeps unknown keys" do + query = "to:be or:not to:be" + + fltr, chan, qury, subs = described_class.from_legacy_filters(query) + + expect(fltr).to eq(described_class.new) + expect(chan).to eq("") + expect(qury).to eq("to:be or:not to:be") + expect(subs).to be_false + end + end + + # ------------------- + # Decode (URL) + # ------------------- + + describe "#from_iv_params" do + it "Decodes type= filter" do + Invidious::Search::Filters::Type.each do |value| + params = HTTP::Params.parse("type=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(type: value)) + end + end + + it "Decodes date= filter" do + Invidious::Search::Filters::Date.each do |value| + params = HTTP::Params.parse("date=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(date: value)) + end + end + + it "Decodes duration= filter" do + Invidious::Search::Filters::Duration.each do |value| + params = HTTP::Params.parse("duration=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(duration: value)) + end + end + + it "Decodes features= filter (single)" do + Invidious::Search::Filters::Features.each do |value| + string = described_class.format_features(value) + params = HTTP::Params.parse("features=#{string}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(features: value)) + end + end + + it "Decodes features= filter (multiple - comma separated)" do + features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons) + params = HTTP::Params.parse("features=vr180%2Ccc%2Chdr") # %2C is a comma + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(features: features)) + end + + it "Decodes features= filter (multiple - URL parameters)" do + features = Invidious::Search::Filters::Features.flags(ThreeSixty, HD, FourK) + params = HTTP::Params.parse("features=4k&features=360&features=hd") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(features: features)) + end + + it "Decodes sort= filter" do + Invidious::Search::Filters::Sort.each do |value| + params = HTTP::Params.parse("sort=#{value}") + + expect(described_class.from_iv_params(params)) + .to eq(described_class.new(sort: value)) + end + end + + it "Ignores junk data" do + params = HTTP::Params.parse("foo=bar&sort=views&answer=42&type=channel") + + expect(described_class.from_iv_params(params)).to eq( + described_class.new( + sort: Invidious::Search::Filters::Sort::Views, + type: Invidious::Search::Filters::Type::Channel + ) + ) + end + end + + # ------------------- + # Encode (URL) + # ------------------- + + describe "#to_iv_params" do + it "Encodes date filter" do + Invidious::Search::Filters::Date.each do |value| + filters = described_class.new(date: value) + params = filters.to_iv_params + + if value.none? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("date=#{value.to_s.underscore}") + end + end + end + + it "Encodes type filter" do + Invidious::Search::Filters::Type.each do |value| + filters = described_class.new(type: value) + params = filters.to_iv_params + + if value.all? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("type=#{value.to_s.underscore}") + end + end + end + + it "Encodes duration filter" do + Invidious::Search::Filters::Duration.each do |value| + filters = described_class.new(duration: value) + params = filters.to_iv_params + + if value.none? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("duration=#{value.to_s.underscore}") + end + end + end + + it "Encodes features filter (single)" do + Invidious::Search::Filters::Features.each do |value| + string = described_class.format_features(value) + filters = described_class.new(features: value) + + expect("#{filters.to_iv_params}") + .to eq("features=" + FEATURES_TEXT[value]) + end + end + + it "Encodes features filter (multiple)" do + features = Invidious::Search::Filters::Features.flags(Subtitles, Live, ThreeSixty) + filters = described_class.new(features: features) + + expect("#{filters.to_iv_params}") + .to eq("features=live%2Csubtitles%2C360") # %2C is a comma + end + + it "Encodes sort filter" do + Invidious::Search::Filters::Sort.each do |value| + filters = described_class.new(sort: value) + params = filters.to_iv_params + + if value.relevance? + expect("#{params}").to eq("") + else + expect("#{params}").to eq("sort=#{value.to_s.underscore}") + end + end + end + + it "Encodes multiple filters" do + filters = described_class.new( + date: Invidious::Search::Filters::Date::Today, + duration: Invidious::Search::Filters::Duration::Medium, + features: Invidious::Search::Filters::Features.flags(Location, Purchased), + sort: Invidious::Search::Filters::Sort::Relevance + ) + + params = filters.to_iv_params + + # Check the `date` param + expect(params).to have_key("date") + expect(params.fetch_all("date")).to contain_exactly("today") + + # Check the `type` param + expect(params).to_not have_key("type") + expect(params["type"]?).to be_nil + + # Check the `duration` param + expect(params).to have_key("duration") + expect(params.fetch_all("duration")).to contain_exactly("medium") + + # Check the `features` param + expect(params).to have_key("features") + expect(params.fetch_all("features")).to contain_exactly("location,purchased") + + # Check the `sort` param + expect(params).to_not have_key("sort") + expect(params["sort"]?).to be_nil + + # Check if there aren't other parameters + params.delete("date") + params.delete("duration") + params.delete("features") + + expect(params).to be_empty + end + end +end diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr new file mode 100644 index 00000000..bf7f21e7 --- /dev/null +++ b/spec/invidious/search/yt_filters_spec.cr @@ -0,0 +1,143 @@ +require "../../../src/invidious/search/filters" + +require "http/params" +require "spectator" + +Spectator.configure do |config| + config.fail_blank + config.randomize +end + +# Encoded filter values are extracted from the search +# page of Youtube with any browser devtools HTML inspector. + +DATE_FILTERS = { + Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D", + Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D", + Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D", + Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D", + Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D", +} + +TYPE_FILTERS = { + Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D", + Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D", + Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D", + Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D", +} + +DURATION_FILTERS = { + Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D", + Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D", + Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D", +} + +FEATURE_FILTERS = { + Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D", + Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D", + Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D", + Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D", + Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D", + Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D", + Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D", + Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D", + Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D", + Invidious::Search::Filters::Features::Location => "EgO4AQE%3D", + Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D", +} + +SORT_FILTERS = { + Invidious::Search::Filters::Sort::Relevance => "", + Invidious::Search::Filters::Sort::Date => "CAI%3D", + Invidious::Search::Filters::Sort::Views => "CAM%3D", + Invidious::Search::Filters::Sort::Rating => "CAE%3D", +} + +Spectator.describe Invidious::Search::Filters do + # ------------------- + # Encode YT params + # ------------------- + + describe "#to_yt_params" do + sample DATE_FILTERS do |value, result| + it "Encodes upload date filter '#{value}'" do + expect(described_class.new(date: value).to_yt_params).to eq(result) + end + end + + sample TYPE_FILTERS do |value, result| + it "Encodes content type filter '#{value}'" do + expect(described_class.new(type: value).to_yt_params).to eq(result) + end + end + + sample DURATION_FILTERS do |value, result| + it "Encodes duration filter '#{value}'" do + expect(described_class.new(duration: value).to_yt_params).to eq(result) + end + end + + sample FEATURE_FILTERS do |value, result| + it "Encodes feature filter '#{value}'" do + expect(described_class.new(features: value).to_yt_params).to eq(result) + end + end + + sample SORT_FILTERS do |value, result| + it "Encodes sort filter '#{value}'" do + expect(described_class.new(sort: value).to_yt_params).to eq(result) + end + end + end + + # ------------------- + # Decode YT params + # ------------------- + + describe "#from_yt_params" do + sample DATE_FILTERS do |value, encoded| + it "Decodes upload date filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(date: value)) + end + end + + sample TYPE_FILTERS do |value, encoded| + it "Decodes content type filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(type: value)) + end + end + + sample DURATION_FILTERS do |value, encoded| + it "Decodes duration filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(duration: value)) + end + end + + sample FEATURE_FILTERS do |value, encoded| + it "Decodes feature filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(features: value)) + end + end + + sample SORT_FILTERS do |value, encoded| + it "Decodes sort filter '#{value}'" do + params = HTTP::Params.parse("sp=#{encoded}") + + expect(described_class.from_yt_params(params)) + .to eq(described_class.new(sort: value)) + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 09320750..6c492e2f 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -8,7 +8,7 @@ require "../src/invidious/channels/*" require "../src/invidious/videos" require "../src/invidious/comments" require "../src/invidious/playlists" -require "../src/invidious/search" +require "../src/invidious/search/ctoken" require "../src/invidious/trending" require "spectator" diff --git a/src/invidious.cr b/src/invidious.cr index a470c6b6..9f3d5d10 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -35,6 +35,7 @@ require "./invidious/frontend/*" require "./invidious/*" require "./invidious/channels/*" require "./invidious/user/*" +require "./invidious/search/*" require "./invidious/routes/**" require "./invidious/jobs/**" diff --git a/src/invidious/exceptions.cr b/src/invidious/exceptions.cr index 490d98cd..bfaa3fd5 100644 --- a/src/invidious/exceptions.cr +++ b/src/invidious/exceptions.cr @@ -1,3 +1,11 @@ +# Exception used to hold the bogus UCID during a channel search. +class ChannelSearchException < InfoException + getter channel : String + + def initialize(@channel) + end +end + # Exception used to hold the name of the missing item # Should be used in all parsing functions class BrokenTubeException < Exception diff --git a/src/invidious/frontend/misc.cr b/src/invidious/frontend/misc.cr new file mode 100644 index 00000000..43ba9f5c --- /dev/null +++ b/src/invidious/frontend/misc.cr @@ -0,0 +1,14 @@ +module Invidious::Frontend::Misc + extend self + + def redirect_url(env : HTTP::Server::Context) + prefs = env.get("preferences").as(Preferences) + + if prefs.automatic_instance_redirect + current_page = env.get?("current_page").as(String) + redirect_url = "/redirect?referer=#{current_page}" + else + redirect_url = "https://redirect.invidious.io#{env.request.resource}" + end + end +end diff --git a/src/invidious/frontend/search_filters.cr b/src/invidious/frontend/search_filters.cr new file mode 100644 index 00000000..68f27b4f --- /dev/null +++ b/src/invidious/frontend/search_filters.cr @@ -0,0 +1,135 @@ +module Invidious::Frontend::SearchFilters + extend self + + # Generate the search filters collapsable widget. + def generate(filters : Search::Filters, query : String, page : Int, locale : String) : String + return String.build(8000) do |str| + str << "
\n" + str << "\t
" + str << "\t\t" << translate(locale, "search_filters_title") << "\n" + + str << "\t\t
\n" + + str << "\t\t\t\n" + str << "\t\t\t\n" + + str << "\t\t\t
" + + filter_wrapper(date) + filter_wrapper(type) + filter_wrapper(duration) + filter_wrapper(features) + filter_wrapper(sort) + + str << "\t\t\t
\n" + + str << "\t\t\t
" + str << "
\n" + + str << "\t\t
\n" + + str << "\t
\n" + str << "
\n" + end + end + + # Generate wrapper HTML (`
`, filter name, etc...) around the + # `` elements of a search filter + macro filter_wrapper(name) + str << "\t\t\t\t
\n" + + str << "\t\t\t\t\t
" + str << translate(locale, "search_filters_{{name}}_label") + str << "
\n" + + str << "\t\t\t\t\t
\n" + make_{{name}}_filter_options(str, filters.{{name}}, locale) + str << "\t\t\t\t\t
" + + str << "\t\t\t\t
\n" + end + + # Generates the HTML for the list of radio buttons of the "date" search filter + def make_date_filter_options(str : String::Builder, value : Search::Filters::Date, locale : String) + {% for value in Invidious::Search::Filters::Date.constants %} + {% date = value.underscore %} + + str << "\t\t\t\t\t\t
" + str << "' + + str << "
\n" + {% end %} + end + + # Generates the HTML for the list of radio buttons of the "type" search filter + def make_type_filter_options(str : String::Builder, value : Search::Filters::Type, locale : String) + {% for value in Invidious::Search::Filters::Type.constants %} + {% type = value.underscore %} + + str << "\t\t\t\t\t\t
" + str << "' + + str << "
\n" + {% end %} + end + + # Generates the HTML for the list of radio buttons of the "duration" search filter + def make_duration_filter_options(str : String::Builder, value : Search::Filters::Duration, locale : String) + {% for value in Invidious::Search::Filters::Duration.constants %} + {% duration = value.underscore %} + + str << "\t\t\t\t\t\t
" + str << "' + + str << "
\n" + {% end %} + end + + # Generates the HTML for the list of checkboxes of the "features" search filter + def make_features_filter_options(str : String::Builder, value : Search::Filters::Features, locale : String) + {% for value in Invidious::Search::Filters::Features.constants %} + {% if value.stringify != "All" && value.stringify != "None" %} + {% feature = value.underscore %} + + str << "\t\t\t\t\t\t
" + str << "' + + str << "
\n" + {% end %} + {% end %} + end + + # Generates the HTML for the list of radio buttons of the "sort" search filter + def make_sort_filter_options(str : String::Builder, value : Search::Filters::Sort, locale : String) + {% for value in Invidious::Search::Filters::Sort.constants %} + {% sort = value.underscore %} + + str << "\t\t\t\t\t\t
" + str << "' + + str << "
\n" + {% end %} + end +end diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index c4d6643a..8650976d 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -251,18 +251,22 @@ module Invidious::Routes::API::V1::Channels def self.search(env) locale = env.get("preferences").as(Preferences).locale + region = env.params.query["region"]? env.response.content_type = "application/json" - ucid = env.params.url["ucid"] + query = Invidious::Search::Query.new(env.params.query, :channel, region) - query = env.params.query["q"]? - query ||= "" + # Required because we can't (yet) pass multiple parameter to the + # `Search::Query` initializer (in this case, an URL segment) + query.channel = env.params.url["ucid"] - page = env.params.query["page"]?.try &.to_i? - page ||= 1 + begin + search_results = query.process + rescue ex + return error_json(400, ex) + end - search_results = channel_search(query, page, ucid) JSON.build do |json| json.array do search_results.each do |item| diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 5666460d..21451d33 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -5,34 +5,14 @@ module Invidious::Routes::API::V1::Search env.response.content_type = "application/json" - query = env.params.query["q"]? - query ||= "" - - page = env.params.query["page"]?.try &.to_i? - page ||= 1 - - sort_by = env.params.query["sort_by"]?.try &.downcase - sort_by ||= "relevance" - - date = env.params.query["date"]?.try &.downcase - date ||= "" - - duration = env.params.query["duration"]?.try &.downcase - duration ||= "" - - features = env.params.query["features"]?.try &.split(",").map(&.downcase) - features ||= [] of String - - content_type = env.params.query["type"]?.try &.downcase - content_type ||= "video" + query = Invidious::Search::Query.new(env.params.query, :regular, region) begin - search_params = produce_search_params(page, sort_by, date, content_type, duration, features) + search_results = query.process rescue ex return error_json(400, ex) end - search_results = search(query, search_params, region) JSON.build do |json| json.array do search_results.each do |item| diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index dbeb4f97..de981d81 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -212,7 +212,10 @@ module Invidious::Routes::Playlists end def self.add_playlist_items_page(env) - locale = env.get("preferences").as(Preferences).locale + prefs = env.get("preferences").as(Preferences) + locale = prefs.locale + + region = env.params.query["region"]? || prefs.region user = env.get? "user" sid = env.get? "sid" @@ -236,15 +239,10 @@ module Invidious::Routes::Playlists return env.redirect referer end - query = env.params.query["q"]? - if query - begin - search_query, items, operators = process_search_query(query, page, user, region: nil) - videos = items.select(SearchVideo).map(&.as(SearchVideo)) - rescue ex - videos = [] of SearchVideo - end - else + begin + query = Invidious::Search::Query.new(env.params.query, :playlist, region) + videos = query.process.select(SearchVideo).map(&.as(SearchVideo)) + rescue ex videos = [] of SearchVideo end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 3f4c7e5e..e60d0081 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -37,37 +37,29 @@ module Invidious::Routes::Search end def self.search(env) - locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? + prefs = env.get("preferences").as(Preferences) + locale = prefs.locale - query = env.params.query["search_query"]? - query ||= env.params.query["q"]? + region = env.params.query["region"]? || prefs.region - if !query || query.empty? + query = Invidious::Search::Query.new(env.params.query, :regular, region) + + if query.empty? # Display the full page search box implemented in #1977 env.set "search", "" templated "search_homepage", navbar_search: false else - page = env.params.query["page"]?.try &.to_i? - page ||= 1 - user = env.get? "user" begin - search_query, videos, operators = process_search_query(query, page, user, region: region) + videos = query.process rescue ex : ChannelSearchException return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") rescue ex return error_template(500, ex) end - operator_hash = {} of String => String - operators.each do |operator| - key, value = operator.downcase.split(":") - operator_hash[key] = value - end - - env.set "search", query + env.set "search", query.text templated "search" end end diff --git a/src/invidious/search.cr b/src/invidious/search.cr deleted file mode 100644 index ae106bf6..00000000 --- a/src/invidious/search.cr +++ /dev/null @@ -1,254 +0,0 @@ -class ChannelSearchException < InfoException - getter channel : String - - def initialize(@channel) - end -end - -def channel_search(query, page, channel) : Array(SearchItem) - response = YT_POOL.client &.get("/channel/#{channel}") - - if response.status_code == 404 - response = YT_POOL.client &.get("/user/#{channel}") - response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404 - initial_data = extract_initial_data(response.body) - ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?) - raise ChannelSearchException.new(channel) if !ucid - else - ucid = channel - end - - continuation = produce_channel_search_continuation(ucid, 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 - - return items -end - -def search(query, search_params = produce_search_params(content_type: "all"), region = nil) : Array(SearchItem) - return [] of SearchItem if query.empty? - - client_config = YoutubeAPI::ClientConfig.new(region: region) - initial_data = YoutubeAPI.search(query, search_params, client_config: client_config) - - return extract_items(initial_data) -end - -def produce_search_params(page = 1, sort : String = "relevance", date : String = "", content_type : String = "", - duration : String = "", features : Array(String) = [] of String) - object = { - "1:varint" => 0_i64, - "2:embedded" => {} of String => Int64, - "9:varint" => ((page - 1) * 20).to_i64, - } - - case sort - when "relevance" - object["1:varint"] = 0_i64 - when "rating" - object["1:varint"] = 1_i64 - when "upload_date", "date" - object["1:varint"] = 2_i64 - when "view_count", "views" - object["1:varint"] = 3_i64 - else - raise "No sort #{sort}" - end - - case date - when "hour" - object["2:embedded"].as(Hash)["1:varint"] = 1_i64 - when "today" - object["2:embedded"].as(Hash)["1:varint"] = 2_i64 - when "week" - object["2:embedded"].as(Hash)["1:varint"] = 3_i64 - when "month" - object["2:embedded"].as(Hash)["1:varint"] = 4_i64 - when "year" - object["2:embedded"].as(Hash)["1:varint"] = 5_i64 - else nil # Ignore - end - - case content_type - when "video" - object["2:embedded"].as(Hash)["2:varint"] = 1_i64 - when "channel" - object["2:embedded"].as(Hash)["2:varint"] = 2_i64 - when "playlist" - object["2:embedded"].as(Hash)["2:varint"] = 3_i64 - when "movie" - object["2:embedded"].as(Hash)["2:varint"] = 4_i64 - when "show" - object["2:embedded"].as(Hash)["2:varint"] = 5_i64 - when "all" - # - else - object["2:embedded"].as(Hash)["2:varint"] = 1_i64 - end - - case duration - when "short" - object["2:embedded"].as(Hash)["3:varint"] = 1_i64 - when "long" - object["2:embedded"].as(Hash)["3:varint"] = 2_i64 - else nil # Ignore - end - - features.each do |feature| - case feature - when "hd" - object["2:embedded"].as(Hash)["4:varint"] = 1_i64 - when "subtitles" - object["2:embedded"].as(Hash)["5:varint"] = 1_i64 - when "creative_commons", "cc" - object["2:embedded"].as(Hash)["6:varint"] = 1_i64 - when "3d" - object["2:embedded"].as(Hash)["7:varint"] = 1_i64 - when "live", "livestream" - object["2:embedded"].as(Hash)["8:varint"] = 1_i64 - when "purchased" - object["2:embedded"].as(Hash)["9:varint"] = 1_i64 - when "4k" - object["2:embedded"].as(Hash)["14:varint"] = 1_i64 - when "360" - object["2:embedded"].as(Hash)["15:varint"] = 1_i64 - when "location" - object["2:embedded"].as(Hash)["23:varint"] = 1_i64 - when "hdr" - object["2:embedded"].as(Hash)["25:varint"] = 1_i64 - else nil # Ignore - end - end - - if object["2:embedded"].as(Hash).empty? - object.delete("2:embedded") - end - - params = 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 params -end - -def produce_channel_search_continuation(ucid, query, page) - if page <= 1 - idx = 0_i64 - else - idx = 30_i64 * (page - 1) - end - - object = { - "80226972:embedded" => { - "2:string" => ucid, - "3:base64" => { - "2:string" => "search", - "6:varint" => 1_i64, - "7:varint" => 1_i64, - "12:varint" => 1_i64, - "15:base64" => { - "3:varint" => idx, - }, - "23:varint" => 0_i64, - }, - "11:string" => query, - "35:string" => "browse-feed#{ucid}search", - }, - } - - 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 continuation -end - -def process_search_query(query, page, user, region) - if user - user = user.as(Invidious::User) - view_name = "subscriptions_#{sha256(user.email)}" - end - - channel = nil - content_type = "all" - date = "" - duration = "" - features = [] of String - sort = "relevance" - subscriptions = nil - - operators = query.split(" ").select(&.match(/\w+:[\w,]+/)) - operators.each do |operator| - key, value = operator.downcase.split(":") - - case key - when "channel", "user" - channel = operator.split(":")[-1] - when "content_type", "type" - content_type = value - when "date" - date = value - when "duration" - duration = value - when "feature", "features" - features = value.split(",") - when "sort" - sort = value - when "subscriptions" - subscriptions = value == "true" - else - operators.delete(operator) - end - end - - search_query = (query.split(" ") - operators).join(" ") - - if channel - items = channel_search(search_query, page, channel) - elsif subscriptions - if view_name - items = PG_DB.query_all("SELECT id,title,published,updated,ucid,author,length_seconds FROM ( - SELECT *, - to_tsvector(#{view_name}.title) || - to_tsvector(#{view_name}.author) - as document - FROM #{view_name} - ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;", search_query, (page - 1) * 20, as: ChannelVideo) - else - items = [] of ChannelVideo - end - else - search_params = produce_search_params(page: page, sort: sort, date: date, content_type: content_type, - duration: duration, features: features) - - items = search(search_query, search_params, region) - end - - # Light processing to flatten search results out of Categories. - # They should ideally be supported in the future. - items_without_category = [] of SearchItem | ChannelVideo - items.each do |i| - if i.is_a? Category - i.contents.each do |nest_i| - if !nest_i.is_a? Video - items_without_category << nest_i - end - end - else - items_without_category << i - end - end - - {search_query, items_without_category, operators} -end diff --git a/src/invidious/search/ctoken.cr b/src/invidious/search/ctoken.cr new file mode 100644 index 00000000..161065e0 --- /dev/null +++ b/src/invidious/search/ctoken.cr @@ -0,0 +1,32 @@ +def produce_channel_search_continuation(ucid, query, page) + if page <= 1 + idx = 0_i64 + else + idx = 30_i64 * (page - 1) + end + + object = { + "80226972:embedded" => { + "2:string" => ucid, + "3:base64" => { + "2:string" => "search", + "6:varint" => 1_i64, + "7:varint" => 1_i64, + "12:varint" => 1_i64, + "15:base64" => { + "3:varint" => idx, + }, + "23:varint" => 0_i64, + }, + "11:string" => query, + "35:string" => "browse-feed#{ucid}search", + }, + } + + 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 continuation +end diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr new file mode 100644 index 00000000..c2b5c758 --- /dev/null +++ b/src/invidious/search/filters.cr @@ -0,0 +1,376 @@ +require "protodec/utils" +require "http/params" + +module Invidious::Search + struct Filters + # Values correspond to { "2:embedded": { "1:varint": }} + # except for "None" which is only used by us (= nothing selected) + enum Date + None = 0 + Hour = 1 + Today = 2 + Week = 3 + Month = 4 + Year = 5 + end + + # Values correspond to { "2:embedded": { "2:varint": }} + # except for "All" which is only used by us (= nothing selected) + enum Type + All = 0 + Video = 1 + Channel = 2 + Playlist = 3 + Movie = 4 + + # Has it been removed? + # (Not available on youtube's UI) + Show = 5 + end + + # Values correspond to { "2:embedded": { "3:varint": }} + # except for "None" which is only used by us (= nothing selected) + enum Duration + None = 0 + Short = 1 # "Under 4 minutes" + Long = 2 # "Over 20 minutes" + Medium = 3 # "4 - 20 minutes" + end + + # Note: flag enums automatically generate + # "none" and "all" members + @[Flags] + enum Features + Live + FourK # "4K" + HD + Subtitles # "Subtitles/CC" + CCommons # "Creative Commons" + ThreeSixty # "360°" + VR180 + ThreeD # "3D" + HDR + Location + Purchased + end + + # Values correspond to { "1:varint": } + enum Sort + Relevance = 0 + Rating = 1 + Date = 2 + Views = 3 + end + + # Parameters are sorted as on Youtube + property date : Date + property type : Type + property duration : Duration + property features : Features + property sort : Sort + + def initialize( + *, # All parameters must be named + @date : Date = Date::None, + @type : Type = Type::All, + @duration : Duration = Duration::None, + @features : Features = Features::None, + @sort : Sort = Sort::Relevance + ) + end + + def default? : Bool + return @date.none? && @type.all? && @duration.none? && \ + @features.none? && @sort.relevance? + end + + # ------------------- + # Invidious params + # ------------------- + + def self.parse_features(raw : Array(String)) : Features + # Initialize return variable + features = Features.new(0) + + raw.each do |ft| + case ft.downcase + when "live", "livestream" + features = features | Features::Live + when "4k" then features = features | Features::FourK + when "hd" then features = features | Features::HD + when "subtitles" then features = features | Features::Subtitles + when "creative_commons", "commons", "cc" + features = features | Features::CCommons + when "360" then features = features | Features::ThreeSixty + when "vr180" then features = features | Features::VR180 + when "3d" then features = features | Features::ThreeD + when "hdr" then features = features | Features::HDR + when "location" then features = features | Features::Location + when "purchased" then features = features | Features::Purchased + end + end + + return features + end + + def self.format_features(features : Features) : String + # Directly return an empty string if there are no features + return "" if features.none? + + # Initialize return variable + str = [] of String + + str << "live" if features.live? + str << "4k" if features.four_k? + str << "hd" if features.hd? + str << "subtitles" if features.subtitles? + str << "commons" if features.c_commons? + str << "360" if features.three_sixty? + str << "vr180" if features.vr180? + str << "3d" if features.three_d? + str << "hdr" if features.hdr? + str << "location" if features.location? + str << "purchased" if features.purchased? + + return str.join(',') + end + + def self.from_legacy_filters(str : String) : {Filters, String, String, Bool} + # Split search query on spaces + members = str.split(' ') + + # Output variables + channel = "" + filters = Filters.new + subscriptions = false + + # Array to hold the non-filter members + query = [] of String + + # Parse! + members.each do |substr| + # Separator operators + operators = substr.split(':') + + case operators[0] + when "user", "channel" + next if operators.size != 2 + channel = operators[1] + # + when "type", "content_type" + next if operators.size != 2 + type = Type.parse?(operators[1]) + filters.type = type if !type.nil? + # + when "date" + next if operators.size != 2 + date = Date.parse?(operators[1]) + filters.date = date if !date.nil? + # + when "duration" + next if operators.size != 2 + duration = Duration.parse?(operators[1]) + filters.duration = duration if !duration.nil? + # + when "feature", "features" + next if operators.size != 2 + features = parse_features(operators[1].split(',')) + filters.features = features if !features.nil? + # + when "sort" + next if operators.size != 2 + sort = Sort.parse?(operators[1]) + filters.sort = sort if !sort.nil? + # + when "subscriptions" + next if operators.size != 2 + subscriptions = {"true", "on", "yes", "1"}.any?(&.== operators[1]) + # + else + query << substr + end + end + + # Re-assemble query (without filters) + cleaned_query = query.join(' ') + + return {filters, channel, cleaned_query, subscriptions} + end + + def self.from_iv_params(params : HTTP::Params) : Filters + # Temporary variables + filters = Filters.new + + if type = params["type"]? + filters.type = Type.parse?(type) || Type::All + params.delete("type") + end + + if date = params["date"]? + filters.date = Date.parse?(date) || Date::None + params.delete("date") + end + + if duration = params["duration"]? + filters.duration = Duration.parse?(duration) || Duration::None + params.delete("duration") + end + + features = params.fetch_all("features") + if !features.empty? + # Un-array input so it can be treated as a comma-separated list + features = features[0].split(',') if features.size == 1 + + filters.features = parse_features(features) || Features::None + params.delete_all("features") + end + + if sort = params["sort"]? + filters.sort = Sort.parse?(sort) || Sort::Relevance + params.delete("sort") + end + + return filters + end + + def to_iv_params : HTTP::Params + # Temporary variables + raw_params = {} of String => Array(String) + + raw_params["date"] = [@date.to_s.underscore] if !@date.none? + raw_params["type"] = [@type.to_s.underscore] if !@type.all? + raw_params["sort"] = [@sort.to_s.underscore] if !@sort.relevance? + + if !@duration.none? + raw_params["duration"] = [@duration.to_s.underscore] + end + + if !@features.none? + raw_params["features"] = [Filters.format_features(@features)] + end + + return HTTP::Params.new(raw_params) + end + + # ------------------- + # Youtube params + # ------------------- + + # Produce the youtube search parameters for the + # innertube API (base64-encoded protobuf object). + def to_yt_params(page : Int = 1) : String + # Initialize the embedded protobuf object + embedded = {} of String => Int64 + + # Add these field only if associated parameter is selected + embedded["1:varint"] = @date.to_i64 if !@date.none? + embedded["2:varint"] = @type.to_i64 if !@type.all? + embedded["3:varint"] = @duration.to_i64 if !@duration.none? + + if !@features.none? + # All features have a value of "1" when enabled, and + # the field is omitted when the feature is no selected. + embedded["4:varint"] = 1_i64 if @features.includes?(Features::HD) + embedded["5:varint"] = 1_i64 if @features.includes?(Features::Subtitles) + embedded["6:varint"] = 1_i64 if @features.includes?(Features::CCommons) + embedded["7:varint"] = 1_i64 if @features.includes?(Features::ThreeD) + embedded["8:varint"] = 1_i64 if @features.includes?(Features::Live) + embedded["9:varint"] = 1_i64 if @features.includes?(Features::Purchased) + embedded["14:varint"] = 1_i64 if @features.includes?(Features::FourK) + embedded["15:varint"] = 1_i64 if @features.includes?(Features::ThreeSixty) + embedded["23:varint"] = 1_i64 if @features.includes?(Features::Location) + embedded["25:varint"] = 1_i64 if @features.includes?(Features::HDR) + embedded["26:varint"] = 1_i64 if @features.includes?(Features::VR180) + end + + # Initialize an empty protobuf object + object = {} of String => (Int64 | String | Hash(String, Int64)) + + # As usual, everything can be omitted if it has no value + object["2:embedded"] = embedded if !embedded.empty? + + # Default sort is "relevance", so when this option is selected, + # the associated field can be omitted. + if !@sort.relevance? + object["1:varint"] = @sort.to_i64 + end + + # Add page number (if provided) + if page > 1 + object["9:varint"] = ((page - 1) * 20).to_i64 + end + + # If the object is empty, return an empty string, + # otherwise encode to protobuf then to base64 + return "" if object.empty? + + return 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) } + end + + # Function to parse the `sp` URL parameter from Youtube + # search page. It's a base64-encoded protobuf object. + def self.from_yt_params(params : HTTP::Params) : Filters + # Initialize output variable + filters = Filters.new + + # Get parameter, and check emptyness + search_params = params["sp"]? + + if search_params.nil? || search_params.empty? + return filters + end + + # Decode protobuf object + object = search_params + .try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + + # Parse items from embedded object + if embedded = object["2:0:embedded"]? + # All the following fields (date, type, duration) are optional. + if date = embedded["1:0:varint"]? + filters.date = Date.from_value?(date.as_i) || Date::None + end + + if type = embedded["2:0:varint"]? + filters.type = Type.from_value?(type.as_i) || Type::All + end + + if duration = embedded["3:0:varint"]? + filters.duration = Duration.from_value?(duration.as_i) || Duration::None + end + + # All features should have a value of "1" when enabled, and + # the field should be omitted when the feature is no selected. + features = 0 + features += (embedded["4:0:varint"]?.try &.as_i == 1_i64) ? Features::HD.value : 0 + features += (embedded["5:0:varint"]?.try &.as_i == 1_i64) ? Features::Subtitles.value : 0 + features += (embedded["6:0:varint"]?.try &.as_i == 1_i64) ? Features::CCommons.value : 0 + features += (embedded["7:0:varint"]?.try &.as_i == 1_i64) ? Features::ThreeD.value : 0 + features += (embedded["8:0:varint"]?.try &.as_i == 1_i64) ? Features::Live.value : 0 + features += (embedded["9:0:varint"]?.try &.as_i == 1_i64) ? Features::Purchased.value : 0 + features += (embedded["14:0:varint"]?.try &.as_i == 1_i64) ? Features::FourK.value : 0 + features += (embedded["15:0:varint"]?.try &.as_i == 1_i64) ? Features::ThreeSixty.value : 0 + features += (embedded["23:0:varint"]?.try &.as_i == 1_i64) ? Features::Location.value : 0 + features += (embedded["25:0:varint"]?.try &.as_i == 1_i64) ? Features::HDR.value : 0 + features += (embedded["26:0:varint"]?.try &.as_i == 1_i64) ? Features::VR180.value : 0 + + filters.features = Features.from_value?(features) || Features::None + end + + if sort = object["1:0:varint"]? + filters.sort = Sort.from_value?(sort.as_i) || Sort::Relevance + end + + # Remove URL parameter and return result + params.delete("sp") + return filters + end + end +end diff --git a/src/invidious/search/processors.cr b/src/invidious/search/processors.cr new file mode 100644 index 00000000..d1409c06 --- /dev/null +++ b/src/invidious/search/processors.cr @@ -0,0 +1,64 @@ +module Invidious::Search + module Processors + extend self + + # Regular search (`/search` endpoint) + def regular(query : Query) : Array(SearchItem) + search_params = query.filters.to_yt_params(page: query.page) + + 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) + end + + # Search a youtube channel + # TODO: clean code, and rely more on YoutubeAPI + def channel(query : Query) : Array(SearchItem) + response = YT_POOL.client &.get("/channel/#{query.channel}") + + if response.status_code == 404 + response = YT_POOL.client &.get("/user/#{query.channel}") + response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404 + initial_data = extract_initial_data(response.body) + ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?) + raise ChannelSearchException.new(query.channel) if !ucid + else + ucid = query.channel + end + + 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 + + return items + end + + # Search inside of user subscriptions + def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo) + view_name = "subscriptions_#{sha256(user.email)}" + + return PG_DB.query_all(" + SELECT id,title,published,updated,ucid,author,length_seconds + FROM ( + SELECT *, + to_tsvector(#{view_name}.title) || + to_tsvector(#{view_name}.author) + as document + FROM #{view_name} + ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;", + query.text, (query.page - 1) * 20, + as: ChannelVideo + ) + end + end +end diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr new file mode 100644 index 00000000..1c2b37d2 --- /dev/null +++ b/src/invidious/search/query.cr @@ -0,0 +1,148 @@ +module Invidious::Search + class Query + enum Type + # Types related to YouTube + Regular # Youtube search page + Channel # Youtube channel search box + + # Types specific to Invidious + Subscriptions # Search user subscriptions + Playlist # "Add playlist item" search + end + + @type : Type = Type::Regular + + @raw_query : String + @query : String = "" + + property filters : Filters = Filters.new + property page : Int32 + property region : String? + property channel : String = "" + + # Return true if @raw_query is either `nil` or empty + private def empty_raw_query? + return @raw_query.empty? + end + + # Same as `empty_raw_query?`, but named for external use + def empty? + return self.empty_raw_query? + end + + # Getter for the query string. + # It is named `text` to reduce confusion (`search_query.text` makes more + # sense than `search_query.query`) + def text + return @query + end + + # Initialize a new search query. + # Parameters are used to get the query string, the page number + # and the search filters (if any). Type tells this function + # where it is being called from (See `Type` above). + def initialize( + params : HTTP::Params, + @type : Type = Type::Regular, + @region : String? = nil + ) + # Get the raw search query string (common to all search types). In + # Regular search mode, also look for the `search_query` URL parameter + if @type.regular? + @raw_query = params["q"]? || params["search_query"]? || "" + else + @raw_query = params["q"]? || "" + end + + # Get the page number (also common to all search types) + @page = params["page"]?.try &.to_i? || 1 + + # Stop here is raw query in empty + # NOTE: maybe raise in the future? + return if self.empty_raw_query? + + # Specific handling + case @type + when .playlist?, .channel? + # In "add playlist item" mode, filters are parsed from the query + # string itself (legacy), and the channel is ignored. + # + # In "channel search" mode, filters are ignored, but we still parse + # the query prevent transmission of legacy filters to youtube. + # + @filters, @query, @channel, _ = Filters.from_legacy_filters(@raw_query || "") + # + when .subscriptions?, .regular? + if params["sp"]? + # Parse the `sp` URL parameter (youtube compatibility) + @filters = Filters.from_yt_params(params) + @query = @raw_query || "" + else + # Parse invidious URL parameters (sort, date, etc...) + @filters = Filters.from_iv_params(params) + @channel = params["channel"]? || "" + + if @filters.default? && @raw_query.includes?(':') + # Parse legacy filters from query + @filters, @query, @channel, subs = Filters.from_legacy_filters(@raw_query || "") + else + @query = @raw_query || "" + end + + if !@channel.empty? + # Switch to channel search mode (filters will be ignored) + @type = Type::Channel + elsif subs + # Switch to subscriptions search mode + @type = Type::Subscriptions + end + end + end + end + + # Run the search query using the corresponding search processor. + # Returns either the results or an empty array of `SearchItem`. + def process(user : Invidious::User? = nil) : Array(SearchItem) | Array(ChannelVideo) + items = [] of SearchItem + + # Don't bother going further if search query is empty + return items if self.empty_raw_query? + + case @type + when .regular?, .playlist? + items = unnest_items(Processors.regular(self)) + # + when .channel? + items = Processors.channel(self) + # + when .subscriptions? + if user + items = Processors.subscriptions(self, user.as(Invidious::User)) + end + end + + return items + end + + # TODO: clean code + private def unnest_items(all_items) : Array(SearchItem) + items = [] of SearchItem + + # Light processing to flatten search results out of Categories. + # They should ideally be supported in the future. + all_items.each do |i| + if i.is_a? Category + i.contents.each do |nest_i| + if !nest_i.is_a? Video + items << nest_i + end + end + else + items << i + end + end + + return items + end + end +end diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index ad50909a..22870317 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -11,7 +11,9 @@ <%= translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %>
- value="<%= HTML.escape(query) %>"<% else %>placeholder="<%= translate(locale, "Search for videos") %>"<% end %>> + value="<%= HTML.escape(query.text) %>"<% end %> + placeholder="<%= translate(locale, "Search for videos") %>">
@@ -38,10 +40,11 @@
<% if query %> + <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%>
- <% if page > 1 %> - + <% if query.page > 1 %> + <%= translate(locale, "Previous page") %> <% end %> @@ -49,7 +52,7 @@
<% if videos.size >= 20 %> - + <%= translate(locale, "Next page") %> <% end %> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 45bbdefc..7110703e 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -1,147 +1,62 @@ <% content_for "header" do %> -<%= search_query.not_nil!.size > 30 ? HTML.escape(query.not_nil![0,30].rstrip(".") + "...") : HTML.escape(query.not_nil!) %> - Invidious +<%= query.text.size > 30 ? HTML.escape(query.text[0,30].rstrip(".")) + "…" : HTML.escape(query.text) %> - Invidious + <% end %> -<% search_query_encoded = env.get?("search").try { |x| URI.encode_www_form(x.as(String), space_to_plus: true) } %> +<%- + search_query_encoded = URI.encode_www_form(query.text, space_to_plus: true) + filter_params = query.filters.to_iv_params + + url_prev_page = "/search?q=#{search_query_encoded}&#{filter_params}&page=#{query.page - 1}" + url_next_page = "/search?q=#{search_query_encoded}&#{filter_params}&page=#{query.page + 1}" + + redirect_url = Invidious::Frontend::Misc.redirect_url(env) +-%> -<% if videos.size == 0 %> -

- "><%= translate(locale, "Broken? Try another Invidious Instance!") %> -

-<% else %> -
- -

<%= translate(locale, "filter") %>

-
-
-
- <%= translate(locale, "date") %> -
- <% ["hour", "today", "week", "month", "year"].each do |date| %> -
- <% if operator_hash.fetch("date", "all") == date %> - <%= translate(locale, date) %> - <% else %> - &page=<%= page %>"> - <%= translate(locale, date) %> - - <% end %> -
- <% end %> -
-
- <%= translate(locale, "content_type") %> -
- <% ["video", "channel", "playlist", "movie", "show"].each do |content_type| %> -
- <% if operator_hash.fetch("content_type", "all") == content_type %> - <%= translate(locale, content_type) %> - <% else %> - &page=<%= page %>"> - <%= translate(locale, content_type) %> - - <% end %> -
- <% end %> -
-
- <%= translate(locale, "duration") %> -
- <% ["short", "long"].each do |duration| %> -
- <% if operator_hash.fetch("duration", "all") == duration %> - <%= translate(locale, duration) %> - <% else %> - &page=<%= page %>"> - <%= translate(locale, duration) %> - - <% end %> -
- <% end %> -
-
- <%= translate(locale, "features") %> -
- <% ["hd", "subtitles", "creative_commons", "3d", "live", "purchased", "4k", "360", "location", "hdr"].each do |feature| %> -
- <% if operator_hash.fetch("features", "all").includes?(feature) %> - <%= translate(locale, feature) %> - <% elsif operator_hash.has_key?("features") %> - &page=<%= page %>"> - <%= translate(locale, feature) %> - - <% else %> - &page=<%= page %>"> - <%= translate(locale, feature) %> - - <% end %> -
- <% end %> -
-
- <%= translate(locale, "sort") %> -
- <% ["relevance", "rating", "date", "views"].each do |sort| %> -
- <% if operator_hash.fetch("sort", "relevance") == sort %> - <%= translate(locale, sort) %> - <% else %> - &page=<%= page %>"> - <%= translate(locale, sort) %> - - <% end %> -
- <% end %> -
-
-
-<% end %> - -<% if videos.size == 0 %> -
-<% else %> -
-<% end %> +<%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %> +
- <% if page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> + <%- if query.page > 1 -%> + <%= translate(locale, "Previous page") %> + <%- end -%>
- <% if videos.size >= 20 %> - - <%= translate(locale, "Next page") %> - - <% end %> + <%- if videos.size >= 20 -%> + <%= translate(locale, "Next page") %> + <%- end -%>
-
- <% videos.each do |item| %> - <%= rendered "components/item" %> - <% end %> +<%- if videos.empty? -%> +
+
+ <%= translate(locale, "search_message_no_results") %>

+ <%= translate(locale, "search_message_change_filters_or_query") %>

+ <%= translate(locale, "search_message_use_another_instance", redirect_url) %> +
+<%- else -%> +
+ <%- videos.each do |item| -%> + <%= rendered "components/item" %> + <%- end -%> +
+<%- end -%>
- <% if page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> + <%- if query.page > 1 -%> + <%= translate(locale, "Previous page") %> + <%- end -%>
- <% if videos.size >= 20 %> - - <%= translate(locale, "Next page") %> - - <% end %> + <%- if videos.size >= 20 -%> + <%= translate(locale, "Next page") %> + <%- end -%>