您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
记录看过的视频
// ==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/[email protected]/notyf.min.js // @resource NotifyCSS https://unpkg.com/[email protected]/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; } */