summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/stale.yml2
-rw-r--r--README.md2
-rw-r--r--assets/css/default.css20
-rw-r--r--assets/js/watched_indicator.js24
-rw-r--r--locales/de.json9
-rw-r--r--locales/el.json4
-rw-r--r--locales/en-US.json2
-rw-r--r--locales/es.json106
-rw-r--r--locales/hi.json11
-rw-r--r--locales/hr.json6
-rw-r--r--locales/ja.json24
-rw-r--r--locales/pt-BR.json7
-rw-r--r--locales/pt-PT.json9
-rw-r--r--locales/ru.json30
-rw-r--r--locales/sq.json6
-rw-r--r--locales/tr.json2
-rw-r--r--src/invidious/routes/account.cr1
-rw-r--r--src/invidious/routes/api/v1/authenticated.cr82
-rw-r--r--src/invidious/routes/api/v1/misc.cr27
-rw-r--r--src/invidious/routes/subscriptions.cr29
-rw-r--r--src/invidious/routing.cr9
-rw-r--r--src/invidious/trending.cr7
-rw-r--r--src/invidious/user/exports.cr35
-rw-r--r--src/invidious/views/add_playlist_items.ecr2
-rw-r--r--src/invidious/views/channel.ecr2
-rw-r--r--src/invidious/views/components/item.ecr17
-rw-r--r--src/invidious/views/edit_playlist.ecr2
-rw-r--r--src/invidious/views/feeds/playlists.ecr2
-rw-r--r--src/invidious/views/feeds/popular.ecr2
-rw-r--r--src/invidious/views/feeds/subscriptions.ecr2
-rw-r--r--src/invidious/views/feeds/trending.ecr2
-rw-r--r--src/invidious/views/hashtag.ecr2
-rw-r--r--src/invidious/views/playlist.ecr2
-rw-r--r--src/invidious/views/search.ecr2
34 files changed, 365 insertions, 126 deletions
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 11168aea..a945da58 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -23,4 +23,4 @@ jobs:
stale-pr-label: "stale"
ascending: true
# Never mark feature requests/enhancements as stale
- exempt-issue-labels: "feature-request,enhancement"
+ exempt-issue-labels: "feature-request,enhancement,exempt-stale"
diff --git a/README.md b/README.md
index 8d668a29..abf57e38 100644
--- a/README.md
+++ b/README.md
@@ -154,6 +154,8 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
- [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV.
- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client.
- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API)
+- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV
+- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android
## Liability
diff --git a/assets/css/default.css b/assets/css/default.css
index 9788e9f7..24910610 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -145,6 +145,24 @@ img.thumbnail {
object-fit: cover;
}
+div.watched-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(255,255,255,.4);
+}
+
+div.watched-indicator {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ height: 4px;
+ width: 100%;
+ background-color: red;
+}
+
.length {
z-index: 100;
position: absolute;
@@ -497,7 +515,7 @@ hr {
#descexpansionbutton ~ div {
overflow: hidden;
- height: 8.3em;
+ max-height: 8.3em;
}
#descexpansionbutton:checked ~ div {
diff --git a/assets/js/watched_indicator.js b/assets/js/watched_indicator.js
new file mode 100644
index 00000000..e971cd80
--- /dev/null
+++ b/assets/js/watched_indicator.js
@@ -0,0 +1,24 @@
+'use strict';
+var save_player_pos_key = 'save_player_pos';
+
+function get_all_video_times() {
+ return helpers.storage.get(save_player_pos_key) || {};
+}
+
+document.querySelectorAll('.watched-indicator').forEach(function (indicator) {
+ var watched_part = get_all_video_times()[indicator.dataset.id];
+ var total = parseInt(indicator.dataset.length, 10);
+ if (watched_part === undefined) {
+ watched_part = total;
+ }
+ var percentage = Math.round((watched_part / total) * 100);
+
+ if (percentage < 5) {
+ percentage = 5;
+ }
+ if (percentage > 90) {
+ percentage = 100;
+ }
+
+ indicator.style.width = percentage + '%';
+});
diff --git a/locales/de.json b/locales/de.json
index 55c40905..c2941d6d 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -472,5 +472,12 @@
"search_filters_duration_option_none": "Beliebige Länge",
"search_filters_date_label": "Upload-Datum",
"search_filters_date_option_none": "Beliebiges Datum",
- "error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. <a href=\"`x`\">Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen.</a>"
+ "error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. <a href=\"`x`\">Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen.</a>",
+ "channel_tab_shorts_label": "Shorts",
+ "channel_tab_streams_label": "Livestreams",
+ "Music in this video": "Musik in diesem Video",
+ "Artist: ": "Künstler: ",
+ "Album: ": "Album: ",
+ "channel_tab_playlists_label": "Wiedergabelisten",
+ "channel_tab_channels_label": "Kanäle"
}
diff --git a/locales/el.json b/locales/el.json
index 3448a4dc..8d0c84dd 100644
--- a/locales/el.json
+++ b/locales/el.json
@@ -366,7 +366,7 @@
"preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "Μεσαία",
"preferences_quality_option_small": "Μικρό",
- "preferences_quality_option_dash": "DASH (προσαρμοστική ποιότητα)",
+ "preferences_quality_option_dash": "DASH (προσαρμόσιμη ποιότητα)",
"preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_option_720p": "720p",
"invidious": "Invidious",
@@ -450,5 +450,5 @@
"search_filters_type_option_show": "Μπάρα προόδου διαβάσματος",
"preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ",
"search_filters_title": "Φίλτρο",
- "search_message_no_results": "Δεν"
+ "search_message_no_results": "Δε βρέθηκαν αποτελέσματα."
}
diff --git a/locales/en-US.json b/locales/en-US.json
index a5c16fd7..86b83a23 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -454,7 +454,7 @@
"footer_documentation": "Documentation",
"footer_source_code": "Source code",
"footer_original_source_code": "Original source code",
- "footer_modfied_source_code": "Modified Source code",
+ "footer_modfied_source_code": "Modified source code",
"adminprefs_modified_source_code_url_label": "URL to modified source code repository",
"none": "none",
"videoinfo_started_streaming_x_ago": "Started streaming `x` ago",
diff --git a/locales/es.json b/locales/es.json
index 6cf721f3..a0d16325 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -52,21 +52,21 @@
"preferences_video_loop_label": "Repetir siempre: ",
"preferences_autoplay_label": "Reproducción automática: ",
"preferences_continue_label": "Reproducir siguiente por defecto: ",
- "preferences_continue_autoplay_label": "Reproducir automáticamente el vídeo siguiente: ",
+ "preferences_continue_autoplay_label": "Reproducir automáticamente el video siguiente: ",
"preferences_listen_label": "Activar el sonido por defecto: ",
- "preferences_local_label": "¿Usar un proxy para los vídeos? ",
+ "preferences_local_label": "¿Usar un proxy para los videos? ",
"preferences_speed_label": "Velocidad por defecto: ",
- "preferences_quality_label": "Calidad de vídeo preferida: ",
+ "preferences_quality_label": "Calidad de video preferida: ",
"preferences_volume_label": "Volumen del reproductor: ",
"preferences_comments_label": "Comentarios por defecto: ",
"youtube": "YouTube",
"reddit": "Reddit",
"preferences_captions_label": "Subtítulos por defecto: ",
"Fallback captions: ": "Subtítulos alternativos: ",
- "preferences_related_videos_label": "¿Mostrar vídeos relacionados? ",
+ "preferences_related_videos_label": "¿Mostrar videos relacionados? ",
"preferences_annotations_label": "¿Mostrar anotaciones por defecto? ",
- "preferences_extend_desc_label": "Extender automáticamente la descripción del vídeo: ",
- "preferences_vr_mode_label": "Vídeos interactivos de 360 grados (necesita WebGL): ",
+ "preferences_extend_desc_label": "Extender automáticamente la descripción del video: ",
+ "preferences_vr_mode_label": "Videos interactivos de 360 grados (necesita WebGL): ",
"preferences_category_visual": "Preferencias visuales",
"preferences_player_style_label": "Estilo de reproductor: ",
"Dark mode: ": "Modo oscuro: ",
@@ -79,16 +79,16 @@
"preferences_category_subscription": "Preferencias de la suscripción",
"preferences_annotations_subscribed_label": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
"Redirect homepage to feed: ": "Redirigir la página de inicio a la fuente: ",
- "preferences_max_results_label": "Número de vídeos mostrados en la fuente: ",
- "preferences_sort_label": "Ordenar los vídeos por: ",
+ "preferences_max_results_label": "Número de videos mostrados en la fuente: ",
+ "preferences_sort_label": "Ordenar los videos por: ",
"published": "fecha de publicación",
"published - reverse": "fecha de publicación: orden inverso",
"alphabetically": "alfabéticamente",
"alphabetically - reverse": "alfabéticamente: orden inverso",
"channel name": "nombre del canal",
"channel name - reverse": "nombre del canal: orden inverso",
- "Only show latest video from channel: ": "Mostrar solo el último vídeo del canal: ",
- "Only show latest unwatched video from channel: ": "Mostrar solo el último vídeo sin ver del canal: ",
+ "Only show latest video from channel: ": "Mostrar solo el último video del canal: ",
+ "Only show latest unwatched video from channel: ": "Mostrar solo el último video sin ver del canal: ",
"preferences_unseen_only_label": "Mostrar solo los no vistos: ",
"preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ",
"Enable web notifications": "Habilitar notificaciones web",
@@ -139,7 +139,7 @@
"Editing playlist `x`": "Editando la lista de reproducción 'x'",
"Show more": "Mostrar más",
"Show less": "Mostrar menos",
- "Watch on YouTube": "Ver el vídeo en YouTube",
+ "Watch on YouTube": "Ver en YouTube",
"Switch Invidious Instance": "Cambiar Instancia de Invidious",
"Hide annotations": "Ocultar anotaciones",
"Show annotations": "Mostrar anotaciones",
@@ -153,7 +153,7 @@
"Shared `x`": "Compartido `x`",
"Premieres in `x`": "Se estrena en `x`",
"Premieres `x`": "Estrenos `x`",
- "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tiene JavaScript desactivado. Haga clic aquí para ver los comentarios, pero tenga en cuenta que pueden tardar un poco más en cargarse.",
+ "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.",
"View YouTube comments": "Ver los comentarios de YouTube",
"View more comments on Reddit": "Ver más comentarios en Reddit",
"View `x` comments": {
@@ -164,7 +164,7 @@
"Hide replies": "Ocultar las respuestas",
"Show replies": "Mostrar las respuestas",
"Incorrect password": "Contraseña incorrecta",
- "Quota exceeded, try again in a few hours": "Cuota excedida, pruebe otra vez en unas horas",
+ "Quota exceeded, try again in a few hours": "Cuota excedida, prueba otra vez en unas horas",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No se puede iniciar sesión, asegúrese de que la autentificación de dos factores (autentificador o SMS) esté habilitada.",
"Invalid TFA code": "Código TFA no válido",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Error de inicio de sesion. Puede deberse a que la autentificación de dos factores no está habilitada en su cuenta.",
@@ -176,7 +176,7 @@
"Wrong username or password": "Nombre o contraseña incorrecto",
"Please sign in using 'Log in with Google'": "Inicie sesión con «Iniciar sesión con Google»",
"Password cannot be empty": "La contraseña no puede estar en blanco",
- "Password cannot be longer than 55 characters": "La contraseña no puede tener más de 55 caracteres",
+ "Password cannot be longer than 55 characters": "La contraseña no debe tener más de 55 caracteres",
"Please log in": "Inicie sesión, por favor",
"Invidious Private Feed for `x`": "Fuente privada de Invidious para `x`",
"channel:`x`": "canal: `x`",
@@ -198,7 +198,7 @@
"No such user": "Usuario no existe",
"Token is expired, please try again": "El símbolo ha caducado, inténtelo de nuevo",
"English": "Inglés",
- "English (auto-generated)": "Inglés (generados automáticamente)",
+ "English (auto-generated)": "Inglés (generado automáticamente)",
"Afrikaans": "Afrikáans",
"Albanian": "Albanés",
"Amharic": "Amárico",
@@ -324,50 +324,51 @@
"permalink": "enlace permanente",
"`x` marked it with a ❤": "`x` lo ha marcado con un ❤",
"Audio mode": "Modo de audio",
- "Video mode": "Modo de vídeo",
- "channel_tab_videos_label": "Vídeos",
+ "Video mode": "Modo de video",
+ "channel_tab_videos_label": "Videos",
"Playlists": "Listas de reproducción",
"channel_tab_community_label": "Comunidad",
- "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_sort_option_relevance": "Relevancia",
+ "search_filters_sort_option_rating": "Valoración",
+ "search_filters_sort_option_date": "Fecha de subida",
+ "search_filters_sort_option_views": "Visualizaciones",
+ "search_filters_type_label": "tipo de contenido",
"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_date_option_hour": "Última hora",
+ "search_filters_date_option_today": "Hoy",
+ "search_filters_date_option_week": "Esta semana",
+ "search_filters_date_option_month": "Este mes",
+ "search_filters_date_option_year": "Este año",
+ "search_filters_type_option_video": "Video",
+ "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": "En directo",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Ubicación",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Versión actual: ",
"next_steps_error_message": "Después de lo cual debes intentar: ",
"next_steps_error_message_refresh": "Recargar la página",
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
- "search_filters_duration_option_short": "Corto (< 4 minutos)",
- "search_filters_duration_option_long": "Largo (> 20 minutos)",
+ "search_filters_duration_option_short": "Menos de 4 minutos",
+ "search_filters_duration_option_medium": "De 4 a 20 minutos",
+ "search_filters_duration_option_long": "Más de 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",
+ "adminprefs_modified_source_code_url_label": "Enlace al repositorio de código fuente modificado",
"footer_source_code": "Código fuente",
"footer_modfied_source_code": "Código fuente modificado",
"footer_donate_page": "Donar",
"preferences_region_label": "País del contenido: ",
- "preferences_quality_dash_label": "Calidad de vídeo DASH preferida: ",
+ "preferences_quality_dash_label": "Calidad de video DASH preferida: ",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "Intermedia",
"preferences_quality_dash_option_auto": "Automática",
@@ -376,7 +377,7 @@
"download_subtitles": "Subtítulos- `x` (.vtt)",
"user_created_playlists": "`x` listas de reproducción creadas",
"user_saved_playlists": "`x` listas de reproducción guardadas",
- "Video unavailable": "Vídeo no disponible",
+ "Video unavailable": "Video no disponible",
"videoinfo_youTube_embed_link": "Insertar",
"preferences_quality_dash_option_2160p": "2160p",
"preferences_quality_dash_option_4320p": "4320p",
@@ -413,8 +414,8 @@
"generic_count_weeks_plural": "{{count}} semanas",
"generic_playlists_count": "{{count}} lista de reproducción",
"generic_playlists_count_plural": "{{count}} listas de reproducción",
- "generic_videos_count": "{{count}} vídeo",
- "generic_videos_count_plural": "{{count}} vídeos",
+ "generic_videos_count": "{{count}} video",
+ "generic_videos_count_plural": "{{count}} videos",
"generic_count_months": "{{count}} mes",
"generic_count_months_plural": "{{count}} meses",
"comments_points_count": "{{count}} punto",
@@ -433,7 +434,7 @@
"crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>",
"crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!",
"crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>",
- "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):",
+ "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:",
"English (United States)": "Inglés (Estados Unidos)",
"Cantonese (Hong Kong)": "Cantonés (Hong Kong)",
"Dutch (auto-generated)": "Neerlandés (generados automáticamente)",
@@ -461,23 +462,22 @@
"search_message_no_results": "No se han encontrado resultados.",
"search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.",
"search_filters_title": "Filtros",
- "search_filters_date_label": "Fecha de subida",
+ "search_filters_date_label": "fecha de subida",
"search_filters_date_option_none": "Cualquier fecha",
"search_filters_type_option_all": "Cualquier tipo",
"search_filters_duration_option_none": "Cualquier duración",
"search_filters_features_option_vr180": "VR180",
- "search_filters_apply_button": "Aplicar filtros seleccionados",
+ "search_filters_apply_button": "Aplicar filtros",
"tokens_count": "{{count}} ficha",
"tokens_count_plural": "{{count}} fichas",
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
- "search_filters_duration_option_medium": "Medio (4 - 20 minutes)",
"Popular enabled: ": "¿Habilitar la sección popular? ",
- "error_video_not_in_playlist": "El vídeo solicitado no existe en esta lista de reproducción. <a href=\"`x`\">Haga clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
+ "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
"channel_tab_streams_label": "Directos",
"channel_tab_channels_label": "Canales",
"channel_tab_shorts_label": "Cortos",
"channel_tab_playlists_label": "Listas de reproducción",
- "Music in this video": "Música en este vídeo",
+ "Music in this video": "Música en este video",
"Artist: ": "Artista: ",
"Album: ": "Álbum: "
}
diff --git a/locales/hi.json b/locales/hi.json
index e576080f..41335266 100644
--- a/locales/hi.json
+++ b/locales/hi.json
@@ -470,5 +470,14 @@
"crash_page_switch_instance": "<a href=\"`x`\">किसी दूसरे उदाहरण का इस्तेमाल करें</a>",
"crash_page_read_the_faq": "<a href=\"`x`\">अक्सर पूछे जाने वाले प्रश्न (FAQ)</a> पढ़ें",
"crash_page_refresh": "<a href=\"`x`\">पृष्ठ को एक बार साफ़ करें</a>",
- "crash_page_search_issue": "<a href=\"`x`\">GitHub पर मौजूदा मुद्दे</a> ढूँढ़ें"
+ "crash_page_search_issue": "<a href=\"`x`\">GitHub पर मौजूदा मुद्दे</a> ढूँढ़ें",
+ "Popular enabled: ": "लोकप्रिय सक्षम: ",
+ "Artist: ": "कलाकार: ",
+ "Music in this video": "इस वीडियो में संगीत",
+ "Album: ": "एल्बम: ",
+ "error_video_not_in_playlist": "अनुरोधित वीडियो इस प्लेलिस्ट में मौजूद नहीं है। <a href=\"`x`\">प्लेलिस्ट के मुखपृष्ठ पर जाने के लिए यहाँ क्लिक करें।</a>",
+ "channel_tab_shorts_label": "शॉर्ट्स",
+ "channel_tab_streams_label": "लाइवस्ट्रीम्स",
+ "channel_tab_playlists_label": "प्लेलिस्ट्स",
+ "channel_tab_channels_label": "चैनल्स"
}
diff --git a/locales/hr.json b/locales/hr.json
index 72cd6a8e..c626ed28 100644
--- a/locales/hr.json
+++ b/locales/hr.json
@@ -359,13 +359,13 @@
"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",
+ "adminprefs_modified_source_code_url_label": "URL do repozitorija prilagođenog izvornog koda",
"search_filters_duration_option_short": "Kratko (< 4 minute)",
"search_filters_duration_option_long": "Dugo (> 20 minute)",
"footer_source_code": "Izvorni kod",
- "footer_modfied_source_code": "Izmijenjeni izvorni kod",
+ "footer_modfied_source_code": "Prilagođen izvorni kod",
"footer_documentation": "Dokumentacija",
- "footer_original_source_code": "Izvoran izvorni kod",
+ "footer_original_source_code": "Prvobitan izvorni kod",
"preferences_region_label": "Zemlja sadržaja: ",
"preferences_quality_dash_label": "Preferirana DASH videokvaliteta: ",
"preferences_quality_option_dash": "DASH (adaptativna kvaliteta)",
diff --git a/locales/ja.json b/locales/ja.json
index 3ad4b494..4d2ed5a0 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -5,7 +5,7 @@
"generic_subscribers_count_0": "{{count}} 人の登録者",
"generic_subscriptions_count_0": "{{count}} 個の登録チャンネル",
"LIVE": "ライブ",
- "Shared `x` ago": "`x`前に共有",
+ "Shared `x` ago": "`x`前に公開",
"Unsubscribe": "登録解除",
"Subscribe": "登録",
"View channel on YouTube": "YouTube でチャンネルを見る",
@@ -56,17 +56,17 @@
"preferences_category_player": "プレイヤーの設定",
"preferences_video_loop_label": "常にループ: ",
"preferences_autoplay_label": "自動再生: ",
- "preferences_continue_label": "デフォルトで次を再生: ",
+ "preferences_continue_label": "次の動画を再生: ",
"preferences_continue_autoplay_label": "次の動画を自動再生: ",
"preferences_listen_label": "デフォルトで音声モードを使用: ",
- "preferences_local_label": "動画をプロキシーに通す: ",
- "preferences_speed_label": "デフォルトの再生速度: ",
+ "preferences_local_label": "動画視聴にプロキシーを経由: ",
+ "preferences_speed_label": "標準の再生速度: ",
"preferences_quality_label": "優先する画質: ",
"preferences_volume_label": "プレイヤーの音量: ",
"preferences_comments_label": "デフォルトのコメント: ",
"youtube": "YouTube",
"reddit": "Reddit",
- "preferences_captions_label": "デフォルトの字幕: ",
+ "preferences_captions_label": "優先する字幕: ",
"Fallback captions: ": "フォールバック時の字幕: ",
"preferences_related_videos_label": "関連動画を表示: ",
"preferences_annotations_label": "デフォルトでアノテーションを表示: ",
@@ -108,7 +108,7 @@
"Watch history": "再生履歴",
"Delete account": "アカウントを削除",
"preferences_category_admin": "管理者設定",
- "preferences_default_home_label": "デフォルトのホーム: ",
+ "preferences_default_home_label": "ホームに表示するページ: ",
"preferences_feed_menu_label": "フィードメニュー: ",
"preferences_show_nick_label": "ニックネームを一番上に表示する: ",
"Top enabled: ": "トップページを有効化: ",
@@ -157,7 +157,7 @@
"Engagement: ": "エンゲージメント: ",
"Whitelisted regions: ": "ホワイトリストの地域: ",
"Blacklisted regions: ": "ブラックリストの地域: ",
- "Shared `x`": "`x`に共有",
+ "Shared `x`": "公開日 `x`",
"Premieres in `x`": "`x`後にプレミア公開",
"Premieres `x`": "`x`にプレミア公開",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "やあ!君は JavaScript を無効にしているのかな?ここをクリックしてコメントを見れるけど、読み込みには少し時間がかかることがあるのを覚えておいてね。",
@@ -191,9 +191,9 @@
"This channel does not exist.": "このチャンネルは存在しません。",
"Could not get channel info.": "チャンネル情報を取得できませんでした。",
"Could not fetch comments": "コメントを取得できませんでした",
- "comments_view_x_replies_0": "{{count}} 件の返信を見る",
+ "comments_view_x_replies_0": "{{count}}件の返信を表示",
"`x` ago": "`x`前",
- "Load more": "もっと読み込む",
+ "Load more": "もっと見る",
"comments_points_count_0": "{{count}}点",
"Could not create mix.": "ミックスを作成できませんでした。",
"Empty playlist": "空の再生リスト",
@@ -366,7 +366,7 @@
"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_live": "ライブ",
"search_filters_features_option_four_k": "4K",
"search_filters_features_option_location": "場所",
"search_filters_features_option_hdr": "HDR",
@@ -377,8 +377,8 @@
"search_filters_duration_option_short": "4 分未満",
"footer_documentation": "文書",
"footer_source_code": "ソースコード",
- "footer_original_source_code": "ソースコード (元)",
- "footer_modfied_source_code": "ソースコード (改変)",
+ "footer_original_source_code": "元のソースコード",
+ "footer_modfied_source_code": "改変して使用",
"adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL",
"search_filters_duration_option_long": "20 分以上",
"preferences_region_label": "地域: ",
diff --git a/locales/pt-BR.json b/locales/pt-BR.json
index afd31ede..ec00d46e 100644
--- a/locales/pt-BR.json
+++ b/locales/pt-BR.json
@@ -381,7 +381,7 @@
"footer_documentation": "Documentação",
"footer_source_code": "Código fonte",
"footer_original_source_code": "Código fonte original",
- "footer_modfied_source_code": "Código Fonte Modificado",
+ "footer_modfied_source_code": "Código-fonte modificado",
"preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ",
"preferences_region_label": "País do conteúdo: ",
"preferences_quality_dash_option_4320p": "4320p",
@@ -476,5 +476,8 @@
"channel_tab_channels_label": "Canais",
"channel_tab_playlists_label": "Listas de reprodução",
"channel_tab_shorts_label": "Curtos",
- "channel_tab_streams_label": "Ao Vivo"
+ "channel_tab_streams_label": "Ao Vivo",
+ "Music in this video": "Música neste vídeo",
+ "Artist: ": "Artista: ",
+ "Album: ": "Álbum: "
}
diff --git a/locales/pt-PT.json b/locales/pt-PT.json
index 1788deb1..43834d70 100644
--- a/locales/pt-PT.json
+++ b/locales/pt-PT.json
@@ -472,5 +472,12 @@
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
- "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>"
+ "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>",
+ "Artist: ": "Artista: ",
+ "Album: ": "Álbum: ",
+ "channel_tab_streams_label": "Diretos",
+ "channel_tab_playlists_label": "Listas de reprodução",
+ "channel_tab_channels_label": "Canais",
+ "Music in this video": "Música neste vídeo",
+ "channel_tab_shorts_label": "Curtos"
}
diff --git a/locales/ru.json b/locales/ru.json
index 733e0be1..7ca5cf1f 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -69,11 +69,11 @@
"preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ",
"preferences_category_visual": "Настройки сайта",
"preferences_player_style_label": "Стиль проигрывателя: ",
- "Dark mode: ": "Тёмное оформление: ",
+ "Dark mode: ": "Темное оформление: ",
"preferences_dark_mode_label": "Тема: ",
- "dark": "тёмная",
+ "dark": "темная",
"light": "светлая",
- "preferences_thin_mode_label": "Облегчённое оформление: ",
+ "preferences_thin_mode_label": "Облегченное оформление: ",
"preferences_category_misc": "Прочие настройки",
"preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ",
"preferences_category_subscription": "Настройки подписок",
@@ -88,7 +88,7 @@
"channel name": "по названию канала",
"channel name - reverse": "по названию канала в обратном порядке",
"Only show latest video from channel: ": "Показывать только последние видео с каналов: ",
- "Only show latest unwatched video from channel: ": "Показывать только непросмотренные видео с каналов: ",
+ "Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ",
"preferences_unseen_only_label": "Показывать только непросмотренные видео: ",
"preferences_notifications_only_label": "Показывать только оповещения, если они есть: ",
"Enable web notifications": "Включить уведомления в браузере",
@@ -147,13 +147,13 @@
"License: ": "Лицензия: ",
"Family friendly? ": "Семейный просмотр: ",
"Wilson score: ": "Оценка Уилсона: ",
- "Engagement: ": "Вовлечённость: ",
+ "Engagement: ": "Вовлеченность: ",
"Whitelisted regions: ": "Доступно в регионах: ",
"Blacklisted regions: ": "Недоступно в регионах: ",
"Shared `x`": "Опубликовано `x`",
"Premieres in `x`": "Премьера через `x`",
"Premieres `x`": "Премьера `x`",
- "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.",
+ "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. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.",
"View YouTube comments": "Показать комментарии с YouTube",
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
"View `x` comments": {
@@ -180,23 +180,23 @@
"Please log in": "Пожалуйста, войдите",
"Invidious Private Feed for `x`": "Приватная лента Invidious для `x`",
"channel:`x`": "канал: `x`",
- "Deleted or invalid channel": "Канал удалён или не найден",
+ "Deleted or invalid channel": "Канал удален или не найден",
"This channel does not exist.": "Такого канала не существует.",
- "Could not get channel info.": "Не удаётся получить информацию об этом канале.",
- "Could not fetch comments": "Не удаётся загрузить комментарии",
+ "Could not get channel info.": "Не удается получить информацию об этом канале.",
+ "Could not fetch comments": "Не удается загрузить комментарии",
"`x` ago": "`x` назад",
- "Load more": "Загрузить ещё",
+ "Load more": "Загрузить еще",
"Could not create mix.": "Не удалось создать микс.",
"Empty playlist": "Плейлист пуст",
- "Not a playlist.": "Некорректный плейлист.",
+ "Not a playlist.": "Это не плейлист.",
"Playlist does not exist.": "Плейлист не существует.",
- "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».",
+ "Could not pull trending pages.": "Не удается загрузить страницы «в тренде».",
"Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»",
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»",
"Erroneous challenge": "Неправильный ответ в «challenge»",
"Erroneous token": "Неправильный токен",
"No such user": "Пользователь не найден",
- "Token is expired, please try again": "Срок действия токена истёк, попробуйте позже",
+ "Token is expired, please try again": "Срок действия токена истек, попробуйте позже",
"English": "Английский",
"English (auto-generated)": "Английский (созданы автоматически)",
"Afrikaans": "Африкаанс",
@@ -453,8 +453,8 @@
"Portuguese (Brazil)": "Португальский (Бразилия)",
"footer_source_code": "Исходный код",
"footer_original_source_code": "Оригинальный исходный код",
- "footer_modfied_source_code": "Изменённый исходный код",
- "user_saved_playlists": "`x` сохранённых плейлистов",
+ "footer_modfied_source_code": "Измененный исходный код",
+ "user_saved_playlists": "`x` сохраненных плейлистов",
"crash_page_search_issue": "поискали <a href=\"`x`\">похожую проблему на GitHub</a>",
"comments_points_count_0": "{{count}} плюс",
"comments_points_count_1": "{{count}} плюса",
diff --git a/locales/sq.json b/locales/sq.json
index 15025750..7f29a035 100644
--- a/locales/sq.json
+++ b/locales/sq.json
@@ -286,7 +286,7 @@
"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",
+ "footer_modfied_source_code": "Kod burim i ndryshuar",
"adminprefs_modified_source_code_url_label": "URL e depos së ndryshuar të kodit burim",
"none": "asnjë",
"videoinfo_started_streaming_x_ago": "Filloi transmetimin `x` më parë",
@@ -468,5 +468,7 @@
"Artist: ": "Artist: ",
"Album: ": "Album: ",
"channel_tab_channels_label": "Kanale",
- "Music in this video": "Muzikë në këtë video"
+ "Music in this video": "Muzikë në këtë video",
+ "channel_tab_shorts_label": "Të shkurtra",
+ "channel_tab_streams_label": "Transmetime të drejtpërdrejta"
}
diff --git a/locales/tr.json b/locales/tr.json
index d98e2038..b7cb3958 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -363,7 +363,7 @@
"footer_documentation": "Belgelendirme",
"footer_source_code": "Kaynak Kodları",
"footer_original_source_code": "Orijinal Kaynak Kodları",
- "footer_modfied_source_code": "Değiştirilmiş Kaynak Kodları",
+ "footer_modfied_source_code": "Değiştirilmiş kaynak kodları",
"adminprefs_modified_source_code_url_label": "Değiştirilmiş Kaynak Kodları Deposunun URL'si",
"footer_donate_page": "Bağış Yap",
"preferences_region_label": "İçerik Ülkesi: ",
diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr
index e6a70ed2..5aa4452c 100644
--- a/src/invidious/routes/account.cr
+++ b/src/invidious/routes/account.cr
@@ -262,6 +262,7 @@ module Invidious::Routes::Account
end
query["token"] = access_token
+ query["username"] = URI.encode_path_segment(user.email)
url.query = query.to_s
env.redirect url.to_s
diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr
index 421355bb..ce2ee812 100644
--- a/src/invidious/routes/api/v1/authenticated.cr
+++ b/src/invidious/routes/api/v1/authenticated.cr
@@ -31,6 +31,88 @@ module Invidious::Routes::API::V1::Authenticated
env.response.status_code = 204
end
+ def self.export_invidious(env)
+ env.response.content_type = "application/json"
+ user = env.get("user").as(User)
+
+ return Invidious::User::Export.to_invidious(user)
+ end
+
+ def self.import_invidious(env)
+ user = env.get("user").as(User)
+
+ begin
+ if body = env.request.body
+ body = env.request.body.not_nil!.gets_to_end
+ else
+ body = "{}"
+ end
+ Invidious::User::Import.from_invidious(user, body)
+ rescue
+ end
+
+ env.response.status_code = 204
+ end
+
+ def self.get_history(env)
+ env.response.content_type = "application/json"
+ user = env.get("user").as(User)
+
+ page = env.params.query["page"]?.try &.to_i?.try &.clamp(0, Int32::MAX)
+ page ||= 1
+
+ max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
+ max_results ||= user.preferences.max_results
+ max_results ||= CONFIG.default_user_preferences.max_results
+
+ start_index = (page - 1) * max_results
+ if user.watched[start_index]?
+ watched = user.watched.reverse[start_index, max_results]
+ end
+ watched ||= [] of String
+
+ return watched.to_json
+ end
+
+ def self.mark_watched(env)
+ user = env.get("user").as(User)
+
+ if !user.preferences.watch_history
+ return error_json(409, "Watch history is disabled in preferences.")
+ end
+
+ id = env.params.url["id"]
+ if !id.match(/^[a-zA-Z0-9_-]{11}$/)
+ return error_json(400, "Invalid video id.")
+ end
+
+ Invidious::Database::Users.mark_watched(user, id)
+ env.response.status_code = 204
+ end
+
+ def self.mark_unwatched(env)
+ user = env.get("user").as(User)
+
+ if !user.preferences.watch_history
+ return error_json(409, "Watch history is disabled in preferences.")
+ end
+
+ id = env.params.url["id"]
+ if !id.match(/^[a-zA-Z0-9_-]{11}$/)
+ return error_json(400, "Invalid video id.")
+ end
+
+ Invidious::Database::Users.mark_unwatched(user, id)
+ env.response.status_code = 204
+ end
+
+ def self.clear_history(env)
+ user = env.get("user").as(User)
+
+ Invidious::Database::Users.clear_watch_history(user)
+ env.response.status_code = 204
+ end
+
def self.feed(env)
env.response.content_type = "application/json"
diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr
index 43d360e6..e499f4d6 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -150,4 +150,31 @@ module Invidious::Routes::API::V1::Misc
response
end
+
+ # resolve channel and clip urls, return the UCID
+ def self.resolve_url(env)
+ env.response.content_type = "application/json"
+ url = env.params.query["url"]?
+
+ return error_json(400, "Missing URL to resolve") if !url
+
+ begin
+ resolved_url = YoutubeAPI.resolve_url(url.as(String))
+ endpoint = resolved_url["endpoint"]
+ pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || ""
+ if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId")
+ elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId")
+ elsif pageType == "WEB_PAGE_TYPE_UNKNOWN"
+ return error_json(400, "Unknown url")
+ end
+ rescue ex
+ return error_json(500, ex)
+ end
+ JSON.build do |json|
+ json.object do
+ json.field "ucid", resolved_ucid.try &.as_s || ""
+ json.field "pageType", pageType
+ end
+ end
+ end
end
diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr
index 7b1fa876..0704c05e 100644
--- a/src/invidious/routes/subscriptions.cr
+++ b/src/invidious/routes/subscriptions.cr
@@ -104,33 +104,8 @@ module Invidious::Routes::Subscriptions
if format == "json"
env.response.content_type = "application/json"
env.response.headers["content-disposition"] = "attachment"
- playlists = Invidious::Database::Playlists.select_like_iv(user.email)
-
- return JSON.build do |json|
- json.object do
- json.field "subscriptions", user.subscriptions
- json.field "watch_history", user.watched
- json.field "preferences", user.preferences
- json.field "playlists" do
- json.array do
- playlists.each do |playlist|
- json.object do
- json.field "title", playlist.title
- json.field "description", html_to_content(playlist.description_html)
- json.field "privacy", playlist.privacy.to_s
- json.field "videos" do
- json.array do
- Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id|
- json.string video_id
- end
- end
- end
- end
- end
- end
- end
- end
- end
+
+ return Invidious::User::Export.to_invidious(user)
else
env.response.content_type = "application/xml"
env.response.headers["content-disposition"] = "attachment"
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index 157e6de7..9e2ade3d 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -254,6 +254,14 @@ module Invidious::Routing
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences
+ get "/api/v1/auth/export/invidious", {{namespace}}::Authenticated, :export_invidious
+ post "/api/v1/auth/import/invidious", {{namespace}}::Authenticated, :import_invidious
+
+ get "/api/v1/auth/history", {{namespace}}::Authenticated, :get_history
+ post "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_watched
+ delete "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_unwatched
+ delete "/api/v1/auth/history", {{namespace}}::Authenticated, :clear_history
+
get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed
get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions
@@ -281,6 +289,7 @@ module Invidious::Routing
get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist
get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist
get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes
+ get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url
{% end %}
end
end
diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr
index 1f957081..134eb437 100644
--- a/src/invidious/trending.cr
+++ b/src/invidious/trending.cr
@@ -4,11 +4,12 @@ def fetch_trending(trending_type, region, locale)
plid = nil
- if trending_type == "Music"
+ case trending_type.try &.downcase
+ when "music"
params = "4gINGgt5dG1hX2NoYXJ0cw%3D%3D"
- elsif trending_type == "Gaming"
+ when "gaming"
params = "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D"
- elsif trending_type == "Movies"
+ when "movies"
params = "4gIKGgh0cmFpbGVycw%3D%3D"
else # Default
params = ""
diff --git a/src/invidious/user/exports.cr b/src/invidious/user/exports.cr
new file mode 100644
index 00000000..b52503c9
--- /dev/null
+++ b/src/invidious/user/exports.cr
@@ -0,0 +1,35 @@
+struct Invidious::User
+ module Export
+ extend self
+
+ def to_invidious(user : User)
+ playlists = Invidious::Database::Playlists.select_like_iv(user.email)
+
+ return JSON.build do |json|
+ json.object do
+ json.field "subscriptions", user.subscriptions
+ json.field "watch_history", user.watched
+ json.field "preferences", user.preferences
+ json.field "playlists" do
+ json.array do
+ playlists.each do |playlist|
+ json.object do
+ json.field "title", playlist.title
+ json.field "description", html_to_content(playlist.description_html)
+ json.field "privacy", playlist.privacy.to_s
+ json.field "videos" do
+ json.array do
+ Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: CONFIG.playlist_length_limit).each do |video_id|
+ json.string video_id
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end # module
+end
diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr
index 22870317..bcba74cf 100644
--- a/src/invidious/views/add_playlist_items.ecr
+++ b/src/invidious/views/add_playlist_items.ecr
@@ -39,6 +39,8 @@
<% end %>
</div>
+<script src="/js/watched_indicator.js"></script>
+
<% if query %>
<%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%>
<div class="pure-g h-box">
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr
index a29315ef..6e62a471 100644
--- a/src/invidious/views/channel.ecr
+++ b/src/invidious/views/channel.ecr
@@ -49,6 +49,8 @@
<% end %>
</div>
+<script src="/js/watched_indicator.js"></script>
+
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-4-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 0e959ff2..fa12374f 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -1,3 +1,5 @@
+<% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %>
+
<div class="pure-u-1 pure-u-md-1-4">
<div class="h-box">
<% case item when %>
@@ -40,6 +42,11 @@
<% if item.length_seconds != 0 %>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %>
+
+ <% if item_watched %>
+ <div class="watched-overlay"></div>
+ <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div>
+ <% end %>
</div>
<% end %>
<p dir="auto"><%= HTML.escape(item.title) %></p>
@@ -67,6 +74,11 @@
<% elsif item.length_seconds != 0 %>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %>
+
+ <% if item_watched %>
+ <div class="watched-overlay"></div>
+ <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div>
+ <% end %>
</div>
<% end %>
<p dir="auto"><%= HTML.escape(item.title) %></p>
@@ -124,6 +136,11 @@
<% elsif item.length_seconds != 0 %>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %>
+
+ <% if item_watched %>
+ <div class="watched-overlay"></div>
+ <div class="watched-indicator" data-length="<%= item.length_seconds %>" data-id="<%= item.id %>"></div>
+ <% end %>
</div>
<% end %>
<p dir="auto"><%= HTML.escape(item.title) %></p>
diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr
index 89819ef0..548104c8 100644
--- a/src/invidious/views/edit_playlist.ecr
+++ b/src/invidious/views/edit_playlist.ecr
@@ -62,6 +62,8 @@
<% end %>
</div>
+<script src="/js/watched_indicator.js"></script>
+
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
<% if page > 1 %>
diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr
index a59344c4..e52a7707 100644
--- a/src/invidious/views/feeds/playlists.ecr
+++ b/src/invidious/views/feeds/playlists.ecr
@@ -32,3 +32,5 @@
<%= rendered "components/item" %>
<% end %>
</div>
+
+<script src="/js/watched_indicator.js"></script>
diff --git a/src/invidious/views/feeds/popular.ecr b/src/invidious/views/feeds/popular.ecr
index e77f35b9..5fbe539c 100644
--- a/src/invidious/views/feeds/popular.ecr
+++ b/src/invidious/views/feeds/popular.ecr
@@ -16,3 +16,5 @@
<%= rendered "components/item" %>
<% end %>
</div>
+
+<script src="/js/watched_indicator.js"></script>
diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr
index 76f2f2bd..9c69c5b0 100644
--- a/src/invidious/views/feeds/subscriptions.ecr
+++ b/src/invidious/views/feeds/subscriptions.ecr
@@ -62,6 +62,8 @@
<% end %>
</div>
+<script src="/js/watched_indicator.js"></script>
+
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
<% if page > 1 %>
diff --git a/src/invidious/views/feeds/trending.ecr b/src/invidious/views/feeds/trending.ecr
index a35c4ee3..7dc416c6 100644
--- a/src/invidious/views/feeds/trending.ecr
+++ b/src/invidious/views/feeds/trending.ecr
@@ -45,3 +45,5 @@
<%= rendered "components/item" %>
<% end %>
</div>
+
+<script src="/js/watched_indicator.js"></script>
diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr
index 0ecfe832..3351c21c 100644
--- a/src/invidious/views/hashtag.ecr
+++ b/src/invidious/views/hashtag.ecr
@@ -24,6 +24,8 @@
<%- end -%>
</div>
+<script src="/js/watched_indicator.js"></script>
+
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
<%- if page > 1 -%>
diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr
index df3112db..a04acf4c 100644
--- a/src/invidious/views/playlist.ecr
+++ b/src/invidious/views/playlist.ecr
@@ -106,6 +106,8 @@
<% end %>
</div>
+<script src="/js/watched_indicator.js"></script>
+
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
<% if page > 1 %>
diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr
index 254449a1..a7469e36 100644
--- a/src/invidious/views/search.ecr
+++ b/src/invidious/views/search.ecr
@@ -37,6 +37,8 @@
</div>
<%- end -%>
+<script src="/js/watched_indicator.js"></script>
+
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
<%- if query.page > 1 -%>