NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Adopt a HANDJOB // @namespace http://tampermonkey.net/ // @version 0.1.2 // @description Download at-risk torrents released by HANDJOB // @author OTMOP // @license MIT // @copyright 2020, OTMOP and HANDJOB // @icon https://ptpimg.me/6uob9q.png // @match https://passthepopcorn.me/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant unsafeWindow // @run-at document-end // ==/UserScript== /* CHANGELOG 0.1.2 - Added option to include or ignore trumpable torrents - Added option to search only for DVDRip encodes 0.1.1 - Minor style tweaks - Refined some option names - Added option to include previously downloaded (but not snatched or leeching) torrents in the search - Users can now repeat the search without reloading the page - Added help icon tooltips to explain each option */ (() => { const PRIMARY_COLOUR = "#3498db"; // eslint-disable-next-line no-undef const savedOptions = GM_getValue("adopt-options") || {}; let TORRENT_CAP = savedOptions.torrentCap || 20; const IGNORE_ZERO_SEEDERS = true; let INCLUDE_TRUMPABLE = savedOptions.includeTrumpable || false; let INCLUDE_DOWNLOADED = savedOptions.includeDownloaded || false; let MAX_SEEDERS = savedOptions.maxSeeders || 2; let SEARCH_DELAY = savedOptions.searchDelay || 1000; let SEARCH_PAGE_OFFSET = savedOptions.searchOffset || 10; let DOWNLOAD_DELAY = savedOptions.downloadDelay || 1000; let RESOLUTION = savedOptions.resolutions || "anysd"; // null, anysd, anyhd, 480p, 576p, 720p, 1080p let AuthKey, TorrentPass; const modal = injectElements(); class Stats { constructor() { this.form = document.querySelector("#handjob__adopt-modal fieldset"); this.form.querySelector("#handjob__adopt-input--resolution").value = RESOLUTION; if (INCLUDE_DOWNLOADED === true) this.form.querySelector("#handjob__adopt-input--include-downloaded").checked = true; if (INCLUDE_TRUMPABLE === true) this.form.querySelector("#handjob__adopt-input--include-trumpable").checked = true; this.button = document.querySelector("#handjob__adopt-submit-button"); this.closeButton = document.querySelector("#handjob__adopt-close-button"); this.closeButton.addEventListener("click", () => modal.classList.add("hidden")); this.searchDisplay = document.querySelector("#handjob__adopt-download-status-pages span"); this.foundDisplay = document.querySelector("#handjob__adopt-download-status-found span"); this.torrentsDisplay = document.querySelector("#handjob__adopt-download-status-torrents span"); this.activeTaskDisplay = document.querySelector("#handjob__adopt-download-active-task span"); this.activeTaskIcon = document.querySelector("#handjob__adopt-download-active-task .handjob__adopt-icon"); this.percentDisplay = document.querySelector(".handjob__adopt-loader-percent"); this.loader = document.querySelector(".handjob__adopt-loader"); this.tasks = []; this.Reset(); this.Refresh(); } Reset() { this.tasks = []; this.searching = true; this.pagesSearched = 0; this.found = 0; this.downloaded = 0; this.percent = 0; this.button.disabled = ""; this.closeButton.disabled = ""; this.loader.style.borderTop = ""; } loadOptions() { const dl = !!this.form.querySelector("#handjob__adopt-input--include-downloaded").checked; INCLUDE_DOWNLOADED = savedOptions.includeDownloaded = dl; const trumpable = !!this.form.querySelector("#handjob__adopt-input--include-trumpable").checked; INCLUDE_TRUMPABLE = savedOptions.includeTrumpable = trumpable; const cap = parseInt(this.form.querySelector("#handjob__adopt-input--torrent-cap").value, 10); TORRENT_CAP = savedOptions.torrentCap = cap ? cap : TORRENT_CAP; const res = this.form.querySelector("#handjob__adopt-input--resolution").value; RESOLUTION = savedOptions.resolutions = res == "any" ? "" : !res ? RESOLUTION : res; if (savedOptions.resolutions === "") savedOptions.resolutions = "any"; const offset = parseInt(this.form.querySelector("#handjob__adopt-input--search-offset").value, 10); SEARCH_PAGE_OFFSET = savedOptions.searchOffset = offset ? offset : SEARCH_PAGE_OFFSET; const maxSeeders = parseInt(this.form.querySelector("#handjob__adopt-input--max-seeders").value, 10); MAX_SEEDERS = savedOptions.maxSeeders = maxSeeders ? maxSeeders : MAX_SEEDERS; const searchDelay = parseInt(this.form.querySelector("#handjob__adopt-input--search-delay").value, 10); SEARCH_DELAY = savedOptions.searchDelay = searchDelay ? searchDelay : SEARCH_DELAY; const downloadDelay = parseInt(this.form.querySelector("#handjob__adopt-input--download-delay").value, 10); DOWNLOAD_DELAY = savedOptions.downloadDelay = downloadDelay ? downloadDelay : DOWNLOAD_DELAY; // eslint-disable-next-line no-undef GM_setValue("adopt-options", savedOptions); } AddPageSearched() { this.pagesSearched++; this.Refresh(); } AddFound(num) { this.found += num; this.Refresh(); } AddDownloaded() { this.downloaded++; this.Refresh(); } AddTask(task) { switch (task.type) { case "error": this.activeTaskIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="M9 1.03c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM10 13H8v-2h2v2zm0-3H8V5h2v5z"/></svg>`; this.activeTaskIcon.className = "handjob__adopt-icon icon-error"; break; case "success": this.activeTaskIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>`; this.activeTaskIcon.className = "handjob__adopt-icon icon-success"; } this.tasks.push(task); const changeTask = () => { if (task) this.activeTaskDisplay.innerHTML = task.text; this.activeTaskDisplay.removeEventListener("transitionend", changeTask); this.activeTaskDisplay.classList.remove("hide-task"); }; this.activeTaskDisplay.addEventListener("transitionend", changeTask); this.activeTaskDisplay.classList.add("hide-task"); } Start() { this.Reset(); this.loadOptions(); this.loader.classList.remove("paused"); this.button.disabled = true; this.closeButton.disabled = true; } End() { this.loader.classList.add("paused"); this.closeButton.disabled = ""; this.button.disabled = ""; } Refresh() { if (this.downloaded >= 1) { this.searching = false; this.loader.style.borderTop = "16px solid #ef7017"; } if (this.searching) { this.percent = Math.round((100 / TORRENT_CAP) * this.found); if (this.percent > 100) this.percent = 100; } else { this.percent = Math.round((100 / TORRENT_CAP) * this.downloaded); } this.searchDisplay.innerHTML = this.pagesSearched; this.torrentsDisplay.innerHTML = this.downloaded; this.foundDisplay.innerHTML = this.found; if (this.percent !== null) this.percentDisplay.innerHTML = this.percent + "%"; } } class Torrent { constructor(json) { this.torrentid = json.TorrentId; this.filename = json.ReleaseName; } Download() { return new Promise((resolve, reject) => { fetch(this.GenerateDL()) .then(res => res.blob()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.style.display = "none"; a.href = url; a.download = this.filename + ".torrent"; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); setTimeout(() => { resolve(this.filename + " downloaded"); }, DOWNLOAD_DELAY); }) .catch(err => { console.log(err); }); }); } GenerateDL() { const root = `https://passthepopcorn.me/torrents.php?`; return root + `action=download&id=${this.torrentid}&authkey=${AuthKey}&torrent_pass=${TorrentPass}`; } } const stats = new Stats(); let torrents = []; function getSearchURL() { let search = "https://passthepopcorn.me/torrents.php?action=advanced&encoding=MKV&format=x264"; if (RESOLUTION === "dvd") search += "&media=DVD&resolution=anysd"; else if (RESOLUTION) search += "&resolution=" + RESOLUTION; search += "&filelist=handjob&order_by=seeders&order_way=asc&grouping=0"; return search; } function getTorrentResults(resultsPage = SEARCH_PAGE_OFFSET) { return new Promise((resolve, reject) => { stats.AddTask({ type: "info", text: "Downloading results from page " + resultsPage }); const url = getSearchURL() + "&page=" + resultsPage; fetch(url) .then(res => res.text()) .then(raw => { stats.AddPageSearched(); const parse = new DOMParser(); const dom = parse.parseFromString(raw, "text/html"); const script = Array.from(dom.querySelectorAll("#content script")).find( x => !x.src && /var PageData/i.test(x.innerHTML) ); if (!script) return reject(torrents); const jsonString = script.textContent.match(/(?:var PageData\s*=\s*)([\w\W]*)(?:;\s*)$/); const json = JSON.parse(jsonString[1]); if (json.Movies.length === 0) return reject(torrents); if (!AuthKey) AuthKey = json.AuthKey; if (!TorrentPass) TorrentPass = json.TorrentPass; const results = json.Movies.map(result => { const torrentResult = result.GroupingQualities[0].Torrents[0]; if (INCLUDE_TRUMPABLE === false) if (/torrent-info__trumpable/i.test(torrentResult.Title)) return null; if (INCLUDE_DOWNLOADED === true) { if (torrentResult.ColorType && torrentResult.ColorType !== "downloaded") return null; } else if (torrentResult.ColorType) return null; if (IGNORE_ZERO_SEEDERS === true && torrentResult.Seeders == 0) return null; if (torrentResult.Seeders > MAX_SEEDERS) return null; return new Torrent(torrentResult); }).filter(x => x); stats.AddFound(results.length); if (results.length === 0) resultsPage += 5; else resultsPage++; torrents.push(...results); if (torrents.length < TORRENT_CAP) { setTimeout(() => { resolve(getTorrentResults(resultsPage)); }, SEARCH_DELAY); } else resolve(torrents); }); }); } function runDownloader() { modal.classList.remove("hidden"); stats.AddTask({ type: "info", text: "Waiting to begin..." }); const cb = () => getTorrentResults() .catch(torrents => { if (torrents.length > 1) { stats.AddTask({ type: "error", text: "No results in search page" }); return torrents; } else { stats.Reset(); throw "No torrents found. Try adjusting search page offset!"; } }) .then(res => { res = res.slice(0, TORRENT_CAP); let i = 0; const downloadTorrent = (torrent = res[i]) => new Promise((resolve, reject) => { stats.AddTask({ type: "info", text: "Downloading torrent file for " + torrent.filename }); torrent.Download().then(() => { stats.AddDownloaded(); i++; if (res[i]) return resolve(downloadTorrent(res[i])); else return resolve(); }); }); downloadTorrent().then(() => { stats.End(); stats.AddTask({ type: "success", text: "All downloads complete" }); torrents = []; }); }) .catch(err => { stats.AddTask({ type: "error", text: err }); stats.End(); }); stats.button.addEventListener("click", () => { stats.Start(); cb(); }); } function injectElements() { const css = ` #handjob__adopt-modal.hidden { opacity: 0; pointer-events: none; } .handjob__adopt-toolbar { width: 100%; background-color: ${PRIMARY_COLOUR}; color: white; font-family: Roboto,Verdana, sans-serif; font-size: 24px; text-align: center; padding: 8px; border-radius: 4px; margin-top: -44px; margin-bottom: 20px; box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); text-transform: uppercase; font-weight: bold; } #handjob__adopt-download-active-task span { transition: opacity 100ms ease-in-out; } #handjob__adopt-download-active-task span.hide-task { opacity: 0; } #handjob__adopt-modal { display: flex; position: fixed; z-index: 9999; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.3); justify-content: center; align-items: center; opacity: 1; transition: opacity 500ms ease-in-out; } .handjob__adopt-loader-container { position: relative; width: 120px; height: 120px; } .handjob__adopt-loader { width: 100%; height: 100%; border: 16px solid #030303; border-radius: 50%; border-top: 16px solid ${PRIMARY_COLOUR}; transition: border-top 500ms ease-in-out; -webkit-animation: spin 2s linear infinite; /* Safari */ animation: spin 2s linear infinite; } .handjob__adopt-loader.paused{ -webkit-animation-play-state:paused; -moz-animation-play-state:paused; -o-animation-play-state:paused; animation-play-state:paused; } .handjob__adopt-loader-percent { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 24px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #handjob__adopt-modal-inner { width: 800px; min-height: 200px; background-color: #EFEFEF; border-radius: 10px; display: flex; align-items: center; flex-flow: column; padding: 20px; } #handjob__adopt-download-status { font-family: Inconsolata, monospace; margin-top: 24px; color: #3c3c3c; font-size: 14px; width: 100%; display: flex; flex-flow: column; align-items: flex-end; } #handjob__adopt-download-status-top { width: 100%; display: flex; justify-content: space-between; } #handjob__adopt-download-status-pages { } #handjob__adopt-download-status-torrents { } #handjob__adopt-download-active-task { width: 100%; display: flex; align-items: center; border-radius: 4px; padding: 16px; margin-top: 16px; max-width: 100%; outline: none; text-decoration: none; transition-property: box-shadow,opacity; overflow-wrap: break-word; position: relative; white-space: normal; transition: box-shadow .28s cubic-bezier(.4,0,.2,1); will-change: box-shadow; box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); } #handjob__adopt-download-active-task svg { margin-right: 16px; } .handjob__adopt-btn { font-family: Roboto, Arial, sans-serif; text-transform: uppercase; display: inline-block; margin-bottom: 0; font-weight: 400; text-align: center; white-space: nowrap; vertical-align: middle; -ms-touch-action: manipulation; touch-action: manipulation; cursor: pointer; background-image: none; border: 1px solid transparent; border-top-color: transparent; border-right-color: transparent; border-bottom-color: transparent; border-left-color: transparent; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; border-radius: 4px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .handjob__adopt-btn:focus { outline: none; } .handjob__adopt-btn.disabled, .handjob__adopt-btn[disabled] { cursor: not-allowed; pointer-events: none; filter: alpha(opacity=65); opacity: .65; -webkit-box-shadow: none; box-shadow: none; } .handjob__adopt-btn-primary { color: #fff; background-color: #337ab7; border-color: #2e6da4; } .handjob__adopt-btn-error { color: #fff; background-color: #d9534f; border-color: #d43f3a; } .handjob__adopt-btn-error:hover { color: #fff; background-color: #c9302c; background-image: none; border-color: #ac2925; } .handjob__adopt-btn-error:active { color: #fff; background-color: #c9302c; background-image: none; border-color: #ac2925; } .handjob__adopt-btn-primary:hover { color: #fff; background-color: #286090; border-color: #204d74; } .handjob__adopt-btn-primary:active { color: #fff; background-color: #286090; background-image: none; border-color: #204d74; } #handjob__adopt-actions { margin-top: 24px; } #handjob__adopt-actions button:not(:last-child) { margin-right: 16px; } #handjob__adopt-modal fieldset { width: 100%; margin-bottom: 16px; border-radius: 4px; } #handjob__adopt-input--torrent-cap, #handjob__adopt-input--search-offset, #handjob__adopt-input--max-seeders { max-width: 50px; text-align: center; } #handjob__adopt-input--search-delay, #handjob__adopt-input--download-delay { max-width: 75px; text-align: center; } #handjob__adopt-input--resolution { max-width: 200px; } #handjob__adopt-modal table { color: #000; } #handjob__adopt-modal table td { padding: 8px 16px; } .handjob__adopt-icon { min-width: 18px; min-height: 18px; } .handjob__adopt-icon.icon-error svg { fill: red; } .handjob__adopt-icon.icon-success svg { fill: #009688; } .handjob__adopt-switch { position: relative; display: inline-block; width: 30px; height: 17px; } .handjob__adopt-switch input { opacity: 0; width: 0; height: 0; } .handjob__adopt-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: .4s; transition: .4s; } .handjob__adopt-slider:before { position: absolute; content: ""; height: 13px; width: 13px; left: 2px; bottom: 2px; background-color: white; -webkit-transition: .4s; transition: .4s; } input:checked + .handjob__adopt-slider { background-color: #2196F3; } input:focus + .handjob__adopt-slider { box-shadow: 0 0 1px #2196F3; } input:checked + .handjob__adopt-slider:before { -webkit-transform: translateX(13px); -ms-transform: translateX(13px); transform: translateX(13px); } /* Rounded sliders */ .handjob__adopt-slider.handjob__adopt-round { border-radius: 17px; } .handjob__adopt-slider.handjob__adopt-round:before { border-radius: 50%; } .handjob__adopt-input-container { display: inline-flex; align-items: center; } .handjob__adopt-help-icon { position: relative; margin-left: 16px; cursor: pointer; } .handjob__adopt-help-icon:before { pointer-events: none; content: ""; position: absolute; right: -16px; transition: opacity 150ms ease-in-out; opacity: 0; width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-right: 5px solid ${PRIMARY_COLOUR}; top: 6px; } .handjob__adopt-help-icon:hover:before { opacity: 1; } .handjob__adopt-help-icon:after { content: attr(data-help-message); position: absolute; background-color: ${PRIMARY_COLOUR}; opacity: 0; transition: opacity 150ms ease-in-out; white-space: nowrap; padding: 4px 8px; color: white; border-radius: 4px; margin-left: 16px; pointer-events: none; } .handjob__adopt-help-icon:hover:after { opacity: 1; } .handjob__adopt-help-icon path { fill: ${PRIMARY_COLOUR}; } `; const styleInsert = document.createElement("style"); styleInsert.innerHTML = css; document.head.appendChild(styleInsert); const insertNode = document.createElement("li"); insertNode.className = "user-info-bar__item"; const activate = insertNode.appendChild(document.createElement("a")); activate.style.cursor = "pointer"; activate.textContent = "Adopt a HANDJOB"; const insertionPoint = document.querySelector("#userinfo_minor"); insertionPoint.appendChild(insertNode); activate.addEventListener("click", runDownloader); const modal = document.createElement("div"); const helpIcon = message => { return `<div class="handjob__adopt-help-icon" data-help-message="${message}"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/></svg></div>`; }; modal.id = "handjob__adopt-modal"; modal.className = "hidden"; const modalInner = modal.appendChild(document.createElement("div")); modalInner.innerHTML = ` <div class="handjob__adopt-toolbar">Adopt A HANDJOB</div> <fieldset> <table> <tr> <td>Starting Search Page: </td> <td> <div class="handjob__adopt-input-container"> <input type="number" min="1" id="handjob__adopt-input--search-offset" value="${SEARCH_PAGE_OFFSET}"> ${helpIcon( "Starting search results page. Increase this if it takes a long while for a first match" )} </div> </td> </tr> <tr> <td>Max Seeders: </td> <td> <div class="handjob__adopt-input-container"> <input type="number" id="handjob__adopt-input--max-seeders" value="${MAX_SEEDERS}"> ${helpIcon("Max no. of seeders a downloaded torrent can have")} </div> </td> </tr> <tr> <td>Torrents Cap: </td> <td> <div class="handjob__adopt-input-container"> <input type="number" id="handjob__adopt-input--torrent-cap" value="${TORRENT_CAP}"> ${helpIcon("Keep searching until this many torrents have been found")} </div> </td> </tr> <tr> <td>Resolutions: </td> <td> <div class="handjob__adopt-input-container"> <select id="handjob__adopt-input--resolution"> <option value="any">Any</option> <option value="dvd">Only DVDRip</option> <option value="anysd">Any Standard Definition</option> <option value="anyhd">Any High Definition</option> <option value="480p">480p</option> <option value="576p">576p</option> <option value="720p">720p</option> <option value="1080p">1080p</option> </select> ${helpIcon("Resolutions of torrents to download")} <div> </td> </tr> <tr> <td>Include Downloaded Torrents?</td> <td> <div class="handjob__adopt-input-container"> <label class="handjob__adopt-switch"> <input type="checkbox" id="handjob__adopt-input--include-downloaded"> <span class="handjob__adopt-slider handjob__adopt-round"></span> </label> ${helpIcon( "Include torrents that you've downloaded before (but are not seeding or leeching)" )} </div> </td> </tr> <tr> <td>Include Trumpable Torrents?</td> <td> <div class="handjob__adopt-input-container"> <label class="handjob__adopt-switch"> <input type="checkbox" id="handjob__adopt-input--include-trumpable"> <span class="handjob__adopt-slider handjob__adopt-round"></span> </label> ${helpIcon("Include torrents that are marked as trumpable")} </div> </td> </tr> <tr> <td>Search Request Delay (ms): </td> <td> <div class="handjob__adopt-input-container"> <input type="number" step="100" id="handjob__adopt-input--search-delay" value="${SEARCH_DELAY}"> ${helpIcon("Number of milliseconds to wait between requests for search result pages")} </div> </td> </tr> <tr> <td>Download Request Delay (ms): </td> <td> <div class="handjob__adopt-input-container"> <input type="number" step="100" id="handjob__adopt-input--download-delay" value="${DOWNLOAD_DELAY}"> ${helpIcon("Number of milliseconds to wait between requests for torrent downloads")} </div> </td> </tr> </table> </fieldset> <div class="handjob__adopt-loader-container"> <div class="handjob__adopt-loader paused"></div> <div class="handjob__adopt-loader-percent"></div> </div> <div id="handjob__adopt-download-status"> <div id="handjob__adopt-download-status-top"> <div style="display: flex; flex-flow: column;"> <div id="handjob__adopt-download-status-pages">Search pages scanned: <span></span></div> <div id="handjob__adopt-download-status-found">Eligible torrents found: <span></span></div> </div> <div id="handjob__adopt-download-status-torrents">Torrents downloaded: <span></span></div> </div> <div id="handjob__adopt-download-active-task"> <div class="handjob__adopt-icon"></div> <span></span> </div> <div id="handjob__adopt-actions"> <button id="handjob__adopt-close-button" class="handjob__adopt-btn handjob__adopt-btn-error">Close</button> <button id="handjob__adopt-submit-button" class="handjob__adopt-btn handjob__adopt-btn-primary">Find Torrents</button> <div> </div> `; modalInner.id = "handjob__adopt-modal-inner"; document.querySelector("#extra2").appendChild(modal); return modal; } })();