Sleazy Fork is available in English.

视频记录

记录看过的视频

// ==UserScript==
// @name         视频记录
// @namespace    xywc-s
// @author       xywc-s
// @version      2.0.0
// @description  记录看过的视频
// @match        https://spankbang.com/*
// @match        https://*.xvideos.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=spankbang.com
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require     https://cdn.jsdelivr.net/npm/idb@7/build/umd.js
// @require      https://unpkg.com/notyf@3.10.0/notyf.min.js
// @resource     NotifyCSS https://unpkg.com/notyf@3.10.0/notyf.min.css
// @run-at       document-end
// @license MIT
// ==/UserScript==
/*jshint esversion: 10 */


const NotifyCSS = GM_getResourceText("NotifyCSS");
GM_addStyle(NotifyCSS);

const notyf = new Notyf({
    position: {
        x: 'right',
        y: 'top'
      }
});



function xvideosRecord() {
  class Button {
    constructor(id, el) {
      this.btn = document.createElement('button')
      this.btn.innerHTML = '<span class="icon-f icf-plus-square"></span>'
      this.btn.onclick = () => {
        const ids = GM_getValue('xvideos', [])
        ids.push(id)
        GM_setValue('xvideos', ids)
        notyf.success('记录成功')
        el?el.remove():this.btn.remove()
      }
      return this.btn
    }
  }

  const ids = GM_getValue('xvideos', [])
  const videoBox = document.querySelector('.mozaique')

  if (videoBox) {
    const videos = videoBox.children
    for (let video of videos) {
      const v_id = video.getAttribute('data-id')
      if (!v_id) {
        video.style.display = 'none';
      }
      if (ids.includes(v_id)) {
        video.style.display = 'none';
      } else {
        const title = video.children[1].children[0]
         if(title) title.prepend(new Button(v_id, video))
      }
    }
  }
  if(html5player){
    const v_id = html5player.id_video
    if(!ids.includes(v_id)){
      const bar = document.querySelector('#v-actions .tabs')
      bar.prepend(new Button(v_id))
    }
  }
}

function spankBangRecord() {
  const storage = localStorage.getItem('ids')

  const list = document.querySelectorAll('div[id^="v_id"]')
  const v = document.querySelector('#video')

  listVideoRecord(list)
  if (v) {
      const right = document.querySelector('.right')
      console.log({right})
      if(right) right.style.display = 'none'
      mainVideoRecord(v)
  }

  function mainVideoRecord(v) {
    const vid = v.getAttribute('data-videoid')
    const title = document.querySelector('.main_content_title')
    if (storage && storage.includes(vid)) {
      // 已记录
    } else {
      const a = document.createElement('a')
      a.innerHTML =
        '<svg class="i_svg i_star"><use xlink:href="/static/desktop/gen/universal.master.6.1.00d54069.svg#star"></use></svg>'
      a.title = '记录'
      a.style.cursor = 'pointer'
      a.onclick = () => {
        localStorage.setItem('ids', storage + ',' + vid)
        notyf.success('记录成功')
        a.remove()
      }
      title.append(a)
    }
  }

  function listVideoRecord(list) {
    list.forEach((item) => {
      const listVID = item.getAttribute('data-id')
      if (storage && storage.includes(listVID)) {
        item.style.display = 'none'
      } else {
        const btn = document.createElement('span')
        btn.classList.add('b')
        btn.innerHTML =
          '<svg class="i_svg i_plus-square"><use xlink:href="/static/desktop/gen/universal.master.6.1.00d54069.svg#plus-square"></use></svg>'
        btn.onclick = () => {
          //localStorage.setItem('ids', localStorage.getItem('ids') + ',' + listVID)
          notyf.success('记录成功')
          //btn.remove()
        }
        item.querySelector('.stats').append(btn)
        item.querySelector('.stats').children[0].remove()
      }
    })
  }
}

/**
 * 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);

class Button {
    constructor(el, db) {
        this.btn = document.createElement('button')
        this.btn.title = 'watched'
        this.btn.innerHTML = '<svg class="i_svg i_star"><use xlink:href="/static/desktop/gen/universal.master.6.1.00d54069.svg#star"></use></svg><span>watched</span>'
        this.btn.onclick = async () => {
            // todo 记录 标记
            const video = getVideo(el)
            const res = await storeVideo(video, db)
            tagImg(el)
            notyf.success('记录成功')
            this.btn.remove()
        }
        return this.btn
    }
}
/**
 * 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.put(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();
    names.forEach((e)=>{
        if (keys.includes(e.id)) {
            tagImg(e)
        }else {
            const bar = e.querySelector('.stats');
            bar && bar.prepend(new Button(e, db))
            console.log('no-tag:',e.id)
        }
    });
    return db;
}

function tagImg(e) {
    const img = e.querySelector("a picture img");
    //console.log(`Marking ${e.innerText} as watched`)
    img.classList.add("watched");
    markDiv(img)
    return img;
}

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!");
    }
}

function getVideo(e){
    const url = e.querySelector('a').href
    const name = e.querySelector('a').title
  const video = { id: e.id, url, name}
  console.log(video)
  return video
}

async function storeVideo(video, db) {
    let writeStore = db.transaction("videos", "readwrite").store;
    return writeStore.add(video);
}

/**
 * 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"));
    return (docQuery.length > 0) ? docQuery.map(markDiv) : [];
}

/**
 * MarkDiv
 */
function markDiv(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);
}

/**
 * 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)
    .catch(e => console.trace(e));

/**
const domain = location.host.split('.').at(-2)
switch (domain) {
  case 'spankbang':
    spankBangRecord()
    break;
  case 'xvideos':
    xvideosRecord();
    break;
  default: ''
    break;
}
*/