Greasy Fork is available in English.
Adds torrent links to trakt.tv. Now with a settings menu!
当前为
// ==UserScript==
// @name The Pirate Calendar (for trakt.tv)
// @version 0.6.2
// @description Adds torrent links to trakt.tv. Now with a settings menu!
// @author luffier
// @namespace PirateCalendar
// @license MIT
// @match *://trakt.tv/
// @match *://trakt.tv/*
// @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@43fd0fe4de1166f343883511e53546e87840aeaf/gm_config.js
// @grant GM_addStyle
// @grant GM_listValues
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @run-at document-idle
// @homepageURL https://github.com/Luffier/the-pirate-calendar
// @supportURL https://github.com/Luffier/the-pirate-calendar/issues
// ==/UserScript==
/* globals GM_config */
/* jshint esversion: 6 */
(() => {
'use strict';
// Single element selector shorthand
const $ = document.querySelector.bind(document);
// Multiple elements selector shorthand
const $$ = document.querySelectorAll.bind(document);
/* VARIABLES */
// Global styles
const style = `
<style>
iframe#PirateCalendarConfig {
height: 480px !important;
width: 500px !important;
}
.actions .tpc {
transition: background-color .2s ease 0s;
font-size: 22px !important;
color: rgb(56, 96, 187);
}
.actions .tpc:hover {
background-color: rgb(255, 255, 255, 0.25);
transition: background-color .2s ease 0s;
color: rgb(18, 40, 89);
}
.action-buttons .btn-tpc {
margin-top: 5px;
color: rgb(56, 96, 187);
background-color: #fff;
border-color: rgb(56, 96, 187);
border: solid 1px rgb(56, 96, 187);
transition: all .5s;
}
.action-buttons .btn-tpc:hover {
background-color: rgb(18, 40, 89);
color: white;
transition: all .5s;
}
.tcp-icon {
font-size: 40px !important;
color: rgb(237, 28, 36);
}
</style>
`;
const regex = {
calendar: /^\/calendars\/my\/shows/,
show: /^\/shows\/([^\/]+)(\/)?$/,
season: /^\/shows\/([^\/]+)\/seasons\/([^\/]+)(\/)?$/,
episode: /^\/shows\/([^\/]+)\/seasons\/([^\/]+)\/episodes\/([^\/]+)(\/)?$/,
movies: /^\/movies(\/(boxoffice|anticipated|popular|trending|recommended|watched|collected))?(\/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, '+')
}
};
// Interval storage
const intervals = {};
/* 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': function() { } ,
'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 */
// 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');
}
// Executes the callback after the calendar finishes loading
function whenCalendarReady(callback, intervalName) {
setTimeout(() => {
intervals[intervalName] = setInterval(() => {
// If the loading indicator and the progress bar aren't visible, the calendar is ready
if (!isVisible($('#loading-bg')) && $$('.turbolinks-progress-bar').length === 0) {
clearInterval(intervals[intervalName]);
callback();
}
}, 100);
}, 200);
}
// 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(
`
<a class="tpc" href="${urlSearch}" target="${target}" title="Search on ${searchEngineName}">
<div class="trakt-icon-skull-bones"></div>
</a>
`
));
}
// 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(
`
<a class="btn btn-block btn-summary btn-tpc" href="${urlSearch}" target="${target}">
<div class="fa fa-fw trakt-icon-skull-bones"></div>
<div class="text">
<div class="main-info">Search on ${searchEngineName}</div>
</div>
</a>
`
));
}
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')) {
whenCalendarReady(() => { scrollCurrentDate(); }, 'autoscroll');
}
// Settings menu icon
let menuIcon = createElement(
`
<a class="tcp-icon" title="The Pirate Calendar Settings">
<div class="fa fa-gear"></div>
</a>
`
);
menuIcon = $('.sidenav-inner').appendChild(menuIcon);
addEventListener(menuIcon, 'click', () => GM_config.open());
// Jump icon
if(isCalendarPageCurrentMonth()) {
let jumpIcon = createElement(
`
<a class="tcp-icon" title="Jump to current day">
<div class="fa fa-calendar-xmark"></div>
</a>
`
);
jumpIcon = $('.sidenav-inner').appendChild(jumpIcon);
addEventListener(jumpIcon, 'click', () => scrollCurrentDate());
}
// Add events to arrows
whenCalendarReady(() => {
for (const el of [...$$('.prev, .next')]) {
addEventListener(
el,
'click',
() => whenCalendarReady(() => processCalendarPage(), 'processAfterChangingMonth'),
'addArrowsEvents'
);
}
});
}
// 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.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();
}
}
// Apply styles
$('head').append(createElement(style));
validateSettings();
processPage();
applySettings();
})();