// ==UserScript== // @name Netflix Mark Watched // @author SirGrypin // @version 1.7 // @namespace watched_netflix // @description Mark Netflix shows as watched. // @include https://www.netflix.com/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js // @grant GM_registerMenuCommand // @license MIT // @downloadURL none // ==/UserScript== // Open (or create) a new database function openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open('NetflixWatchedShows', 1); request.onupgradeneeded = function(event) { const db = event.target.result; if (!db.objectStoreNames.contains('shows')) { db.createObjectStore('shows', { keyPath: 'id' }); } }; request.onsuccess = function(event) { resolve(event.target.result); }; request.onerror = function(event) { reject(event.target.error); }; }); } // Function to mark a show as watched async function markAsWatched(videoId) { const db = await openDatabase(); const transaction = db.transaction(['shows'], 'readwrite'); const store = transaction.objectStore('shows'); store.put({ id: videoId, watched: true }); } // Function to mark a show as unwatched async function markAsUnwatched(videoId) { const db = await openDatabase(); const transaction = db.transaction(['shows'], 'readwrite'); const store = transaction.objectStore('shows'); store.put({ id: videoId, watched: false }); } // Function to check if a show is marked as watched async function isWatched(videoId) { const db = await openDatabase(); const transaction = db.transaction(['shows'], 'readonly'); const store = transaction.objectStore('shows'); return new Promise((resolve) => { const request = store.get(videoId); request.onsuccess = function(event) { resolve(event.target.result ? event.target.result.watched : false); }; }); } function addWatchedButton(element) { var id = JSON.parse(decodeURI(element.data('ui-tracking-context'))).video_id; id = parseInt(id, 10); // Convert to number isWatched(id).then((watched) => { if (watched) { element.closest('.title-card-container').addClass('g_watched'); } }); var watchedEye = $('
👁
'); watchedEye.click(function () { var cardContainer = $(this).closest('.title-card-container'); var isWatched = cardContainer.hasClass('g_watched'); if (isWatched) { cardContainer.removeClass('g_watched'); markAsUnwatched(id); } else { cardContainer.addClass('g_watched'); markAsWatched(id); } }); element.closest('.title-card-container').append(watchedEye); } // Backup data related to watched shows to a JSON file async function backupWatchedShows() { const db = await openDatabase(); const transaction = db.transaction(['shows'], 'readonly'); const store = transaction.objectStore('shows'); const request = store.getAll(); request.onsuccess = function(event) { const data = JSON.stringify(event.target.result); const blob = new Blob([data], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "NetflixWatchedBackup.json"; a.style.display = "none"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; } // Restore data related to watched shows from a JSON file async function restoreWatchedShows() { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.click(); input.addEventListener("change", async function () { const file = input.files[0]; const reader = new FileReader(); reader.onload = async function () { try { const data = JSON.parse(reader.result); const db = await openDatabase(); const transaction = db.transaction(['shows'], 'readwrite'); const store = transaction.objectStore('shows'); // Check if the data is in the old format if (typeof data === 'object' && Object.keys(data).some(key => key.startsWith("watched_"))) { // Convert old localStorage format to the new format for (let key in data) { if (key.startsWith("watched_")) { const videoId = parseInt(key.replace("watched_", ""), 10); // Convert to number const watched = data[key] === "true"; store.put({ id: videoId, watched: watched }); } } } else if (Array.isArray(data)) { // Assume the data is in the new format and restore directly for (let show of data) { store.put(show); } } else { throw new Error("Unrecognized data format."); } alert("Watched data restored successfully."); } catch (e) { alert("Error restoring data: " + e); } }; if (file) { reader.readAsText(file); } }); } // Migrate data from localStorage to IndexedDB, if not already done async function migrateLocalStorageToIndexedDB() { const migrationFlag = localStorage.getItem('migration_done'); if (!migrationFlag) { const db = await openDatabase(); const transaction = db.transaction(['shows'], 'readwrite'); const store = transaction.objectStore('shows'); // Migrate all watched shows from localStorage to IndexedDB for (let key in localStorage) { if (key.startsWith("watched_")) { const videoId = parseInt(key.replace("watched_", ""), 10); // Convert to number const watched = localStorage.getItem(key) === "true"; store.put({ id: videoId, watched: watched }); localStorage.removeItem(key); } } // Set migration flag localStorage.setItem('migration_done', 'true'); console.log("Migration from localStorage to IndexedDB completed."); } } $(document).ready(function () { // Perform migration from localStorage to IndexedDB migrateLocalStorageToIndexedDB().then(() => { // Initial execution for the elements present on the page load $('[data-ui-tracking-context]').each(function () { addWatchedButton($(this)); }); // Add a MutationObserver to detect when new elements are added var observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.addedNodes) { $(mutation.addedNodes).find('[data-ui-tracking-context]').each(function () { addWatchedButton($(this)); }); } }); }); // Observe changes in the DOM, including dynamically loaded content observer.observe(document.body, { childList: true, subtree: true }); GM_registerMenuCommand("Backup Watched", backupWatchedShows); GM_registerMenuCommand("Restore Watched", restoreWatchedShows); $('head').append(` `); }); });