// ==UserScript== // @name The Pirate Calendar (for trakt.tv) // @version 0.6.6 // @description Adds torrent links to trakt.tv. Now with a settings menu! // @author luffier // @namespace PirateCalendar // @license MIT // @match https://trakt.tv/* // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @grant GM_getValue // @grant GM_setValue // @grant GM.getValue // @grant GM.setValue // @run-at document-idle // @sandbox raw // @homepageURL https://github.com/Luffier/the-pirate-calendar // @supportURL https://github.com/Luffier/the-pirate-calendar/issues // @downloadURL none // ==/UserScript== /* globals GM_config */ /* jshint esversion: 6 */ (() => { 'use strict'; /* VARIABLES */ // Global styles const style = ` `; const regex = { calendar: /^\/calendars\/my\/shows/, shows: /^\/shows(\/(trending|popular|favorited|recommended|watched|collected|anticipated))?(\/weekly)?$/, show: /^\/shows\/([^\/]+)(\/)?$/, season: /^\/shows\/([^\/]+)\/seasons\/([^\/]+)(\/)?$/, episode: /^\/shows\/([^\/]+)\/seasons\/([^\/]+)\/episodes\/([^\/]+)(\/)?$/, movies: /^\/movies(\/(trending|popular|favorited|recommended|watched|collected|anticipated|boxoffice))?(\/weekly)?$/, movie: /^\/movies\/([^\/]+-[0-9]{4})$/ }; // Default search engines parameters const searchEngines = { '1337x': { 'defaultUrl': 'https://1337x.to/', 'defaultSearch': 'sort-search/%s/size/desc/1/', 'cleanQuery': (query) => encodeURIComponent(query).replace(/%20/g, '+') }, 'Torrent Galaxy': { 'defaultUrl': 'https://torrentgalaxy.to/', 'defaultSearch': 'torrents.php?search=%s&lang=0&nox=2&sort=size&order=desc', 'cleanQuery': (query) => encodeURIComponent(query).replace(/%20/g, '+') }, 'Custom': { 'defaultUrl': 'Write a custom URL', 'defaultSearch': 'Write a custom query string', 'cleanQuery': (query) => encodeURIComponent(query).replace(/%20/g, '+') } }; // Helper for whenPageReady function const pageReady = { timeout: true, startTime: null } /* SETTINGS MENU */ GM_config.init({ 'id': 'PirateCalendarConfig', 'title': 'The Pirate Calendar Settings', 'fields': { 'openInNewTab': { 'label': 'Open links in new tab:', 'type': 'checkbox', 'default': true, 'section': ['General'] }, 'autoscrollToday': { 'label': 'Auto scroll to current day:', 'type': 'checkbox', 'default': true, 'section': ['Calendar'] }, 'hideCollectIcon': { 'label': 'Hide collect icon:', 'type': 'checkbox', 'default': false }, 'hideListIcon': { 'label': 'Hide list icon:', 'type': 'checkbox', 'default': false }, 'hideWatchtIcon': { 'label': 'Hide watch-now icon:', 'type': 'checkbox', 'default': false }, 'torrentSearchEngine': { 'label': 'Preferred torrent search engine:', 'type': 'select', 'options': Object.keys(searchEngines), 'default': Object.keys(searchEngines)[0], 'section': ['Search engine'] }, 'customUrl': { 'label': '· URL:', 'title': 'For a custom URL (like a proxy)', 'type': 'text', 'default': searchEngines[Object.keys(searchEngines)[0]].defaultUrl }, 'customSearch': { 'label': '· Search query:', 'title': 'For a custom search query. Place "%s" where the query should be', 'type': 'text', 'default': searchEngines[Object.keys(searchEngines)[0]].defaultSearch } }, 'css': ` body#PirateCalendarConfig { position: relative !important; font-family: 'proxima nova', 'Helvetica', 'Arial', 'sans-serif' !important; margin: 0 !important; } #PirateCalendarConfig .config_var { margin: 8px 8px 8px 12px !important; } #PirateCalendarConfig .config_var input[type="text"] { border: 2px inset black !important; } #PirateCalendarConfig_buttons_holder { position: relative !important; } #PirateCalendarConfig_header { background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; padding: 20px 0 10px 0; } #PirateCalendarConfig_buttons_holder { right: 20px; } #PirateCalendarConfig_buttons_holder button { color: #fff; font-size: 12px; padding: 4px 9px !important; height: auto !important; cursor: pointer; border: 1px solid transparent; } #PirateCalendarConfig_buttons_holder .reset_holder { position: absolute; right: 12px; bottom: -20px; } #PirateCalendarConfig_saveBtn { background-color: #ed1c24; border-color: #de1219; } #PirateCalendarConfig_closeBtn { background-color: #aaa; border: 1px solid transparent; } #PirateCalendarConfig_field_customSearch { width: 48ex; } .config_var#PirateCalendarConfig_customUrl_var, .config_var#PirateCalendarConfig_customSearch_var { display: flex; align-items: center; } .config_var input[type="text"] { flex-grow: 1; } `, 'events': { 'init': init, 'open': function() { // Set default URL and search path when the search engine changes this.fields.torrentSearchEngine.node.addEventListener('change', function() { let searchEngine = searchEngines[this.value]; let section = this.parentElement.parentElement; section.querySelector('#PirateCalendarConfig_field_customUrl').value = searchEngine.defaultUrl; section.querySelector('#PirateCalendarConfig_field_customSearch').value = searchEngine.defaultSearch; }); }, 'save': function() { applySettings(); this.close(); }, } }); /* FUNCTIONS */ // Single element selector shorthand const $ = document.querySelector.bind(document); // Multiple elements selector shorthand const $$ = document.querySelectorAll.bind(document); // Create element function createElement(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstChild; } // Function to replicate the `toggle` function in jQuery function toggle(el, option) { if (typeof option === 'boolean') { if (option) { el.style.display = ''; } else { el.style.display = 'none'; } } else { if (el.style.display === 'none') { el.style.display = ''; } else { el.style.display = 'none'; } } } // Function to replicate the `on` function in jQuery function addEventListener(el, eventName, eventHandler, selector) { if (selector) { const wrappedHandler = (e) => { if (e.target && e.target.matches(selector)) { eventHandler(e); } }; el.addEventListener(eventName, wrappedHandler); return wrappedHandler; } else { el.addEventListener(eventName, eventHandler); return eventHandler; } } // Function to replicate the `:visible` selector used in jQuery function isVisible(el) { return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); } // Pad number with leading zeros function zeroPad (number, places) { return String(number).padStart(places, '0'); } // Validate settings in case stored settings no longer exist in the current script version function validateSettings() { let searchEngine = GM_config.get('torrentSearchEngine'); if (!searchEngines.hasOwnProperty(searchEngine)) { GM_config.set('torrentSearchEngine', GM_config.fields.torrentSearchEngine.default); } } // Apply settings from the setting's menu function applySettings() { // Apply calendar settings if (regex.calendar.test(location.pathname)) { // Hide unwanted icons for (const el of [...$$('.quick-icons .collect')]) { toggle(el, !GM_config.get('hideCollectIcon')); } for (const el of [...$$('.quick-icons .list')]) { toggle(el, !GM_config.get('hideListIcon')); } for (const el of [...$$('.quick-icons .watch-now')]) { toggle(el, !GM_config.get('hideWatchtIcon')); } // Remove and add all the links again for (const el of [...$$('.grid-item[data-type="episode"] a.tpc')]) { el.remove(); } for (const el of [...$$('.grid-item[data-type="episode"]')]) { addLinkToGridItem(el, 'episode'); } } } function makeTorrentURL(query) { let baseURL = GM_config.get('customUrl'); let queryPath = GM_config.get('customSearch'); let searchEngine = GM_config.get('torrentSearchEngine'); let queryCleaned = searchEngines[searchEngine].cleanQuery(query); let url = baseURL + queryPath.replace(/%s/g, queryCleaned); return url; } function extractQueryFromLink(link, type) { let itemLinkMatches = link.match(regex[type]); if (itemLinkMatches === null) { return link.replace(/-/g, ' ').replace(/\//g, ' '); } else { let title = itemLinkMatches[1].replace(/-/g, ' '); let seasonNumber = itemLinkMatches[2]; let query = title; if (type === 'season') { let codeNumber = `S${zeroPad(seasonNumber,2)}`; query = query + ' ' + codeNumber; } else if (type === 'episode') { let episodeNumber = itemLinkMatches[3]; let codeNumber = `S${zeroPad(seasonNumber, 2)}E${zeroPad(episodeNumber, 2)}`; query = query + ' ' + codeNumber; } return query; } } // Adds a search link to a grid item (like those from the calendar) function addLinkToGridItem(item, type) { let actions = item.querySelector(`:scope ${'> div.quick-icons > div.actions'}`); let itemLink = item.querySelector(`:scope ${'a'}`).getAttribute('href'); let query = extractQueryFromLink(itemLink, type); let urlSearch = makeTorrentURL(query); let target = GM_config.get('openInNewTab') ? '_blank' : '_self'; let searchEngineName = GM_config.get('torrentSearchEngine'); actions.append(createElement( `
` )); } // Adds a search link to an actions list (like the ones in an episode's page) function addLinkToActionList(actionList, type) { let itemLink = location.pathname; let query = extractQueryFromLink(itemLink, type); let urlSearch = makeTorrentURL(query); let target = GM_config.get('openInNewTab') ? '_blank' : '_self'; let searchEngineName = GM_config.get('torrentSearchEngine'); actionList.append(createElement( `
Search on ${searchEngineName}
` )); } function isCalendarPageCurrentMonth() { let today = new Date(); // Extract the calendar date from the URL let calendarDate = new Date(window.location.href.substring(window.location.href.lastIndexOf('/') + 1)); // If there's no date (current month) or it's the current month then return true return (isNaN(calendarDate) || (calendarDate.getMonth() === today.getMonth() && calendarDate.getYear() === today.getYear())) } // Autoscroll to current date function scrollCurrentDate() { if(isCalendarPageCurrentMonth()) { let todayCard = [...$$('.date-separator:not(.filler) .date')].filter((el) => { return el.textContent == (new Date()).getDate(); })[0]; if (todayCard) { todayCard.scrollIntoView(true); // Scroll up to compensate top navbar let topNav = $('#top-nav'); let offset = -window.getComputedStyle(topNav).getPropertyValue('height').slice(0, -2); window.scrollBy(0, offset); } } } // Process calendar page function processCalendarPage() { // Torrent links for (const el of [...$$('.grid-item[data-type="episode"]')]) { addLinkToGridItem(el, 'episode'); } if (GM_config.get('autoscrollToday')) { scrollCurrentDate(); } // Settings menu icon let menuIcon = createElement( `
` ); menuIcon = $('.sidenav-inner').appendChild(menuIcon); addEventListener(menuIcon, 'click', () => GM_config.open()); // Jump icon if(isCalendarPageCurrentMonth()) { let jumpIcon = createElement( `
` ); jumpIcon = $('body').appendChild(jumpIcon); addEventListener(jumpIcon, 'click', () => scrollCurrentDate()); } } // Process show page function processShowsPage() { for (const el of [...$$('.grid-item[data-type="show"]')]) { addLinkToGridItem(el, 'show'); } } // Process show page function processShowPage() { for (const el of [...$$('.grid-item[data-type="season"]')]) { addLinkToGridItem(el, 'season'); } addLinkToActionList($('.action-buttons'), 'show'); } // Process season page function processSeasonPage() { for (const el of [...$$('.grid-item[data-type="episode"]')]) { addLinkToGridItem(el, 'episode'); } addLinkToActionList($('.action-buttons'), 'season'); } // Process episode page function processEpisodePage() { addLinkToActionList($('.action-buttons'), 'episode'); } // Process movies page function processMoviesPage() { for (const el of [...$$('.grid-item[data-type="movie"]')]) { addLinkToGridItem(el, 'movie'); } } // Process movie page function processMoviePage() { addLinkToActionList($('.action-buttons'), 'movie'); } // Main function function processPage() { if (regex.calendar.test(location.pathname)) { processCalendarPage(); } else if (regex.shows.test(location.pathname)) { processShowsPage(); } else if (regex.show.test(location.pathname)) { processShowPage(); } else if (regex.season.test(location.pathname)) { processSeasonPage(); } else if (regex.episode.test(location.pathname)) { processEpisodePage(); } else if (regex.movies.test(location.pathname)) { processMoviesPage(); } else if (regex.movie.test(location.pathname)) { processMoviePage(); } } // Executes the callback after the page finishes loading // Using a MutationObserver, a timout is set every time a new mutation happens, // if either the elapsed time bewteen mutations is greater than intervalTime or // the complete elapsed time is greater than maxWaitTime the callback is executed function whenPageReady(callback, intervalTime, maxWaitTime = 2500) { pageReady.startTimer = Date.now(); console.log('The Pirate Calendar: waiting for page to load'); let observerCallback = (mutationList, observer) => { if (pageReady.timeout) { let delay = (Date.now() - pageReady.startTimer) > maxWaitTime ? 0 : intervalTime; clearTimeout(pageReady.timeout); pageReady.timeout = setTimeout(() => { console.log(`The Pirate Calendar: page ready in ${Date.now() - pageReady.startTimer}ms!`); clearTimeout(pageReady.timeout); pageReady.timeout = null; observer.disconnect(); callback(); }, intervalTime) } else { observer.disconnect(); } }; let observer = new MutationObserver(observerCallback); observer.observe($('body'), { attributes: true, childList: true, subtree: true }); } function init() { whenPageReady(() => { // Apply styles $('head').append(createElement(style)); validateSettings(); processPage(); applySettings(); }, 250); } })();