// ==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(`
`);
});
});