// ==UserScript== // @name YouTube Playback Plox // @name:en YouTube Playback Plox // @name:es YouTube Reproducción Plox // @name:fr YouTube Lecture Plox // @name:de YouTube Wiedergabe Plox // @name:it YouTube Riproduzione Plox // @name:pt-BR YouTube Reprodução Plox // @name:nl YouTube Afspelen Plox // @name:pl YouTube Odtwarzanie Plox // @name:sv YouTube Uppspelning Plox // @name:da YouTube Afspilning Plox // @name:no YouTube Avspilling Plox // @name:fi YouTube Toisto Plox // @name:cs YouTube Přehrávání Plox // @name:sk YouTube Prehrávanie Plox // @name:hu YouTube Lejátszás Plox // @name:ro YouTube Redare Plox // @name:be YouTube Воспроизведение Plox // @name:bg YouTube Възпроизвеждане Plox // @name:el YouTube Αναπαραγωγή Plox // @name:sr YouTube Репродукција Plox // @name:hr YouTube Reprodukcija Plox // @name:sl YouTube Predvajanje Plox // @name:lt YouTube Grotuvas Plox // @name:lv YouTube Atskaņošana Plox // @name:uk YouTube Відтворення Plox // @name:ru YouTube Воспроизведение Plox // @name:tr YouTube Oynatma Plox // @name:ar يوتيوب بلايباك Plox // @name:fa پخش یوتیوب Plox // @name:he YouTube השמעה Plox // @name:hi YouTube प्लेबैक Plox // @name:bn YouTube প্লেব্যাক Plox // @name:te YouTube ప్లేబ్యాక్ Plox // @name:ta YouTube பிளேபாக் Plox // @name:mr YouTube प्लेबॅक Plox // @name:zh-CN YouTube 播放 Plox // @name:zh-TW YouTube 播放 Plox // @name:zh-HK YouTube 播放 Plox // @name:ja YouTube 再生 Plox // @name:ko YouTube 재생 Plox // @name:th YouTube เล่นต่อ Plox // @name:vi YouTube Phát lại Plox // @name:id YouTube Pemutaran Plox // @name:ms YouTube Main Semula Plox // @name:tl YouTube Playback Plox // @name:my YouTube ဖလေ့ဘက် Plox // @name:sw YouTube Uchezesha Plox // @name:am የYouTube ተጫዋች Plox // @name:ha YouTube Playback Plox // @name:ur YouTube پلے بیک Plox // @name:ca YouTube Reproducció Plox // @name:zu YouTube Playback Plox // @name:yue YouTube 播放 Plox // @name:es-419 YouTube Reproducción Plox // @description Guarda y retoma automáticamente el progreso de vídeos en YouTube sin necesidad de iniciar sesión. // @description:en Automatically saves and resumes video playback progress on YouTube without needing to log in. // @description:es Guarda y retoma automáticamente el progreso de vídeos en YouTube sin necesidad de iniciar sesión. // @description:fr Enregistre et reprend automatiquement la progression de la lecture des vidéos sur YouTube sans avoir besoin de se connecter. // @description:de Speichert und setzt den Fortschritt von YouTube-Videos automatisch fort, ohne dass eine Anmeldung erforderlich ist. // @description:it Salva e riprende automaticamente la riproduzione dei video su YouTube senza bisogno di accedere. // @description:pt-BR Salva e retoma automaticamente o progresso da reprodução de vídeos no YouTube sem precisar fazer login. // @description:nl Slaat automatisch de voortgang van video's op YouTube op en hervat deze zonder in te loggen. // @description:pl Automatycznie zapisuje i wznawia postęp odtwarzania wideo na YouTube bez logowania. // @description:sv Sparar och återupptar automatiskt videoframsteg på YouTube utan att behöva logga in. // @description:da Gemmer og genoptager automatisk videoafspilning på YouTube uden at logge ind. // @description:no Lagrer og gjenopptar automatisk videofremdrift på YouTube uten å logge inn. // @description:fi Tallentaa ja jatkaa automaattisesti YouTube-videoiden toistopistettä ilman kirjautumista. // @description:cs Automaticky ukládá a obnovuje postup přehrávání videí na YouTube bez nutnosti přihlášení. // @description:sk Automaticky ukladá a obnovuje priebeh prehrávania videí na YouTube bez potreby prihlásenia. // @description:hu Automatikusan menti és folytatja a YouTube-videók lejátszási előrehaladását bejelentkezés nélkül. // @description:ro Salvează și reia automat progresul redării videoclipurilor pe YouTube fără a fi nevoie să te conectezi. // @description:be Автоматично зберігає та відновлює прогрес відтворення відео на YouTube без входу в акаунт. // @description:bg Автоматично записва и възобновява прогреса на видеото в YouTube без нужда от вход. // @description:el Αποθηκεύει και συνεχίζει αυτόματα την πρόοδο αναπαραγωγής βίντεο στο YouTube χωρίς να χρειάζεται σύνδεση. // @description:sr Аутоматски чува и наставља напредак репродукције видео записа на YouTube-у без пријављивања. // @description:hr Automatski sprema i nastavlja napredak reprodukcije videozapisa na YouTubeu bez prijave. // @description:sl Samodejno shrani in nadaljuje napredek predvajanja videoposnetkov na YouTubu brez prijave. // @description:lt Automatiškai išsaugo ir atnaujina YouTube vaizdo įrašų atkūrimo pažangą be prisijungimo. // @description:lv Automātiski saglabā un atsāk video atskaņošanas progresu YouTube bez pieteikšanās. // @description:uk Автоматично зберігає та відновлює прогрес відтворення відео на YouTube без входу в акаунт. // @description:ru Автоматически сохраняет и возобновляет прогресс воспроизведения видео на YouTube без входа в аккаунт. // @description:tr YouTube'daki video oynatma ilerlemesini otomatik olarak kaydeder ve devam ettirir, giriş yapmaya gerek yok. // @description:ar يقوم بحفظ واستئناف تقدم تشغيل الفيديوهات على يوتيوب تلقائيًا دون الحاجة لتسجيل الدخول. // @description:fa پیشرفت پخش ویدیوها در یوتیوب را به صورت خودکار ذخیره و ادامه می‌دهد بدون نیاز به ورود. // @description:he שומר ומחדש אוטומטית את התקדמות הניגון של סרטונים ביוטיוב ללא צורך בהתחברות. // @description:hi YouTube पर वीडियो प्लेबैक की प्रगति को स्वचालित रूप से सहेजें और पुनः प्रारंभ करें, लॉगिन की आवश्यकता नहीं। // @description:bn YouTube ভিডিও প্লেব্যাকের অগ্রগতি স্বয়ংক্রিয়ভাবে সংরক্ষণ এবং পুনরায় শুরু করুন, লগইনের প্রয়োজন নেই। // @description:te YouTube వీడియో ప్లేబ్యాక్ పురోగతిని ఆటోమేటిక్‌గా సేవ్ చేసి, తిరిగి ప్రారంభిస్తుంది, లాగిన్ అవసరం లేదు. // @description:ta YouTube வீடியோக்களின் பிளேபாக் முன்னேற்றத்தை தானாகச் சேமித்து மீண்டும் தொடங்கும், உள்நுழைவு தேவையில்லை. // @description:mr YouTube व्हिडिओ प्लेबॅक प्रगती आपोआप जतन करते आणि पुन्हा सुरू करते, लॉगिन आवश्यक नाही. // @description:zh-CN 自动保存并恢复 YouTube 视频的播放进度,无需登录。 // @description:zh-TW 自動儲存及繼續 YouTube 影片播放進度,無需登入。 // @description:zh-HK 自動儲存及繼續 YouTube 影片播放進度,無需登入。 // @description:ja YouTube の動画再生の進行状況を自動で保存・再開します。ログインは不要です。 // @description:ko YouTube 동영상 재생 진행 상황을 자동으로 저장하고 이어서 재생합니다. 로그인 불필요. // @description:th บันทึกและเล่นต่อความคืบหน้าของวิดีโอบน YouTube โดยอัตโนมัติ โดยไม่ต้องเข้าสู่ระบบ. // @description:vi Tự động lưu và tiếp tục tiến trình phát video trên YouTube mà không cần đăng nhập. // @description:id Menyimpan dan melanjutkan kemajuan pemutaran video di YouTube secara otomatis tanpa perlu login. // @description:ms Menyimpan dan menyambung semula kemajuan main balik video di YouTube secara automatik tanpa perlu log masuk. // @description:tl Awtomatikong ini-save at ipinagpapatuloy ang progreso ng video playback sa YouTube nang hindi nagla-log in. // @description:my YouTube ဗီဒီယိုဖလေ့ဘက် တိုးတက်မှုကို အလိုအလျောက် သိမ်းဆည်းပြီး ထပ်မံစတင်နိုင်သည်။ ဝင်ရောက်ရန် မလိုအပ်ပါ။ // @description:sw Hifadhi na endelea kwa kiotomatiki maendeleo ya uchezaji wa video kwenye YouTube bila kuingia. // @description:am በYouTube ላይ የቪዲዮ መጫወቻ እድገትን በራሱ ያስቀምጣል እና ያቀጥላል በመግባት ያስፈልጋል። // @description:ha Ajiye kuma ci gaba da ci gaban kallon bidiyo a YouTube ta atomatik ba tare da shiga ba. // @description:ur YouTube پر ویڈیوز کی پلے بیک کی پیش رفت کو خودکار طریقے سے محفوظ اور دوبارہ شروع کریں، لاگ ان کی ضرورت نہیں۔ // @description:ca Desa i reprèn automàticament el progrés de reproducció de vídeos a YouTube sense necessitat d'iniciar sessió. // @description:zu Igcina futhi uqhubeke ngokuzenzakalelayo nokuqhubeka kwevidiyo ku-YouTube ngaphandle kokungena. // @description:yue 自動儲存及繼續 YouTube 影片播放進度,無需登入。 // @description:es-419 Guarda y reanuda automáticamente el progreso de reproducción de videos en YouTube sin necesidad de iniciar sesión. // @homepage https://github.com/Alplox/Youtube-Playback-Plox // @supportURL https://github.com/Alplox/Youtube-Playback-Plox/issues // @version 0.0.9-3 // @author Alplox // @match https://www.youtube.com/* // @exclude https://www.youtube.com/live_chat* // @icon https://raw.githubusercontent.com/Alplox/StartpagePlox/refs/heads/main/assets/favicon/favicon.ico // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_addElement // @run-at document-end // @namespace youtube-playback-plox // @license MIT // @require https://update.greasyfork.icu/scripts/549881/1783571/YouTube%20Helper%20API.js // @downloadURL none // ==/UserScript== // ------------------------------------------ // MARK: 🔍 SISTEMA DE LOGGING // ------------------------------------------ (function () { 'use strict'; const L = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 }; const level = L.silent; // Cambiar a 'debug' para ver todo, o 'warn'/'error' para menos const S = { debug: 'color:#6a9955;', info: 'color:#4FC1FF;', warn: 'color:#ce9178;font-weight:bold;', error: 'color:#f44747;font-weight:bold;' }; const noop = () => { }; const build = (t, l) => (level >= l || t === 'error') ? (c, ...a) => console[t](`%c[${c}]`, S[t], ...a) : noop; window.MyScriptLogger = { log: build('debug', L.debug), debug: build('debug', L.debug), info: build('info', L.info), warn: build('warn', L.warn), error: build('error', L.error) // Los errores siempre se muestran }; })(); // Atajo para no tener que escribir window.MyScriptLogger cada vez const { log: logLog, info: logInfo, warn: logWarn, error: logError } = window.MyScriptLogger; // --- INICIO CARGA LÓGICA PRINCIPAL DEL USERSCRIPT --- (() => { 'use strict'; const SCRIPT_VERSION = '0.0.9-3'; /** * Polyfill ligero para CustomEvent en navegadores antiguos. * Crea window.CustomEvent si no existe o no es una función nativa. * @returns {void} */ (function polyfillCustomEvent() { try { if (typeof window.CustomEvent === 'function') return; } catch (_) { /* noop */ } try { function CustomEventPolyfill(event, params) { params = params || { bubbles: false, cancelable: false, detail: null }; const evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; } CustomEventPolyfill.prototype = (window.Event || function () { }).prototype; window.CustomEvent = CustomEventPolyfill; } catch (_) { /* noop */ } })(); // ------------------------------------------ // MARK: 🌐 Carga de Traducciones // ------------------------------------------ // URL del archivo de traducciones const TRANSLATIONS_URL = 'https://raw.githubusercontent.com/Alplox/Youtube-Playback-Plox/refs/heads/main/translations.json'; const TRANSLATIONS_URL_BACKUP = 'https://cdn.jsdelivr.net/gh/Alplox/Youtube-Playback-Plox@refs/heads/main/translations.json'; const TRANSLATIONS_EXPECTED_VERSION = SCRIPT_VERSION; // Variables globales para las traducciones let TRANSLATIONS = {}; let LANGUAGE_FLAGS = {}; // Traducciones básicas de fallback en caso de error const FALLBACK_FLAGS = { "en-US": { "emoji": "🇺🇸", "code": "en-US", "name": "English (US)" }, "es-ES": { "emoji": "🇪🇸", "code": "es-ES", "name": "Español" }, "fr": { "emoji": "🇫🇷", "code": "fr", "name": "Français" } }; const FALLBACK_TRANSLATIONS = { "en-US": { "settings": "Settings", "savedVideos": "View saved videos", "close": "Close", "save": "Save", "cancel": "Cancel", "delete": "Delete", "undo": "Undo", "enableSavingFor": "Enable saving for", "regularVideos": "Regular videos", "shorts": "Shorts", "liveStreams": "Live streams", "live": "Live", "showNotifications": "Show save notifications", "minSecondsBetweenSaves": "Minimum seconds between saves", "showFloatingButton": "Show floating button", "language": "Language", "alertStyle": "Alert style in playback bar", "alertIconText": "Icon + Text", "alertIconOnly": "Icon Only", "alertTextOnly": "Text Only", "alertHidden": "Hidden", "noSavedVideos": "No saved videos.", "sortBy": "Sort by", "mostRecent": "Most recent", "oldest": "Oldest", "titleAZ": "Title (A-Z)", "filterByType": "Filter by type", "all": "All", "videos": "Videos", "playlist": "Playlist", "searchByTitleOrAuthor": "Search by title or author...", "export": "Export", "import": "Import", "progressSaved": "Progress saved", "storageFull": "Storage full - Unable to save progress", "dataExported": "Data exported", "itemsImported": "Imported {count} items", "importError": "Error importing. Make sure the file is valid.", "exportError": "Error exporting data", "invalidFormat": "Invalid format", "invalidJson": "Invalid JSON", "invalidDatabase": "Invalid database", "noValidVideos": "No valid videos found to import", "allDataCleared": "All data cleared", "noDataToRestore": "No data to restore", "allDataRestored": "All data restored", "clearAllDataConfirm": "Are you sure you want to delete all data?", "omitedVideos": "Omitted videos", "fileTooLarge": "File is too large (max {size})", "importingFromFreeTube": "Importing from FreeTube...", "importingFromFreeTubeAsSQLite": "Importing from FreeTube as SQLite...", "videosImported": "videos imported", "noVideosImported": "no videos could be imported", "errors": "errors", "noVideosFoundInFreeTubeDB": "No videos found in FreeTube database", "videosImportedFromFreeTubeDB": "videos imported from FreeTube database", "noVideosImportedFromFreeTubeDB": "no videos could be imported from FreeTube database", "fileEmpty": "File is empty", "processingFile": "Processing file...", "configurationSaved": "Configuration saved", "startTimeSet": "Start time set to", "fixedTimeRemoved": "Fixed time removed.", "itemDeleted": "deleted.", "unknownError": "Unknown error", "retryNow": "Retry now", "retryCompleted": "Retry completed", "progress": "Progress", "alwaysStartFrom": "Always start from", "resumedAt": "Resumed at", "percentWatched": "% watched", "remaining": "remaining", "setStartTime": "Set start time", "changeOrRemoveStartTime": "Always start from {time} (Click to change or remove)", "enterStartTime": "Enter the start time you always want to use (example: 1:23)", "enterStartTimeOrEmpty": "Enter the start time you always want to use (example: 1:23) or leave empty to remove", "deleteEntry": "Delete entry", "youtubePlaybackPlox": "YouTube Playback Plox", "playlistPrefix": "Playlist", "unknown": "Unknown", "notAvailable": "N/A", "clearAll": "Clear all", "clearAllConfirm": "Are you sure you want to delete ALL saved videos? This action can be undone.", "allItemsCleared": "All items cleared", "viewAllHistory": "View all history", "viewCompletedVideos": "View completed videos", "completed": "Completed", "completedVideos": "Completed videos", "videosWithFixedTime": "Videos with fixed time", "views": "Views", "enableProgressBarGradient": "Enable color gradient in progress bar", "staticFinishPercent": "Percentage to mark video as completed", "openChannel": "Open channel", "openPlaylist": "Open playlist", "createPlaylist": "Create playlist", "selectVideos": "Select videos", "selectedVideos": "Selected videos", "generatePlaylistLink": "Generate playlist link", "playlistLinkGenerated": "Playlist link generated", "copyLink": "Copy link", "linkCopied": "Link copied to clipboard", "selectAtLeastOne": "Select at least one video", "tooManyVideos": "Too many videos selected (max 200)", "miniplayerVideos": "Miniplayer videos", "inlinePreviews": "Inline previews (Home)", "removeFromPlaylist": "Remove from playlist", "confirmRemoveFromPlaylist": "Are you sure you want to remove this video from the playlist? It will be kept as an individual video.", "playlistAssociationRemoved": "Playlist association removed", "loading": "Loading", "rendered": "rendered", "previews": "Previews", "migratingData": "Migrating saved data from previous version...", "migratingDataProgress": "Migrating data... {count} entries processed", "migrationComplete": "Migration completed: {migrated} videos successfully migrated", "migrationNoData": "No data found to migrate" }, "es-ES": { "settings": "Configuración", "savedVideos": "Ver videos guardados", "close": "Cerrar", "save": "Guardar", "cancel": "Cancelar", "delete": "Eliminar", "undo": "Deshacer", "enableSavingFor": "Activar guardado para", "regularVideos": "Videos regulares", "shorts": "Shorts", "liveStreams": "Directos (Livestreams)", "live": "Directo", "showNotifications": "Mostrar notificaciones de guardado", "minSecondsBetweenSaves": "Intervalo segundos mínimos entre guardados", "showFloatingButton": "Mostrar botón flotante", "language": "Idioma", "alertStyle": "Estilo de alertas en la barra de reproducción", "alertIconText": "Icono + Texto", "alertIconOnly": "Solo Icono", "alertTextOnly": "Solo Texto", "alertHidden": "Oculto", "noSavedVideos": "No hay videos guardados.", "sortBy": "Ordenar por", "mostRecent": "Más recientes", "oldest": "Más antiguos", "titleAZ": "Título (A-Z)", "filterByType": "Filtrar por tipo", "all": "Todos", "videos": "Videos", "playlist": "Playlist", "searchByTitleOrAuthor": "Buscar por título o autor...", "export": "Exportar", "import": "Importar", "progressSaved": "Progreso guardado", "storageFull": "Almacenamiento lleno - No se puede guardar el progreso", "dataExported": "Datos exportados", "itemsImported": "Importados {count} elementos", "importError": "Error al importar. Asegúrate de que el archivo sea válido.", "exportError": "Error al exportar datos", "invalidFormat": "Formato inválido", "invalidJson": "JSON inválido", "invalidDatabase": "Base de datos inválida", "noValidVideos": "No se encontraron videos válidos para importar", "allDataCleared": "Todos los datos eliminados", "noDataToRestore": "No hay datos para restaurar", "allDataRestored": "Todos los datos restaurados", "clearAllDataConfirm": "¿Estás seguro de que quieres eliminar todos los datos?", "omitedVideos": "Videos omitidos", "fileTooLarge": "El archivo es demasiado grande (máx {size})", "importingFromFreeTube": "Importando desde FreeTube...", "importingFromFreeTubeAsSQLite": "Importando desde FreeTube como SQLite...", "videosImported": "videos importados", "noVideosImported": "no se pudo importar ningún video", "errors": "errores", "noVideosFoundInFreeTubeDB": "No se encontraron videos en la base de datos de FreeTube", "videosImportedFromFreeTubeDB": "videos importados desde la base de datos de FreeTube", "noVideosImportedFromFreeTubeDB": "no se pudo importar ningún video desde la base de datos de FreeTube", "fileEmpty": "El archivo está vacío", "processingFile": "Procesando archivo...", "configurationSaved": "Configuración guardada", "startTimeSet": "Tiempo de inicio establecido en", "fixedTimeRemoved": "Tiempo fijo eliminado.", "itemDeleted": "eliminado.", "unknownError": "Error desconocido", "retryNow": "Reintentar ahora", "retryCompleted": "Reintentos completados", "progress": "Progreso", "alwaysStartFrom": "Siempre desde", "resumedAt": "Reanudado en", "percentWatched": "% visto", "remaining": "restantes", "setStartTime": "Establecer tiempo de inicio", "changeOrRemoveStartTime": "Siempre empezar en {time} (Click para cambiar o eliminar)", "enterStartTime": "Introduce el tiempo de inicio que siempre quieres usar (ejemplo: 1:23)", "enterStartTimeOrEmpty": "Introduce el tiempo de inicio que siempre quieres usar (ejemplo: 1:23) o deja vacío para eliminar", "deleteEntry": "Eliminar entrada", "youtubePlaybackPlox": "YouTube Playback Plox", "playlistPrefix": "Playlist", "unknown": "Desconocido", "notAvailable": "N/A", "clearAll": "Eliminar todo", "clearAllConfirm": "¿Estás seguro de que quieres eliminar TODOS los videos guardados? Esta acción se puede deshacer.", "allItemsCleared": "Todos los elementos eliminados", "viewAllHistory": "Ver todo el historial", "viewCompletedVideos": "Ver videos completados", "completed": "Completado", "completedVideos": "Videos completados", "videosWithFixedTime": "Videos con tiempo fijo", "views": "Vistas", "enableProgressBarGradient": "Habilitar degradado de colores en barra de progreso", "staticFinishPercent": "Porcentaje para marcar video como completado", "openChannel": "Abrir canal", "openPlaylist": "Abrir playlist", "createPlaylist": "Crear playlist", "selectVideos": "Seleccionar videos", "selectedVideos": "Videos seleccionados", "generatePlaylistLink": "Generar enlace de playlist", "playlistLinkGenerated": "Enlace de playlist generado", "copyLink": "Copiar enlace", "linkCopied": "Enlace copiado al portapapeles", "selectAtLeastOne": "Selecciona al menos un video", "tooManyVideos": "Demasiados videos seleccionados (máx 200)", "miniplayerVideos": "Videos en miniplayer", "inlinePreviews": "Previsualizaciones en inicio (Home)", "removeFromPlaylist": "Quitar de la playlist", "confirmRemoveFromPlaylist": "¿Estás seguro de que quieres quitar este video de la playlist? Se mantendrá como video individual.", "playlistAssociationRemoved": "Asociación de playlist eliminada", "loading": "Cargando", "rendered": "renderizados", "previews": "Previsualizaciones", "migratingData": "Migrando datos guardados desde versión anterior...", "migratingDataProgress": "Migrando datos... {count} entradas procesadas", "migrationComplete": "Migración completada: {migrated} videos migrados correctamente", "migrationNoData": "No se encontraron datos para migrar" }, "fr": { "settings": "Paramètres", "savedVideos": "Voir les vidéos enregistrées", "close": "Fermer", "save": "Enregistrer", "cancel": "Annuler", "delete": "Supprimer", "undo": "Annuler", "enableSavingFor": "Activer la sauvegarde pour", "regularVideos": "Vidéos régulières", "shorts": "Shorts", "liveStreams": "Diffusions en direct", "live": "Diffusions en direct", "showNotifications": "Afficher les notifications de sauvegarde", "minSecondsBetweenSaves": "Secondes minimales entre les sauvegardes", "showFloatingButton": "Afficher le bouton flottant", "language": "Langue", "alertStyle": "Style d'alerte dans la barre de lecture", "alertIconText": "Icône + Texte", "alertIconOnly": "Icône uniquement", "alertTextOnly": "Texte uniquement", "alertHidden": "Masqué", "noSavedVideos": "Aucune vidéo enregistrée.", "sortBy": "Trier par", "mostRecent": "Plus récent", "oldest": "Plus ancien", "titleAZ": "Titre (A-Z)", "filterByType": "Filtrer par type", "all": "Tous", "videos": "Vidéos", "playlist": "Playlist", "searchByTitleOrAuthor": "Rechercher par titre ou auteur...", "export": "Exporter", "import": "Importer", "progressSaved": "Progrès enregistré", "dataExported": "Données exportées", "itemsImported": "{count} éléments importés", "importError": "Erreur lors de l'importation. Assurez-vous que le fichier est valide.", "exportError": "Erreur lors de l'exportation des données", "invalidFormat": "Format invalide", "invalidJson": "JSON invalide", "invalidDatabase": "Base de données invalide", "noValidVideos": "Aucune vidéo valide trouvée à importer", "allDataCleared": "Toutes les données ont été effacées", "noDataToRestore": "Aucune donnée à restaurer", "allDataRestored": "Toutes les données restaurées", "clearAllDataConfirm": "Êtes-vous sûr de vouloir supprimer toutes les données ?", "omitedVideos": "Vidéos omises", "fileTooLarge": "Le fichier est trop volumineux (max {size})", "importingFromFreeTube": "Importation depuis FreeTube...", "importingFromFreeTubeAsSQLite": "Importation depuis FreeTube en tant que SQLite...", "videosImported": "vidéos importées", "noVideosImported": "aucune vidéo n'a pu être importée", "errors": "erreurs", "noVideosFoundInFreeTubeDB": "Aucune vidéo trouvée dans la base de données FreeTube", "videosImportedFromFreeTubeDB": "vidéos importées depuis la base de données FreeTube", "noVideosImportedFromFreeTubeDB": "aucune vidéo n'a pu être importée depuis la base de données FreeTube", "fileEmpty": "Le fichier est vide", "processingFile": "Traitement du fichier...", "configurationSaved": "Configuration enregistrée", "startTimeSet": "Heure de début définie à", "fixedTimeRemoved": "Heure fixe supprimée.", "itemDeleted": "supprimé.", "unknownError": "Erreur inconnue", "retryNow": "Réessayer maintenant", "retryCompleted": "Réessais terminés", "progress": "Progrès", "alwaysStartFrom": "Toujours commencer à", "resumedAt": "Repris à", "percentWatched": "% regardé", "remaining": "restant", "setStartTime": "Définir l'heure de début", "changeOrRemoveStartTime": "Toujours commencer à {time} (Cliquez pour changer ou supprimer)", "enterStartTime": "Entrez l'heure de début que vous souhaitez toujours utiliser (exemple: 1:23)", "enterStartTimeOrEmpty": "Entrez l'heure de début que vous souhaitez toujours utiliser (exemple: 1:23) ou laissez vide pour supprimer", "deleteEntry": "Supprimer l'entrée", "youtubePlaybackPlox": "YouTube Playback Plox", "playlistPrefix": "Playlist", "unknown": "Inconnu", "notAvailable": "N/A", "clearAll": "Tout effacer", "clearAllConfirm": "Êtes-vous sûr de vouloir supprimer TOUTES les vidéos enregistrées ? Cette action peut être annulée.", "allItemsCleared": "Tous les éléments effacés", "viewAllHistory": "Voir tout l'historique", "viewCompletedVideos": "Voir les vidéos terminées", "completed": "Terminé", "completedVideos": "Vidéos terminées", "videosWithFixedTime": "Vidéos avec un temps fixe", "views": "Vues", "enableProgressBarGradient": "Activer le dégradé de couleurs dans la barre de progression", "staticFinishPercent": "Pourcentage pour marquer la vidéo comme terminée", "openChannel": "Ouvrir la chaîne", "openPlaylist": "Ouvrir la playlist", "createPlaylist": "Créer une playlist", "selectVideos": "Sélectionner des vidéos", "selectedVideos": "Vidéos sélectionnées", "generatePlaylistLink": "Générer le lien de la playlist", "playlistLinkGenerated": "Lien de la playlist généré", "copyLink": "Copier le lien", "linkCopied": "Lien copié dans le presse-papiers", "selectAtLeastOne": "Sélectionnez au moins une vidéo", "tooManyVideos": "Trop de vidéos sélectionnées (max 200)", "miniplayerVideos": "Vidéos en miniplayer", "inlinePreviews": "Aperçus intégrés (Accueil)", "removeFromPlaylist": "Retirer de la playlist", "confirmRemoveFromPlaylist": "Êtes-vous sûr de vouloir retirer cette vidéo de la playlist ? Elle sera conservée comme vidéo individuelle.", "playlistAssociationRemoved": "Association de playlist supprimée", "loading": "Chargement", "rendered": "rendus", "previews": "Aperçus", "migratingData": "Migration des données enregistrées depuis la version précédente...", "migratingDataProgress": "Migration des données... {count} éléments traités", "migrationComplete": "Migration terminée : {migrated} vidéos migrées avec succès", "migrationNoData": "Aucune donnée trouvée à migrer" } }; // Función para cargar las traducciones desde el archivo JSON externo async function loadTranslations() { const CACHE_KEY = `${CONFIG.storagePrefix}translations_cache_v1`; const TTL_MS = 6 * 60 * 60 * 1000; // 6 horas // 1) Intentar usar caché (GM_* preferido; luego localStorage) try { if (typeof GM_getValue === 'function') { const raw = await GM_getValue(CACHE_KEY, null); if (raw) { const cached = JSON.parse(raw); const isFresh = cached?.ts && (Date.now() - cached.ts) < TTL_MS; const cachedVersion = cached?.version ?? cached?.data?.VERSION; const versionMatches = !TRANSLATIONS_EXPECTED_VERSION || cachedVersion === TRANSLATIONS_EXPECTED_VERSION; if (isFresh && cached?.data && versionMatches) { logInfo('loadTranslations', 'Usando traducciones desde caché GM_*'); return cached.data; } } } } catch (_) { } try { const raw = localStorage.getItem(CACHE_KEY); if (raw) { const cached = JSON.parse(raw); const isFresh = cached?.ts && (Date.now() - cached.ts) < TTL_MS; const cachedVersion = cached?.version ?? cached?.data?.VERSION; const versionMatches = !TRANSLATIONS_EXPECTED_VERSION || cachedVersion === TRANSLATIONS_EXPECTED_VERSION; if (isFresh && cached?.data && versionMatches) { logInfo('loadTranslations', 'Usando traducciones desde caché localStorage'); return cached.data; } } } catch (_) { } // 2) Helper para cargar desde URL con GM_xmlhttpRequest o fetch const fetchUrl = async (url) => { if (typeof GM_xmlhttpRequest === 'function') { return await new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: 'GET', url, timeout: 5000, onload: (response) => { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); } }, onerror: (e) => reject(e), ontimeout: () => reject(new Error('timeout')) }); } catch (err) { reject(err); } }); } // Fallback a fetch nativo if (typeof fetch === 'function') { const resp = await fetch(url, { cache: 'no-store' }); const text = await resp.text(); return JSON.parse(text); } throw new Error('No hay método de red disponible'); }; // 3) Intentar URLs primarias/secundarias const urls = [TRANSLATIONS_URL, TRANSLATIONS_URL_BACKUP]; let data = null; for (const url of urls) { try { const candidate = await fetchUrl(url); if (candidate?.LANGUAGE_FLAGS && Object.keys(candidate.LANGUAGE_FLAGS).length > 0 && candidate?.TRANSLATIONS && Object.keys(candidate.TRANSLATIONS).length > 0) { logInfo('loadTranslations', 'Traducciones externas cargadas correctamente desde: ' + url); data = candidate; break; } else { logWarn('loadTranslations', 'Traducciones inválidas desde: ' + url); } } catch (e) { logWarn('loadTranslations', 'Fallo al cargar traducciones desde ' + url, e); } } if (!data) { logError('loadTranslations', 'No se pudieron cargar traducciones externas, usando fallback'); data = { LANGUAGE_FLAGS: FALLBACK_FLAGS, TRANSLATIONS: FALLBACK_TRANSLATIONS }; } // 4) Guardar en caché const cachePayload = JSON.stringify({ ts: Date.now(), version: data?.VERSION ?? TRANSLATIONS_EXPECTED_VERSION ?? null, data }); try { if (typeof GM_setValue === 'function') await GM_setValue(CACHE_KEY, cachePayload); } catch (_) { } try { localStorage.setItem(CACHE_KEY, cachePayload); } catch (_) { } return data; } // ------------------------------------------ // MARK: 📦 Config // ------------------------------------------ const CONFIG = { /** Diferencia mínima (en segundos) para considerar un cambio de posición como válido */ minSeekDiff: 1.5, /** Prefijo para claves en localStorage */ storagePrefix: 'YT_PLAYBACK_PLOX_', /** Enumeración de estilos de alerta */ alertStylesSettings: { icon_only: 'iconOnly', text_only: 'textOnly', icon_and_text: 'iconText', no_icon_no_text: 'hidden' }, /** Clave para guardar configuraciones del usuario en GM_* */ userSettingsKey: 'YT_PLAYBACK_PLOX_userSettings', /** Valores predeterminados para configuraciones del usuario */ defaultSettings: { showNotifications: true, minSecondsBetweenSaves: 1, showFloatingButtons: false, saveRegularVideos: true, // Por defecto, guardar videos regulares saveShorts: false, // Por defecto, no guardar Shorts saveLiveStreams: false, // Por defecto, no guardar directos de URL tipo "/live" o "/watch" con player en directo, si ya es VOD lo toma como regular language: 'en-US', // Idioma predeterminado alertStyle: 'iconText', // Estilo de alerta predeterminado enableProgressBarGradient: true, // Por defecto, habilitar degradado de colores en barra de progreso staticFinishPercent: 95, // Porcentaje desde el final para considerar video como completado (95% = 5% antes del final) saveInlinePreviews: false, // Guardar previsualizaciones inline (Homepage) desactivado por defecto saveMiniplayerVideos: true, // Guardar videos en miniplayer (default: activo) }, /** Clave para guardar filtros del usuario en GM_* */ userFiltersKey: 'YT_PLAYBACK_PLOX_userFilters', /** Valores predeterminados para filtros del usuario */ defaultFilters: { orderBy: "recent", filterBy: "all", searchQuery: "" } }; // MARK: Selectors // === VIDEOS /watch === // Jerarquía simplificada de YouTube Video en el DOM (acorde a url /watch): // // // └─ // └─ // └─ // └─ // └─ // └─ // └─ // └─ // 🟢 └─ // └─ // └─ -> Video activo (Existe 3 veces en DOM; una dentro #movie_player, #shorts-player y #inline-preview-player) // === SHORTS /shorts === // Jerarquía simplificada de YouTube Shorts en el DOM (acorde a url /shorts): // // └─ -> Contenedor interno donde se renderiza el visor de Shorts, incluye botones de control, etc // └─ -> Irrelevante, serviria solo para primera carga de short luego queda stale (enlazado a ese primer short cargado) // └─ -> Existen multiples de estos divs donde cada short va asignado a un incremental numero en su id (id="1", id="2", etc) Son indistingibles de no ser por sus ids // └─ // └─ -> Contenedor interno donde se renderiza el video de Shorts Activo // └─ // └─ -> id="player-container" puede exitir 3 veces en DOM; dentro de #video-preview, #masthead-player y #shorts-container // └─ id="player" Existe 2 veces en DOM; en anuncios homepage #masthead-player // 🟢 └─ -> Elemento que representa el Short activo (video actual) // └─ -> Existe 2 veces en DOM; una dentro de #movie_player (Por miniplayer) y la otra en #shorts-player (puede existir igual en anuncios homepage #masthead-player) // └─ -> Video activo (Existe 2 veces en DOM; una dentro de #movie_player > div.html5-video-container (Por miniplayer) y la otra en #shorts-player > div.html5-video-container) (puede existir igual en anuncios homepage #masthead-player > div.html5-video-container) // === MINIPLAYER / (homepage) === // -> Aqui lo importan es clase .ytdMiniplayerComponentVisible que indica que miniplayer esta activo y visible // └─ // └─ // └─ // └─ // └─ // └─ // 🟢 └─ // └─ // └─ -> Video activo // === INLINE PREVIEWS / (homepage) === // └─ // └─ // └─ // └─ // └─ // └─ // └─ // └─ // └─ // 🟢 └─ // └─ // └─ const selector = { class: c => `.${c}`, id: id => `#${id}`, attr: a => `[${a}]`, element: e => e }; // ELEMENTS (Elementos simples ) const ELEMENTS = { // === SHORTS === YTD_SHORTS: 'ytd-shorts', REEL_VIDEO_RENDERER: 'ytd-reel-video-renderer', // Elemento que contiene el video de Shorts Activo // === MINIPLAYER === MINIPLAYER_ELEMENT: 'ytd-miniplayer', // === INLINE PREVIEW === INLINE_PREVIEW_ELEMENT: 'ytd-video-preview', // Elemento principal del inline preview // === RICH GRID RENDERER === RICH_GRID_RENDERER: 'ytd-rich-grid-renderer', // Elemento que contiene la grilla de videos } // CLASES (Añadir . antes de cada clase con S.CLASSES) const CLASSES = { // Se usan en todos los tipos de videos HTML5_VIDEO_PLAYER: 'html5-video-player', // Clase que acompaña a IDs de elementos comunmente; #movie_player (videos y miniplayer), #shorts-player y #inline-preview-player HTML5_VIDEO_CONTAINER: 'html5-video-container', // Clases que acompañan a elemento