Sleazy Fork is available in English.

Mark Watched Videos for SpankBang

Marks videos that you've previously seen as watched, across the entire site.

Verze ze dne 31. 05. 2021. Zobrazit nejnovější verzi.

// ==UserScript==
// @namespace   LewdPursuits
// @name        Mark Watched Videos for SpankBang
// @match       *://spankbang.com/*
// @version     0.5.2
// @author      LewdPursuits
// @description Marks videos that you've previously seen as watched, across the entire site.
// @license     GPL-3.0-or-later
// @require     https://unpkg.com/idb/build/iife/index-min.js
// ==/UserScript==

/**
 * This creates the CSS needed to gray out the thumbnail and display the Watched text over it
 * The style element is added to the bottom of the body so it's the last style sheet processed
 * this ensures these styles take highest priority
 */
const style = document.createElement("style");
style.textContent = `img.watched {
		filter: grayscale(80%);
	}
	div.centered{
		position: absolute;
		color: white;
		height: 100%;
		width: 100%;
		transform: translate(0, -100%);
		z-index: 999;
		text-align: center;
	}
	div.centered p {
		position: relative;
		top: 40%;
		font-size: 1.5rem;
		background: rgba(0,0,0,0.5);
		display: inline;
		padding: 2%;
	}`;
document.body.appendChild(style);
/**
 * Splits a floating point number, and returns the digits from after the decimal point.
 * @param float A floating point number.
 * @returns A number.
 */
function after(float) {
    const fraction = float.toString().split('.')[1];
    return parseInt(fraction);
}
/**
 * Fetches a webpage from a given URL and returns a promise for the parsed document.
 * @param url The URL to be fetched.
 * @returns A parsed copy of the document found at URL.
 */
async function getPage(url) {
    const response = await fetch(url);
    const parser = new DOMParser();
    if (!response.ok) {
        throw new Error(`getPage: HTTP error. Status: ${response.status}`);
    }
    // We turn the response into a string representing the page as text
    // We run the text through a DOM parser, which turns it into a useable HTML document
    return parser.parseFromString(await response.text(), "text/html");
}
/**
 * Fetches all videos from the account history, and adds them to the empty database.
 * @param db The empty database to populate.
 * @returns An array of keys for the new database entries.
 */
async function buildVideoHistory(db) {
    const historyURL = "https://spankbang.com/users/history?page=";
    let pages = [];
    pages.push(await getPage(`${historyURL}1`));
    // This gets the heading that says the number of watched videos, uses regex for 1 or more numbers
    // gets the matched number as a string, converts it to the number type, then divides by 34
    const num = Number(pages[0].querySelector("div.data h2").innerText.match(/\d+/)[0]) / 34;
    const numPages = after(num) ? Math.trunc(num) + 1 : num;
    function getVideos(historyDoc) {
        const videos = Array.from(historyDoc.querySelectorAll('div[id^="v_id"]'));
        return videos.map(div => {
            const thumb = div.querySelector("a.thumb");
            const _name = div.querySelector("a.n");
            return { id: div.id, url: thumb.href, name: _name.innerText };
        });
    }
    //If history has more than 34 videos, pages will be > 1
    //We fetch all the pages concurrently.
    if (numPages > 1) {
        const urls = [];
        for (let i = 2; i <= numPages; i++) {
            urls.push(`${historyURL}${i}`);
        }
        pages = pages.concat(await Promise.all(urls.map(getPage)));
    }
    let toAdd = pages.reduce((videos, page) => videos.concat(getVideos(page)), []);
    const writeStore = db.transaction("videos", "readwrite").store;
    return Promise.all(toAdd.map(video => writeStore.add(video)));
}
/**
 * Checks the videos object store for entries, and populates it if empty.
 * @param db The database.
 * @returns The database.
 */
async function checkStoreLength(db) {
    const readStore = await db.getAllKeys("videos");
    if (readStore.length === 0) {
        await buildVideoHistory(db);
    }
    return db;
}
/**
 * Checks the database for any watched videos on the current page.
 * @param db The database containing watched history.
 * @returns The database.
 */
async function tagAsWatched(db) {
    // We check for the existance of any watched videos on the current page
    // If there are any, we move to the thumbnail and add the .watched class
    // This applys the CSS style above, and allows us to easily find the videos again
    const names = Array.from(document.querySelectorAll('div[id^="v_id"]'));
    const readStore = db.transaction("videos").store;
    const keys = await readStore.getAllKeys();
    function tagImg(e) {
        if (keys.includes(e.id)) {
            const img = e.querySelector("a picture img");
            //console.log(`Marking ${e.innerText} as watched`)
            img.classList.add("watched");
            return img;
        }
    }
    names.forEach(tagImg);
    return db;
}
function getVideoID() {
    try {
        const div = document.querySelector("div#video");
        return `v_id_${div.dataset.videoid}`;
    }
    catch {
        throw new Error("getVideoID: div#video not found!");
    }
}
function getVideoURL() {
    try {
        return document.querySelector('meta[property="og:url"]').content;
    }
    catch {
        throw new Error("getVideoURL: meta element not found!");
    }
}
function getVideoName() {
    try {
        const heading = document.querySelector("div.left h1");
        return heading ? heading.innerText : "Untitled";
    }
    catch {
        throw new Error("getVideoName: heading element not found!");
    }
}
/**
 * Checks for the current video in the database, and adds it if not found.
 * @param db The database containing watched history.
 * @returns A promise for the key of the added video.
 */
async function checkStoreForVideo(db) {
    const url = `${window.location}`;
    if (!/spankbang\.com\/\w+\/video\//.test(url) &&
        !/spankbang\.com\/\w+-\w+\/playlist\//.test(url)) {
        return;
    }
    const video = { id: getVideoID(), url: "", name: "" };
    let readStore = db.transaction("videos").store;
    const lookup = await readStore.get(video.id);
    if (lookup !== undefined) {
        return;
    }
    video.url = getVideoURL();
    video.name = getVideoName();
    let writeStore = db.transaction("videos", "readwrite").store;
    return writeStore.add(video);
}
/**
 * Checks the current page for any videos marked as watched, and adds the watched text in front of them.
 * @returns An array containing the newly created Div elements
 */
function filterWatched() {
    const docQuery = Array.from(document.querySelectorAll("img.watched"));
    function makeDiv(e) {
        const newPara = document.createElement("p");
        newPara.textContent = "Watched";
        const newDiv = document.createElement("div");
        newDiv.classList.add("centered");
        newDiv.appendChild(newPara);
        return e.parentElement.parentElement.appendChild(newDiv);
    }
    return (docQuery.length > 0) ? docQuery.map(makeDiv) : [];
}
/**
 * Callback function for upgrade event on openDB()
 * @param db The database
 */
function upgrade(db) {
    const store = db.createObjectStore("videos", {
        keyPath: "id",
        autoIncrement: false,
    });
    store.createIndex("url", "url", { unique: true });
}
idb.openDB("history", 1, { upgrade })
    .then(checkStoreLength)
    .then(tagAsWatched)
    .then(checkStoreForVideo)
    .then(filterWatched)
    .catch(e => console.trace(e));