Greasy Fork is available in English.

Itch.io Web Integration

Shows if an Itch.io link has been claimed or not

2020-06-09 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name Itch.io Web Integration
  3. // @namespace Lex@GreasyFork
  4. // @match *://*/*
  5. // @grant GM_xmlhttpRequest
  6. // @grant GM_getValue
  7. // @grant GM_setValue
  8. // @version 0.1.8.4
  9. // @author Lex
  10. // @description Shows if an Itch.io link has been claimed or not
  11. // @connect itch.io
  12. // ==/UserScript==
  13.  
  14. (function(){
  15. 'use strict';
  16.  
  17. const CACHE_VERSION_KEY = "CacheVersion";
  18. const INVALIDATION_TIME = 5*60*60*1000; // 5 hour cache time
  19. const ITCH_GAME_CACHE_KEY = 'ItchGameCache';
  20. var ItchGameCache;
  21. // Promise wrapper for GM_xmlhttpRequest
  22. const Request = details => new Promise((resolve, reject) => {
  23. details.onerror = details.ontimeout = reject;
  24. details.onload = resolve;
  25. GM_xmlhttpRequest(details);
  26. });
  27. function versionCacheInvalidator() {
  28. const sVersion = v => {
  29. if (typeof v !== 'string' || !v.match(/\d+\.\d+/)) return 0;
  30. return parseFloat(v.match(/\d+\.\d+/)[0]);
  31. }
  32. const prev = sVersion(GM_getValue(CACHE_VERSION_KEY, '0.0'));
  33. if (prev < 0.1) {
  34. console.log(`${GM_info.script.version} > ${prev}`);
  35. console.log(`New minor version of ${GM_info.script.name} detected. Invalidating cache.`)
  36. _clearItchCache();
  37. }
  38. GM_setValue(CACHE_VERSION_KEY, GM_info.script.version);
  39. }
  40. function _clearItchCache() {
  41. ItchGameCache = {};
  42. _saveItchCache();
  43. }
  44. function loadItchCache() {
  45. ItchGameCache = JSON.parse(GM_getValue(ITCH_GAME_CACHE_KEY, '{}'));
  46. }
  47. function _saveItchCache() {
  48. if (ItchGameCache === undefined) return;
  49. GM_setValue(ITCH_GAME_CACHE_KEY, JSON.stringify(ItchGameCache));
  50. }
  51. function setItchGameCache(key, game) {
  52. loadItchCache(); // refresh our cache in case another tab has edited it
  53. ItchGameCache[key] = game;
  54. _saveItchCache();
  55. }
  56. function deleteItchGameCache(key) {
  57. if (key === undefined) return;
  58. loadItchCache();
  59. delete ItchGameCache[key];
  60. _saveItchCache();
  61. }
  62. function getItchGameCache(link) {
  63. if (!ItchGameCache) loadItchCache();
  64. if (Object.prototype.hasOwnProperty.call(ItchGameCache, link)) {
  65. return ItchGameCache[link];
  66. }
  67. return null;
  68. }
  69. async function claimGame(url) {
  70. const parser = new DOMParser();
  71. const purchase_url = url + "/purchase";
  72. console.log("Getting purchase page: " + purchase_url);
  73. const purchase_resp = await Request({method: "GET", url: purchase_url});
  74. const purchase_dom = parser.parseFromString(purchase_resp.responseText, 'text/html');
  75. const download_csrf_token = purchase_dom.querySelector("form.form").csrf_token.value;
  76. const download_url_resp = await Request({
  77. method: "POST",
  78. url: url + "/download_url",
  79. headers: {
  80. "Content-Type": "application/x-www-form-urlencoded"
  81. },
  82. data: 'csrf_token='+encodeURIComponent(download_csrf_token)
  83. });
  84. const downloadUrl = JSON.parse(download_url_resp.responseText).url;
  85. console.log("Received download url: " + downloadUrl);
  86.  
  87. const download_resp = await Request({method: "GET", url: downloadUrl});
  88. const dom = parser.parseFromString(download_resp.responseText, 'text/html');
  89. const claimForm = dom.querySelector(".claim_to_download_box form");
  90. const claim_csrf_token = claimForm.csrf_token.value;
  91. const claim_key_url = claimForm.action;
  92.  
  93. console.log("Claiming game using " + claim_key_url);
  94. const claim_key_resp = await Request({
  95. method: "POST",
  96. url: claim_key_url,
  97. headers: {
  98. "Content-Type": "application/x-www-form-urlencoded"
  99. },
  100. data: 'csrf_token='+encodeURIComponent(claim_csrf_token)
  101. });
  102. return /You claimed this/.test(claim_key_resp.responseText);
  103. }
  104. // Parses a DOM into a game object
  105. function parsePage(url, dom) {
  106. // Gets the inner text of an element if it can be found otherwise returns undefined
  107. const txt = query => { const e = dom.querySelector(query); return e && e.innerText.trim(); };
  108. const game = {};
  109. game.cachetime = (new Date()).getTime();
  110. game.url = url;
  111. game.title = txt('h1.game_title');
  112. game.isOwned = dom.querySelector(".purchase_banner_inner .key_row .ownership_reason") !== null;
  113. game.isClaimable = [...dom.querySelectorAll(".buy_btn")].filter(e => e.innerText == "Download or claim").length > 0;
  114. game.isFree = [...dom.querySelectorAll("span[itemprop=price]")].filter(e => e.innerText === "$0.00 USD").length > 0;
  115. game.hasPurchase = [...dom.querySelectorAll("span[itemprop=price]")].filter(e => e.innerText !== "$0.00 USD").length > 0;
  116. game.hasFreeDownload = [...dom.querySelectorAll("a.download_btn,a.buy_btn")].filter(e => e.innerText == "Download" || e.innerText == "Download Now").length > 0;
  117. game.hasCommunityCopies = document.querySelector(".reward_footer") !== null;
  118. const copiesBlock = document.querySelector(".remaining_count");
  119. game.communityCopies = copiesBlock && copiesBlock.innerText.match(/\d+/) && copiesBlock.innerText.match(/\d+/)[0];
  120. game.communityCopies = game.communityCopies || 0;
  121. game.original_price = txt("span.original_price");
  122. game.price = txt("span[itemprop=price]");
  123. game.saleRate = txt(".sale_rate");
  124. game.breadcrumbs = txt(".breadcrumbs");
  125. return game;
  126. }
  127. // Sends an XHR request and parses the results into a game object
  128. async function fetchItchGame(url) {
  129. const response = await Request({method: "GET",
  130. url: url});
  131. if (response.status != 200) {
  132. console.log(`Error ${response.status} fetching page ${url}`);
  133. return null;
  134. }
  135. const parser = new DOMParser();
  136. const dom = parser.parseFromString(response.responseText, 'text/html');
  137. return parsePage(url, dom);
  138. }
  139. // Loads an itch game from cache or fetches the page if needed
  140. async function getItchGame(url) {
  141. let game = getItchGameCache(url);
  142. if (game !== null) {
  143. const isExpired = (new Date()).getTime() - game.cachetime > INVALIDATION_TIME;
  144. // Expiration checking currently disabled
  145. /*if (isExpired) {
  146. game = null;
  147. }*/
  148. }
  149. if (game === null) {
  150. game = await fetchItchGame(url);
  151. if (game !== null)
  152. setItchGameCache(url, game);
  153. }
  154. return game;
  155. }
  156. async function claimClicked(a, game) {
  157. console.log("Attempting to claim " + game.url);
  158. a.innerText += ' ⌛';
  159. a.onclick = null;
  160. const success = await claimGame(game.url);
  161. if (success === true) {
  162. a.style.display = "none";
  163. const ownMark = a.parentElement.firstChild;
  164. ownMark.innerHTML = `<span title="Successfully claimed">✔️</span>`;
  165. deleteItchGameCache(game.url);
  166. } else {
  167. a.innerHTML = `❗ Error`;
  168. }
  169. }
  170. // Appends the isOwned tag to an anchor link
  171. function appendTags(a, game) {
  172. const div = document.createElement("div");
  173. div.style.display = "inline-block";
  174. const span = document.createElement("span");
  175. div.append(span);
  176. span.style = "margin-left: 5px; background:rgb(230,230,230); padding: 2px; border-radius: 2px";
  177. if (game === null) {
  178. span.innerHTML = `<span title="Status unknown. Try refreshing.">❓</span>`;
  179. } else if (game.isOwned) {
  180. span.innerHTML = `<span title="Game is already claimed on itch.io">✔️</span>`;
  181. } else {
  182. if (!game.isClaimable) {
  183. if (game.hasFreeDownload && !game.hasPurchase) {
  184. span.innerHTML = `<span title="Game is a free download but not claimable">🆓</span>`;
  185. } else if (game.price) {
  186. span.innerHTML = `<span title="🛒 Game costs ${game.price}">🛒</span>`;
  187. } else {
  188. span.innerHTML = `<span title="Status unknown">👽</span>`;
  189. }
  190. } else {
  191. const origPrice = game.original_price ? ` 🛒 Original price: ${game.original_price} 💸 Current Price: ${game.price}` : '';
  192. span.innerHTML = `<span title="Game is claimable but you haven't claimed it.${origPrice}">❌</span>`;
  193. const claimBtn = document.createElement("span");
  194. claimBtn.style = `margin-left: 2px; padding: 2px; cursor:pointer; background:rgb(220,220,220); border-radius: 5px`;
  195. claimBtn.className = "ClaimButton";
  196. claimBtn.innerText = "🛄 Claim Game";
  197. claimBtn.onclick = function(event) { claimClicked(event.target, game); };
  198. span.after(claimBtn);
  199. }
  200. }
  201. if (game.hasCommunityCopies) {
  202. const communityTag = document.createElement("span");
  203. communityTag.title = `This game has ${game.communityCopies} Community Copies availible.`;
  204. communityTag.innerText = '👪';
  205. span.append(communityTag);
  206. }
  207. if (game !== null && game.breadcrumbs) {
  208. span.firstChild.title += ' ℹ️ ' + game.breadcrumbs;
  209. if (!a.title)
  210. a.title = game.breadcrumbs;
  211. const tags = {
  212. //"Games": { icon: '🎮', title: "Video game" },
  213. "Tools": { icon: '🛠️', title: "Tool" },
  214. "Game assets": { icon: '🗃️', title: "Game asset" },
  215. "Comics": { icon: '🗨️', title: "Comic" },
  216. "Books": { icon: '📘', title: "Book" },
  217. "Physical games": { icon: '📖', title: "Physical game" },
  218. "Soundtracks": { icon: '🎵', title: "Soundtrack" },
  219. "Game mods": { icon: '⚙️', title: "Game mod" },
  220. }
  221. const category = game.breadcrumbs.split("›")[0].trim();
  222. if (Object.prototype.hasOwnProperty.call(tags, category)) {
  223. const tag = document.createElement("span");
  224. tag.title = tags[category].title;
  225. tag.innerText = tags[category].icon;
  226. span.append(tag);
  227. }
  228. }
  229. a.after(div);
  230. }
  231. function addClickHandler(a) {
  232. a.addEventListener('mouseup', event => {
  233. deleteItchGameCache(event.target.href);
  234. });
  235. }
  236.  
  237. // Handles an itch.io link on a page
  238. async function handleLink(a) {
  239. addClickHandler(a);
  240. const game = await getItchGame(a.href);
  241. appendTags(a, game);
  242. }
  243. function isGameUrl(url) {
  244. return /^https:\/\/[^.]+\.itch\.io\/[^/]+$/.test(url);
  245. }
  246. // Finds all the itch.io links on the current page
  247. function getItchLinks() {
  248. let links = [...document.querySelectorAll("a[href*='itch.io/']")];
  249. links = links.filter(a => isGameUrl(a.href));
  250. links = links.filter(a => !a.classList.contains("return_link"));
  251. links = links.filter(a => { const t = a.textContent.trim(); return t !== "" && t !== "GIF"; });
  252. return links;
  253. }
  254. function handlePage() {
  255. if (isGameUrl(window.location.href)) {
  256. // If we're on an Itch game page, update the cached details
  257. const game = parsePage(window.location.href, document);
  258. setItchGameCache(window.location.href, game);
  259. }
  260. // Try to find any itch links on the page and tag them
  261. const as = getItchLinks();
  262. as.forEach(handleLink);
  263. }
  264. versionCacheInvalidator();
  265. handlePage();
  266. })();