Mark Watched Videos for SpankBang

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

  1. // ==UserScript==
  2. // @namespace LewdPursuits
  3. // @name Mark Watched Videos for SpankBang
  4. // @match *://spankbang.com/*
  5. // @version 0.5.3
  6. // @author LewdPursuits
  7. // @description Marks videos that you've previously seen as watched, across the entire site.
  8. // @license GPL-3.0-or-later
  9. // @require https://cdn.jsdelivr.net/npm/idb@7/build/umd.js
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. /* jshint esversion: 10 */
  14. /**
  15. * This creates the CSS needed to gray out the thumbnail and display the Watched text over it
  16. * The style element is added to the bottom of the body so it's the last style sheet processed
  17. * this ensures these styles take highest priority
  18. */
  19. const style = document.createElement("style");
  20. style.textContent = `img.watched {
  21. filter: grayscale(80%);
  22. }
  23. div.centered{
  24. position: absolute;
  25. color: white;
  26. height: 100%;
  27. width: 100%;
  28. transform: translate(0, -100%);
  29. z-index: 999;
  30. text-align: center;
  31. }
  32. div.centered p {
  33. position: relative;
  34. top: 40%;
  35. font-size: 1.5rem;
  36. background: rgba(0,0,0,0.5);
  37. display: inline;
  38. padding: 2%;
  39. }`;
  40. document.body.appendChild(style);
  41. /**
  42. * Splits a floating point number, and returns the digits from after the decimal point.
  43. * @param float A floating point number.
  44. * @returns A number.
  45. */
  46. function after(float) {
  47. const fraction = float.toString().split('.')[1];
  48. return parseInt(fraction);
  49. }
  50. /**
  51. * Fetches a webpage from a given URL and returns a promise for the parsed document.
  52. * @param url The URL to be fetched.
  53. * @returns A parsed copy of the document found at URL.
  54. */
  55. async function getPage(url) {
  56. const response = await fetch(url);
  57. const parser = new DOMParser();
  58. if (!response.ok) {
  59. throw new Error(`getPage: HTTP error. Status: ${response.status}`);
  60. }
  61. // We turn the response into a string representing the page as text
  62. // We run the text through a DOM parser, which turns it into a useable HTML document
  63. return parser.parseFromString(await response.text(), "text/html");
  64. }
  65. /**
  66. * Fetches all videos from the account history, and adds them to the empty database.
  67. * @param db The empty database to populate.
  68. * @returns An array of keys for the new database entries.
  69. */
  70. async function buildVideoHistory(db) {
  71. const historyURL = "https://spankbang.com/users/history?page=";
  72. let pages = [];
  73. pages.push(await getPage(`${historyURL}1`));
  74. // This gets the heading that says the number of watched videos, uses regex for 1 or more numbers
  75. // gets the matched number as a string, converts it to the number type, then divides by 34
  76. const num = Number(pages[0].querySelector("div.data h2").innerText.match(/\d+/)[0]) / 34;
  77. const numPages = after(num) ? Math.trunc(num) + 1 : num;
  78. function getVideos(historyDoc) {
  79. const videos = Array.from(historyDoc.querySelectorAll('div[id^="v_id"]'));
  80. return videos.map(div => {
  81. const thumb = div.querySelector("a.thumb");
  82. const _name = div.querySelector("a.n");
  83. return { id: div.id, url: thumb.href, name: _name.innerText };
  84. });
  85. }
  86. //If history has more than 34 videos, pages will be > 1
  87. //We fetch all the pages concurrently.
  88. if (numPages > 1) {
  89. const urls = [];
  90. for (let i = 2; i <= numPages; i++) {
  91. urls.push(`${historyURL}${i}`);
  92. }
  93. pages = pages.concat(await Promise.all(urls.map(getPage)));
  94. }
  95. let toAdd = pages.reduce((videos, page) => videos.concat(getVideos(page)), []);
  96. const writeStore = db.transaction("videos", "readwrite").store;
  97. return Promise.all(toAdd.map(video => writeStore.put(video)));
  98. }
  99. /**
  100. * Checks the videos object store for entries, and populates it if empty.
  101. * @param db The database.
  102. * @returns The database.
  103. */
  104. async function checkStoreLength(db) {
  105. const readStore = await db.getAllKeys("videos");
  106. if (readStore.length === 0) {
  107. await buildVideoHistory(db);
  108. }
  109. return db;
  110. }
  111. /**
  112. * Checks the database for any watched videos on the current page.
  113. * @param db The database containing watched history.
  114. * @returns The database.
  115. */
  116. async function tagAsWatched(db) {
  117. // We check for the existance of any watched videos on the current page
  118. // If there are any, we move to the thumbnail and add the .watched class
  119. // This applys the CSS style above, and allows us to easily find the videos again
  120. const names = Array.from(document.querySelectorAll('div[id^="v_id"]'));
  121. const readStore = db.transaction("videos").store;
  122. const keys = await readStore.getAllKeys();
  123. function tagImg(e) {
  124. if (keys.includes(e.id)) {
  125. const img = e.querySelector("a picture img");
  126. //console.log(`Marking ${e.innerText} as watched`)
  127. img.classList.add("watched");
  128. return img;
  129. }
  130. }
  131. names.forEach(tagImg);
  132. return db;
  133. }
  134. function getVideoID() {
  135. try {
  136. const div = document.querySelector("div#video");
  137. return `v_id_${div.dataset.videoid}`;
  138. }
  139. catch {
  140. throw new Error("getVideoID: div#video not found!");
  141. }
  142. }
  143. function getVideoURL() {
  144. try {
  145. return document.querySelector('meta[property="og:url"]').content;
  146. }
  147. catch {
  148. throw new Error("getVideoURL: meta element not found!");
  149. }
  150. }
  151. function getVideoName() {
  152. try {
  153. const heading = document.querySelector("div.left h1");
  154. return heading ? heading.innerText : "Untitled";
  155. }
  156. catch {
  157. throw new Error("getVideoName: heading element not found!");
  158. }
  159. }
  160. /**
  161. * Checks for the current video in the database, and adds it if not found.
  162. * @param db The database containing watched history.
  163. * @returns A promise for the key of the added video.
  164. */
  165. async function checkStoreForVideo(db) {
  166. const url = `${window.location}`;
  167. if (!/spankbang\.com\/\w+\/video\//.test(url) &&
  168. !/spankbang\.com\/\w+-\w+\/playlist\//.test(url)) {
  169. return;
  170. }
  171. const video = { id: getVideoID(), url: "", name: "" };
  172. let readStore = db.transaction("videos").store;
  173. const lookup = await readStore.get(video.id);
  174. if (lookup !== undefined) {
  175. return;
  176. }
  177. video.url = getVideoURL();
  178. video.name = getVideoName();
  179. let writeStore = db.transaction("videos", "readwrite").store;
  180. return writeStore.add(video);
  181. }
  182. /**
  183. * Checks the current page for any videos marked as watched, and adds the watched text in front of them.
  184. * @returns An array containing the newly created Div elements
  185. */
  186. function filterWatched() {
  187. const docQuery = Array.from(document.querySelectorAll("img.watched"));
  188. function makeDiv(e) {
  189. const newPara = document.createElement("p");
  190. newPara.textContent = "Watched";
  191. const newDiv = document.createElement("div");
  192. newDiv.classList.add("centered");
  193. newDiv.appendChild(newPara);
  194. return e.parentElement.parentElement.appendChild(newDiv);
  195. }
  196. return (docQuery.length > 0) ? docQuery.map(makeDiv) : [];
  197. }
  198. /**
  199. * Callback function for upgrade event on openDB()
  200. * @param db The database
  201. */
  202. function upgrade(db) {
  203. const store = db.createObjectStore("videos", {
  204. keyPath: "id",
  205. autoIncrement: false,
  206. });
  207. store.createIndex("url", "url", { unique: true });
  208. }
  209. idb.openDB("history", 1, { upgrade })
  210. .then(checkStoreLength)
  211. .then(tagAsWatched)
  212. .then(checkStoreForVideo)
  213. .then(filterWatched)
  214. .catch(e => console.trace(e));