├── .github
└── workflows
│ └── javascript.yml
├── PTP-Add-Time-Column-and-Highlight-Recent-Torrents-anut.js
├── README
├── YAETS.js
├── ant-add-filter-all-releases-anut.js
├── ant-to-radarr.js
├── ant-upcoming-releases.js
├── basic-imdb-intospection.js
├── basic-title-field-introspection.js
├── basic-title-introspction.js
├── gazelle-file-count.js
├── ops-red-add-releases.js
├── ptp-add-cast-photos.js
├── ptp-add-filter-all-releases-anut.js
├── ptp-artist-images.js
├── ptp-collage-add.js
├── ptp-cross-seed-checker.user,js
├── ptp-cross-seed-checker.user.js
├── ptp-get-tvdb-from-sonarr.js
├── ptp-get-tvdb-from-wikidata.js
├── ptp-get-tvdb-id.js
├── ptp-get-tvmaze-id.js
├── ptp-imdb-box-office.js
├── ptp-imdb-combined.js
├── ptp-screenshots.js
├── ptp-seeding-highlighter.js
├── ptp-similar-movies.js
├── ptp-soundtracks.js
├── ptp-technical-specifications.js
├── ptp-tmdb-trailers.js
├── ptp-to-radarr-mod.js
├── ptp-torrent-row-group-toggle.js
├── ptp-upcoming-releases.js
├── release-name-parser.js
├── scene_groups.js
└── unit3d-imdb-combined.js
/.github/workflows/javascript.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths:
7 | - '*.js'
8 | pull_request:
9 | branches: [ main ]
10 | paths:
11 | - '*.js'
12 | schedule:
13 | - cron: '00 00 * * 5'
14 | workflow_dispatch:
15 |
16 | concurrency:
17 | group: ${{ github.workflow }}-${{ github.ref }}
18 | cancel-in-progress: true
19 |
20 | jobs:
21 | analyze:
22 | name: Analyze
23 | runs-on: windows-2022
24 | permissions:
25 | actions: read
26 | contents: read
27 | security-events: write
28 |
29 | strategy:
30 | fail-fast: false
31 | matrix:
32 | language: [ 'javascript' ]
33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
34 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v4
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v3
43 | with:
44 | languages: ${{ matrix.language }}
45 |
46 | # Autobuild isn't necessary for JavaScript, so this step can be removed.
47 |
48 | # Performs CodeQL Analysis
49 | - name: Perform CodeQL Analysis
50 | uses: github/codeql-action/analyze@v3
--------------------------------------------------------------------------------
/PTP-Add-Time-Column-and-Highlight-Recent-Torrents-anut.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Add Time Column and Highlight Recent Torrents
3 | // @namespace PTP-Add-Time-Column-and-Highlight-Recent-Torrents
4 | // @version 0.5.6
5 | // @description Add a Time column to the Torrent Group Page, Collage Page,
6 | // Artist Page, and Bookmark Page.
7 | // Highlight recent and latest torrent within a group.
8 | // @author mcnellis (additions by Audionut)
9 | // @match https://passthepopcorn.me/torrents.php*
10 | // @match https://passthepopcorn.me/collages.php?*
11 | // @match https://passthepopcorn.me/artist.php?*
12 | // @match https://passthepopcorn.me/bookmarks.php*
13 | // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.js
14 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/PTP-Add-Time-Column-and-Highlight-Recent-Torrents-anut.js
15 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/PTP-Add-Time-Column-and-Highlight-Recent-Torrents-anut.js
16 | // ==/UserScript==
17 | /* globals $, moment, coverViewJsonData, ungroupedCoverViewJsonData */
18 |
19 | (function() {
20 | 'use strict';
21 |
22 | const TIME_FORMAT = 'rounded-relative'; // 'relative', 'rounded-relative', or any format value supported by moment.js
23 | const HIGHLIGHT_LATEST_TEXT_COLOR = 'black';
24 | const HIGHLIGHT_LATEST_BACKGROUND_COLOR = 'silver';
25 | const HIGHLIGHT_LATEST_FONT_WEIGHT = 'bold';
26 | const HIGHLIGHT_RECENT_TEXT_COLOR = 'black';
27 | const HIGHLIGHT_RECENT_BACKGROUND_COLOR = 'gainsboro';
28 | const HIGHLIGHT_RECENT_FONT_WEIGHT = 'bold';
29 | const RECENT_DAYS_IN_MILLIS = 7 * 24 * 60 * 60 * 1000;
30 |
31 | function main() {
32 | if (isIgnoredTorrentsPage()) {
33 | return;
34 | } else if (isTorrentsGroupPage()) {
35 | torrentsGroupPage();
36 | } else if (isTorrentsPage()) {
37 | torrentsPage();
38 | } else if (isCollageSubscriptionsPage()) {
39 | collageSubscriptionsPage();
40 | } else if (isCollagesPage() || isArtistPage() || isBookmarksPage()) {
41 | artistAndBookmarksAndCollagesPage();
42 | }
43 | }
44 |
45 | function torrentsGroupPage() {
46 | if (!$('.torrent_table thead tr th:contains("Time")').length) {
47 | $('.torrent_table thead tr th:nth(0)').after($('
Time | '));
48 | }
49 | $('.group_torrent td.basic-movie-list__torrent-edition').each((i, edition) => {
50 | $(edition).attr('colspan', parseInt($(edition).attr('colspan')) + 1);
51 | });
52 | $('.torrent_info_row td').each((i, info) => {
53 | $(info).attr('colspan', parseInt($(info).attr('colspan')) + 1);
54 | });
55 |
56 | handleExistingTorrents();
57 | handleNewTorrents();
58 | }
59 |
60 | function handleExistingTorrents() {
61 | let times = [];
62 | $('.group_torrent_header').each(function(i, element) {
63 | const torrentRow = $(element);
64 | if (!torrentRow.find('td.time-cell').length) {
65 | let time = torrentRow.next().find('span.time').first();
66 | if (time.length) {
67 | const timeTitle = time.attr('title');
68 | const parsedDate = moment.utc(timeTitle, "MMM DD YYYY, HH:mm");
69 |
70 | if (parsedDate.isValid()) {
71 | const isoString = parsedDate.toISOString();
72 | const formattedTime = formatTime(parsedDate);
73 | times.push(timeTitle);
74 |
75 | const clonedTime = time.clone().html(formattedTime).addClass('nobr');
76 | const timeCell = $(' | ').append(clonedTime);
77 | torrentRow.find('td:nth(0)').after(timeCell);
78 | } else {
79 | console.error('Invalid date:', timeTitle);
80 | const timeCell = $(' | ').append($('').addClass('nobr').text('Invalid date'));
81 | torrentRow.find('td:nth(0)').after(timeCell);
82 | }
83 | }
84 | }
85 | });
86 | highlightTimes(times);
87 | }
88 |
89 | function handleNewTorrents() {
90 | let times = [];
91 | $('.group_torrent_header').each(function(i, element) {
92 | const torrentRow = $(element);
93 | if (!torrentRow.find('td.time-cell').length) {
94 | let time = torrentRow.find('span.release.time').first();
95 | if (time.length) {
96 | const unixTimestamp = parseInt(time.attr('title'));
97 | if (!isNaN(unixTimestamp)) {
98 | const formattedTime = formatTime(moment.unix(unixTimestamp));
99 | time.html(formattedTime);
100 | times.push(time.attr('title'));
101 |
102 | $(time).addClass('nobr');
103 | const timeCell = $(' | ').append(time);
104 | torrentRow.find('td:nth(0)').after(timeCell);
105 | }
106 | } else {
107 | const timeCell = $(' | ').append($('').addClass('nobr'));
108 | torrentRow.find('td:nth(0)').after(timeCell);
109 | }
110 | }
111 | });
112 | highlightTimes(times);
113 | }
114 |
115 | function highlightTimes(times) {
116 | times.sort(descendingDate);
117 | const nowMillis = new Date().getTime();
118 |
119 | setTimeout(() => {
120 | let latestTimeHighlighted = false;
121 | for (let i in times) {
122 | const time = times[i];
123 | let timeMillis;
124 | if (isNaN(time)) {
125 | timeMillis = moment.utc(time, "MMM DD YYYY, HH:mm [UTC]").valueOf();
126 | } else {
127 | timeMillis = parseInt(time) * 1000;
128 | }
129 | if (nowMillis - timeMillis < RECENT_DAYS_IN_MILLIS) {
130 | highlightRecentTime(time, '.group_torrent_header');
131 | }
132 | if (!latestTimeHighlighted && timeMillis) {
133 | highlightLatestTime(time, '.group_torrent_header');
134 | latestTimeHighlighted = true;
135 | }
136 | }
137 | }, 100); // 100 milliseconds delay
138 | }
139 |
140 | function torrentsPage() {
141 | $('.torrent_table tbody').each(function(i, group) {
142 | let times = collectTimes(group);
143 | times.sort(descendingDate);
144 |
145 | const nowMillis = new Date().getTime();
146 | for (let i in times) {
147 | const time = times[i];
148 | const parsedDate = moment.utc(time, "MMM DD YYYY, HH:mm");
149 | if (parsedDate.isValid() && nowMillis - parsedDate.valueOf() < RECENT_DAYS_IN_MILLIS) {
150 | highlightRecentTime(time, '.basic-movie-list__torrent-row', group);
151 | }
152 | }
153 |
154 | if (times.length > 0) {
155 | highlightLatestTime(times[0], '.basic-movie-list__torrent-row', group);
156 | }
157 | });
158 | }
159 |
160 | function artistAndBookmarksAndCollagesPage() {
161 | if (!$('.torrent_table:visible thead tr th:contains("Time")').length) {
162 | $('.torrent_table:visible thead tr th:nth(1)').after($('Time | '));
163 | }
164 | $('.torrent_table:visible td.basic-movie-list__torrent-edition').each((i, edition) => {
165 | $(edition).attr('colspan', parseInt($(edition).attr('colspan')) + 1);
166 | });
167 | $('.basic-movie-list__details-row').each((i, detailsRow) => {
168 | const detailsCell = $(detailsRow).find('td:nth(1)');
169 | detailsCell.attr('colspan', parseInt(detailsCell.attr('colspan')) + 1);
170 | });
171 |
172 | const torrentGroupTimes = {};
173 | const torrentIdToTime = {};
174 | const torrentGroups = new Set();
175 | $(coverViewJsonData).each((i, data) => {
176 | $(data.Movies).each((j, movieGroup) => {
177 | const groupId = movieGroup.GroupId;
178 | torrentGroups.add(groupId);
179 | $(movieGroup.GroupingQualities).each((k, edition) => {
180 | $(edition.Torrents).each((m, torrent) => {
181 | if (!torrentGroupTimes[groupId]) {
182 | torrentGroupTimes[groupId] = [];
183 | }
184 | const timeTitle = $(torrent.Time).attr('title');
185 | const parsedDate = moment.utc(timeTitle, "MMM DD YYYY, HH:mm");
186 | if (parsedDate.isValid()) {
187 | torrentGroupTimes[groupId].push(parsedDate.toISOString());
188 | torrentIdToTime[torrent.TorrentId] = formatTime(parsedDate);
189 | } else {
190 | console.error('Invalid date:', timeTitle);
191 | torrentIdToTime[torrent.TorrentId] = 'Invalid date';
192 | }
193 | });
194 | });
195 | });
196 | });
197 |
198 | $('.basic-movie-list__torrent-row .basic-movie-list__torrent__action').each((i, element) => {
199 | const parent = $(element).parent();
200 | if (!parent.find('td.time-cell').length) {
201 | const href = parent.find('a.torrent-info-link').attr('href');
202 | const hrefParts = href.match(/torrents.php\?id=([0-9]+)&torrentid=([0-9]+)/);
203 | const groupId = hrefParts[1];
204 | const torrentId = hrefParts[2];
205 | const timeText = torrentIdToTime[torrentId] || 'Unknown';
206 | parent.after($('' + timeText + ' | '));
207 | }
208 | });
209 |
210 | torrentGroups.forEach(groupId => {
211 | torrentGroupTimes[groupId] = torrentGroupTimes[groupId].sort(descendingDate);
212 | });
213 |
214 | const nowMillis = new Date().getTime();
215 | for (const i in torrentGroupTimes) {
216 | const times = torrentGroupTimes[i];
217 | for (const j in times) {
218 | const time = times[j];
219 | if (nowMillis - moment.utc(time).valueOf() < RECENT_DAYS_IN_MILLIS) {
220 | highlightRecentTime(time, '.basic-movie-list__torrent-row');
221 | }
222 | }
223 |
224 | if (times.length > 1) {
225 | highlightLatestTime(times[0], '.basic-movie-list__torrent-row');
226 | }
227 | }
228 | }
229 |
230 | function collageSubscriptionsPage() {
231 | const torrentGroupTimes = {};
232 | const torrentIdToTime = {};
233 | const torrentGroups = new Set();
234 | $(coverViewJsonData).each((j, jsonData) => {
235 | $(jsonData.Movies).each((i, movieGroup) => {
236 | const groupId = movieGroup.GroupId;
237 | torrentGroups.add(groupId);
238 | $(movieGroup.GroupingQualities).each((j, edition) => {
239 | $(edition.Torrents).each((k, torrent) => {
240 | if (!torrentGroupTimes[groupId]) {
241 | torrentGroupTimes[groupId] = [];
242 | }
243 | const timeTitle = $(torrent.Time).attr('title');
244 | const unixTimestamp = moment.utc(timeTitle, "MMM DD YYYY, HH:mm").unix();
245 | if (!isNaN(unixTimestamp)) {
246 | torrentGroupTimes[groupId].push(timeTitle);
247 | torrentIdToTime[torrent.TorrentId] = formatTime(moment.unix(unixTimestamp));
248 | } else {
249 | console.error('Invalid date:', timeTitle);
250 | torrentIdToTime[torrent.TorrentId] = 'Invalid date';
251 | }
252 | });
253 | });
254 | });
255 | });
256 |
257 | torrentGroups.forEach(groupId => {
258 | torrentGroupTimes[groupId] = torrentGroupTimes[groupId].sort(descendingDate);
259 | });
260 |
261 | const nowMillis = new Date().getTime();
262 | for (const i in torrentGroupTimes) {
263 | const times = torrentGroupTimes[i];
264 | for (const j in times) {
265 | const time = times[j];
266 | if (nowMillis - moment.utc(time + ' UTC').valueOf() < RECENT_DAYS_IN_MILLIS) {
267 | highlightRecentTime(time, '.basic-movie-list__torrent-row');
268 | }
269 | }
270 |
271 | if (times.length > 1) {
272 | highlightLatestTime(times[0], '.basic-movie-list__torrent-row');
273 | }
274 | }
275 | }
276 |
277 | function isArtistPage() {
278 | return window.location.pathname === '/artist.php';
279 | }
280 |
281 | function isBookmarksPage() {
282 | return window.location.pathname === '/bookmarks.php';
283 | }
284 |
285 | function isCollageSubscriptionsPage() {
286 | return (
287 | window.location.pathname === '/collages.php' &&
288 | window.location.search.includes('action=subscriptions')
289 | );
290 | }
291 |
292 | function isCollagesPage() {
293 | return window.location.pathname === '/collages.php';
294 | }
295 |
296 | function isTorrentsPage() {
297 | return window.location.pathname === '/torrents.php';
298 | }
299 |
300 | function isTorrentsGroupPage() {
301 | return isTorrentsPage() && window.location.search.includes('id=');
302 | }
303 |
304 | function isIgnoredTorrentsPage() {
305 | return (
306 | isTorrentsPage() &&
307 | (
308 | window.location.search.includes('type=downloaded') ||
309 | window.location.search.includes('type=uploaded') ||
310 | window.location.search.includes('type=leeching') ||
311 | window.location.search.includes('type=snatched') ||
312 | window.location.search.includes('type=seeding')
313 | )
314 | );
315 | }
316 |
317 | function collectTimes(group) {
318 | let times = [];
319 | $(group).find('.basic-movie-list__torrent-row').each(function (i, torrentRow) {
320 | const spanTime = $(torrentRow).find('td span.time');
321 | if (spanTime && spanTime.length > 0) {
322 | const timeTitle = spanTime.attr('title');
323 | times.push(moment.utc(timeTitle, "MMM DD YYYY, HH:mm").toISOString());
324 | }
325 | });
326 | return times;
327 | }
328 |
329 | function descendingDate(a, b) {
330 | const aDate = new Date(a);
331 | const bDate = new Date(b);
332 | if (aDate === bDate) return 0;
333 | return aDate > bDate ? -1 : 1;
334 | }
335 |
336 | function highlightTime(time, rowClassName, textColor, backgroundColor, fontWeight) {
337 | $(rowClassName + " span.time[title='" + time + "'], " + rowClassName + " span.release.time[title='" + time + "']").each(function(i, span) {
338 | highlightSpan(span, textColor, backgroundColor, fontWeight);
339 | });
340 | }
341 |
342 | function highlightSpan(span, textColor, backgroundColor, fontWeight) {
343 | if (textColor) $(span).css('color', textColor);
344 | if (backgroundColor) $(span).parent().css('background-color', backgroundColor);
345 | if (fontWeight) $(span).css('font-weight', fontWeight);
346 | }
347 |
348 | function highlightRecentTime(time, rowClassName) {
349 | highlightTime(time, rowClassName, HIGHLIGHT_RECENT_TEXT_COLOR, HIGHLIGHT_RECENT_BACKGROUND_COLOR, HIGHLIGHT_RECENT_FONT_WEIGHT);
350 | }
351 |
352 | function highlightLatestTime(time, rowClassName) {
353 | highlightTime(time, rowClassName, HIGHLIGHT_LATEST_TEXT_COLOR, HIGHLIGHT_LATEST_BACKGROUND_COLOR, HIGHLIGHT_LATEST_FONT_WEIGHT);
354 | }
355 |
356 | function formatRoundedRelative(time) {
357 | const relativeTimeText = time.html();
358 | if (relativeTimeText !== undefined) {
359 | const relativeTimeParts = relativeTimeText.split(' ');
360 | if (relativeTimeParts.length > 1 && relativeTimeText !== 'Just now') {
361 | time.html(`${relativeTimeParts[0]} ${relativeTimeParts[1].replace(',', '')} ago`);
362 | } else if (relativeTimeText === 'Just now') {
363 | time.html('Just now');
364 | }
365 | } else {
366 | time.html('Undefined');
367 | }
368 | }
369 |
370 | function formatTime(momentTime) {
371 | if (TIME_FORMAT === 'relative') {
372 | return momentTime.fromNow();
373 | } else if (TIME_FORMAT === 'rounded-relative') {
374 | const relativeTimeText = momentTime.fromNow(true);
375 | const relativeTimeParts = relativeTimeText.split(' ');
376 | if (relativeTimeParts.length > 1 && relativeTimeText !== 'Just now') {
377 | return `${relativeTimeParts[0]} ${relativeTimeParts[1].replace(',', '')} ago`;
378 | } else if (relativeTimeText === 'Just now') {
379 | return 'Just now';
380 | }
381 | } else {
382 | return momentTime.format(TIME_FORMAT);
383 | }
384 | }
385 |
386 | main();
387 |
388 | document.addEventListener('PTPAddReleasesFromOtherTrackersComplete', () => {
389 | console.log("Rerunning Time Column to fix added releases");
390 | handleNewTorrents(); // Run the script again for new torrents
391 | });
392 | })();
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | TODO:
2 |
3 | Maybe more sites: - Adding a UNIT3D codebase site is relatively easy - https://github.com/Audionut/add-trackers/commit/8a42dd4d3ad232e665ff607a864ab0a3870c1a8a
4 | Maybe a wiki to direct users for adding other sites:
5 | Shuffle releases to other areas, like the "3D" and "Other" groups whilst retaining underlying quality assignment:
--------------------------------------------------------------------------------
/basic-imdb-intospection.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name iMDB query
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.0.1
5 | // @description Run iMDB queries
6 | // @author Audionut
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @icon https://passthepopcorn.me/favicon.ico
9 | // @grant GM_xmlhttpRequest
10 | // @connect api.graphql.imdb.com
11 | // ==/UserScript==
12 |
13 | (function () {
14 | 'use strict';
15 |
16 | const fetchIntrospectionData = async () => {
17 | const url = `https://api.graphql.imdb.com/`;
18 | const query = {
19 | query: `
20 | {
21 | __type(name: "Query") {
22 | name
23 | fields {
24 | name
25 | args {
26 | name
27 | type {
28 | name
29 | kind
30 | }
31 | }
32 | type {
33 | name
34 | kind
35 | ofType {
36 | name
37 | kind
38 | ofType {
39 | name
40 | kind
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 | `
48 | };
49 |
50 | GM_xmlhttpRequest({
51 | method: "POST",
52 | url: url,
53 | headers: {
54 | "Content-Type": "application/json"
55 | },
56 | data: JSON.stringify(query),
57 | onload: function (response) {
58 | if (response.status >= 200 && response.status < 300) {
59 | const data = JSON.parse(response.responseText);
60 | console.log("Introspection data:", data);
61 | } else {
62 | console.error("Failed to fetch introspection data", response);
63 | }
64 | },
65 | onerror: function (response) {
66 | console.error("Request error", response);
67 | }
68 | });
69 | };
70 |
71 | fetchIntrospectionData();
72 | })();
--------------------------------------------------------------------------------
/basic-title-field-introspection.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name iMDB field Introspection
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.0.3
5 | // @description Run iMDB queries and introspect the IMDb API for Runtime type
6 | // @author Audionut
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @icon https://passthepopcorn.me/favicon.ico
9 | // @grant GM_xmlhttpRequest
10 | // @connect api.graphql.imdb.com
11 | // ==/UserScript==
12 |
13 | (function () {
14 | 'use strict';
15 |
16 | // Helper: get IMDb ID from the page
17 | function getImdbId() {
18 | const link = document.querySelector("a#imdb-title-link.rating");
19 | if (!link) return null;
20 | const imdbUrl = link.getAttribute("href");
21 | if (!imdbUrl) return null;
22 | const imdbId = imdbUrl.split("/")[4];
23 | return imdbId || null;
24 | }
25 |
26 | // Fetch TitleKeyword details for a given IMDb ID
27 | function fetchTitleKeywords(imdbId) {
28 | const url = `https://api.graphql.imdb.com/`;
29 | const query = {
30 | query: `
31 | {
32 | title(id: "${imdbId}") {
33 | keywords(first: 10) {
34 | edges {
35 | node {
36 | interestScore {
37 | score
38 | }
39 | itemCategory {
40 | text
41 | }
42 | keyword {
43 | text
44 | }
45 | legacyId
46 | }
47 | }
48 | }
49 | }
50 | }
51 | `
52 | };
53 |
54 | GM_xmlhttpRequest({
55 | method: "POST",
56 | url: url,
57 | headers: {
58 | "Content-Type": "application/json"
59 | },
60 | data: JSON.stringify(query),
61 | onload: function (response) {
62 | if (response.status >= 200 && response.status < 300) {
63 | const data = JSON.parse(response.responseText);
64 | console.log("IMDb TitleKeyword details:", data);
65 | // You can process and display the data here as needed
66 | } else {
67 | console.error("Failed to fetch TitleKeyword data", response);
68 | }
69 | },
70 | onerror: function (response) {
71 | console.error("Request error", response);
72 | }
73 | });
74 | }
75 |
76 | // Main
77 | const imdbId = getImdbId();
78 | if (imdbId) {
79 | fetchTitleKeywords(imdbId);
80 | } else {
81 | console.error("IMDb ID not found on page.");
82 | }
83 | })();
84 |
--------------------------------------------------------------------------------
/basic-title-introspction.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name iMDB title query
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.0.2
5 | // @description Run iMDB queries and introspect the IMDb API
6 | // @author Audionut
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @icon https://passthepopcorn.me/favicon.ico
9 | // @grant GM_xmlhttpRequest
10 | // @connect api.graphql.imdb.com
11 | // ==/UserScript==
12 |
13 | (function () {
14 | 'use strict';
15 |
16 | const fetchIntrospectionData = async () => {
17 | const url = `https://api.graphql.imdb.com/`;
18 | const query = {
19 | query: `
20 | {
21 | __type(name: "Title") {
22 | name
23 | fields {
24 | name
25 | type {
26 | name
27 | kind
28 | ofType {
29 | name
30 | kind
31 | ofType {
32 | name
33 | kind
34 | }
35 | }
36 | }
37 | }
38 | }
39 | }
40 | `
41 | };
42 |
43 | GM_xmlhttpRequest({
44 | method: "POST",
45 | url: url,
46 | headers: {
47 | "Content-Type": "application/json"
48 | },
49 | data: JSON.stringify(query),
50 | onload: function (response) {
51 | if (response.status >= 200 && response.status < 300) {
52 | const data = JSON.parse(response.responseText);
53 | console.log("Introspection data:", data);
54 | } else {
55 | console.error("Failed to fetch introspection data", response);
56 | }
57 | },
58 | onerror: function (response) {
59 | console.error("Request error", response);
60 | }
61 | });
62 | };
63 |
64 | fetchIntrospectionData();
65 | })();
--------------------------------------------------------------------------------
/gazelle-file-count.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Gazelle File Count
3 | // @namespace NWCD/OPS/RED
4 | // @description Shows the number of tracks and/or files in each torrent
5 | // @version 2.0.3
6 | // @match https://notwhat.cd/torrents.php*id=*
7 | // @match https://orpheus.network/torrents.php*id=*
8 | // @match https://redacted.ch/torrents.php*id=*
9 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/gazelle-file-count.js
10 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/gazelle-file-count.js
11 | // @grant none
12 | // ==/UserScript==
13 |
14 | // _____________________________________________________________
15 | // _____________ Preferences ___________________________________
16 |
17 |
18 | // How to display the file count:
19 |
20 | // 1 = Total number of files in torrent (15)
21 | // 2 = Number of tracks out of total files (12/15)
22 | // 3 = Number of tracks plus extra files (12+3)
23 | // 4 = Only the number of tracks (12)
24 |
25 | var display = 1;
26 |
27 |
28 |
29 | // Highlight editions with conflicting track counts:
30 |
31 | var checkEditions = true;
32 |
33 |
34 |
35 | // Highlight torrents with extra files (usually artwork)
36 | // exceeding this size (in MB; 0 = disable):
37 |
38 | var extraSizeLimit = 40;
39 |
40 |
41 |
42 | // Always show the size of extras when hovering over a
43 | // torrent size (false = only the highlighted ones):
44 |
45 | var tooltipAll = false;
46 |
47 |
48 | // _____________________________________________________________
49 | // __________ End of Preferences _______________________________
50 |
51 |
52 | function toBytes(size) {
53 | var num = parseFloat(size.replace(',', ''));
54 | var i = ' KMGT'.indexOf(size.charAt(size.length-2));
55 | return Math.round(num * Math.pow(1024, i));
56 | }
57 |
58 | function toSize(bytes) {
59 | if (bytes <= 0) return '0 B';
60 | var i = Math.floor(Math.log(bytes) / Math.log(1024));
61 | var num = Math.round(bytes / Math.pow(1024, i));
62 | return num + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
63 | }
64 |
65 | function addStyle(css) {
66 | var s = document.createElement('style');
67 | s.type = 'text/css';
68 | s.textContent = css;
69 | document.head.appendChild(s);
70 | }
71 |
72 | function setTitle(elem, str) {
73 | elem.title = str;
74 | if (window.jQuery && jQuery.fn.tooltipster) {
75 | jQuery(elem).tooltipster({ delay: 500, maxWidth: 400 });
76 | }
77 | }
78 |
79 | var table = document.getElementById('torrent_details');
80 | if (table) {
81 |
82 | var isMusic = !!document.querySelector('.box_artists');
83 | extraSizeLimit = extraSizeLimit * 1048576;
84 |
85 | addStyle(
86 | '.gmfc_files { cursor: pointer; }' +
87 | '.gmfc_extrasize { background-color: rgba(228, 169, 29, 0.12) !important; }'
88 | );
89 |
90 | table.rows[0].insertCell(1).innerHTML = 'Files';
91 |
92 | var rows = table.querySelectorAll('.edition, .torrentdetails');
93 | for (var i = rows.length; i--; ) {
94 | ++rows[i].cells[0].colSpan;
95 | }
96 |
97 | rows = table.getElementsByClassName('torrent_row');
98 | var editions = {};
99 |
100 | for (var i = rows.length; i--; ) {
101 |
102 | var fileRows = rows[i].nextElementSibling.
103 | querySelectorAll('.filelist_table tr:not(:first-child)');
104 | var numFiles = fileRows.length;
105 | var numTracks = 0;
106 |
107 | if (isMusic) {
108 | var extraSize = 0;
109 |
110 | for (var j = numFiles; j--; ) {
111 | if (/\.(flac|mp3|m4a|ac3|dts)\s*$/i.test(fileRows[j].cells[0].textContent)) {
112 | ++numTracks;
113 | } else if (extraSizeLimit || tooltipAll) {
114 | extraSize += toBytes(fileRows[j].cells[1].textContent);
115 | }
116 | }
117 |
118 | if (checkEditions) {
119 | var ed = /edition_\d+/.exec(rows[i].className)[0];
120 | editions[ed] = ed in editions && editions[ed] !== numTracks ? -1 : numTracks;
121 | }
122 |
123 | var largeExtras = extraSizeLimit && extraSize > extraSizeLimit;
124 | if (largeExtras || tooltipAll) {
125 | var sizeCell = rows[i].cells[1];
126 | setTitle(sizeCell, 'Extras: ' + toSize(extraSize));
127 | if (largeExtras) {
128 | sizeCell.classList.add('gmfc_extrasize');
129 | }
130 | }
131 |
132 | } else {
133 | display = 0;
134 | }
135 |
136 | var cell = rows[i].insertCell(1);
137 | cell.textContent = display < 2 ? numFiles : numTracks;
138 | cell.className = 'gmfc_files';
139 | if (display != 3) {
140 | cell.className += ' number_column';
141 | } else {
142 | var numExtras = numFiles - numTracks;
143 | if (numExtras) {
144 | var sml = document.createElement('small');
145 | sml.textContent = '+' + numExtras;
146 | cell.appendChild(sml);
147 | }
148 | }
149 | if (display == 2) {
150 | cell.textContent += '/' + numFiles;
151 | }
152 | }
153 |
154 | if (checkEditions) {
155 | var sel = '';
156 | for (var ed in editions) {
157 | if (editions.hasOwnProperty(ed) && editions[ed] < 1) {
158 | sel += [sel ? ',.' : '.', ed, '>.gmfc_files'].join('');
159 | }
160 | }
161 | if (sel) addStyle(sel + '{background-color: rgba(236, 17, 0, 0.09) !important;}');
162 | }
163 |
164 | // Show filelist on filecount click
165 |
166 | table.addEventListener('click', function (e) {
167 |
168 | function get(type) {
169 | return document.getElementById([type, id].join('_'));
170 | }
171 |
172 | var elem = e.target.nodeName != 'SMALL' ? e.target : e.target.parentNode;
173 | if (elem.classList.contains('gmfc_files')) {
174 |
175 | var id = elem.parentNode.id.replace('torrent', '');
176 | var tEl = get('torrent');
177 | var fEl = get('files');
178 | var show = [tEl.className, fEl.className].join().indexOf('hidden') > -1;
179 |
180 | tEl.classList[show ? 'remove' : 'add']('hidden');
181 | fEl.classList[show ? 'remove' : 'add']('hidden');
182 |
183 | if (show) {
184 | var sections = ['peers', 'downloads', 'snatches', 'reported', 'logs'];
185 | for (var i = sections.length; i--; ) {
186 | var el = get(sections[i]);
187 | if (el) el.classList.add('hidden');
188 | }
189 | }
190 |
191 | }
192 | }, false);
193 |
194 | function checkAndDispatchEvents() {
195 | if (display === 2 || display === 3) {
196 | const event = new CustomEvent('vardisplay3');
197 | document.dispatchEvent(event);
198 | } else if (display === 1 || display === 4) {
199 | const event = new CustomEvent('vardisplay4');
200 | document.dispatchEvent(event);
201 | }
202 | }
203 |
204 | // Run the function once when the script first runs
205 | checkAndDispatchEvents();
206 |
207 | // Set up an interval to repeat the event dispatching
208 | const interval1 = setInterval(() => {
209 | checkAndDispatchEvents();
210 | }, 200); // Repeat every 200 ms
211 |
212 | // Listen for the custom event 'OPSaddREDreleasescomplete'
213 | document.addEventListener('OPSaddREDreleasescomplete', function () {
214 | // console.log("Detected OPSaddREDreleasescomplete event, stopping event dispatching");
215 |
216 | // Stop further dispatching of vardisplay3 and vardisplay4
217 | clearInterval(interval1);
218 |
219 | // Get all elements with the class 'RED_filecount_placeholder'
220 | const fileCountElements = document.querySelectorAll('td.RED_filecount_placeholder');
221 |
222 | // Loop through the elements and remove the 'hidden' class
223 | fileCountElements.forEach(function (element) {
224 | element.classList.remove('hidden');
225 | });
226 | //console.log("Finished processing RED_filecount_placeholder elements");
227 | });
228 | }
--------------------------------------------------------------------------------
/ptp-add-cast-photos.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP Add cast photos
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 3.0.6
5 | // @description Adds cast photos to movie pages
6 | // @author Chameleon (mods by Audionut to use IMDB API)
7 | // @include http*://*passthepopcorn.me/torrents.php?id=*
8 | // @grant GM_xmlhttpRequest
9 | // @grant GM_setValue
10 | // @grant GM_getValue
11 | // @require https://code.jquery.com/jquery-3.6.0.min.js
12 | // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
13 | // @connect api.graphql.imdb.com
14 | // ==/UserScript==
15 |
16 | (function () {
17 | 'use strict';
18 |
19 | // Set cache duration in days (user can change this)
20 | const cacheDuration = 28; // default is 7 days
21 |
22 | // Helper function to compress data
23 | const compress = (data) => LZString.compress(JSON.stringify(data));
24 |
25 | // Helper function to decompress data
26 | const decompress = (data) => JSON.parse(LZString.decompress(data));
27 |
28 | // Helper function to calculate if cache is valid based on timestamp
29 | const isCacheValid = (timestamp, days) => {
30 | const now = new Date().getTime();
31 | return now - timestamp < days * 24 * 60 * 60 * 1000;
32 | };
33 |
34 | // Fetch image URLs for individual nameIds, with caching per nameId
35 | const fetchPrimaryImageUrl = async (nameIds) => {
36 | const uncachedNameIds = [];
37 | const cachedResults = [];
38 |
39 | // Check cache for each nameId
40 | nameIds.forEach((nameId) => {
41 | const cacheKey = `primaryImageUrl_${nameId}`;
42 | const cachedData = GM_getValue(cacheKey);
43 |
44 | if (cachedData) {
45 | const { timestamp, data } = JSON.parse(decompress(cachedData));
46 | if (isCacheValid(timestamp, cacheDuration)) {
47 | console.log(`Loaded ${nameId} from cache:`, data);
48 | cachedResults.push(data);
49 | } else {
50 | console.log(`Cache expired for ${nameId}, need to refetch`);
51 | uncachedNameIds.push(nameId);
52 | }
53 | } else {
54 | uncachedNameIds.push(nameId);
55 | }
56 | });
57 |
58 | // If there are uncached nameIds, fetch their data from the API
59 | if (uncachedNameIds.length > 0) {
60 | const url = `https://api.graphql.imdb.com/`;
61 | const query = {
62 | query: `
63 | query {
64 | names(ids: ${JSON.stringify(uncachedNameIds)}) {
65 | id
66 | nameText {
67 | text
68 | }
69 | primaryImage {
70 | url
71 | }
72 | }
73 | }
74 | `
75 | };
76 |
77 | GM_xmlhttpRequest({
78 | method: "POST",
79 | url: url,
80 | headers: {
81 | "Content-Type": "application/json"
82 | },
83 | data: JSON.stringify(query),
84 | onload: function (response) {
85 | if (response.status >= 200 && response.status < 300) {
86 | const data = JSON.parse(response.responseText);
87 | console.log("Fetched new data for uncached nameIds:", data);
88 |
89 | data.data.names.forEach((name) => {
90 | const cacheKey = `primaryImageUrl_${name.id}`;
91 | const cacheValue = {
92 | timestamp: new Date().getTime(),
93 | data: name
94 | };
95 | GM_setValue(cacheKey, compress(JSON.stringify(cacheValue)));
96 | cachedResults.push(name);
97 | });
98 |
99 | // Combine cached and freshly fetched results
100 | gotCredits(cachedResults);
101 | } else {
102 | console.error("Failed to fetch primary image URLs", response);
103 | }
104 | },
105 | onerror: function (response) {
106 | console.error("Request error", response);
107 | }
108 | });
109 | } else {
110 | // If all data was in cache, return the cached results
111 | gotCredits(cachedResults);
112 | }
113 | };
114 |
115 | // Fetch credits data and cache it
116 | const fetchCreditsData = async () => {
117 | const imdb_id = document.getElementById('imdb-title-link');
118 | if (imdb_id) {
119 | const imdbId = imdb_id.href.split('/title/tt')[1].split('/')[0];
120 | const cacheKey = `creditsData_${imdbId}`;
121 | const cachedData = GM_getValue(cacheKey);
122 |
123 | // Check if credits data is cached
124 | if (cachedData) {
125 | const { timestamp, data } = JSON.parse(decompress(cachedData));
126 | if (isCacheValid(timestamp, cacheDuration)) {
127 | console.log("Loaded credits data from cache:", data);
128 | const nameIds = data.title.credits.edges.map(edge => edge.node.name.id);
129 | fetchPrimaryImageUrl(nameIds);
130 | return;
131 | } else {
132 | console.log("Cache expired for credits data, fetching new data...");
133 | }
134 | }
135 |
136 | // Fetch new credits data if not cached
137 | const url = `https://api.graphql.imdb.com/`;
138 | const query = {
139 | query: `
140 | query {
141 | title(id: "tt${imdbId}") {
142 | credits(first: 40) {
143 | edges {
144 | node {
145 | name {
146 | id
147 | nameText {
148 | text
149 | }
150 | }
151 | category {
152 | id
153 | text
154 | }
155 | title {
156 | id
157 | titleText {
158 | text
159 | }
160 | }
161 | }
162 | }
163 | }
164 | }
165 | }
166 | `
167 | };
168 |
169 | GM_xmlhttpRequest({
170 | method: "POST",
171 | url: url,
172 | headers: {
173 | "Content-Type": "application/json"
174 | },
175 | data: JSON.stringify(query),
176 | onload: function (response) {
177 | if (response.status >= 200 && response.status < 300) {
178 | const data = JSON.parse(response.responseText);
179 | console.log("Fetched new credits data:", data);
180 | const nameIds = data.data.title.credits.edges.map(edge => edge.node.name.id);
181 |
182 | // Cache credits data
183 | const cacheValue = {
184 | timestamp: new Date().getTime(),
185 | data: data.data
186 | };
187 | GM_setValue(cacheKey, compress(JSON.stringify(cacheValue)));
188 |
189 | fetchPrimaryImageUrl(nameIds);
190 | } else {
191 | console.error("Failed to fetch credits data", response);
192 | }
193 | },
194 | onerror: function (response) {
195 | console.error("Request error", response);
196 | }
197 | });
198 | }
199 | };
200 |
201 | const gotCredits = (names) => {
202 | const castPhotosCount = window.localStorage.castPhotosCount ? parseInt(window.localStorage.castPhotosCount) : 4;
203 |
204 | let cast = names.map(name => ({
205 | photo: name.primaryImage ? name.primaryImage.url.replace('w66_and_h66', 'w300_and_h300') : 'https://ptpimg.me/9wv452.png',
206 | name: name.nameText.text,
207 | imdbId: name.id,
208 | role: 'Unknown', // Role data will be updated below
209 | link: '' // Link will be updated below
210 | }));
211 |
212 | const actorRows = document.querySelectorAll('.table--panel-like tbody tr');
213 | actorRows.forEach(row => {
214 | const actorNameElement = row.querySelector('.movie-page__actor-column a');
215 | const roleNameElement = row.querySelector('td:nth-child(2)');
216 | if (actorNameElement && roleNameElement) {
217 | const actorName = actorNameElement.textContent;
218 | const roleName = roleNameElement.textContent;
219 | const actorLink = actorNameElement.href;
220 | const castMember = cast.find(member => member.name === actorName);
221 | if (castMember) {
222 | castMember.role = roleName;
223 | castMember.link = actorLink;
224 | }
225 | }
226 | });
227 |
228 | // Sort cast members, those with primary image first
229 | cast.sort((a, b) => (a.photo === 'https://ptpimg.me/9wv452.png') - (b.photo === 'https://ptpimg.me/9wv452.png'));
230 |
231 | const actors = document.getElementsByClassName('movie-page__actor-column');
232 |
233 | const cDiv = document.createElement('div');
234 | cDiv.setAttribute('class', 'panel');
235 | cDiv.innerHTML = 'iMDB Cast
';
236 | const castDiv = document.createElement('div');
237 | castDiv.setAttribute('style', 'text-align:center; display:table; width:100%; border-collapse: separate; border-spacing:4px;');
238 | cDiv.appendChild(castDiv);
239 | const a = document.createElement('a');
240 | a.innerHTML = '(Show all cast photos)';
241 | a.href = 'javascript:void(0);';
242 | a.setAttribute('style', 'float:right;');
243 | a.setAttribute('stle', 'fontSize:0.9em');
244 |
245 | cDiv.firstElementChild.appendChild(a);
246 | a.addEventListener('click', function (a) {
247 | const divs = castDiv.getElementsByClassName('castRow');
248 | let disp = 'none';
249 | a.innerHTML = '(Show all cast photos)';
250 | if (divs[4].style.display == 'none') {
251 | disp = 'table-row';
252 | a.innerHTML = '(Hide extra cast photos)';
253 | }
254 | for (let i = 3; i < divs.length; i++) {
255 | divs[i].style.display = disp;
256 | }
257 | }.bind(undefined, a));
258 | const before = actors[0].parentNode.parentNode.parentNode;
259 | before.parentNode.insertBefore(cDiv, before);
260 | before.style.display = 'none';
261 | let count = 0;
262 | let dr = document.createElement('div');
263 | dr.setAttribute('style', 'display:table-row;');
264 | dr.setAttribute('class', 'castRow');
265 | castDiv.appendChild(dr);
266 |
267 | const bg = getComputedStyle(document.getElementsByClassName('movie-page__torrent__panel')[0]).backgroundColor;
268 | const width = 100 / castPhotosCount;
269 | let fontSize = 1;
270 | cast.forEach(person => {
271 | const d = document.createElement('div');
272 | dr.appendChild(d);
273 | if ((count + 1) % castPhotosCount === 0) {
274 | dr = document.createElement('div');
275 | dr.setAttribute('style', 'display:table-row;');
276 | if (count >= 11) dr.style.display = 'none';
277 | dr.setAttribute('class', 'castRow');
278 | castDiv.appendChild(dr);
279 | }
280 | if (window.localStorage.castPhotosSmallText == 'true') {
281 | fontSize = (1 + (4 / castPhotosCount)) / 2;
282 | }
283 | d.setAttribute('style', `width:${width}%; display:table-cell; text-align:center; background-color:${bg}; border-radius:10px; overflow:hidden; font-size:${fontSize}em;`);
284 | d.innerHTML = `
285 |
286 |
287 |

288 |
289 |
290 |
291 | ${person.name}
292 | `;
293 | d.firstElementChild.nextElementSibling.innerHTML = person.name;
294 | d.firstElementChild.nextElementSibling.nextElementSibling.nextElementSibling.innerHTML = person.role;
295 | count++;
296 | });
297 | };
298 |
299 | fetchCreditsData();
300 | })();
--------------------------------------------------------------------------------
/ptp-artist-images.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP Artist Image Enhancer
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.2
5 | // @description Fetch and display IMDb images and details for artists
6 | // @match https://passthepopcorn.me/artist.php?id=*
7 | // @icon https://passthepopcorn.me/favicon.ico
8 | // @grant GM_xmlhttpRequest
9 | // @grant GM.setValue
10 | // @grant GM.getValue
11 | // @connect api.graphql.imdb.com
12 | // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
13 | // ==/UserScript==
14 |
15 | (function () {
16 | 'use strict';
17 |
18 | const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
19 |
20 | // Compress and set cache with expiration
21 | const setCache = async (key, data) => {
22 | const cacheData = {
23 | timestamp: Date.now(),
24 | data: LZString.compress(JSON.stringify(data)) // Compress the data before storing
25 | };
26 | await GM.setValue(key, JSON.stringify(cacheData));
27 | };
28 |
29 | // Decompress and get cache with expiration check
30 | const getCache = async (key) => {
31 | const cached = await GM.getValue(key, null);
32 | if (cached) {
33 | const cacheData = JSON.parse(cached);
34 | const currentTime = Date.now();
35 |
36 | // Check if the cache is expired
37 | if (currentTime - cacheData.timestamp < CACHE_DURATION) {
38 | const decompressedData = LZString.decompress(cacheData.data);
39 | return JSON.parse(decompressedData); // Return the decompressed and parsed data
40 | } else {
41 | console.log("Cache expired for key:", key);
42 | return null; // Cache expired, return null
43 | }
44 | }
45 | return null; // No cache found
46 | };
47 |
48 | const calculateAge = (birthDate) => {
49 | const birth = new Date(birthDate);
50 | const today = new Date();
51 | let age = today.getFullYear() - birth.getFullYear();
52 | const monthDiff = today.getMonth() - birth.getMonth();
53 | if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
54 | age--;
55 | }
56 | return age;
57 | };
58 |
59 | const formatDate = (dateStr) => {
60 | const options = { year: 'numeric', month: 'long', day: 'numeric' };
61 | const date = new Date(dateStr);
62 | return date.toLocaleDateString(undefined, options);
63 | };
64 |
65 | const fetchDetails = async (imdbID, data) => {
66 | const cacheKey = `imdb_details_${imdbID}`;
67 |
68 | // Check the cache first
69 | const cachedData = await getCache(cacheKey);
70 | if (cachedData) {
71 | console.log("Using cached data for artist details");
72 | return cachedData;
73 | }
74 |
75 | const url = `https://api.graphql.imdb.com/`;
76 | const query = {
77 | query: `
78 | query {
79 | name(id: "${imdbID}") {
80 | akas(first: 5) {
81 | total
82 | edges {
83 | node {
84 | text
85 | }
86 | }
87 | }
88 | bio {
89 | text {
90 | plainText
91 | }
92 | }
93 | birthDate {
94 | date
95 | }
96 | deathDate {
97 | date
98 | }
99 | prestigiousAwardSummary {
100 | wins
101 | nominations
102 | }
103 | id
104 | nameText {
105 | text
106 | }
107 | primaryImage {
108 | url
109 | }
110 | images(first: 30) {
111 | edges {
112 | node {
113 | url
114 | }
115 | }
116 | }
117 | }
118 | }
119 | `
120 | };
121 |
122 | return new Promise((resolve, reject) => {
123 | GM_xmlhttpRequest({
124 | method: "POST",
125 | url: url,
126 | headers: {
127 | "Content-Type": "application/json"
128 | },
129 | data: JSON.stringify(query),
130 | onload: async function (response) {
131 | if (response.status >= 200 && response.status < 300) {
132 | try {
133 | const data = JSON.parse(response.responseText);
134 | //console.log("Live data received:", data);
135 |
136 | // Cache the data with expiration
137 | setCache(cacheKey, data.data);
138 | resolve(data.data); // Resolve with the fetched data
139 | } catch (error) {
140 | console.error("Error parsing IMDb response:", error); // Log parsing error
141 | reject(new Error("Failed to parse IMDb response"));
142 | }
143 | } else {
144 | console.error("Failed to fetch details, status:", response.status); // Log failure
145 | reject(new Error(`Failed to fetch details, status: ${response.status}`));
146 | }
147 | },
148 | onerror: function (response) {
149 | console.error("Request error", response); // Log request error
150 | reject(new Error("Request error"));
151 | }
152 | });
153 | });
154 | };
155 |
156 | const addDetailsToPanel = (details) => {
157 | const artistInfoPanel = document.querySelector('#artistinfo');
158 | if (artistInfoPanel) {
159 | const panelBody = artistInfoPanel.querySelector('.panel__body ul');
160 | if (!panelBody) return;
161 |
162 | const existingText = panelBody.innerText.toLowerCase();
163 | const bioText = details.bio && details.bio.text.plainText ? details.bio.text.plainText : null;
164 | const birthDate = details.birthDate ? formatDate(details.birthDate.date) : null;
165 | const deathDate = details.deathDate ? formatDate(details.deathDate.date) : null;
166 | const age = details.birthDate ? calculateAge(details.birthDate.date) : null;
167 | const awards = details.prestigiousAwardSummary ?
168 | `Wins: ${details.prestigiousAwardSummary.wins || "N/A"}, Nominations: ${details.prestigiousAwardSummary.nominations || "N/A"}`
169 | : null;
170 | const akas = details.akas && details.akas.edges.length > 0 ? details.akas.edges.map(edge => edge.node.text).join(', ') : null;
171 |
172 | if (birthDate && !existingText.includes('born:')) {
173 | const birthItem = document.createElement('li');
174 | birthItem.innerHTML = `Born: ${birthDate} (age: ${age || 'N/A'})`;
175 | panelBody.appendChild(birthItem);
176 | }
177 |
178 | if (deathDate && !existingText.includes('died:')) {
179 | const deathItem = document.createElement('li');
180 | deathItem.innerHTML = `Died: ${deathDate}`;
181 | panelBody.appendChild(deathItem);
182 | }
183 |
184 | if (akas && !existingText.includes('akas:')) {
185 | const akasItem = document.createElement('li');
186 | akasItem.innerHTML = `AKAs: ${akas}`;
187 | panelBody.appendChild(akasItem);
188 | }
189 |
190 | if (awards && !existingText.includes('oscars:')) {
191 | const awardsItem = document.createElement('li');
192 | awardsItem.innerHTML = `Oscars: ${awards}`;
193 | panelBody.appendChild(awardsItem);
194 | }
195 |
196 | if (bioText && !existingText.includes('bio:')) {
197 | const bioItem = document.createElement('li');
198 | bioItem.innerHTML = `Bio: ${bioText.substring(0, 100)}`;
199 | panelBody.appendChild(bioItem);
200 | }
201 | }
202 | };
203 |
204 | const addImagePanel = (nameData) => {
205 | const primaryImage = nameData.primaryImage ? nameData.primaryImage.url : null;
206 | const images = nameData.images && nameData.images.edges.length > 0 ? nameData.images.edges.map(edge => edge.node.url) : [];
207 |
208 | if (!primaryImage && images.length === 0) {
209 | console.log("No images available for this artist.");
210 | return; // Exit if no images are available
211 | }
212 |
213 | if (primaryImage) {
214 | images.unshift(primaryImage); // Add primary image to the start of the array
215 | }
216 |
217 | const sidebar = document.querySelector('.sidebar');
218 | if (sidebar) {
219 | let existingPanel = sidebar.querySelector('.panel img.sidebar-cover-image');
220 | if (existingPanel) {
221 | images.unshift(existingPanel.src);
222 | } else {
223 | existingPanel = document.createElement('img');
224 | existingPanel.className = 'sidebar-cover-image';
225 | existingPanel.alt = nameData.nameText.text;
226 |
227 | const newPanel = document.createElement('div');
228 | newPanel.className = 'panel';
229 |
230 | const headingDiv = document.createElement('div');
231 | headingDiv.className = 'panel__heading';
232 |
233 | const titleSpan = document.createElement('span');
234 | titleSpan.className = 'panel__heading__title';
235 | titleSpan.innerText = nameData.nameText.text;
236 |
237 | const bodyDiv = document.createElement('div');
238 | bodyDiv.className = 'panel__body';
239 |
240 | bodyDiv.appendChild(existingPanel);
241 | headingDiv.appendChild(titleSpan);
242 | newPanel.appendChild(headingDiv);
243 | newPanel.appendChild(bodyDiv);
244 | sidebar.insertBefore(newPanel, sidebar.firstChild);
245 | }
246 |
247 | let currentIndex = 0;
248 | existingPanel.src = images[currentIndex];
249 | setInterval(() => {
250 | currentIndex = (currentIndex + 1) % images.length;
251 | existingPanel.src = images[currentIndex];
252 | }, 5000);
253 | }
254 | };
255 |
256 | const init = async () => {
257 | const artistInfoPanel = document.querySelector('#artistinfo');
258 | if (artistInfoPanel) {
259 | const imdbLink = artistInfoPanel.querySelector('a[href*="http://www.imdb.com/name/"]');
260 | if (imdbLink) {
261 | const imdbUrl = new URL(imdbLink.href);
262 | const imdbId = imdbUrl.pathname.split('/')[2];
263 |
264 | fetchDetails(imdbId)
265 | .then((data) => {
266 | //console.log("Data fetched from API:", data);
267 | if (data && data.name) {
268 | addDetailsToPanel(data.name); // Ensure details are correctly passed
269 | addImagePanel(data.name); // Ensure image panel is updated correctly
270 | } else {
271 | console.error("API returned incomplete or invalid data.");
272 | }
273 | })
274 | .catch(error => console.error('Failed to fetch details:', error));
275 | }
276 | }
277 | };
278 |
279 | init();
280 | })();
--------------------------------------------------------------------------------
/ptp-collage-add.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Add to collage from search
3 | // @version 1.3
4 | // @description Search for torrents matching a collection, filter and add, with caching
5 | // @namespace https://github.com/Audionut/add-trackers
6 | // @icon https://passthepopcorn.me/favicon.ico
7 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/ptp-collage-add.js
8 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/ptp-collage-add.js
9 | // @match https://passthepopcorn.me/torrents.php?action=*
10 | // @match https://passthepopcorn.me/torrents.php?page=*&action=*
11 | // @grant GM_setValue
12 | // @grant GM_getValue
13 | // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
14 | // ==/UserScript==
15 |
16 | (function() {
17 | 'use strict';
18 |
19 | const CACHE_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1000; // 1 month in milliseconds
20 |
21 | function getCollageCache(collageId) {
22 | const compressedData = GM_getValue(`collage_${collageId}`, null);
23 | if (!compressedData) return null;
24 |
25 | const decompressedData = LZString.decompress(compressedData);
26 | const { timestamp, data } = JSON.parse(decompressedData);
27 |
28 | if (Date.now() - timestamp > CACHE_EXPIRATION_MS) {
29 | // Cache expired, return null to indicate the cache should be refreshed
30 | return null;
31 | }
32 |
33 | return data;
34 | }
35 |
36 | function setCollageCache(collageId, data) {
37 | const cacheEntry = {
38 | timestamp: Date.now(),
39 | data
40 | };
41 | const compressedData = LZString.compress(JSON.stringify(cacheEntry));
42 | GM_setValue(`collage_${collageId}`, compressedData);
43 | }
44 |
45 | const CURRENTPAGE_CACHE_EXPIRATION = 10 * 60 * 1000; // 10 minutes in milliseconds
46 |
47 | function setCurrentPageCache(collageId) {
48 | const timestamp = Date.now(); // Current time in milliseconds
49 | GM_setValue('currentpage_collageid', collageId);
50 | GM_setValue('currentpage_timestamp', timestamp); // Set a unique timestamp for this key
51 | }
52 |
53 | function getCurrentPageCache() {
54 | const cachedCollageId = GM_getValue('currentpage_collageid');
55 | const cachedTimestamp = GM_getValue('currentpage_timestamp');
56 | const now = Date.now();
57 |
58 | // Check if the currentpage cache is still valid
59 | if (cachedCollageId && cachedTimestamp && (now - cachedTimestamp) < CURRENTPAGE_CACHE_EXPIRATION) {
60 | return cachedCollageId;
61 | } else {
62 | // Cache expired or not set; clear the currentpage cache and return null
63 | GM_deleteValue('currentpage_collageid');
64 | GM_deleteValue('currentpage_timestamp');
65 | return null;
66 | }
67 | }
68 |
69 | function showLoadingSpinner() {
70 | const spinner = document.createElement('div');
71 | spinner.classList.add('loading-container');
72 | spinner.innerHTML = `
73 |
74 | Fetching titles from collection...
75 | `;
76 | ul.innerHTML = ''; // Clear previous results
77 | ul.appendChild(spinner);
78 | }
79 |
80 | function hideLoadingSpinner() {
81 | const spinner = ul.querySelector('.loading-container');
82 | if (spinner) {
83 | spinner.remove();
84 | }
85 | }
86 |
87 | async function fetchAndCacheCollage(collageId) {
88 | showLoadingSpinner(); // Show the loading spinner when fetching starts
89 |
90 | const response = await fetch(`https://passthepopcorn.me/collages.php?id=${collageId}`);
91 | const html = await response.text();
92 | const parser = new DOMParser();
93 | const doc = parser.parseFromString(html, 'text/html');
94 |
95 | const collageData = {
96 | name: doc.querySelector('h2.page__title').textContent,
97 | links: Array.from(doc.querySelectorAll('#collection_movielist .list a[href*="torrents.php?id="]'))
98 | .map(link => new URL(link.href, 'https://passthepopcorn.me').origin + new URL(link.href).pathname + "?id=" + new URL(link.href).searchParams.get("id"))
99 | };
100 |
101 | setCollageCache(collageId, collageData); // Cache the fetched data
102 |
103 | hideLoadingSpinner(); // Hide the loading spinner after fetching completes
104 | return collageData;
105 | }
106 |
107 | async function fetchAndDisplayCollage(collageId) {
108 | // Check if we're on the match page
109 | const currentUrl = window.location.href;
110 | const isMatchPage = /https:\/\/passthepopcorn\.me\/torrents\.php\?action=/.test(currentUrl);
111 | const isRefreshedPage = /https:\/\/passthepopcorn\.me\/torrents\.php\?page=.*&action=/.test(currentUrl);
112 |
113 | // Set a new cache key if we're on the match page
114 | if (isMatchPage) {
115 | setCurrentPageCache(collageId);
116 | }
117 |
118 | // Proceed only if we have a collageId
119 | if (!collageId) {
120 | console.warn('No collage ID found for the current page.');
121 | return;
122 | }
123 |
124 | // Try to retrieve the collage data from the cache first
125 | let collageData = getCollageCache(collageId);
126 | if (!collageData) {
127 | // Fetch and cache if data is not in cache or is expired
128 | collageData = await fetchAndCacheCollage(collageId);
129 | }
130 |
131 | const collageName = collageData.name;
132 |
133 | // Display collage name with target="_blank"
134 | panelHeaderTitle.innerHTML = `Releases not in selected collection: ${collageName}`;
135 |
136 | const antiCsrfToken = document.body.getAttribute('data-anticsrftoken');
137 |
138 | // Ensure links are loaded before filtering
139 | if (!links || links.length === 0) {
140 | console.warn('No links found on the page to filter.');
141 | return;
142 | }
143 |
144 | const uniqueUrls = new Set();
145 | const filteredLinks = links.filter(link => {
146 | const url = new URL(link.href);
147 | const baseLink = url.origin + url.pathname + "?id=" + url.searchParams.get("id");
148 |
149 | // Ensure link is not in collage data and is unique
150 | const isUnique = !uniqueUrls.has(baseLink);
151 | if (isUnique && !collageData.links.includes(baseLink)) {
152 | uniqueUrls.add(baseLink); // Track unique links
153 | return true;
154 | }
155 | return false;
156 | });
157 |
158 | ul.innerHTML = '';
159 | if (filteredLinks.length > 0) {
160 | filteredLinks.forEach(link => {
161 | const li = document.createElement('li');
162 | const a = document.createElement('a');
163 | a.href = link.href;
164 | a.textContent = `${link.title} ${link.year} by ${link.director}`;
165 | a.target = "_blank"; // Open in new tab
166 | a.style.display = 'inline-block';
167 | a.style.marginRight = '10px';
168 |
169 | const addButton = document.createElement('input');
170 | addButton.type = "submit";
171 | addButton.setAttribute('value', 'Add to collection');
172 | addButton.onclick = async function() {
173 | if (confirm(`Add ${link.title} to collage?`)) {
174 | try {
175 | const formData = new FormData();
176 | formData.append('AntiCsrfToken', antiCsrfToken);
177 | formData.append('action', 'add_torrent');
178 | formData.append('collageid', collageId);
179 | formData.append('url', link.href);
180 |
181 | await fetch('https://passthepopcorn.me/collages.php', {
182 | method: 'POST',
183 | body: formData
184 | });
185 |
186 | // Update the cache with the new link
187 | collageData.links.push(link.href);
188 | setCollageCache(collageId, collageData);
189 |
190 | alert(`${link.title} added to collage.`);
191 | } catch (error) {
192 | console.error('Failed to add torrent to collage:', error);
193 | alert('Error adding torrent to collage.');
194 | }
195 | }
196 | };
197 |
198 | li.appendChild(a);
199 | li.appendChild(addButton);
200 | ul.appendChild(li);
201 | });
202 | } else {
203 | const noResults = document.createElement('li');
204 | noResults.textContent = 'No links found after filtering.';
205 | ul.appendChild(noResults);
206 | }
207 | }
208 |
209 | // When retrieving the currentpage cache, use the unique expiration time
210 | window.addEventListener("load", () => {
211 | const currentUrl = window.location.href;
212 | const isPagedUrl = /https:\/\/passthepopcorn\.me\/torrents\.php\?page=.*&action=/.test(currentUrl);
213 |
214 | if (isPagedUrl) {
215 | const cachedCollageId = getCurrentPageCache();
216 |
217 | if (cachedCollageId) {
218 | fetchAndDisplayCollage(cachedCollageId);
219 | }
220 | }
221 | });
222 |
223 | // Add styles for the spinner
224 | const style = document.createElement('style');
225 | style.textContent = `
226 | .loading-container {
227 | display: flex;
228 | flex-direction: column;
229 | align-items: center;
230 | margin-top: 20px;
231 | font-size: 1em;
232 | color: #ffffff;
233 | }
234 | .spinner {
235 | width: 40px;
236 | height: 40px;
237 | border: 4px solid rgba(255, 255, 255, 0.3);
238 | border-radius: 50%;
239 | border-top-color: #ffffff;
240 | animation: spin 1s ease-in-out infinite;
241 | }
242 | @keyframes spin {
243 | to { transform: rotate(360deg); }
244 | }
245 | `;
246 | document.head.appendChild(style);
247 |
248 | // Original functionality for finding title rows
249 | const allTitleRows = document.querySelectorAll('.basic-movie-list__movie__title-row');
250 | const links = Array.from(allTitleRows).map(row => {
251 | const link = row.querySelector('a.basic-movie-list__movie__title');
252 | const yearElement = row.querySelector('.basic-movie-list__movie__year');
253 | const directorElement = row.querySelector('.basic-movie-list__movie__director-list');
254 |
255 | // Use optional chaining and default values to handle null values gracefully
256 | const href = link?.href || ''; // Default to an empty string if link or href is null
257 | const title = link?.textContent || ''; // Default title if textContent is null
258 | const year = yearElement?.textContent || ''; // Default year if year element is null
259 | const director = directorElement?.textContent || ''; // Default director if director element is null
260 |
261 | return { href, title, year, director };
262 | });
263 |
264 | const targetDiv = document.getElementById('torrents-movie-view');
265 | if (!targetDiv) {
266 | console.warn('Target div with id "torrents-movie-view" not found.');
267 | return;
268 | }
269 |
270 | const panel = document.createElement('div');
271 | panel.classList.add('panel');
272 | panel.id = 'collage_add';
273 |
274 | const panelHeader = document.createElement('div');
275 | panelHeader.classList.add('panel__heading');
276 |
277 | const panelHeaderTitle = document.createElement('span');
278 | panelHeaderTitle.classList.add('panel__heading__title');
279 | panelHeaderTitle.textContent = 'Filter shown torrents by a collection id';
280 |
281 | const inputBox = document.createElement('input');
282 | inputBox.type = 'text';
283 | inputBox.placeholder = 'Input Collage ID...';
284 | inputBox.style = 'float:right;margin-right:10px;font-size:0.9em';
285 |
286 | const filterButton = document.createElement('input');
287 | filterButton.type = "submit";
288 | filterButton.setAttribute('value', 'Filter torrents');
289 | filterButton.style = 'float:right;font-size:0.9em';
290 |
291 | const findCollectionsButton = document.createElement('input');
292 | findCollectionsButton.type = "submit";
293 | findCollectionsButton.setAttribute('value', 'Find all matching collections');
294 | findCollectionsButton.style = 'float:right;font-size:0.9em;margin-right:10px;';
295 |
296 | panelHeader.appendChild(findCollectionsButton);
297 | panelHeader.appendChild(inputBox);
298 | panelHeader.appendChild(filterButton);
299 | panelHeader.appendChild(panelHeaderTitle);
300 | panel.appendChild(panelHeader);
301 |
302 | const panelBody = document.createElement('div');
303 | panelBody.classList.add('panel__body');
304 | const ul = document.createElement('ul');
305 | ul.style.padding = '10px';
306 | panelBody.appendChild(ul);
307 | panel.appendChild(panelBody);
308 | targetDiv.parentNode.insertBefore(panel, targetDiv);
309 |
310 | filterButton.onclick = async function() {
311 | const collageId = inputBox.value.trim();
312 | if (collageId) {
313 | try {
314 | await fetchAndDisplayCollage(collageId);
315 | } catch (error) {
316 | console.error('Failed to load Collage ID content:', error);
317 | alert('Error loading Collage ID content.');
318 | }
319 | } else {
320 | alert('Please enter a Collage ID.');
321 | }
322 | };
323 |
324 | findCollectionsButton.onclick = async function() {
325 | const editionTitleInput = document.getElementById('edition_title');
326 | if (editionTitleInput && editionTitleInput.value.trim()) {
327 | const searchQuery = encodeURIComponent(editionTitleInput.value.trim());
328 | const searchUrl = `https://passthepopcorn.me/collages.php?action=search&search=${searchQuery}`;
329 | try {
330 | const response = await fetch(searchUrl);
331 | const html = await response.text();
332 | const parser = new DOMParser();
333 | const doc = parser.parseFromString(html, 'text/html');
334 |
335 | const matchingCollages = Array.from(doc.querySelectorAll('a[href^="collages.php?id="]'))
336 | .map(link => ({ id: link.href.match(/id=(\d+)/)[1], name: link.textContent }));
337 |
338 | ul.innerHTML = '';
339 | if (matchingCollages.length > 0) {
340 | matchingCollages.forEach(collage => {
341 | const li = document.createElement('li');
342 | const a = document.createElement('a');
343 | a.href = `https://passthepopcorn.me/collages.php?id=${collage.id}`;
344 | a.textContent = collage.name;
345 | a.target = "_blank"; // Open in new tab
346 | a.style.cursor = 'pointer';
347 | a.onclick = async function(event) {
348 | event.preventDefault();
349 | await fetchAndDisplayCollage(collage.id);
350 | };
351 |
352 | li.appendChild(a);
353 | ul.appendChild(li);
354 | });
355 | } else {
356 | const noResults = document.createElement('li');
357 | noResults.textContent = 'No matching collections found.';
358 | ul.appendChild(noResults);
359 | }
360 | } catch (error) {
361 | console.error('Failed to load matching collections:', error);
362 | alert('Error loading matching collections.');
363 | }
364 | } else {
365 | alert('Please ensure there is a value in the "Edition Title" field.');
366 | }
367 | };
368 |
369 | })();
--------------------------------------------------------------------------------
/ptp-cross-seed-checker.user,js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP Cross-Seed Checker
3 | // @version 0.0.7
4 | // @author Ignacio (additions by Audionut)
5 | // @description Find cross-seedable and Add cross-seed markers to non-ptp releases
6 | // downloadURL https://github.com/Audionut/add-trackers/raw/main/ptp-cross-seed-checker.user.js
7 | // updateURL https://github.com/Audionut/add-trackers/raw/main/ptp-cross-seed-checker.user.js
8 | // @match https://passthepopcorn.me/torrents.php*
9 | // @grant GM_setValue
10 | // @grant GM_getValue
11 | // @grant GM_registerMenuCommand
12 | // @run-at document-start
13 | // ==/UserScript==
14 |
15 | (function() {
16 | 'use strict';
17 |
18 | let dnum = GM_getValue('dnumlimit', 5); // Size tolerance
19 | let pnum = GM_getValue('pnumlimit', 10);
20 | let size = GM_getValue('rowhtsize', 8); // Empty row height
21 | let pcs = GM_getValue('pcscheckbox', true); // Optional partial cross-seedable releases recognition
22 |
23 | let defaultdnum = 5;
24 | let defaultpnum = 10;
25 | let defaultsize = 8;
26 | let defaultpcs = true;
27 |
28 | function settingsmenu() {
29 | const settingsDialog = document.createElement('div');
30 | settingsDialog.id = 'cs_settingsDialog';
31 | settingsDialog.innerHTML = `
32 |
33 |
34 |
Settings
35 |
36 |
37 |
55 |
56 |
57 |
58 |
59 |
60 | `;
61 |
62 | document.body.appendChild(settingsDialog);
63 |
64 | document.getElementById('cs_resetSettings').addEventListener('click', function() {
65 | document.getElementById('dnumLimit').value = dnum;
66 | document.getElementById('pnumLimit').value = pnum;
67 | document.getElementById('rowhtSize').value = size;
68 | document.getElementById('pcsCheckbox').checked = pcs;
69 |
70 | GM_setValue('dnumlimit', defaultdnum);
71 | GM_setValue('pnumlimit', defaultpnum);
72 | GM_setValue('rowhtsize', defaultsize);
73 | GM_setValue('pcscheckbox', defaultpcs);
74 |
75 | alert('Settings reset to defaults and saved successfully!');
76 |
77 | document.getElementById('cs_settingsDialog').style.display = 'none';
78 | });
79 |
80 | document.getElementById('cs_saveSettings').addEventListener('click', function() {
81 | const newDnumLimit = parseInt(document.getElementById('dnumLimit').value, 10);
82 | const newPnumLimit = parseInt(document.getElementById('pnumLimit').value, 10);
83 | const newRowhtSize = parseInt(document.getElementById('rowhtSize').value, 10);
84 | const newPcsCheckbox = document.getElementById('pcsCheckbox').checked;
85 |
86 | dnum = newDnumLimit;
87 | pnum = newPnumLimit;
88 | size = newRowhtSize;
89 | pcs = newPcsCheckbox;
90 |
91 | GM_setValue('dnumlimit', newDnumLimit);
92 | GM_setValue('pnumlimit', newPnumLimit);
93 | GM_setValue('rowhtsize', newRowhtSize);
94 | GM_setValue('pcscheckbox', newPcsCheckbox);
95 |
96 | alert('Settings saved successfully!');
97 |
98 | document.getElementById('cs_settingsDialog').style.display = 'none';
99 | });
100 |
101 | document.getElementById('cs_closeSettings').addEventListener('click', function() {
102 | settingsDialog.style.display = 'none';
103 | });
104 |
105 | }
106 |
107 | GM_registerMenuCommand('Open Settings', function() {
108 | let settingsDialog = document.getElementById('cs_settingsDialog');
109 | if (!settingsDialog) {
110 | settingsmenu();
111 | settingsDialog = document.getElementById('cs_settingsDialog');
112 | }
113 | settingsDialog.style.display = 'block';
114 | });
115 | function rowSorter(pcs = false, dnum, pnum) {
116 | return new Promise((resolve, reject) => {
117 | try {
118 | const allRows = document.querySelectorAll('.torrent_table tr.group_torrent.group_torrent_header');
119 | const regex = /ptp_\d+/;
120 | const { ptpRows, otherRows } = classifyRows(allRows, regex);
121 | const ptpRowsData = parsePtpRowsData(ptpRows);
122 | const otherRowsData = parseOtherRowsData(otherRows);
123 |
124 | matchRows(ptpRows, ptpRowsData, otherRowsData, pcs, dnum, pnum);
125 |
126 | resolve("Rows sorted successfully");
127 | } catch (error) {
128 | reject("Error in row sorter: " + error);
129 | }
130 | });
131 | }
132 |
133 |
134 | function classifyRows(rows, regex) {
135 | const ptpRows = [];
136 | const otherRows = [];
137 |
138 | rows.forEach(row => {
139 | if (Array.from(row.classList).some(className => regex.test(className))) {
140 | ptpRows.push(row);
141 | } else {
142 | otherRows.push(row);
143 | }
144 | });
145 |
146 | return { ptpRows, otherRows };
147 | }
148 |
149 | function parsePtpRowsData(ptpRows) {
150 | return Array.from(ptpRows).map(row => {
151 | const group = row.getAttribute('data-releasegroup') || '';
152 | const NameEl = row.querySelector('.torrent-info-link') || '';
153 | const rawName = NameEl ? NameEl.textContent : '';
154 | const sizeEl = row.querySelector('.nobr span[title]');
155 | const rawSize = sizeEl ? sizeEl.getAttribute('title') : '';
156 | const size = rawSize ? parseInt(rawSize.replace(/[^0-9]/g, '')) : 0;
157 | const { pl, dl } = actionElsparser(row);
158 | return { group, size, pl, dl, rawName };
159 | });
160 | }
161 |
162 | function actionElsparser(row) {
163 | const actionSpan = row.querySelector('.basic-movie-list__torrent__action');
164 | const plEl = actionSpan ? actionSpan.querySelector('a[title="Permalink"]') : null;
165 | const pl = plEl ? plEl.getAttribute('href') : '';
166 | const dlEl = actionSpan ? actionSpan.querySelector('a[title="Download"]') : null;
167 | const dl = dlEl ? dlEl.getAttribute('href') : '';
168 | return { pl, dl };
169 | }
170 |
171 | function parseOtherRowsData(otherRows) {
172 | return Array.from(otherRows).map(row => {
173 | const rawGroup = row.getAttribute('data-releasegroup') || '';
174 | const NameEl = row.querySelector('.torrent-info-link') || '';
175 | const rawName = NameEl ? NameEl.textContent : '';
176 | const sizeEl = row.querySelector('.nobr .size-span');
177 | const rawSize = sizeEl ? sizeEl.getAttribute('title') : '';
178 | const size = rawSize ? parseInt(rawSize.replace(/[^0-9]/g, '')) : 0;
179 | const classList = Array.from(row.classList);
180 | const classid = classList.length > 0 ? classList[classList.length - 1] : '';
181 | return { group: rawGroup, size, classid, rawName };
182 | });
183 | }
184 |
185 | function matchRows(ptpRows, ptpRowsData, otherRowsData, enablePCS, dnum, pnum) {
186 | otherRowsData.forEach(otherRow => {
187 | const matchingPTPRow = TCSfinder(ptpRowsData, otherRow, dnum);
188 |
189 | if (matchingPTPRow) {
190 | const tcsLink = createLink('TCS', matchingPTPRow.pl, 'Total Cross-Seed compatibility - Identical Release', 'greenyellow');
191 | const infoLink = createInfoLink();
192 |
193 | actionSpnupdater(otherRow, tcsLink, infoLink);
194 | infoLinklistener(infoLink, ptpRows, matchingPTPRow);
195 | } else if (enablePCS && otherRow.size) {
196 | const matchingPTPRowPCS = PCSfinder(ptpRowsData, otherRow, pnum);
197 |
198 | if (matchingPTPRowPCS) {
199 | const pcsLink = createLink('PCS', matchingPTPRowPCS.pl, 'Partial Cross-Seed compatibility - Re-verify File/Folder Structure', 'orange');
200 | const infoLinkPCS = createInfoLink();
201 |
202 | actionSpnupdater(otherRow, pcsLink, infoLinkPCS);
203 | infoLinklistener(infoLinkPCS, ptpRows, matchingPTPRowPCS);
204 | }
205 | }
206 | });
207 | }
208 |
209 | function TCSfinder(ptpRowsData, otherRow, num) {
210 | const toleranceBytes = num * 1024 * 1024;
211 | return ptpRowsData.find(ptpRow =>
212 | ptpRow.group.toLowerCase() === otherRow.group.toLowerCase() &&
213 | Math.abs(ptpRow.size - otherRow.size) <= toleranceBytes
214 | );
215 | }
216 |
217 | function PCSfinder(ptpRowsData, otherRow, pnum) {
218 | const toleranceBytes = pnum * 1024 * 1024; // Convert MiB to bytes
219 |
220 | if (otherRow.rawName.includes("Audio Only Track")) {
221 | return null;
222 | }
223 |
224 | // Find a match with the same "DV" status
225 | const otherRowHasDV = otherRow.rawName.includes("DV");
226 |
227 | // Find a match with the same "HDR" status
228 | const otherRowHasHDR = /HDR/i.test(otherRow.rawName);
229 |
230 | return ptpRowsData.find(ptpRow => {
231 | const ptpRowHasDV = ptpRow.rawName.includes("DV");
232 | const ptpRowHasHDR = /HDR/i.test(ptpRow.rawName);
233 |
234 | return (
235 | (otherRow.group.length === 0 || ptpRow.group.toLowerCase() === otherRow.group.toLowerCase()) &&
236 | Math.abs(ptpRow.size - otherRow.size) <= toleranceBytes && // Size within PCS tolerance
237 | !(ptpRow.group.toLowerCase() === otherRow.group.toLowerCase() && Math.abs(ptpRow.size - otherRow.size) <= (dnum * 1024 * 1024)) && // Ensure not a TCS match
238 | otherRowHasDV === ptpRowHasDV && // Ensure DV matches
239 | otherRowHasHDR === ptpRowHasHDR // Ensure HDR matches
240 | );
241 | });
242 | }
243 | function createLink(text, href, title, color) {
244 | const link = document.createElement('a');
245 | link.href = href;
246 | link.className = 'link_2';
247 | link.title = title;
248 | link.textContent = text;
249 | link.style.color = color;
250 | link.style.textShadow = '0 0 5px ' + color;
251 | return link;
252 | }
253 |
254 | function createInfoLink() {
255 | const infoLink = document.createElement('a');
256 | infoLink.href = '#';
257 | infoLink.className = 'torrent-info-link.link4';
258 | infoLink.textContent = 'INFO';
259 | infoLink.style.color = '#ff1493';
260 | infoLink.style.textShadow = '0 0 5px #ff1493';
261 | return infoLink;
262 | }
263 |
264 | function actionSpnupdater(otherRow, mainLink, infoLink) {
265 | const otherRowElement = document.querySelector(`.${otherRow.classid}`);
266 | const existingActionSpan = otherRowElement ? otherRowElement.querySelector('.basic-movie-list__torrent__action') : null;
267 |
268 | if (existingActionSpan) {
269 | // Check if the mainLink already exists
270 | const existingLinks = Array.from(existingActionSpan.querySelectorAll('a'));
271 | const mainLinkExists = existingLinks.some(link => link.href === mainLink.href);
272 |
273 | if (!mainLinkExists) {
274 | const existingDownloadLink = existingActionSpan.querySelector('a[title="Download"]');
275 | if (existingDownloadLink) {
276 | existingActionSpan.removeChild(existingActionSpan.lastChild);
277 | existingActionSpan.appendChild(document.createTextNode('| '));
278 | existingActionSpan.appendChild(mainLink);
279 | existingActionSpan.appendChild(document.createTextNode(' | '));
280 | existingActionSpan.appendChild(infoLink);
281 | existingActionSpan.appendChild(document.createTextNode(' ]'));
282 | }
283 | }
284 | }
285 | }
286 |
287 | function infoLinklistener(infoLink, ptpRows) {
288 | infoLink.addEventListener('click', function(event) {
289 | event.preventDefault();
290 | const linkHref = this.parentNode.querySelector('a.link_2').href;
291 | const ptpRow = ptpRows.find(row => {
292 | const plAnc = row.querySelector('a[title="Permalink"]');
293 | return plAnc && plAnc.href === linkHref;
294 | });
295 | if (ptpRow) {
296 | const infoAnc = ptpRow.querySelector('.torrent-info-link');
297 | if (infoAnc) {
298 | infoAnc.click();
299 | this.focus();
300 | }
301 | }
302 | });
303 | }
304 |
305 | function rearranger(size) {
306 | let spx = `${size}px`;
307 |
308 | return new Promise((resolve, reject) => {
309 | try {
310 | function getRowsWithTextContent(content) {
311 | const allRows = document.querySelectorAll('.torrent_table tr.group_torrent.group_torrent_header');
312 | const filteredRows = Array.from(allRows).filter(row => {
313 | const link = row.querySelector('a.link_2');
314 | return link && (link.textContent.toLowerCase() === content.toLowerCase());
315 | });
316 | return filteredRows;
317 | }
318 |
319 | function getRowsWithPLAnchors() {
320 | const allRows = document.querySelectorAll('.torrent_table tr.group_torrent.group_torrent_header');
321 | const filteredRows = Array.from(allRows).filter(row => {
322 | const plAnchor = row.querySelector('a[title="Permalink"]');
323 | return plAnchor !== null;
324 | });
325 | return filteredRows;
326 | }
327 |
328 | function getRowsWithPCS() {
329 | const allRows = document.querySelectorAll('.torrent_table tr.group_torrent.group_torrent_header');
330 | const filteredRows = Array.from(allRows).filter(row => {
331 | const link = row.querySelector('a.link_2');
332 | return link && (link.textContent.toLowerCase() === 'pcs');
333 | });
334 | return filteredRows;
335 | }
336 |
337 | const rowsWithTCS = getRowsWithTextContent('tcs');
338 | const rowsWithPCS = getRowsWithPCS();
339 | const rowsWithPLAnchors = getRowsWithPLAnchors();
340 |
341 | rowsWithPLAnchors.forEach(plRow => {
342 | const plAnchor = plRow.querySelector('a[title="Permalink"]');
343 | if (plAnchor) {
344 | const plHref = plAnchor.getAttribute('href');
345 | // console.log("PL anchor href:", plHref);
346 | const matchingTCSRows = rowsWithTCS.filter(tcsRow => {
347 | const tcsAnchor = tcsRow.querySelector('a.link_2');
348 | return tcsAnchor && tcsAnchor.getAttribute('href') === plHref;
349 | });
350 | const matchingPCSRows = rowsWithPCS.filter(pcsRow => {
351 | const pcsAnchor = pcsRow.querySelector('a.link_2');
352 | return pcsAnchor && pcsAnchor.getAttribute('href') === plHref;
353 | });
354 | // console.log("Matching TCS rows:", matchingTCSRows);
355 | // console.log("Matching PCS rows:", matchingPCSRows);
356 | const combinedRows = [...matchingPCSRows, ...matchingTCSRows];
357 |
358 | combinedRows.forEach((combinedRow, index) => {
359 | let emptyRowAbovePl = plRow.previousElementSibling;
360 | if (!emptyRowAbovePl || !emptyRowAbovePl.classList.contains('empty-row')) {
361 | emptyRowAbovePl = document.createElement('tr');
362 | emptyRowAbovePl.className = 'empty-row';
363 | emptyRowAbovePl.style.height = spx;
364 | plRow.parentNode.insertBefore(emptyRowAbovePl, plRow);
365 | }
366 |
367 | let siblingRow = plRow.nextElementSibling;
368 | while (siblingRow && !siblingRow.classList.contains('torrent_info_row')) {
369 | siblingRow = siblingRow.nextElementSibling;
370 | }
371 |
372 | if (siblingRow) {
373 | plRow.parentNode.insertBefore(combinedRow, siblingRow.nextSibling);
374 | } else {
375 | plRow.parentNode.insertBefore(combinedRow, plRow.nextSibling);
376 | }
377 |
378 | if (index === 0) {
379 | const emptyRowAfterFirstCombined = document.createElement('tr');
380 | emptyRowAfterFirstCombined.className = 'empty-row';
381 | emptyRowAfterFirstCombined.style.height = spx;
382 | plRow.parentNode.insertBefore(emptyRowAfterFirstCombined, combinedRow.nextSibling);
383 | }
384 | });
385 | }
386 | });
387 | resolve();
388 | } catch (error) {
389 | reject("Error in rearranger: " + error);
390 | }
391 | });
392 | }
393 |
394 | function cleaner() {
395 | return new Promise((resolve, reject) => {
396 | try {
397 | const allRows = document.querySelectorAll('.torrent_table tr');
398 |
399 | const featureFilmRows = Array.from(allRows).filter(row => {
400 | const spanElements = row.querySelectorAll('span');
401 | return Array.from(spanElements).some(span =>
402 | span.textContent.includes('Feature Film') || span.textContent.includes('Miniseries') || span.textContent.includes('Short Film') || span.textContent.includes('Stand-up Comedy') || span.textContent.includes('Live Performance') || span.textContent.includes('Movie Collection')
403 | );
404 | });
405 |
406 | featureFilmRows.forEach(featureFilmRow => {
407 | let prevSibling = featureFilmRow.previousElementSibling;
408 | while (prevSibling && prevSibling.classList.contains('empty-row')) {
409 | featureFilmRow.parentNode.removeChild(prevSibling);
410 | prevSibling = featureFilmRow.previousElementSibling;
411 | }
412 |
413 | let nxtSibling = featureFilmRow.nextElementSibling;
414 | while (nxtSibling && nxtSibling.classList.contains('empty-row')) {
415 | featureFilmRow.parentNode.removeChild(nxtSibling);
416 | nxtSibling = featureFilmRow.nextElementSibling;
417 | }
418 | });
419 |
420 | const updatedAllRows = document.querySelectorAll('.torrent_table tr');
421 | let previousRowWasEmpty = false;
422 | Array.from(updatedAllRows).forEach(row => {
423 | if (row.classList.contains('empty-row')) {
424 | if (previousRowWasEmpty) {
425 | row.parentNode.removeChild(row);
426 | } else {
427 | previousRowWasEmpty = true;
428 | }
429 | } else {
430 | previousRowWasEmpty = false;
431 | }
432 | });
433 | resolve();
434 | } catch (error) {
435 | reject("Error in cleaner: " + error);
436 | }
437 | });
438 | }
439 |
440 | function recleaner() {
441 | return new Promise((resolve, reject) => {
442 | try {
443 | const allRows = document.querySelectorAll('.torrent_table tr');
444 | const lastRow = allRows[allRows.length - 1];
445 |
446 | if (lastRow && lastRow.classList.contains('empty-row')) {
447 | lastRow.remove();
448 | resolve();
449 | } else {
450 | resolve();
451 | }
452 | } catch (error) {
453 | reject("Error in recleaner: " + error);
454 | }
455 | });
456 | }
457 |
458 |
459 | document.addEventListener('PTPAddReleasesFromOtherTrackersComplete', function(event) {
460 | rowSorter(pcs, dnum, pnum)
461 | .then(() => {
462 | return rearranger(size);
463 | })
464 | .then(() => {
465 | return cleaner();
466 | })
467 | .then(() => {
468 | return recleaner();
469 | })
470 | .catch(error => {
471 | console.error('An error occurred:', error);
472 | });
473 | });
474 | document.addEventListener('SortingComplete', function(event) {
475 | rowSorter(pcs, dnum, pnum)
476 | .then(() => {
477 | return rearranger(size);
478 | })
479 | .then(() => {
480 | return cleaner();
481 | })
482 | .then(() => {
483 | return recleaner();
484 | })
485 | .catch(error => {
486 | console.error('An error occurred:', error);
487 | });
488 | });
489 |
490 | })();
491 |
--------------------------------------------------------------------------------
/ptp-get-tvdb-from-sonarr.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Get TVDB ID from IMDb ID using Sonarr API
3 | // @version 1.3
4 | // @description Fetch TVDB ID using IMDb ID on PTP torrent pages and dispatch an event with the result using Sonarr API.
5 | // @match https://passthepopcorn.me/torrents.php?*id=*
6 | // @namespace https://github.com/Audionut/add-trackers
7 | // @grant GM.xmlHttpRequest
8 | // @grant GM.registerMenuCommand
9 | // @grant GM.setValue
10 | // @grant GM.getValue
11 | // ==/UserScript==
12 |
13 | 'use strict';
14 |
15 | // Function to prompt for Sonarr API key and URL
16 | function promptForSonarrConfig() {
17 | const apiKey = prompt("Please enter your Sonarr API key:", "");
18 | const apiUrl = prompt("Please enter your Sonarr URL:", "http://localhost:8989");
19 | if (apiKey && apiUrl) {
20 | GM.setValue('sonarr_api_key', apiKey);
21 | GM.setValue('sonarr_api_url', apiUrl);
22 | }
23 | return { apiKey, apiUrl };
24 | }
25 |
26 | // Function to get Sonarr config from GM storage or prompt for it
27 | async function getSonarrConfig() {
28 | let apiKey = await GM.getValue('sonarr_api_key');
29 | let apiUrl = await GM.getValue('sonarr_api_url');
30 | if (!apiKey || !apiUrl) {
31 | const config = promptForSonarrConfig();
32 | apiKey = config.apiKey;
33 | apiUrl = config.apiUrl;
34 | }
35 | return { apiKey, apiUrl };
36 | }
37 |
38 | // Function to store TVDB ID and dispatch event
39 | function storeTvdbIdAndDispatchEvent(ptpId, tvdbId) {
40 | GM.setValue(`tvdb_id_${ptpId}`, tvdbId);
41 | const event = new CustomEvent('tvdbIdFetched', { detail: { ptpId, tvdbId } });
42 | document.dispatchEvent(event);
43 | }
44 |
45 | // Function to handle configuration errors
46 | function handleConfigError(message) {
47 | console.error(message);
48 | const event = new CustomEvent('tvdbIdFetchError', { detail: { message } });
49 | document.dispatchEvent(event);
50 | }
51 |
52 | // Function to fetch TVDB ID using Sonarr API
53 | function fetchTvdbIdFromSonarr(apiKey, apiUrl, imdbId, ptpId) {
54 | const url = `${apiUrl}/api/v3/series/lookup?term=imdb:${imdbId}`;
55 |
56 | GM.xmlHttpRequest({
57 | method: "GET",
58 | url: url,
59 | headers: {
60 | "X-Api-Key": apiKey
61 | },
62 | onload: function(response) {
63 | const data = JSON.parse(response.responseText);
64 | if (data && data.length > 0 && data[0].tvdbId) {
65 | const tvdbId = data[0].tvdbId;
66 | storeTvdbIdAndDispatchEvent(ptpId, tvdbId);
67 | } else {
68 | const event = new CustomEvent('tvdbIdFetchError', { detail: { message: "TVDB ID not found in Sonarr response." } });
69 | document.dispatchEvent(event);
70 | }
71 | },
72 | onerror: function() {
73 | const event = new CustomEvent('tvdbIdFetchError', { detail: { message: "Failed to fetch TVDB ID from Sonarr API." } });
74 | document.dispatchEvent(event);
75 | }
76 | });
77 | }
78 |
79 | // Add menu command to set Sonarr API key and URL
80 | GM.registerMenuCommand("Set Sonarr API Key and URL", promptForSonarrConfig);
81 |
82 | // Initialize script
83 | (async function init() {
84 | const ptpId = new URL(window.location.href).searchParams.get("id");
85 |
86 | const imdbLinkElement = document.getElementById("imdb-title-link");
87 | if (!imdbLinkElement) {
88 | return;
89 | }
90 |
91 | const imdbId = imdbLinkElement.href.match(/title\/(tt\d+)\//)[1];
92 | const { apiKey, apiUrl } = await getSonarrConfig();
93 |
94 | if (apiKey && apiUrl) {
95 | fetchTvdbIdFromSonarr(apiKey, apiUrl, imdbId, ptpId);
96 | } else {
97 | handleConfigError("Sonarr API key and URL are not configured.");
98 | }
99 | })();
--------------------------------------------------------------------------------
/ptp-get-tvdb-from-wikidata.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Get TVDB ID from IMDb ID using Wikidata
3 | // @version 1.0
4 | // @description Fetch TVDB ID using IMDb ID on PTP torrent pages and dispatch an event with the result using Wikidata.
5 | // @match https://passthepopcorn.me/torrents.php?*id=*
6 | // @namespace https://github.com/Audionut
7 | // @grant GM.xmlHttpRequest
8 | // @grant GM.registerMenuCommand
9 | // @grant GM.setValue
10 | // @grant GM.getValue
11 | // ==/UserScript==
12 |
13 | 'use strict';
14 |
15 | // Function to store TVDB ID and dispatch event
16 | function storeTvdbIdAndDispatchEvent(ptpId, tvdbId) {
17 | GM.setValue(`tvdb_id_${ptpId}`, tvdbId);
18 | const event = new CustomEvent('tvdbIdFetched', { detail: { ptpId, tvdbId } });
19 | document.dispatchEvent(event);
20 | }
21 |
22 | // Function to handle configuration errors
23 | function handleConfigError(message) {
24 | console.error(message);
25 | const event = new CustomEvent('tvdbIdFetchError', { detail: { message } });
26 | document.dispatchEvent(event);
27 | }
28 |
29 | // Function to fetch TVDB ID using Wikidata
30 | function fetchTvdbIdFromWikidata(imdbId, ptpId) {
31 | const query = `
32 | SELECT ?item ?itemLabel ?tvdbID WHERE {
33 | ?item wdt:P345 "${imdbId}" .
34 | ?item wdt:P4835 ?tvdbID .
35 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
36 | }
37 | `;
38 | const url = `https://query.wikidata.org/sparql?query=${encodeURIComponent(query)}&format=json`;
39 |
40 | GM.xmlHttpRequest({
41 | method: "GET",
42 | url: url,
43 | onload: function(response) {
44 | const data = JSON.parse(response.responseText);
45 | if (data && data.results && data.results.bindings.length > 0) {
46 | const tvdbId = data.results.bindings[0].tvdbID.value;
47 | storeTvdbIdAndDispatchEvent(ptpId, tvdbId);
48 | } else {
49 | const event = new CustomEvent('tvdbIdFetchError', { detail: { message: "TVDB ID not found in Wikidata response." } });
50 | document.dispatchEvent(event);
51 | }
52 | },
53 | onerror: function() {
54 | const event = new CustomEvent('tvdbIdFetchError', { detail: { message: "Failed to fetch TVDB ID from Wikidata." } });
55 | document.dispatchEvent(event);
56 | }
57 | });
58 | }
59 |
60 | // Initialize script
61 | (function init() {
62 | const ptpId = new URL(window.location.href).searchParams.get("id");
63 |
64 | const imdbLinkElement = document.getElementById("imdb-title-link");
65 | if (!imdbLinkElement) {
66 | return;
67 | }
68 |
69 | const imdbId = imdbLinkElement.href.match(/title\/(tt\d+)\//)[1];
70 | if (imdbId) {
71 | fetchTvdbIdFromWikidata(imdbId, ptpId);
72 | } else {
73 | handleConfigError("IMDb ID not found.");
74 | }
75 | })();
--------------------------------------------------------------------------------
/ptp-get-tvdb-id.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Get TVDB ID from IMDb ID
3 | // @version 1.1
4 | // @description Fetch TVDB ID using IMDb ID on PTP torrent pages and dispatch an event with the result.
5 | // @match https://passthepopcorn.me/torrents.php?*id=*
6 | // @namespace https://github.com/Audionut/add-trackers
7 | // @grant GM.xmlHttpRequest
8 | // @grant GM_registerMenuCommand
9 | // ==/UserScript==
10 |
11 | 'use strict';
12 |
13 | // Function to prompt for API key
14 | function promptForApiKey() {
15 | const apiKey = prompt("Please enter your TVDB API key:", "");
16 | if (apiKey) {
17 | localStorage.setItem('tvdb_api_key', apiKey);
18 | }
19 | return apiKey;
20 | }
21 |
22 | // Function to get the API key from localStorage or prompt for it
23 | function getApiKey() {
24 | let apiKey = localStorage.getItem('tvdb_api_key');
25 | if (!apiKey) {
26 | apiKey = promptForApiKey();
27 | }
28 | return apiKey;
29 | }
30 |
31 | // TVDB API configuration
32 | const TVDB_LOGIN_URL = 'https://api.thetvdb.com/login';
33 | const TVDB_SEARCH_URL = 'https://api.thetvdb.com/search/series?imdbId=';
34 |
35 | // Function to get JWT token from TVDB
36 | function getTvdbToken(apiKey, callback) {
37 | const loginData = {
38 | apikey: apiKey,
39 | };
40 |
41 | GM.xmlHttpRequest({
42 | method: "POST",
43 | url: TVDB_LOGIN_URL,
44 | data: JSON.stringify(loginData),
45 | headers: {
46 | "Content-Type": "application/json"
47 | },
48 | onload: function(response) {
49 | const data = JSON.parse(response.responseText);
50 | if (data && data.token) {
51 | callback(data.token);
52 | } else {
53 | console.error("Failed to retrieve TVDB token.");
54 | }
55 | },
56 | onerror: function() {
57 | console.error("Failed to login to TVDB.");
58 | }
59 | });
60 | }
61 |
62 | // Function to store TVDB ID and dispatch event
63 | function storeTvdbIdAndDispatchEvent(ptpId, tvdbId) {
64 | localStorage.setItem(`tvdb_id_${ptpId}`, tvdbId);
65 | const event = new CustomEvent('tvdbIdFetched', { detail: { ptpId, tvdbId } });
66 | document.dispatchEvent(event);
67 | }
68 |
69 | // Function to fetch TVDB ID using IMDb ID
70 | function fetchTvdbId(token, imdbId, ptpId) {
71 | const url = `${TVDB_SEARCH_URL}${imdbId}`;
72 |
73 | GM.xmlHttpRequest({
74 | method: "GET",
75 | url: url,
76 | headers: {
77 | "Authorization": `Bearer ${token}`
78 | },
79 | onload: function(response) {
80 | const data = JSON.parse(response.responseText);
81 | if (data && data.data && data.data.length > 0 && data.data[0].id) {
82 | const tvdbId = data.data[0].id;
83 | storeTvdbIdAndDispatchEvent(ptpId, tvdbId);
84 | } else {
85 | console.error("TVDB ID not found in response.");
86 | }
87 | },
88 | onerror: function() {
89 | console.error("Failed to fetch TVDB ID from TVDB API.");
90 | }
91 | });
92 | }
93 |
94 | // Add menu command to set API key
95 | GM_registerMenuCommand("Set TVDB API Key", promptForApiKey);
96 |
97 | // Initialize script
98 | (function init() {
99 | const ptpId = new URL(window.location.href).searchParams.get("id");
100 |
101 | const imdbLinkElement = document.getElementById("imdb-title-link");
102 | if (!imdbLinkElement) {
103 | console.warn("No IMDb ID found, aborting.");
104 | return;
105 | }
106 |
107 | const imdbId = imdbLinkElement.href.match(/title\/(tt\d+)\//)[1];
108 | const apiKey = getApiKey();
109 |
110 | if (apiKey) {
111 | getTvdbToken(apiKey, function(token) {
112 | fetchTvdbId(token, imdbId, ptpId);
113 | });
114 | }
115 | })();
--------------------------------------------------------------------------------
/ptp-get-tvmaze-id.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Get TVmaze ID from IMDb ID
3 | // @version 1.1
4 | // @description Fetch TVmaze ID using IMDb ID on PTP torrent pages and dispatch an event with the result.
5 | // @match https://passthepopcorn.me/torrents.php?*id=*
6 | // @namespace https://github.com/Audionut/add-trackers
7 | // @grant GM.xmlHttpRequest
8 | // ==/UserScript==
9 |
10 | 'use strict';
11 |
12 | // Get PTP ID
13 | const ptpId = new URL(window.location.href).searchParams.get("id");
14 |
15 | // Get IMDb URL
16 | const imdbLinkElement = document.getElementById("imdb-title-link");
17 | if (!imdbLinkElement) {
18 | console.warn("No IMDb ID found, aborting.");
19 | return;
20 | }
21 | const imdbId = imdbLinkElement.href.match(/title\/(tt\d+)\//)[1];
22 | const tvmazeUrl = `https://api.tvmaze.com/lookup/shows?imdb=${imdbId}`;
23 |
24 | // Function to dispatch event with TVmaze ID
25 | function dispatchTvmazeEvent(tvmazeId) {
26 | const event = new CustomEvent('tvmazeIdFetched', { detail: { ptpId, tvmazeId } });
27 | document.dispatchEvent(event);
28 | }
29 |
30 | // Parse TVmaze response
31 | function parseTvmazeResponse(response) {
32 | const data = JSON.parse(response.responseText);
33 | if (data && data.id) {
34 | waitForMainScript(() => {
35 | dispatchTvmazeEvent(data.id);
36 | });
37 | } else {
38 | console.error("TVmaze ID not found in response.");
39 | }
40 | }
41 |
42 | // Fetch TV show information from TVmaze
43 | function fetchTvmazeData() {
44 | GM.xmlHttpRequest({
45 | method: "GET",
46 | url: tvmazeUrl,
47 | timeout: 10000,
48 | onload: parseTvmazeResponse,
49 | onerror: () => console.error("Failed to fetch TVmaze data."),
50 | });
51 | }
52 |
53 | // Function to wait for the main script to be ready
54 | function waitForMainScript(callback) {
55 | if (document.readyState === "complete") {
56 | callback();
57 | } else {
58 | setTimeout(() => waitForMainScript(callback), 100); // Adjust the interval as needed
59 | }
60 | }
61 |
62 | // Initialize script
63 | (function init() {
64 | fetchTvmazeData();
65 | })();
--------------------------------------------------------------------------------
/ptp-imdb-box-office.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP iMDB Box Office
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.0.2
5 | // @description Add "Box Office" details onto PTP from IMDB API
6 | // @author Audionut
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @icon https://passthepopcorn.me/favicon.ico
9 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/ptp-imdb-box-office.js
10 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/ptp-imdb-box-office.js
11 | // @grant GM_xmlhttpRequest
12 | // @grant GM.setValue
13 | // @grant GM.getValue
14 | // @connect api.graphql.imdb.com
15 | // ==/UserScript==
16 |
17 | (function () {
18 | 'use strict';
19 |
20 | var link = document.querySelector("a#imdb-title-link.rating");
21 | if (!link) {
22 | console.error("IMDB link not found");
23 | return;
24 | }
25 |
26 | var imdbUrl = link.getAttribute("href");
27 | if (!imdbUrl) {
28 | console.error("IMDB URL not found");
29 | return;
30 | }
31 |
32 | let imdbId = imdbUrl.split("/")[4];
33 | if (!imdbId) {
34 | console.error("IMDB ID not found");
35 | return;
36 | }
37 |
38 | var newPanel = document.createElement('div');
39 | newPanel.className = 'panel';
40 | newPanel.id = 'box_office';
41 | var panelHeading = document.createElement('div');
42 | panelHeading.className = 'panel__heading';
43 | var title = document.createElement('span');
44 | title.className = 'panel__heading__title';
45 |
46 | var imdb = document.createElement('span');
47 | imdb.style.color = '#F2DB83';
48 | imdb.textContent = 'iMDB';
49 | title.appendChild(imdb);
50 | title.appendChild(document.createTextNode(' Box Office'));
51 |
52 | var toggle = document.createElement('a');
53 | toggle.className = 'panel__heading__toggler';
54 | toggle.title = 'Toggle';
55 | toggle.href = '#';
56 | toggle.textContent = 'Toggle';
57 |
58 | toggle.onclick = function () {
59 | var panelBody = document.querySelector('#box_office .panel__body');
60 | panelBody.style.display = (panelBody.style.display === 'none') ? 'block' : 'none';
61 | return false;
62 | };
63 |
64 | panelHeading.appendChild(title);
65 | panelHeading.appendChild(toggle);
66 | newPanel.appendChild(panelHeading);
67 |
68 | var panelBody = document.createElement('div');
69 | panelBody.className = 'panel__body';
70 | newPanel.appendChild(panelBody);
71 |
72 | var sidebar = document.querySelector('div.sidebar');
73 | if (!sidebar) {
74 | console.error("Sidebar not found");
75 | return;
76 | }
77 | sidebar.insertBefore(newPanel, sidebar.childNodes[4]);
78 |
79 | const fetchBoxOfficeData = async (imdbId, boxOfficeArea) => {
80 | const cacheboxKey = `boxOffice_${imdbId}_${boxOfficeArea}`;
81 | const cachedData = await GM.getValue(cacheboxKey);
82 | const cacheTimestamp = await GM.getValue(`${cacheboxKey}_timestamp`);
83 |
84 | if (cachedData && cacheTimestamp) {
85 | const currentTime = new Date().getTime();
86 | if (currentTime - cacheTimestamp < 24 * 60 * 60 * 1000) {
87 | console.log("Using cached data for box office");
88 | displayBoxOffice(JSON.parse(cachedData), boxOfficeArea);
89 | return;
90 | }
91 | }
92 |
93 | const url = `https://api.graphql.imdb.com/`;
94 | const query = {
95 | query: `
96 | query {
97 | title(id: "${imdbId}") {
98 | rankedLifetimeGross(boxOfficeArea: ${boxOfficeArea}) {
99 | total {
100 | amount
101 | }
102 | rank
103 | }
104 | openingWeekendGross(boxOfficeArea: ${boxOfficeArea}) {
105 | gross {
106 | total {
107 | amount
108 | }
109 | }
110 | theaterCount
111 | weekendEndDate
112 | weekendStartDate
113 | }
114 | productionBudget {
115 | budget {
116 | amount
117 | }
118 | }
119 | }
120 | }
121 | `
122 | };
123 |
124 | GM_xmlhttpRequest({
125 | method: "POST",
126 | url: url,
127 | headers: {
128 | "Content-Type": "application/json"
129 | },
130 | data: JSON.stringify(query),
131 | onload: function (response) {
132 | if (response.status >= 200 && response.status < 300) {
133 | const data = JSON.parse(response.responseText);
134 | GM.setValue(cacheboxKey, JSON.stringify(data));
135 | GM.setValue(`${cacheboxKey}_timestamp`, new Date().getTime());
136 | displayBoxOffice(data, boxOfficeArea);
137 | } else {
138 | console.error("Failed to fetch box office data", response);
139 | }
140 | },
141 | onerror: function (response) {
142 | console.error("Request error", response);
143 | }
144 | });
145 | };
146 |
147 | const displayBoxOffice = (data, boxOfficeArea) => {
148 | const titleData = data.data.title || {};
149 | const panelBody = document.getElementById('box_office').querySelector('.panel__body');
150 |
151 | const boxOfficeContainer = document.createElement('div');
152 | boxOfficeContainer.className = 'boxOffice';
153 | boxOfficeContainer.style.color = "#fff";
154 | boxOfficeContainer.style.fontSize = "1em";
155 |
156 | const formatCurrency = (amount) => {
157 | return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }).format(amount);
158 | };
159 |
160 | const formatRankedGross = (title, boxOfficeData) => {
161 | if (boxOfficeData && boxOfficeData.total && boxOfficeData.total.amount) {
162 | return `${title} (${boxOfficeArea}): USD ${formatCurrency(boxOfficeData.total.amount)} (Rank: ${boxOfficeData.rank})
`;
163 | }
164 | return "";
165 | };
166 |
167 | const formatOpeningWeekendGross = (title, boxOfficeData) => {
168 | if (boxOfficeData && boxOfficeData.gross && boxOfficeData.gross.total.amount) {
169 | return `${title} (${boxOfficeArea}): USD ${formatCurrency(boxOfficeData.gross.total.amount)}
`;
170 | }
171 | return "";
172 | };
173 |
174 | const formatProductionBudget = (budgetData) => {
175 | if (budgetData && budgetData.amount) {
176 | return `Production Budget: USD ${formatCurrency(budgetData.amount)}
`;
177 | }
178 | return "";
179 | };
180 |
181 | let output = '';
182 |
183 | if (boxOfficeArea === 'WORLDWIDE') {
184 | if (titleData.productionBudget && titleData.productionBudget.budget) {
185 | output += formatProductionBudget(titleData.productionBudget.budget);
186 | }
187 | if (titleData.rankedLifetimeGross) {
188 | output += formatRankedGross("Gross", titleData.rankedLifetimeGross);
189 | }
190 | } else if (boxOfficeArea === 'DOMESTIC') {
191 | if (titleData.rankedLifetimeGross) {
192 | output += formatRankedGross("Gross", titleData.rankedLifetimeGross);
193 | }
194 | if (titleData.openingWeekendGross) {
195 | output += formatOpeningWeekendGross("Opening Weekend Gross", titleData.openingWeekendGross);
196 | if (titleData.openingWeekendGross) {
197 | output += `Theater Count: ${titleData.openingWeekendGross.theaterCount}
198 | Weekend Start Date: ${titleData.openingWeekendGross.weekendStartDate}
199 | Weekend End Date: ${titleData.openingWeekendGross.weekendEndDate}
`;
200 | }
201 | }
202 | } else if (boxOfficeArea === 'INTERNATIONAL') {
203 | if (titleData.rankedLifetimeGross) {
204 | output += formatRankedGross("Gross", titleData.rankedLifetimeGross);
205 | }
206 | if (titleData.openingWeekendGross) {
207 | output += formatOpeningWeekendGross("Opening Weekend Gross", titleData.openingWeekendGross);
208 | }
209 | }
210 |
211 | boxOfficeContainer.innerHTML = output;
212 | panelBody.appendChild(boxOfficeContainer);
213 | };
214 |
215 | const boxOfficeAreas = ['WORLDWIDE', 'DOMESTIC', 'INTERNATIONAL'];
216 |
217 | boxOfficeAreas.forEach(area => {
218 | fetchBoxOfficeData(imdbId, area);
219 | });
220 | })();
--------------------------------------------------------------------------------
/ptp-similar-movies.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP Similar Movies Helper
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.0.5
5 | // @description Add "Movies Like This" onto PTP from IMDB API
6 | // @author Audionut
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @icon https://passthepopcorn.me/favicon.ico
9 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/ptp-similar-movies.js
10 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/ptp-similar-movies.js
11 | // @grant GM_xmlhttpRequest
12 | // @grant GM.setValue
13 | // @grant GM.getValue
14 | // @connect api.graphql.imdb.com
15 | // ==/UserScript==
16 |
17 | (function () {
18 | 'use strict';
19 |
20 | const PLACE_UNDER_CAST = false;
21 |
22 | let style = document.createElement('style');
23 | style.type = 'text/css';
24 | style.innerHTML = `
25 | .panel__heading__toggler {
26 | margin-left: auto;
27 | cursor: pointer;
28 | }
29 | `;
30 | document.getElementsByTagName('head')[0].appendChild(style);
31 |
32 | var link = document.querySelector("a#imdb-title-link.rating");
33 | if (!link) {
34 | console.error("IMDB link not found");
35 | return;
36 | }
37 |
38 | var imdbUrl = link.getAttribute("href");
39 | if (!imdbUrl) {
40 | console.error("IMDB URL not found");
41 | return;
42 | }
43 |
44 | var newPanel = document.createElement('div');
45 | newPanel.className = 'panel';
46 | newPanel.id = 'similar_movies';
47 | var panelHeading = document.createElement('div');
48 | panelHeading.className = 'panel__heading';
49 | var title = document.createElement('span');
50 | title.className = 'panel__heading__title';
51 |
52 | var imdb = document.createElement('span');
53 | imdb.style.color = '#F2DB83';
54 | imdb.textContent = 'iMDB';
55 | title.appendChild(imdb);
56 | title.appendChild(document.createTextNode(' More like this'));
57 |
58 | var toggle = document.createElement('a');
59 | toggle.href = 'javascript:void(0);';
60 | toggle.style.float = "right";
61 | toggle.textContent = '(Show all movies)';
62 |
63 | panelHeading.appendChild(title);
64 | panelHeading.appendChild(toggle);
65 | newPanel.appendChild(panelHeading);
66 |
67 | var panelBody = document.createElement('div');
68 | panelBody.style.position = 'relative';
69 | panelBody.style.display = 'block';
70 | panelBody.style.paddingTop = "0px";
71 | panelBody.style.width = "100%";
72 | newPanel.appendChild(panelBody);
73 |
74 | const insertAfterElement = (referenceNode, newNode) => {
75 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
76 | };
77 |
78 | let displayMethod = '';
79 |
80 | if (PLACE_UNDER_CAST) {
81 | const targetTable = document.querySelector('.table.table--panel-like.table--bordered.table--striped');
82 | if (targetTable) {
83 | insertAfterElement(targetTable, newPanel);
84 | displayMethod = 'table';
85 | } else {
86 | console.error("Target table not found");
87 | return;
88 | }
89 | } else {
90 | const parentGuidePanel = document.querySelector('div.panel#parents_guide');
91 | if (parentGuidePanel) {
92 | parentGuidePanel.parentNode.insertBefore(newPanel, parentGuidePanel.nextSibling);
93 | displayMethod = 'flex';
94 | // Update toggle for sidebar
95 | toggle.textContent = 'Toggle';
96 | toggle.className = 'panel__heading__toggler';
97 | toggle.title = 'Toggle';
98 | toggle.onclick = function () {
99 | panelBody.style.display = (panelBody.style.display === 'none') ? 'block' : 'none';
100 | return false;
101 | };
102 | } else {
103 | const sidebar = document.querySelector('div.sidebar');
104 | if (!sidebar) {
105 | console.error("Sidebar not found");
106 | return;
107 | }
108 | sidebar.insertBefore(newPanel, sidebar.childNodes[4]);
109 | displayMethod = 'flex';
110 |
111 | // Update toggle for sidebar
112 | toggle.textContent = 'Toggle';
113 | toggle.className = 'panel__heading__toggler';
114 | toggle.title = 'Toggle';
115 | toggle.onclick = function () {
116 | panelBody.style.display = (panelBody.style.display === 'none') ? 'block' : 'none';
117 | return false;
118 | };
119 | }
120 | }
121 |
122 | let imdbId = imdbUrl.split("/")[4];
123 | if (!imdbId) {
124 | console.error("IMDB ID not found");
125 | return;
126 | }
127 |
128 | const fetchSimilarMovies = async (imdbId) => {
129 | const cacheKey = `similarMovies_${imdbId}`;
130 | const cachedData = await GM.getValue(cacheKey);
131 | const cacheTimestamp = await GM.getValue(`${cacheKey}_timestamp`);
132 |
133 | if (cachedData && cacheTimestamp) {
134 | const currentTime = new Date().getTime();
135 | if (currentTime - cacheTimestamp < 24 * 60 * 60 * 1000) {
136 | console.log("Using cached data for similar movies");
137 | displaySimilarMovies(JSON.parse(cachedData));
138 | return;
139 | }
140 | }
141 |
142 | const url = `https://api.graphql.imdb.com/`;
143 | const query = {
144 | query: `
145 | query {
146 | title(id: "${imdbId}") {
147 | moreLikeThisTitles(first: 10) {
148 | edges {
149 | node {
150 | titleText {
151 | text
152 | }
153 | primaryImage {
154 | url
155 | }
156 | id
157 | }
158 | }
159 | }
160 | }
161 | }
162 | `
163 | };
164 | GM_xmlhttpRequest({
165 | method: "POST",
166 | url: url,
167 | headers: {
168 | "Content-Type": "application/json"
169 | },
170 | data: JSON.stringify(query),
171 | onload: function (response) {
172 | if (response.status >= 200 && response.status < 300) {
173 | let data = JSON.parse(response.responseText);
174 | let similarMovies = data.data.title.moreLikeThisTitles.edges;
175 |
176 | if (similarMovies.length === 0) {
177 | console.warn("No similar movies found");
178 | return;
179 | }
180 |
181 | GM.setValue(cacheKey, JSON.stringify(similarMovies));
182 | GM.setValue(`${cacheKey}_timestamp`, new Date().getTime());
183 | displaySimilarMovies(similarMovies);
184 | } else {
185 | console.error("Failed to fetch similar movies", response);
186 | }
187 | },
188 | onerror: function (response) {
189 | console.error("Request error", response);
190 | }
191 | });
192 | };
193 |
194 | const displaySimilarMovies = (similarMovies) => {
195 | var similarMoviesDiv = document.createElement('div');
196 | if (displayMethod === 'table') {
197 | similarMoviesDiv.style.textAlign = 'center';
198 | similarMoviesDiv.style.display = 'table';
199 | similarMoviesDiv.style.width = '100%';
200 | similarMoviesDiv.style.borderCollapse = 'separate';
201 | similarMoviesDiv.style.borderSpacing = '4px';
202 | } else {
203 | similarMoviesDiv.style.display = 'flex';
204 | similarMoviesDiv.style.flexWrap = 'wrap';
205 | similarMoviesDiv.style.justifyContent = 'center';
206 | similarMoviesDiv.style.padding = '4px';
207 | similarMoviesDiv.style.width = '100%';
208 | similarMoviesDiv.style.borderCollapse = 'separate';
209 | }
210 |
211 | let count = 0;
212 | let rowDiv = document.createElement('div');
213 | if (displayMethod === 'table') {
214 | rowDiv.style.display = 'table-row';
215 | } else {
216 | rowDiv.style.display = 'flex';
217 | rowDiv.style.justifyContent = 'center';
218 | rowDiv.style.width = '100%';
219 | rowDiv.style.marginBottom = '2px';
220 | }
221 | similarMoviesDiv.appendChild(rowDiv);
222 |
223 | similarMovies.forEach((edge) => {
224 | let movie = edge.node;
225 |
226 | if (!movie.primaryImage) {
227 | console.warn("No like this image found for movie:", movie.titleText.text);
228 | return;
229 | }
230 |
231 | let title = movie.titleText.text;
232 | let searchLink = `https://passthepopcorn.me/torrents.php?action=advanced&searchstr=${movie.id}`;
233 | let image = movie.primaryImage.url;
234 |
235 | var movieDiv = document.createElement('div');
236 | if (displayMethod === 'table') {
237 | movieDiv.style.width = '25%';
238 | movieDiv.style.display = 'table-cell';
239 | movieDiv.style.textAlign = 'center';
240 | movieDiv.style.backgroundColor = '#2c2c2c';
241 | movieDiv.style.borderRadius = '10px';
242 | movieDiv.style.overflow = 'hidden';
243 | movieDiv.style.fontSize = '1em';
244 | } else {
245 | movieDiv.style.width = '33%';
246 | movieDiv.style.textAlign = 'center';
247 | movieDiv.style.backgroundColor = '#2c2c2c';
248 | movieDiv.style.borderRadius = '10px';
249 | movieDiv.style.overflow = 'hidden';
250 | movieDiv.style.fontSize = '1em';
251 | movieDiv.style.margin = '3px 3px 1px 1px';
252 | }
253 | movieDiv.innerHTML = `
${title}`;
254 | rowDiv.appendChild(movieDiv);
255 |
256 | count++;
257 | if (displayMethod === 'table' && count % 4 === 0) {
258 | rowDiv = document.createElement('div');
259 | rowDiv.style.display = 'table-row';
260 | similarMoviesDiv.appendChild(rowDiv);
261 | } else if (displayMethod === 'flex' && count % 3 === 0) {
262 | rowDiv = document.createElement('div');
263 | rowDiv.style.display = 'flex';
264 | rowDiv.style.justifyContent = 'center';
265 | rowDiv.style.width = '100%';
266 | rowDiv.style.marginBottom = '2px';
267 | similarMoviesDiv.appendChild(rowDiv);
268 | }
269 | });
270 |
271 | if (displayMethod === 'table' && similarMoviesDiv.children.length > 2) {
272 | Array.from(similarMoviesDiv.children).slice(2).forEach(child => child.style.display = 'none');
273 | } else if (displayMethod === 'flex' && similarMoviesDiv.children.length > 3) {
274 | Array.from(similarMoviesDiv.children).slice(3).forEach(child => child.style.display = 'none');
275 | }
276 |
277 | if (displayMethod === 'table') {
278 | toggle.addEventListener('click', function () {
279 | const rows = Array.from(similarMoviesDiv.children);
280 | const isHidden = rows.slice(2).some(row => row.style.display === 'none');
281 | rows.slice(2).forEach(row => {
282 | row.style.display = isHidden ? 'table-row' : 'none';
283 | });
284 | toggle.textContent = isHidden ? '(Hide extra movies)' : '(Show all movies)';
285 | });
286 | }
287 |
288 | panelBody.appendChild(similarMoviesDiv);
289 | };
290 |
291 | fetchSimilarMovies(imdbId);
292 | })();
--------------------------------------------------------------------------------
/ptp-soundtracks.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Add iMDB Soundtracks
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.0
5 | // @description Add soundtracks from IMDB API
6 | // @author passthepopcorn_cc (mods by Audionut)
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @grant GM_xmlhttpRequest
9 | // @connect api.graphql.imdb.com
10 | // ==/UserScript==
11 |
12 | (function () {
13 | 'use strict';
14 |
15 | const fetchNames = async (uniqueIds) => {
16 | const url = `https://api.graphql.imdb.com/`;
17 | const query = {
18 | query: `
19 | query {
20 | names(ids: ${JSON.stringify(uniqueIds)}) {
21 | id
22 | nameText {
23 | text
24 | }
25 | }
26 | }
27 | `
28 | };
29 |
30 | return new Promise((resolve, reject) => {
31 | GM_xmlhttpRequest({
32 | method: "POST",
33 | url: url,
34 | headers: {
35 | "Content-Type": "application/json"
36 | },
37 | data: JSON.stringify(query),
38 | onload: function (response) {
39 | if (response.status >= 200 && response.status < 300) {
40 | const data = JSON.parse(response.responseText);
41 | console.log("Name ID query output:", data.data.names); // Log the output
42 | resolve(data.data.names);
43 | } else {
44 | reject("Failed to fetch names");
45 | }
46 | },
47 | onerror: function (response) {
48 | reject("Request error");
49 | }
50 | });
51 | });
52 | };
53 |
54 | const fetchSoundtrackData = async (titleId) => {
55 | const url = `https://api.graphql.imdb.com/`;
56 | const query = {
57 | query: `
58 | query {
59 | title(id: "${titleId}") {
60 | soundtrack(first: 30) {
61 | edges {
62 | node {
63 | id
64 | text
65 | comments {
66 | markdown
67 | }
68 | amazonMusicProducts {
69 | amazonId {
70 | asin
71 | }
72 | artists {
73 | artistName {
74 | text
75 | }
76 | }
77 | format {
78 | text
79 | }
80 | productTitle {
81 | text
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | `
90 | };
91 |
92 | GM_xmlhttpRequest({
93 | method: "POST",
94 | url: url,
95 | headers: {
96 | "Content-Type": "application/json"
97 | },
98 | data: JSON.stringify(query),
99 | onload: async function (response) {
100 | if (response.status >= 200 && response.status < 300) {
101 | const data = JSON.parse(response.responseText);
102 | const soundtracks = data.data.title.soundtrack.edges;
103 |
104 | // Extract unique IDs from comments
105 | const uniqueIds = [];
106 | soundtracks.forEach(edge => {
107 | edge.node.comments.forEach(comment => {
108 | const match = comment.markdown.match(/\[link=nm(\d+)\]/);
109 | if (match) {
110 | uniqueIds.push(`nm${match[1]}`);
111 | }
112 | });
113 | });
114 |
115 | const uniqueIdSet = [...new Set(uniqueIds)];
116 | const names = await fetchNames(uniqueIdSet);
117 |
118 | // Map IDs to names
119 | const idToNameMap = {};
120 | names.forEach(name => {
121 | idToNameMap[name.id] = name.nameText.text;
122 | });
123 | console.log("ID to Name Map:", idToNameMap); // Log the ID to Name mapping
124 |
125 | // Process the soundtrack data
126 | const processedSoundtracks = soundtracks.map(edge => {
127 | const soundtrack = edge.node;
128 | let artistName = null;
129 | let artistLink = null;
130 | let artistId = null;
131 |
132 | // Try to find "Performed by" first
133 | soundtrack.comments.forEach(comment => {
134 | const performedByMatch = comment.markdown.match(/Performed by \[link=nm(\d+)\]/);
135 | if (performedByMatch && !artistName) {
136 | artistId = `nm${performedByMatch[1]}`;
137 | artistName = idToNameMap[artistId];
138 | artistLink = `https://www.imdb.com/name/${artistId}/`;
139 | console.log(`Matched "Performed by" ID: ${artistId}, Artist Name: ${artistName}, Artist Link: ${artistLink}`);
140 | }
141 | });
142 |
143 | // If no "Performed by" found, try to find other roles
144 | if (!artistName) {
145 | soundtrack.comments.forEach(comment => {
146 | const match = comment.markdown.match(/\[link=nm(\d+)\]/);
147 | if (match && !artistName) {
148 | artistId = `nm${match[1]}`;
149 | artistName = idToNameMap[artistId] || match[0];
150 | artistLink = `https://www.imdb.com/name/${artistId}/`;
151 | console.log(`Matched other role ID: ${artistId}, Artist Name: ${artistName}, Artist Link: ${artistLink}`);
152 | } else if (!match && !artistName) {
153 | const performedByMatch = comment.markdown.match(/Performed by (.*)/);
154 | if (performedByMatch) {
155 | artistName = performedByMatch[1];
156 | artistLink = `https://duckduckgo.com/?q=${encodeURIComponent(artistName)}`;
157 | console.log(`Performed by match: Artist Name: ${artistName}, Artist Link: ${artistLink}`); // Log performed by match
158 | }
159 | }
160 | });
161 | }
162 |
163 | // Fallback to amazonMusicProducts if no artist name found
164 | if (!artistName && soundtrack.amazonMusicProducts.length > 0) {
165 | const product = soundtrack.amazonMusicProducts[0];
166 | if (product.artists && product.artists.length > 0) {
167 | artistName = product.artists[0].artistName.text;
168 | artistLink = `https://duckduckgo.com/?q=${encodeURIComponent(artistName)}`;
169 | console.log(`Amazon Music Product Artist: Artist Name: ${artistName}, Artist Link: ${artistLink}`); // Log amazon music product artist
170 | } else {
171 | artistName = product.productTitle.text;
172 | artistLink = `https://duckduckgo.com/?q=${encodeURIComponent(artistName)}`;
173 | console.log(`Amazon Music Product Title: Artist Name: ${artistName}, Artist Link: ${artistLink}`); // Log amazon music product title
174 | }
175 | }
176 |
177 | // Final fallback to text
178 | if (!artistName) {
179 | artistName = soundtrack.text;
180 | artistLink = `https://duckduckgo.com/?q=${encodeURIComponent(artistName)}`;
181 | console.log(`Fallback to soundtrack text: Artist Name: ${artistName}, Artist Link: ${artistLink}`); // Log fallback to soundtrack text
182 | }
183 |
184 | return {
185 | title: soundtrack.text,
186 | artist: artistName,
187 | link: artistLink,
188 | artistId: artistId
189 | };
190 | });
191 |
192 | appendSongs(processedSoundtracks, idToNameMap);
193 | } else {
194 | console.error("Failed to fetch soundtrack data", response);
195 | }
196 | },
197 | onerror: function (response) {
198 | console.error("Request error", response);
199 | }
200 | });
201 | };
202 |
203 | const appendSongs = (songs, idToNameMap) => {
204 | let movie_title = document.querySelector(".page__title").textContent.split("[")[0].trim();
205 | if (movie_title.includes(" AKA ")) movie_title = movie_title.split(" AKA ")[1]; // 0 = title in foreign lang, 1 = title in eng lang
206 |
207 | let cast_container = [...document.querySelectorAll("table")].find(e => e.textContent.trim().startsWith("Actor\n"));
208 | let bg_color_1 = window.getComputedStyle(cast_container.querySelector("tbody > tr > td"), null).getPropertyValue("background-color").split("none")[0];
209 | let bg_color_2 = window.getComputedStyle(cast_container.querySelector("tbody > tr"), null).getPropertyValue("background-color").split("none")[0];
210 | let border_color = window.getComputedStyle(cast_container.querySelector("tbody > tr > td"), null).getPropertyValue("border-color").split("none")[0];
211 |
212 | let new_panel = document.createElement("div");
213 | new_panel.id = "imdb-soundtrack";
214 | new_panel.className = "panel";
215 | new_panel.style.padding = 0;
216 | new_panel.style.margin = "18px 0";
217 |
218 | new_panel.innerHTML = '';
219 |
220 | new_panel.querySelector(".panel__heading").style.display = "flex";
221 | new_panel.querySelector(".panel__heading").style.justifyContent = "space-between";
222 |
223 | let yt_search = document.createElement("a");
224 | yt_search.href = "https://www.youtube.com/results?search_query=" + movie_title + " soundtrack";
225 | yt_search.textContent = "(YouTube search)";
226 | yt_search.target = "_blank";
227 |
228 | let yt_search_wrapper = document.createElement("span");
229 | yt_search_wrapper.style.float = "right";
230 | yt_search_wrapper.style.fontSize = "0.9em";
231 | yt_search_wrapper.appendChild(yt_search);
232 |
233 | new_panel.querySelector(".panel__heading").appendChild(yt_search_wrapper);
234 |
235 | let songs_container = document.createElement("div");
236 | songs_container.className = "panel__body";
237 | songs_container.style.display = "flex";
238 | songs_container.style.padding = 0;
239 |
240 | if (songs.length === 0) {
241 | let no_songs_container = document.createElement("div");
242 | no_songs_container.style.padding = "11px";
243 | no_songs_container.textContent = "No soundtrack information found on IMDb.";
244 | no_songs_container.style.textAlign = "center";
245 | new_panel.appendChild(no_songs_container);
246 | cast_container.parentNode.insertBefore(new_panel, cast_container);
247 | return;
248 | }
249 |
250 | let songs_col_1 = document.createElement("div");
251 | songs_col_1.style.display = "inline-block";
252 | songs_col_1.style.width = "50%";
253 | songs_col_1.style.padding = 0;
254 | songs_col_1.style.borderRight = "1px solid " + border_color;
255 |
256 | let songs_col_2 = document.createElement("div");
257 | songs_col_2.style.display = "inline-block";
258 | songs_col_2.style.width = "50%";
259 | songs_col_2.style.padding = 0;
260 |
261 | let j = 0;
262 | for (let i = 0; i < songs.length / 2; i++) {
263 | let div = createSongDiv(songs[i], movie_title, j, bg_color_1, bg_color_2, idToNameMap);
264 | songs_col_1.appendChild(div);
265 | j++;
266 | }
267 |
268 | for (let i = Math.ceil(songs.length / 2); i < songs.length; i++) {
269 | let div = createSongDiv(songs[i], movie_title, j, bg_color_1, bg_color_2, idToNameMap);
270 | songs_col_2.appendChild(div);
271 | j++;
272 | }
273 |
274 | songs_container.appendChild(songs_col_1);
275 | songs_container.appendChild(songs_col_2);
276 | new_panel.appendChild(songs_container);
277 |
278 | cast_container.parentNode.insertBefore(new_panel, cast_container);
279 | };
280 |
281 | const createSongDiv = (song, movie_title, index, bg_color_1, bg_color_2, idToNameMap) => {
282 | let div = document.createElement("div");
283 | div.style.height = "31px";
284 | div.style.overflow = "hidden";
285 | div.style.padding = "6px 14px";
286 |
287 | let track_line = document.createElement("a");
288 | track_line.textContent = song.title;
289 | track_line.title = song.title;
290 | track_line.href = "https://www.youtube.com/results?search_query=" + movie_title.replace(/&/, " and ") + " " + song.title.replace(/&/, " and ");
291 | track_line.target = "_blank";
292 |
293 | let seperator = document.createElement("span");
294 | seperator.innerHTML = "- ";
295 |
296 | let artist_link = document.createElement("a");
297 | artist_link.textContent = song.artistId ? idToNameMap[song.artistId] : song.artist;
298 | artist_link.href = song.link;
299 | artist_link.target = "_blank";
300 | artist_link.style.marginRight = "10px";
301 |
302 | if (index % 2 === 0) div.style.background = bg_color_1;
303 | else div.style.background = bg_color_2;
304 |
305 | div.appendChild(artist_link);
306 | div.appendChild(seperator);
307 | div.appendChild(track_line);
308 |
309 | return div;
310 | };
311 |
312 | const imdbUrl = document.querySelector("a#imdb-title-link.rating").getAttribute("href");
313 | const imdbId = imdbUrl.split("/")[4];
314 |
315 | fetchSoundtrackData(imdbId);
316 | })();
--------------------------------------------------------------------------------
/ptp-technical-specifications.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP Technical Specifications
3 | // @namespace https://github.com/Audionut/add-trackers
4 | // @version 1.1.3
5 | // @description Add "Technical Specifications" onto PTP from IMDB API
6 | // @author Audionut
7 | // @match https://passthepopcorn.me/torrents.php?id=*
8 | // @icon https://passthepopcorn.me/favicon.ico
9 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/ptp-technical-specifications.js
10 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/ptp-technical-specifications.js
11 | // @grant GM_xmlhttpRequest
12 | // @grant GM.setValue
13 | // @grant GM.getValue
14 | // @connect api.graphql.imdb.com
15 | // ==/UserScript==
16 |
17 | (function () {
18 | 'use strict';
19 |
20 | var link = document.querySelector("a#imdb-title-link.rating");
21 | if (!link) {
22 | console.error("IMDB link not found");
23 | return;
24 | }
25 |
26 | var imdbUrl = link.getAttribute("href");
27 | if (!imdbUrl) {
28 | console.error("IMDB URL not found");
29 | return;
30 | }
31 |
32 | let imdbId = imdbUrl.split("/")[4];
33 | if (!imdbId) {
34 | console.error("IMDB ID not found");
35 | return;
36 | }
37 |
38 | var newPanel = document.createElement('div');
39 | newPanel.className = 'panel';
40 | newPanel.id = 'technical_specifications';
41 | var panelHeading = document.createElement('div');
42 | panelHeading.className = 'panel__heading';
43 | var title = document.createElement('span');
44 | title.className = 'panel__heading__title';
45 |
46 | var imdb = document.createElement('span');
47 | imdb.style.color = '#F2DB83';
48 | imdb.textContent = 'iMDB';
49 | title.appendChild(imdb);
50 | title.appendChild(document.createTextNode(' Technical Specifications'));
51 |
52 | var imdbLink = document.createElement('a');
53 | imdbLink.href = `https://www.imdb.com/title/${imdbId}/technical/?ref_=tt_spec_sm`;
54 | imdbLink.title = 'IMDB Url';
55 | imdbLink.textContent = 'IMDB Url';
56 | imdbLink.target = '_blank';
57 | imdbLink.style.marginLeft = '5px';
58 |
59 | var toggle = document.createElement('a');
60 | toggle.className = 'panel__heading__toggler';
61 | toggle.title = 'Toggle';
62 | toggle.href = '#';
63 | toggle.textContent = 'Toggle';
64 |
65 | toggle.onclick = function () {
66 | var panelBody = document.querySelector('#technical_specifications .panel__body');
67 | panelBody.style.display = (panelBody.style.display === 'none') ? 'block' : 'none';
68 | return false;
69 | };
70 |
71 | panelHeading.appendChild(title);
72 | panelHeading.appendChild(imdbLink);
73 | panelHeading.appendChild(toggle);
74 | newPanel.appendChild(panelHeading);
75 |
76 | var panelBody = document.createElement('div');
77 | panelBody.className = 'panel__body';
78 | newPanel.appendChild(panelBody);
79 |
80 | var sidebar = document.querySelector('div.sidebar');
81 | if (!sidebar) {
82 | console.error("Sidebar not found");
83 | return;
84 | }
85 | sidebar.insertBefore(newPanel, sidebar.childNodes[4]);
86 |
87 | const fetchTechnicalSpecifications = async () => {
88 | const cachespecsKey = `technicalSpecifications_${imdbId}`;
89 | const cachedData = await GM.getValue(cachespecsKey);
90 | const cacheTimestamp = await GM.getValue(`${cachespecsKey}_timestamp`);
91 |
92 | if (cachedData && cacheTimestamp) {
93 | const currentTime = new Date().getTime();
94 | if (currentTime - cacheTimestamp < 24 * 60 * 60 * 1000) {
95 | console.log("Using cached data for technical specifications");
96 | displayTechnicalSpecifications(JSON.parse(cachedData));
97 | return;
98 | }
99 | }
100 |
101 | const url = `https://api.graphql.imdb.com/`;
102 | const query = {
103 | query: `
104 | query {
105 | title(id: "${imdbId}") {
106 | runtime {
107 | displayableProperty {
108 | value {
109 | plainText
110 | }
111 | }
112 | }
113 | technicalSpecifications {
114 | aspectRatios {
115 | items {
116 | aspectRatio
117 | attributes {
118 | text
119 | }
120 | displayableProperty {
121 | qualifiersInMarkdownList {
122 | markdown
123 | }
124 | value {
125 | markdown
126 | expandedMarkdown
127 | plaidHtml
128 | plainText
129 | }
130 | }
131 | }
132 | }
133 | cameras {
134 | items {
135 | camera
136 | attributes {
137 | text
138 | }
139 | }
140 | }
141 | colorations {
142 | items {
143 | text
144 | attributes {
145 | text
146 | }
147 | }
148 | }
149 | laboratories {
150 | items {
151 | laboratory
152 | attributes {
153 | text
154 | }
155 | }
156 | }
157 | negativeFormats {
158 | items {
159 | negativeFormat
160 | attributes {
161 | text
162 | }
163 | }
164 | }
165 | printedFormats {
166 | items {
167 | printedFormat
168 | attributes {
169 | text
170 | }
171 | }
172 | }
173 | processes {
174 | items {
175 | process
176 | attributes {
177 | text
178 | }
179 | }
180 | }
181 | soundMixes {
182 | items {
183 | text
184 | attributes {
185 | text
186 | }
187 | }
188 | }
189 | filmLengths {
190 | items {
191 | filmLength
192 | countries {
193 | text
194 | }
195 | numReels
196 | }
197 | }
198 | }
199 | }
200 | }
201 | `
202 | };
203 |
204 | GM_xmlhttpRequest({
205 | method: "POST",
206 | url: url,
207 | headers: {
208 | "Content-Type": "application/json"
209 | },
210 | data: JSON.stringify(query),
211 | onload: function (response) {
212 | if (response.status >= 200 && response.status < 300) {
213 | const data = JSON.parse(response.responseText);
214 | GM.setValue(cachespecsKey, JSON.stringify(data));
215 | GM.setValue(`${cachespecsKey}_timestamp`, new Date().getTime());
216 | displayTechnicalSpecifications(data);
217 | } else {
218 | console.error("Failed to fetch technical specifications", response);
219 | }
220 | },
221 | onerror: function (response) {
222 | console.error("Request error", response);
223 | }
224 | });
225 | };
226 |
227 | const displayTechnicalSpecifications = (data) => {
228 | const specs = data.data.title.technicalSpecifications || {};
229 | const runtime = data.data.title.runtime?.displayableProperty?.value?.plainText || 'N/A';
230 | const panelBody = document.getElementById('technical_specifications').querySelector('.panel__body');
231 |
232 | const specContainer = document.createElement('div');
233 | specContainer.className = 'technicalSpecification';
234 | specContainer.style.color = "#fff";
235 | specContainer.style.fontSize = "1em";
236 |
237 | const formatSpec = (title, items, key, attributesKey) => {
238 | if (items && items.length > 0) {
239 | let values = items.map(item => {
240 | let value = item[key];
241 | if (item[attributesKey] && item[attributesKey].length > 0) {
242 | value += ` (${item[attributesKey].map(attr => attr.text).join(", ")})`;
243 | }
244 | return value;
245 | }).filter(value => value).join(", ");
246 | return `${title}: ${values}
`;
247 | }
248 | return "";
249 | };
250 |
251 | const formatFilmLengths = (items) => {
252 | if (items && items.length > 0) {
253 | let values = items.map(item => {
254 | let value = `${item.filmLength} m`;
255 | if (item.countries && item.countries.length > 0) {
256 | value += ` (${item.countries.map(country => country.text).join(", ")})`;
257 | }
258 | if (item.numReels) {
259 | value += ` (${item.numReels} reels)`;
260 | }
261 | return value;
262 | }).filter(value => value).join(", ");
263 | return `Film Length: ${values}
`;
264 | }
265 | return "";
266 | };
267 |
268 | specContainer.innerHTML += `Runtime: ${runtime}
`;
269 | specContainer.innerHTML += formatSpec("Aspect Ratio", specs.aspectRatios?.items || [], "aspectRatio", "attributes");
270 | specContainer.innerHTML += formatSpec("Camera", specs.cameras?.items || [], "camera", "attributes");
271 | specContainer.innerHTML += formatSpec("Color", specs.colorations?.items || [], "text", "attributes");
272 | specContainer.innerHTML += formatSpec("Laboratory", specs.laboratories?.items || [], "laboratory", "attributes");
273 | specContainer.innerHTML += formatSpec("Negative Format", specs.negativeFormats?.items || [], "negativeFormat", "attributes");
274 | specContainer.innerHTML += formatSpec("Printed Film Format", specs.printedFormats?.items || [], "printedFormat", "attributes");
275 | specContainer.innerHTML += formatSpec("Cinematographic Process", specs.processes?.items || [], "process", "attributes");
276 | specContainer.innerHTML += formatSpec("Sound Mix", specs.soundMixes?.items || [], "text", "attributes");
277 | specContainer.innerHTML += formatFilmLengths(specs.filmLengths?.items || []);
278 |
279 | panelBody.appendChild(specContainer);
280 | };
281 |
282 | fetchTechnicalSpecifications();
283 | })();
--------------------------------------------------------------------------------
/ptp-tmdb-trailers.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - TMDB Trailer Selector
3 | // @version 1.2
4 | // @description Add a dropdown to switch between various TMDB videos
5 | // @match https://passthepopcorn.me/torrents.php?id=*
6 | // @icon https://passthepopcorn.me/favicon.ico
7 | // @author Audionut
8 | // ==/UserScript==
9 |
10 | (function() {
11 | 'use strict';
12 |
13 | // Define your TMDB API Key
14 | const TMDB_API_KEY = '';
15 |
16 | // Base64-encoded TMDB icon
17 | const tmdbIconBase64 = 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDg5LjA0IDM1LjQiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDp1cmwoI2xpbmVhci1ncmFkaWVudCk7fTwvc3R5bGU+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHkxPSIxNy43IiB4Mj0iNDg5LjA0IiB5Mj0iMTcuNyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzkwY2VhMSIvPjxzdG9wIG9mZnNldD0iMC41NiIgc3RvcC1jb2xvcj0iIzNjYmVjOSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwYjNlNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0aXRsZT5Bc3NldCA1PC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTI5My41LDBoOC45bDguNzUsMjMuMmguMUwzMjAuMTUsMGg4LjM1TDMxMy45LDM1LjRoLTYuMjVabTQ2LjYsMGg3LjhWMzUuNGgtNy44Wm0yMi4yLDBoMjQuMDVWNy4ySDM3MC4xdjYuNmgxNS4zNVYyMUgzNzAuMXY3LjJoMTcuMTV2Ny4ySDM2Mi4zWm01NSwwSDQyOWEzMy41NCwzMy41NCwwLDAsMSw4LjA3LDFBMTguNTUsMTguNTUsMCwwLDEsNDQzLjc1LDRhMTUuMSwxNS4xLDAsMCwxLDQuNTIsNS41M0ExOC41LDE4LjUsMCwwLDEsNDUwLDE3LjhhMTYuOTEsMTYuOTEsMCwwLDEtMS42Myw3LjU4LDE2LjM3LDE2LjM3LDAsMCwxLTQuMzcsNS41LDE5LjUyLDE5LjUyLDAsMCwxLTYuMzUsMy4zN0EyNC41OSwyNC41OSwwLDAsMSw0MzAsMzUuNEg0MTcuMjlabTcuODEsMjguMmg0YTIxLjU3LDIxLjU3LDAsMCwwLDUtLjU1LDEwLjg3LDEwLjg3LDAsMCwwLDQtMS44Myw4LjY5LDguNjksMCwwLDAsMi42Ny0zLjM0LDExLjkyLDExLjkyLDAsMCwwLDEtNS4wOCw5Ljg3LDkuODcsMCwwLDAtMS00LjUyLDksOSwwLDAsMC0yLjYyLTMuMTgsMTEuNjgsMTEuNjgsMCwwLDAtMy44OC0xLjg4LDE3LjQzLDE3LjQzLDAsMCwwLTQuNjctLjYyaC00LjZaTTQ2MS4yNCwwaDEzLjJhMzQuNDIsMzQuNDIsMCwwLDEsNC42My4zMiwxMi45LDEyLjksMCwwLDEsNC4xNywxLjMsNy44OCw3Ljg4LDAsMCwxLDMsMi43M0E4LjM0LDguMzQsMCwwLDEsNDg3LjM5LDlhNy40Miw3LjQyLDAsMCwxLTEuNjcsNSw5LjI4LDkuMjgsMCwwLDEtNC40MywyLjgydi4xYTEwLDEwLDAsMCwxLDMuMTgsMSw4LjM4LDguMzgsMCwwLDEsMi40NSwxLjg1LDcuNzksNy43OSwwLDAsMSwxLjU3LDIuNjIsOS4xNiw5LjE2LDAsMCwxLC41NSwzLjIsOC41Miw4LjUyLDAsMCwxLTEuMiw0LjY4LDkuNDIsOS40MiwwLDAsMS0zLjEsMywxMy4zOCwxMy4zOCwwLDAsMS00LjI3LDEuNjUsMjMuMTEsMjMuMTEsMCwwLDEtNC43My41aC0xNC41Wk00NjksMTQuMTVoNS42NWE4LjE2LDguMTYsMCwwLDAsMS43OC0uMkE0Ljc4LDQuNzgsMCwwLDAsNDc4LDEzLjNhMy4zNCwzLjM0LDAsMCwwLDEuMTMtMS4yLDMuNjMsMy42MywwLDAsMCwuNDItMS44LDMuMjIsMy4yMiwwLDAsMC0uNDctMS44MiwzLjMzLDMuMzMsMCwwLDAtMS4yMy0xLjEzLDUuNzcsNS43NywwLDAsMC0xLjctLjU4LDEwLjc5LDEwLjc5LDAsMCwwLTEuODUtLjE3SDQ2OVptMCwxNC42NWg3YTguOTEsOC45MSwwLDAsMCwxLjgzLS4yLDQuNzgsNC43OCwwLDAsMCwxLjY3LS43LDQsNCwwLDAsMCwxLjIzLTEuMywzLjcxLDMuNzEsMCwwLDAsLjQ3LTIsMy4xMywzLjEzLDAsMCwwLS42Mi0yQTQsNCwwLDAsMCw0NzksMjEuNDUsNy44Myw3LjgzLDAsMCwwLDQ3NywyMC45YTE1LjEyLDE1LjEyLDAsMCwwLTIuMDUtLjE1SDQ2OVptLTI2NSw2LjUzSDI3MWExNy42NiwxNy42NiwwLDAsMCwxNy42Ni0xNy42NmgwQTE3LjY3LDE3LjY3LDAsMCwwLDI3MSwwSDIwNC4wNkExNy42NywxNy42NywwLDAsMCwxODYuNCwxNy42N2gwQTE3LjY2LDE3LjY2LDAsMCwwLDIwNC4wNiwzNS4zM1pNMTAuMSw2LjlIMFYwSDI4VjYuOUgxNy45VjM1LjRIMTAuMVpNMzksMGg3LjhWMTMuMkg2MS45VjBoNy44VjM1LjRINjEuOVYyMC4xSDQ2Ljc1VjM1LjRIMzlaTTgwLjIsMGgyNFY3LjJIODh2Ni42aDE1LjM1VjIxSDg4djcuMmgxNy4xNXY3LjJoLTI1Wm01NSwwSDE0N2w4LjE1LDIzLjFoLjFMMTYzLjQ1LDBIMTc1LjJWMzUuNGgtNy44VjguMjVoLS4xTDE1OCwzNS40aC01Ljk1bC05LTI3LjE1SDE0M1YzNS40aC03LjhaIi8+PC9nPjwvZz48L3N2Zz4=';
18 |
19 | // Extract IMDb ID from the page
20 | const imdbLinkElement = document.getElementById("imdb-title-link");
21 | if (!imdbLinkElement) {
22 | console.warn("No IMDb ID found, aborting.");
23 | return;
24 | }
25 | const imdbId = imdbLinkElement.href.match(/title\/(tt\d+)\//)[1];
26 | if (!imdbId) {
27 | console.warn("Invalid IMDb ID, aborting.");
28 | return;
29 | }
30 |
31 | let movieId = ''; // Declare movieId in a higher scope to use later
32 | let isTVShow = 0;
33 |
34 | // Function to fetch all TMDB video types using IMDb ID
35 | function searchTMDBVideos(imdbId, callback) {
36 | const searchUrl = `https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_API_KEY}&external_source=imdb_id`;
37 | console.warn(`TMDB URL: ${searchUrl}`);
38 |
39 | fetch(searchUrl)
40 | .then(response => {
41 | if (!response.ok) throw new Error('Failed to fetch from TMDB');
42 | return response.json();
43 | })
44 | .then(data => {
45 | if (data && data.movie_results && data.movie_results.length > 0) {
46 | movieId = data.movie_results[0].id; // Save movieId for later (movie)
47 | console.warn(`TMDB Movie ID: ${movieId}`);
48 | const videoUrl = `https://api.themoviedb.org/3/movie/${movieId}/videos?api_key=${TMDB_API_KEY}`;
49 | return fetch(videoUrl);
50 | } else if (data && data.tv_results && data.tv_results.length > 0) {
51 | movieId = data.tv_results[0].id; // Save tvId for later (TV show)
52 | console.warn(`TMDB TV Show ID: ${movieId}`);
53 | const videoUrl = `https://api.themoviedb.org/3/tv/${movieId}/videos?api_key=${TMDB_API_KEY}`;
54 | isTVShow = 1;
55 | return fetch(videoUrl);
56 | } else {
57 | console.warn('No TMDB movie or TV show found for the IMDb ID:', imdbId);
58 | return Promise.resolve({ results: [] });
59 | }
60 | })
61 | .then(response => response.json())
62 | .then(data => {
63 | const tmdbVideos = (data.results || [])
64 | .map(video => ({
65 | title: `${video.type}: ${video.name}`,
66 | videoId: video.key,
67 | type: video.type.toLowerCase(), // Used for sorting
68 | site: video.site
69 | }));
70 | callback(tmdbVideos);
71 | })
72 | .catch(error => console.error('Error fetching TMDB videos:', error));
73 | }
74 |
75 | // Function to set the highest resolution available
76 | function getHighestResolutionVideoUrl(videoId) {
77 | const resolutions = ['hd2160', 'hd1440', 'hd1080', 'hd720'];
78 | const baseUrl = `https://www.youtube.com/embed/${videoId}`;
79 | return `${baseUrl}?vq=${resolutions[0]}`;
80 | }
81 |
82 | // Function to sort videos in the desired order
83 | function sortVideos(videos) {
84 | const sortOrder = {
85 | trailer: 1,
86 | teaser: 2,
87 | featurette: 3,
88 | 'behind the scenes': 4,
89 | clip: 5
90 | };
91 |
92 | return videos.sort((a, b) => {
93 | const aOrder = sortOrder[a.type] || 100; // Use 100 for unsorted types
94 | const bOrder = sortOrder[b.type] || 100;
95 | if (aOrder === bOrder) {
96 | return a.title.localeCompare(b.title);
97 | }
98 |
99 | return aOrder - bOrder;
100 | });
101 | }
102 |
103 | function showinfo(info, node) {
104 | const showinfo_class = "tmdb_copyinfobox";
105 |
106 | // Ensure parent has relative positioning to anchor the pop-up correctly
107 | const parentNode = node.parentElement;
108 | if (window.getComputedStyle(parentNode).position === 'static') {
109 | parentNode.style.position = 'relative';
110 | }
111 |
112 | // Check if the pop-up already exists, if not create it
113 | let el = parentNode.getElementsByClassName(showinfo_class)[0];
114 | if (!el) {
115 | el = document.createElement("div");
116 | el.classList.add(showinfo_class);
117 | el.style = `
118 | position: absolute;
119 | right: 10px;
120 | top: -40px;
121 | background-color: white;
122 | border: 1px solid red;
123 | border-radius: 5px;
124 | padding: 5px;
125 | color: black;
126 | opacity: 0;
127 | transition: opacity 0.5s ease-in-out;`;
128 | parentNode.insertAdjacentElement("beforeend", el);
129 | }
130 |
131 | // Set the content and make it visible
132 | el.textContent = info;
133 | el.style.opacity = 1; // Fade in
134 | el.style.visibility = "visible";
135 |
136 | // Start the fade-out after 2 seconds
137 | setTimeout(() => {
138 | el.style.opacity = 0; // Fade out
139 | setTimeout(() => {
140 | el.style.visibility = "hidden"; // Hide after fading out
141 | }, 500); // Match the transition duration
142 | }, 2000); // Keep visible for 2 seconds
143 | }
144 |
145 | // Function to copy text to clipboard and show the pop-up confirmation
146 | async function copyToClipboard(text, node) {
147 | try {
148 | await navigator.clipboard.writeText(text);
149 | showinfo("YouTube link copied!", node); // Use the pop-up instead of alert
150 | } catch (err) {
151 | showinfo("Failed to copy link.\nCheck console for errors.", node);
152 | console.error('Failed to copy text: ', err);
153 | }
154 | }
155 |
156 | // Modify the populateDropdowns function to add TMDB link correctly and a copy button
157 | function populateDropdowns(videos, videoDropdown, originalIframe, videoDiv, originalLoaded, isTVShow) {
158 | // Clear existing options except the original
159 | videoDropdown.innerHTML = '';
160 |
161 | // Add default option for the original video
162 | const originalOption = document.createElement('option');
163 | originalOption.value = 'original';
164 | originalOption.textContent = 'Original Video';
165 | videoDropdown.appendChild(originalOption);
166 |
167 | // Sort videos before populating the dropdown
168 | const sortedVideos = sortVideos(videos);
169 |
170 | // Populate video dropdown with TMDB videos
171 | sortedVideos.forEach(video => {
172 | const option = document.createElement('option');
173 | option.value = video.videoId;
174 | option.textContent = video.title;
175 | videoDropdown.appendChild(option);
176 | });
177 |
178 | // Create a "Copy YouTube Link" button
179 | const copyLinkSpan = document.createElement('span');
180 | copyLinkSpan.style = 'float:right;font-size:0.9em';
181 | const copyLinkButton = document.createElement('a');
182 | copyLinkButton.textContent = '(Copy YouTube Link)';
183 | //copyLinkButton.style.marginLeft = '10px';
184 | copyLinkButton.style.cursor = 'pointer';
185 | copyLinkButton.disabled = true; // Initially disabled
186 |
187 | // Add event listener to copy the YouTube link when the button is clicked
188 | copyLinkButton.addEventListener('click', () => {
189 | const selectedVideoId = videoDropdown.value;
190 | const selectedSite = sortedVideos.find(video => video.videoId === selectedVideoId)?.site;
191 | const youtubeWatchUrl = `https://www.youtube.com/watch?v=${selectedVideoId}`;
192 | copyToClipboard(youtubeWatchUrl, copyLinkButton);
193 | });
194 |
195 | // Automatically load the original video if it's still selected
196 | if (originalLoaded) {
197 | videoDiv.innerHTML = originalIframe;
198 | }
199 |
200 | // Load the selected video when changed
201 | videoDropdown.addEventListener('change', function() {
202 | const selectedVideoId = videoDropdown.value;
203 | const selectedSite = sortedVideos.find(video => video.videoId === selectedVideoId)?.site;
204 |
205 | if (selectedVideoId === 'original') {
206 | videoDiv.innerHTML = originalIframe;
207 | copyLinkButton.disabled = true; // Disable copy button for original video
208 | } else if (selectedSite === 'YouTube') {
209 | videoDiv.innerHTML = ``;
210 | copyLinkButton.disabled = false; // Enable copy button for YouTube videos
211 | } else {
212 | videoDiv.innerHTML = `Video hosted on ${selectedSite}, cannot auto-play here.`;
213 | copyLinkButton.disabled = true; // Disable copy button for non-YouTube videos
214 | }
215 | });
216 |
217 | // Ensure the first TMDB video is loaded only if the original isn't already playing
218 | if (!originalLoaded && sortedVideos.length > 0 && sortedVideos[0].site === 'YouTube') {
219 | const firstVideo = sortedVideos[0];
220 | videoDiv.innerHTML = ``;
221 | copyLinkButton.disabled = false; // Enable copy button for YouTube videos
222 | }
223 |
224 | // Add TMDB link with the correct type
225 | const tmdbLink = addTMDBLink(isTVShow);
226 | videoDropdown.parentElement.appendChild(tmdbLink);
227 | copyLinkSpan.appendChild(copyLinkButton);
228 | videoDropdown.parentElement.appendChild(copyLinkSpan);
229 | }
230 |
231 | // Function to add TMDB link for both movies and TV shows
232 | function addTMDBLink(isTVShow) {
233 | const tmdbLink = document.createElement('a');
234 | if (isTVShow === 1) {
235 | tmdbLink.href = `https://www.themoviedb.org/tv/${movieId}`; // TV show link
236 | } else {
237 | tmdbLink.href = `https://www.themoviedb.org/movie/${movieId}`; // Movie link
238 | }
239 | tmdbLink.target = '_blank';
240 | tmdbLink.style.marginLeft = '10px';
241 |
242 | const tmdbIcon = document.createElement('img');
243 | tmdbIcon.title = 'TMDB Link';
244 | tmdbIcon.style.height = '18px';
245 | tmdbIcon.style.verticalAlign = 'middle';
246 | tmdbIcon.src = `data:image/svg+xml;base64,${tmdbIconBase64}`;
247 | tmdbIcon.alt = 'TMDB Link';
248 | tmdbIcon.title = 'TMDB Link';
249 |
250 | tmdbLink.appendChild(tmdbIcon);
251 | return tmdbLink;
252 | }
253 |
254 | // Main function to initialize the script
255 | window.onload = function() {
256 | console.log('TMDB Video Selector Loaded.');
257 |
258 | // Check for the required elements
259 | const synopsisPanel = document.querySelector('#synopsis-and-trailer');
260 | const panelBody = synopsisPanel ? synopsisPanel.querySelector('.panel__body') : null;
261 | const videoDiv = panelBody ? panelBody.querySelector('#trailer') : null;
262 |
263 | if (!synopsisPanel || !panelBody || !videoDiv) {
264 | console.error('Required elements not found.');
265 | return;
266 | }
267 |
268 | // Save the original YouTube iframe
269 | const originalIframe = videoDiv.innerHTML;
270 |
271 | // Check if the original trailer is loaded
272 | let originalLoaded = true;
273 |
274 | // Create the dropdown
275 | const containerDiv = document.createElement('div');
276 | containerDiv.style.display = 'flex';
277 | containerDiv.style.justifyContent = 'space-between';
278 | containerDiv.style.alignItems = 'center';
279 | containerDiv.style.marginBottom = '10px';
280 |
281 | // Video dropdown
282 | const videoDropdown = document.createElement('select');
283 | videoDropdown.style.marginRight = '10px';
284 | containerDiv.appendChild(videoDropdown);
285 |
286 | // Insert the container with dropdown before the video div
287 | panelBody.insertBefore(containerDiv, videoDiv);
288 |
289 | // Automatically load and populate videos from TMDB based on IMDb ID
290 | searchTMDBVideos(imdbId, (videos) => {
291 | if (videos.length > 0) {
292 | populateDropdowns(videos, videoDropdown, originalIframe, videoDiv, originalLoaded, isTVShow);
293 | } else {
294 | console.error('No videos found.');
295 | }
296 | });
297 | };
298 | })();
299 |
--------------------------------------------------------------------------------
/ptp-torrent-row-group-toggle.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name PTP - Torrent Row Group Toggle
3 | // @match https://passthepopcorn.me/torrents.php*
4 | // @match https://passthepopcorn.me/collages.php?*
5 | // @match https://passthepopcorn.me/artist.php?*
6 | // @match https://passthepopcorn.me/bookmarks.php*
7 | // @namespace https://github.com/Audionut/add-trackers
8 | // @grant GM_addStyle
9 | // @grant GM_getValue
10 | // @grant GM_setValue
11 | // @grant GM_registerMenuCommand
12 | // @downloadURL https://github.com/Audionut/add-trackers/raw/main/ptp-torrent-row-group-toggle.js
13 | // @updateURL https://github.com/Audionut/add-trackers/raw/main/ptp-torrent-row-group-toggle.js
14 | // @version 1.2
15 | // @icon https://passthepopcorn.me/favicon.ico
16 | // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@43fd0fe4de1166f343883511e53546e87840aeaf/gm_config.js
17 | // ==/UserScript==
18 |
19 | function toggleRowGroup(startRow, hide) {
20 | let row = startRow.nextElementSibling;
21 | while (row && !row.querySelector('.basic-movie-list__torrent-edition__sub') && row.tagName !== 'TBODY') {
22 | if (!row.classList.contains('initially-hidden')) {
23 | if (hide) {
24 | if (row.style.display === 'table-row') {
25 | row.dataset.originalDisplay = 'table-row';
26 | row.style.display = '';
27 | }
28 | row.classList.add('hidden');
29 | } else {
30 | row.classList.remove('hidden');
31 | if (row.dataset.originalDisplay) {
32 | row.style.display = row.dataset.originalDisplay;
33 | delete row.dataset.originalDisplay;
34 | }
35 | }
36 | }
37 | row = row.nextElementSibling;
38 | }
39 | }
40 |
41 | function addRowGroupToggleButtons() {
42 | const rowGroups = document.querySelectorAll('.basic-movie-list__torrent-edition__sub');
43 |
44 | document.querySelectorAll('.row-toggle-button').forEach(button => button.remove());
45 |
46 | rowGroups.forEach(rowGroup => {
47 | const toggleButton = document.createElement('a');
48 | toggleButton.innerHTML = '(Hide)';
49 | toggleButton.classList.add('row-toggle-button');
50 | toggleButton.style = 'margin-left: 10px; font-size:0.9em; font-weight: normal; cursor: pointer;';
51 |
52 | toggleButton.addEventListener('click', (e) => {
53 | e.preventDefault();
54 | const row = rowGroup.closest('tr');
55 | const hide = toggleButton.innerHTML === '(Hide)';
56 | toggleRowGroup(row, hide);
57 | toggleButton.innerHTML = hide ? '(Show)' : '(Hide)';
58 | });
59 |
60 | rowGroup.parentElement.appendChild(toggleButton);
61 | });
62 | }
63 |
64 | function markInitiallyHiddenRows() {
65 | const rows = document.querySelectorAll('#torrent-table tr.hidden:not(.initially-hidden)');
66 | rows.forEach(row => row.classList.add('initially-hidden'));
67 | }
68 |
69 | function applySettings() {
70 | const rowGroups = document.querySelectorAll('.basic-movie-list__torrent-edition__sub');
71 | const applyForMultipleGroupsOnly = GM_config.get('applyForMultipleGroupsOnly');
72 |
73 | if (!applyForMultipleGroupsOnly || rowGroups.length > 1) {
74 | rowGroups.forEach(rowGroup => {
75 | const labelText = rowGroup.textContent.trim();
76 | const isChecked = GM_getValue(`setting-${labelText}`, true);
77 | const row = rowGroup.closest('tr');
78 | toggleRowGroup(row, !isChecked);
79 | const toggleButton = rowGroup.parentElement.querySelector('.row-toggle-button');
80 | if (toggleButton) {
81 | toggleButton.innerHTML = isChecked ? '(Hide)' : '(Show)';
82 | }
83 | });
84 | }
85 | }
86 |
87 | function createSettingsMenu() {
88 | const rowGroups = document.querySelectorAll('.basic-movie-list__torrent-edition__sub');
89 | const fields = {
90 | applyForMultipleGroupsOnly: {
91 | label: "Don't hide group if it's the only group",
92 | type: 'checkbox',
93 | default: GM_getValue('applyForMultipleGroupsOnly', true)
94 | }
95 | };
96 |
97 | rowGroups.forEach((rowGroup, index) => {
98 | const labelText = rowGroup.textContent.trim();
99 | fields[`setting-${labelText}`] = {
100 | label: labelText,
101 | type: 'checkbox',
102 | default: GM_getValue(`setting-${labelText}`, true)
103 | };
104 | });
105 |
106 | GM_config.init({
107 | id: 'TorrentRowToggleConfig',
108 | title: 'Torrent Row Toggle Settings',
109 | fields: fields,
110 | css: `
111 | #TorrentRowToggleConfig {
112 | background: #333333;
113 | margin: 0;
114 | padding: 20px 20px;
115 | width: 90%; /* Adjust the width as needed */
116 | max-width: 500px; /* Optional: Set a max-width */
117 | }
118 | #TorrentRowToggleConfig .field_label {
119 | color: #fff;
120 | width: 90%;
121 | }
122 | #TorrentRowToggleConfig .config_header {
123 | color: #fff;
124 | padding-bottom: 10px;
125 | font-weight: 100;
126 | }
127 | #TorrentRowToggleConfig .reset {
128 | color: #e8d3d3;
129 | text-decoration: none;
130 | }
131 | #TorrentRowToggleConfig .config_var {
132 | display: flex;
133 | flex-direction: row;
134 | text-align: left;
135 | justify-content: center;
136 | align-items: center;
137 | width: 90%; /* Adjust the width to fit the new background size */
138 | margin: 4px auto;
139 | padding: 4px 0;
140 | border-bottom: 1px solid #7470703d;
141 | }
142 | #TorrentRowToggleConfig_buttons_holder {
143 | display: grid;
144 | gap: 10px;
145 | grid-template-columns: 1fr 1fr 1fr;
146 | grid-template-rows: 1fr 1fr 1fr;
147 | width: 90%; /* Adjust the width to fit the new background size */
148 | height: 100px;
149 | margin: 0 auto;
150 | text-align: center;
151 | align-items: center;
152 | }
153 | #TorrentRowToggleConfig_saveBtn {
154 | grid-column: 1;
155 | grid-row: 1;
156 | cursor: pointer;
157 | }
158 | #TorrentRowToggleConfig_closeBtn {
159 | grid-column: 3;
160 | grid-row: 1;
161 | cursor: pointer;
162 | }
163 | #TorrentRowToggleConfig .reset_holder {
164 | grid-column: 2;
165 | grid-row: 2;
166 | }
167 | #TorrentRowToggleConfig .config_var input[type="checkbox"] {
168 | cursor: pointer;
169 | }
170 | `,
171 | events: {
172 | open: function (doc) {
173 | let style = this.frame.style;
174 | style.width = "500px"; // Adjust the width as needed
175 | style.height = "400px"; // Adjust the height as needed
176 | style.inset = "";
177 | style.top = "10%"; // Adjust the top position as needed
178 | style.right = "10%"; // Adjust the right position as needed
179 | style.borderRadius = "10px"; // Adjust the border radius as needed
180 | console.log("Config window opened");
181 |
182 | // Add tooltips
183 | for (const field in fields) {
184 | if (fields.hasOwnProperty(field) && fields[field].tooltip) {
185 | let label = doc.querySelector(`label[for="TorrentRowToggleConfig_field_${field}"]`);
186 | if (label) {
187 | label.title = fields[field].tooltip;
188 | }
189 | }
190 | }
191 | },
192 | save: function () {
193 | const fields = GM_config.fields;
194 | for (const field in fields) {
195 | if (fields.hasOwnProperty(field)) {
196 | const isChecked = GM_config.get(field);
197 | GM_setValue(field, isChecked);
198 | }
199 | }
200 |
201 | const rowGroups = document.querySelectorAll('.basic-movie-list__torrent-edition__sub');
202 | rowGroups.forEach(rowGroup => {
203 | const labelText = rowGroup.textContent.trim();
204 | const isChecked = GM_config.get(`setting-${labelText}`);
205 | GM_setValue(`setting-${labelText}`, isChecked);
206 | const row = rowGroup.closest('tr');
207 | toggleRowGroup(row, !isChecked);
208 | const toggleButton = rowGroup.parentElement.querySelector('.row-toggle-button');
209 | if (toggleButton) {
210 | toggleButton.innerHTML = isChecked ? '(Hide)' : '(Show)';
211 | }
212 | });
213 | }
214 | }
215 | });
216 |
217 | GM_registerMenuCommand("Toggle Settings", () => { GM_config.open(); });
218 | }
219 |
220 | function initializeScript() {
221 | console.log('Initializing group hidden script');
222 | if (!document.querySelector('body').classList.contains('script-initialized')) {
223 | document.querySelector('body').classList.add('script-initialized');
224 | markInitiallyHiddenRows();
225 | }
226 | createSettingsMenu();
227 | addRowGroupToggleButtons();
228 | applySettings();
229 | }
230 |
231 | (function () {
232 | 'use strict';
233 |
234 | initializeScript();
235 |
236 | document.addEventListener('PTPAddReleasesFromOtherTrackersComplete', () => {
237 | console.log("Rerunning to hide added releases");
238 | setTimeout(() => {
239 | createSettingsMenu();
240 | addRowGroupToggleButtons();
241 | applySettings();
242 | }, 100);
243 | });
244 |
245 | document.addEventListener('AddReleasesStatusChanged', () => {
246 | console.log('AddReleasesStatusChanged event triggered');
247 | setTimeout(() => {
248 | createSettingsMenu();
249 | addRowGroupToggleButtons();
250 | applySettings();
251 | }, 10);
252 | });
253 | })();
--------------------------------------------------------------------------------
/release-name-parser.js:
--------------------------------------------------------------------------------
1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ts = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i S01
141 | * The old regex would only capture seasons when there was an included episode
142 | * The new regex will capture the S01 but needs to trim the S before parsing
143 | */
144 | if (key === 'season' && clean[0].match(/[Ss]/)) {
145 | clean = clean.slice(1);
146 | }
147 |
148 | clean = parseInt(clean, 10);
149 | }
150 | }
151 |
152 | if(key === 'group') {
153 | if(clean.match(patterns.codec) || clean.match(patterns.quality)) {
154 | continue;
155 | }
156 |
157 | if(clean.match(/[^ ]+ [^ ]+ .+/)) {
158 | key = 'episodeName';
159 | }
160 |
161 | /**
162 | * If the container name is at the end of the string it will get parsed with a group name (".mkv")
163 | * This will remove the group from the string if matches are found against the container pattern.
164 | */
165 | let containerMatch;
166 | if (containerMatch = clean.match(patterns.container)) {
167 | if (containerMatch[0] && containerMatch[1]) {
168 | clean = clean.replace(containerMatch[0], '');
169 | }
170 | }
171 | }
172 |
173 | /**
174 | * In an effort to catch any version of DTS which includes HD Master Audio
175 | * we can end up with trailing hyphens because DTS-HD-MA... needs to be captured
176 | * which means DTS-/DTS-HD/DTS-HD-MA- will also capture.
177 | * This will shave off the trailing separator
178 | */
179 | if (key === 'audio' && clean[clean.length - 1].match(/[\-\. ]/g)) {
180 | clean = clean.slice(0, -1);
181 | }
182 |
183 | part = {
184 | name: key,
185 | match: match,
186 | raw: match[index.raw],
187 | clean: clean
188 | };
189 |
190 | if(key === 'episode') {
191 | core.emit('map', torrent.name.replace(part.raw, '{episode}'));
192 | }
193 |
194 | core.emit('part', part);
195 | }
196 |
197 | core.emit('common');
198 | });
199 |
200 | core.on('late', function (part) {
201 | if(part.name === 'group') {
202 | core.emit('part', part);
203 | }
204 | else if(part.name === 'episodeName') {
205 | part.clean = part.clean.replace(/[\._]/g, ' ');
206 | part.clean = part.clean.replace(/_+$/, '').trim();
207 | core.emit('part', part);
208 | }
209 | });
210 |
211 | },{"../core":1}],4:[function(require,module,exports){
212 | 'use strict';
213 |
214 | var core = require('../core');
215 |
216 | var torrent, raw, groupRaw;
217 | var escapeRegex = function(string) {
218 | return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
219 | };
220 |
221 | core.on('setup', function (data) {
222 | torrent = data;
223 | raw = torrent.name;
224 | groupRaw = '';
225 | });
226 |
227 | core.on('part', function (part) {
228 | if(part.name === 'excess') {
229 | return;
230 | }
231 | else if(part.name === 'group') {
232 | groupRaw = part.raw;
233 | }
234 |
235 | // remove known parts from the excess
236 | raw = raw.replace(part.raw, '');
237 | });
238 |
239 | core.on('map', function (map) {
240 | torrent.map = map;
241 | });
242 |
243 | core.on('end', function () {
244 | var clean, groupPattern, episodeNamePattern;
245 |
246 | // clean up excess
247 | clean = raw.replace(/(^[-\. ]+)|([-\. ]+$)/g, '');
248 | clean = clean.replace(/[\(\)\/]/g, ' ');
249 | clean = clean.split(/\.\.+| +/).filter(Boolean);
250 |
251 | if(clean.length !== 0) {
252 | groupPattern = escapeRegex(clean[clean.length - 1] + groupRaw) + '$';
253 |
254 | if(torrent.name.match(new RegExp(groupPattern))) {
255 | core.emit('late', {
256 | name: 'group',
257 | clean: clean.pop() + groupRaw
258 | });
259 | }
260 |
261 | if(torrent.map && clean[0]) {
262 | episodeNamePattern = '{episode}' + escapeRegex(clean[0].replace(/_+$/, ''));
263 |
264 | if(torrent.map.match(new RegExp(episodeNamePattern))) {
265 | core.emit('late', {
266 | name: 'episodeName',
267 | clean: clean.shift()
268 | });
269 | }
270 | }
271 | }
272 |
273 | if(clean.length !== 0) {
274 | core.emit('part', {
275 | name: 'excess',
276 | raw: raw,
277 | clean: clean.length === 1 ? clean[0] : clean
278 | });
279 | }
280 | });
281 |
282 | },{"../core":1}],5:[function(require,module,exports){
283 | 'use strict';
284 |
285 | var core = require('../core');
286 |
287 | require('./common');
288 |
289 | var torrent, start, end, raw;
290 |
291 | core.on('setup', function (data) {
292 | torrent = data;
293 | start = 0;
294 | end = undefined;
295 | raw = undefined;
296 | });
297 |
298 | core.on('part', function (part) {
299 | if(!part.match) {
300 | return;
301 | }
302 |
303 | if(part.match.index === 0) {
304 | start = part.match[0].length;
305 |
306 | return;
307 | }
308 |
309 | if(!end || part.match.index < end) {
310 | end = part.match.index;
311 | }
312 | });
313 |
314 | core.on('common', function () {
315 | var raw = end ? torrent.name.substr(start, end - start).split('(')[0] : torrent.name;
316 | var clean = raw;
317 |
318 | // clean up title
319 | clean = raw.replace(/^ -/, '');
320 |
321 | if(clean.indexOf(' ') === -1 && clean.indexOf('.') !== -1) {
322 | clean = clean.replace(/\./g, ' ');
323 | }
324 |
325 | clean = clean.replace(/_/g, ' ');
326 | clean = clean.replace(/([\(_]|- )$/, '').trim();
327 |
328 | core.emit('part', {
329 | name: 'title',
330 | raw: raw,
331 | clean: clean
332 | });
333 | });
334 |
335 | },{"../core":1,"./common":3}],6:[function(require,module,exports){
336 | // Copyright Joyent, Inc. and other Node contributors.
337 | //
338 | // Permission is hereby granted, free of charge, to any person obtaining a
339 | // copy of this software and associated documentation files (the
340 | // "Software"), to deal in the Software without restriction, including
341 | // without limitation the rights to use, copy, modify, merge, publish,
342 | // distribute, sublicense, and/or sell copies of the Software, and to permit
343 | // persons to whom the Software is furnished to do so, subject to the
344 | // following conditions:
345 | //
346 | // The above copyright notice and this permission notice shall be included
347 | // in all copies or substantial portions of the Software.
348 | //
349 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
350 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
351 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
352 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
353 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
354 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
355 | // USE OR OTHER DEALINGS IN THE SOFTWARE.
356 |
357 | 'use strict';
358 |
359 | var R = typeof Reflect === 'object' ? Reflect : null
360 | var ReflectApply = R && typeof R.apply === 'function'
361 | ? R.apply
362 | : function ReflectApply(target, receiver, args) {
363 | return Function.prototype.apply.call(target, receiver, args);
364 | }
365 |
366 | var ReflectOwnKeys
367 | if (R && typeof R.ownKeys === 'function') {
368 | ReflectOwnKeys = R.ownKeys
369 | } else if (Object.getOwnPropertySymbols) {
370 | ReflectOwnKeys = function ReflectOwnKeys(target) {
371 | return Object.getOwnPropertyNames(target)
372 | .concat(Object.getOwnPropertySymbols(target));
373 | };
374 | } else {
375 | ReflectOwnKeys = function ReflectOwnKeys(target) {
376 | return Object.getOwnPropertyNames(target);
377 | };
378 | }
379 |
380 | function ProcessEmitWarning(warning) {
381 | if (console && console.warn) console.warn(warning);
382 | }
383 |
384 | var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
385 | return value !== value;
386 | }
387 |
388 | function EventEmitter() {
389 | EventEmitter.init.call(this);
390 | }
391 | module.exports = EventEmitter;
392 | module.exports.once = once;
393 |
394 | // Backwards-compat with node 0.10.x
395 | EventEmitter.EventEmitter = EventEmitter;
396 |
397 | EventEmitter.prototype._events = undefined;
398 | EventEmitter.prototype._eventsCount = 0;
399 | EventEmitter.prototype._maxListeners = undefined;
400 |
401 | // By default EventEmitters will print a warning if more than 10 listeners are
402 | // added to it. This is a useful default which helps finding memory leaks.
403 | var defaultMaxListeners = 10;
404 |
405 | function checkListener(listener) {
406 | if (typeof listener !== 'function') {
407 | throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
408 | }
409 | }
410 |
411 | Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
412 | enumerable: true,
413 | get: function() {
414 | return defaultMaxListeners;
415 | },
416 | set: function(arg) {
417 | if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
418 | throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
419 | }
420 | defaultMaxListeners = arg;
421 | }
422 | });
423 |
424 | EventEmitter.init = function() {
425 |
426 | if (this._events === undefined ||
427 | this._events === Object.getPrototypeOf(this)._events) {
428 | this._events = Object.create(null);
429 | this._eventsCount = 0;
430 | }
431 |
432 | this._maxListeners = this._maxListeners || undefined;
433 | };
434 |
435 | // Obviously not all Emitters should be limited to 10. This function allows
436 | // that to be increased. Set to zero for unlimited.
437 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
438 | if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
439 | throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
440 | }
441 | this._maxListeners = n;
442 | return this;
443 | };
444 |
445 | function _getMaxListeners(that) {
446 | if (that._maxListeners === undefined)
447 | return EventEmitter.defaultMaxListeners;
448 | return that._maxListeners;
449 | }
450 |
451 | EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
452 | return _getMaxListeners(this);
453 | };
454 |
455 | EventEmitter.prototype.emit = function emit(type) {
456 | var args = [];
457 | for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
458 | var doError = (type === 'error');
459 |
460 | var events = this._events;
461 | if (events !== undefined)
462 | doError = (doError && events.error === undefined);
463 | else if (!doError)
464 | return false;
465 |
466 | // If there is no 'error' event listener then throw.
467 | if (doError) {
468 | var er;
469 | if (args.length > 0)
470 | er = args[0];
471 | if (er instanceof Error) {
472 | // Note: The comments on the `throw` lines are intentional, they show
473 | // up in Node's output if this results in an unhandled exception.
474 | throw er; // Unhandled 'error' event
475 | }
476 | // At least give some kind of context to the user
477 | var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
478 | err.context = er;
479 | throw err; // Unhandled 'error' event
480 | }
481 |
482 | var handler = events[type];
483 |
484 | if (handler === undefined)
485 | return false;
486 |
487 | if (typeof handler === 'function') {
488 | ReflectApply(handler, this, args);
489 | } else {
490 | var len = handler.length;
491 | var listeners = arrayClone(handler, len);
492 | for (var i = 0; i < len; ++i)
493 | ReflectApply(listeners[i], this, args);
494 | }
495 |
496 | return true;
497 | };
498 |
499 | function _addListener(target, type, listener, prepend) {
500 | var m;
501 | var events;
502 | var existing;
503 |
504 | checkListener(listener);
505 |
506 | events = target._events;
507 | if (events === undefined) {
508 | events = target._events = Object.create(null);
509 | target._eventsCount = 0;
510 | } else {
511 | // To avoid recursion in the case that type === "newListener"! Before
512 | // adding it to the listeners, first emit "newListener".
513 | if (events.newListener !== undefined) {
514 | target.emit('newListener', type,
515 | listener.listener ? listener.listener : listener);
516 |
517 | // Re-assign `events` because a newListener handler could have caused the
518 | // this._events to be assigned to a new object
519 | events = target._events;
520 | }
521 | existing = events[type];
522 | }
523 |
524 | if (existing === undefined) {
525 | // Optimize the case of one listener. Don't need the extra array object.
526 | existing = events[type] = listener;
527 | ++target._eventsCount;
528 | } else {
529 | if (typeof existing === 'function') {
530 | // Adding the second element, need to change to array.
531 | existing = events[type] =
532 | prepend ? [listener, existing] : [existing, listener];
533 | // If we've already got an array, just append.
534 | } else if (prepend) {
535 | existing.unshift(listener);
536 | } else {
537 | existing.push(listener);
538 | }
539 |
540 | // Check for listener leak
541 | m = _getMaxListeners(target);
542 | if (m > 0 && existing.length > m && !existing.warned) {
543 | existing.warned = true;
544 | // No error code for this since it is a Warning
545 | // eslint-disable-next-line no-restricted-syntax
546 | var w = new Error('Possible EventEmitter memory leak detected. ' +
547 | existing.length + ' ' + String(type) + ' listeners ' +
548 | 'added. Use emitter.setMaxListeners() to ' +
549 | 'increase limit');
550 | w.name = 'MaxListenersExceededWarning';
551 | w.emitter = target;
552 | w.type = type;
553 | w.count = existing.length;
554 | ProcessEmitWarning(w);
555 | }
556 | }
557 |
558 | return target;
559 | }
560 |
561 | EventEmitter.prototype.addListener = function addListener(type, listener) {
562 | return _addListener(this, type, listener, false);
563 | };
564 |
565 | EventEmitter.prototype.on = EventEmitter.prototype.addListener;
566 |
567 | EventEmitter.prototype.prependListener =
568 | function prependListener(type, listener) {
569 | return _addListener(this, type, listener, true);
570 | };
571 |
572 | function onceWrapper() {
573 | if (!this.fired) {
574 | this.target.removeListener(this.type, this.wrapFn);
575 | this.fired = true;
576 | if (arguments.length === 0)
577 | return this.listener.call(this.target);
578 | return this.listener.apply(this.target, arguments);
579 | }
580 | }
581 |
582 | function _onceWrap(target, type, listener) {
583 | var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
584 | var wrapped = onceWrapper.bind(state);
585 | wrapped.listener = listener;
586 | state.wrapFn = wrapped;
587 | return wrapped;
588 | }
589 |
590 | EventEmitter.prototype.once = function once(type, listener) {
591 | checkListener(listener);
592 | this.on(type, _onceWrap(this, type, listener));
593 | return this;
594 | };
595 |
596 | EventEmitter.prototype.prependOnceListener =
597 | function prependOnceListener(type, listener) {
598 | checkListener(listener);
599 | this.prependListener(type, _onceWrap(this, type, listener));
600 | return this;
601 | };
602 |
603 | // Emits a 'removeListener' event if and only if the listener was removed.
604 | EventEmitter.prototype.removeListener =
605 | function removeListener(type, listener) {
606 | var list, events, position, i, originalListener;
607 |
608 | checkListener(listener);
609 |
610 | events = this._events;
611 | if (events === undefined)
612 | return this;
613 |
614 | list = events[type];
615 | if (list === undefined)
616 | return this;
617 |
618 | if (list === listener || list.listener === listener) {
619 | if (--this._eventsCount === 0)
620 | this._events = Object.create(null);
621 | else {
622 | delete events[type];
623 | if (events.removeListener)
624 | this.emit('removeListener', type, list.listener || listener);
625 | }
626 | } else if (typeof list !== 'function') {
627 | position = -1;
628 |
629 | for (i = list.length - 1; i >= 0; i--) {
630 | if (list[i] === listener || list[i].listener === listener) {
631 | originalListener = list[i].listener;
632 | position = i;
633 | break;
634 | }
635 | }
636 |
637 | if (position < 0)
638 | return this;
639 |
640 | if (position === 0)
641 | list.shift();
642 | else {
643 | spliceOne(list, position);
644 | }
645 |
646 | if (list.length === 1)
647 | events[type] = list[0];
648 |
649 | if (events.removeListener !== undefined)
650 | this.emit('removeListener', type, originalListener || listener);
651 | }
652 |
653 | return this;
654 | };
655 |
656 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
657 |
658 | EventEmitter.prototype.removeAllListeners =
659 | function removeAllListeners(type) {
660 | var listeners, events, i;
661 |
662 | events = this._events;
663 | if (events === undefined)
664 | return this;
665 |
666 | // not listening for removeListener, no need to emit
667 | if (events.removeListener === undefined) {
668 | if (arguments.length === 0) {
669 | this._events = Object.create(null);
670 | this._eventsCount = 0;
671 | } else if (events[type] !== undefined) {
672 | if (--this._eventsCount === 0)
673 | this._events = Object.create(null);
674 | else
675 | delete events[type];
676 | }
677 | return this;
678 | }
679 |
680 | // emit removeListener for all listeners on all events
681 | if (arguments.length === 0) {
682 | var keys = Object.keys(events);
683 | var key;
684 | for (i = 0; i < keys.length; ++i) {
685 | key = keys[i];
686 | if (key === 'removeListener') continue;
687 | this.removeAllListeners(key);
688 | }
689 | this.removeAllListeners('removeListener');
690 | this._events = Object.create(null);
691 | this._eventsCount = 0;
692 | return this;
693 | }
694 |
695 | listeners = events[type];
696 |
697 | if (typeof listeners === 'function') {
698 | this.removeListener(type, listeners);
699 | } else if (listeners !== undefined) {
700 | // LIFO order
701 | for (i = listeners.length - 1; i >= 0; i--) {
702 | this.removeListener(type, listeners[i]);
703 | }
704 | }
705 |
706 | return this;
707 | };
708 |
709 | function _listeners(target, type, unwrap) {
710 | var events = target._events;
711 |
712 | if (events === undefined)
713 | return [];
714 |
715 | var evlistener = events[type];
716 | if (evlistener === undefined)
717 | return [];
718 |
719 | if (typeof evlistener === 'function')
720 | return unwrap ? [evlistener.listener || evlistener] : [evlistener];
721 |
722 | return unwrap ?
723 | unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
724 | }
725 |
726 | EventEmitter.prototype.listeners = function listeners(type) {
727 | return _listeners(this, type, true);
728 | };
729 |
730 | EventEmitter.prototype.rawListeners = function rawListeners(type) {
731 | return _listeners(this, type, false);
732 | };
733 |
734 | EventEmitter.listenerCount = function(emitter, type) {
735 | if (typeof emitter.listenerCount === 'function') {
736 | return emitter.listenerCount(type);
737 | } else {
738 | return listenerCount.call(emitter, type);
739 | }
740 | };
741 |
742 | EventEmitter.prototype.listenerCount = listenerCount;
743 | function listenerCount(type) {
744 | var events = this._events;
745 |
746 | if (events !== undefined) {
747 | var evlistener = events[type];
748 |
749 | if (typeof evlistener === 'function') {
750 | return 1;
751 | } else if (evlistener !== undefined) {
752 | return evlistener.length;
753 | }
754 | }
755 |
756 | return 0;
757 | }
758 |
759 | EventEmitter.prototype.eventNames = function eventNames() {
760 | return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
761 | };
762 |
763 | function arrayClone(arr, n) {
764 | var copy = new Array(n);
765 | for (var i = 0; i < n; ++i)
766 | copy[i] = arr[i];
767 | return copy;
768 | }
769 |
770 | function spliceOne(list, index) {
771 | for (; index + 1 < list.length; index++)
772 | list[index] = list[index + 1];
773 | list.pop();
774 | }
775 |
776 | function unwrapListeners(arr) {
777 | var ret = new Array(arr.length);
778 | for (var i = 0; i < ret.length; ++i) {
779 | ret[i] = arr[i].listener || arr[i];
780 | }
781 | return ret;
782 | }
783 |
784 | function once(emitter, name) {
785 | return new Promise(function (resolve, reject) {
786 | function errorListener(err) {
787 | emitter.removeListener(name, resolver);
788 | reject(err);
789 | }
790 |
791 | function resolver() {
792 | if (typeof emitter.removeListener === 'function') {
793 | emitter.removeListener('error', errorListener);
794 | }
795 | resolve([].slice.call(arguments));
796 | };
797 |
798 | eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
799 | if (name !== 'error') {
800 | addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
801 | }
802 | });
803 | }
804 |
805 | function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
806 | if (typeof emitter.on === 'function') {
807 | eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
808 | }
809 | }
810 |
811 | function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
812 | if (typeof emitter.on === 'function') {
813 | if (flags.once) {
814 | emitter.once(name, listener);
815 | } else {
816 | emitter.on(name, listener);
817 | }
818 | } else if (typeof emitter.addEventListener === 'function') {
819 | // EventTarget does not have `error` event semantics like Node
820 | // EventEmitters, we do not listen for `error` events here.
821 | emitter.addEventListener(name, function wrapListener(arg) {
822 | // IE does not have builtin `{ once: true }` support so we
823 | // have to do it manually.
824 | if (flags.once) {
825 | emitter.removeEventListener(name, wrapListener);
826 | }
827 | listener(arg);
828 | });
829 | } else {
830 | throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
831 | }
832 | }
833 |
834 | },{}]},{},[2])(2)
835 | });
--------------------------------------------------------------------------------
/scene_groups.js:
--------------------------------------------------------------------------------
1 | const sceneGroups = [
2 | "0MNiDVD", "0TV", "1920", "2HD", "2PaCaVeLi", "420RipZ", "433", "4FR", "4HM", "4kHD", "7SinS",
3 | "A4O", "aAF", "AaS", "ABEZ", "ABD", "ACCLAIM", "ACED", "ADHD", "ADMiRALS", "adrenaline", "AEGiS",
4 | "AEN", "AEROHOLiCS", "AFO", "aGGr0", "AiR3D", "AiRFORCE", "AiRLiNE", "AiRWAVES", "ALDi", "ALLiANCE",
5 | "ALLURE", "AMIABLE", "AMBiTiOUS", "AMRAP", "AMSTEL", "ANARCHY", "aNBc", "ANGELiC", "ANiHLS",
6 | "ANiVCD", "ApL", "AQUA", "ARCHiViST", "Argon", "ARiGOLD", "ARiSCO", "ARiSCRAPAYSiTES", "ARROW",
7 | "ARTHOUSE", "ASAP", "AsiSter", "aTerFalleT", "ATS", "AVCDVD", "AVCHD", "AVS", "AVS720", "aWake",
8 | "AzNiNVASiAN", "AZuRRaY", "BaCKToRG", "BaCo", "BAJSKORV", "BAKED", "BaLD", "BALLS", "BAMBOOZLE",
9 | "BAND1D0S", "BARGE", "BATV", "BAVARiA", "BAWLS", "BBDvDR", "BDA", "BDDK", "BDGRP", "BDMV", "BDP",
10 | "BestHD", "BeStDivX", "BETAMAX", "BFF", "BiA", "BiEN", "BiERBUiKEN", "BiFOS", "BiGBruv", "BiGFiL",
11 | "BiPOLAR", "BiRDHOUSE", "BLooDWeiSeR", "B0MBARDiERS", "BountyHunters", "BOSSLiKE", "BOW", "BRASTEMP", "BRDC",
12 | "BREiN", "BrG", "BRICKSQUaD", "BRiGANDBiL", "BRMP", "BRUTUS", "BTVG", "BULLDOZER", "BURGER", "BWB",
13 | "c0nFuSed", "C4TV", "CADAVER", "CAELUM", "CAFFiNE", "CAMELOT", "CAMERA", "CarVeR", "CBGB", "CBFM", "CCAT",
14 | "CCT", "CDP", "Centropy", "CFE", "CFH", "CG", "Chakra", "CHaWiN", "CHECKMATE", "CHRONO", "CiA",
15 | "CiELO", "CiNEFiLE", "Cinemaniacs", "CiNEMATiC", "CiNEVCD", "CiPA", "CiRCLE", "CiTRiN", "CLASSiC",
16 | "ClassicBluray", "CLiX", "CLUE", "CMBHD", "CME", "CNHD", "cNLDVDR", "COALiTiON", "COASTER",
17 | "COCAIN", "COJONUDO", "COMPRISED", "COMPULSiON", "CONDITION", "CONSCiENCE", "CONVOY",
18 | "CookieMonster", "Counterfeit", "COUP", "CoWRY", "CRAVERS", "CREED", "CREEPSHOW", "CRiMSON",
19 | "CROSSBOW", "CROSSFiT", "CROOKS", "CRUCiAL", "CTU", "CULTHD", "CULTXviD", "CYBERMEN",
20 | "D0PE", "D3Si", "DA", "DAA", "DAH", "DarduS", "DARKFLiX", "DASH", "DATA", "DcN", "DCP", "DDX",
21 | "DEADPiXEL", "DEADPOOL", "DEAL", "DeBCz", "DeBijenkorf", "DeBTViD", "DEFACED", "DEFLATE",
22 | "DEFUSED", "DEiMOS", "DEiTY", "DEMAND", "DEPRAViTY", "DEPTH", "DERANGED", "DFA", "DGX", "DHD",
23 | "DiAMOND", "DiCH", "DIE", "DigitalVX", "DIMENSION", "DioXidE", "DiSPLAY", "DiSPOSABLE", "DivBD",
24 | "DiVERGE", "DiVERSE", "DiVXCZ", "DJUNGELVRAL", "DMT", "DNA", "DnB", "DNR", "DOCERE", "DOCUMENT",
25 | "DOMiNATE", "DOMiNiON", "DOMiNO", "DoNE", "DoR2", "dotTV", "DOWN", "DPiMP", "DRABBITS",
26 | "DREAMLiGHT", "DROiDS", "DTFS", "DUH", "DuSS", "DvF", "DVL", "DvP",
27 | "EDHD", "EDRP", "EDUCATiON", "ELiA", "EMERALD", "EPiSODE", "ERyX", "ESPiSE", "ESPN", "ETACH",
28 | "EViLISO", "EVOLVE", "EwDp", "EXiLE", "EXIST3NC3", "EXT", "EXViD", "EXViDiNT",
29 | "FA", "FADE", "FAiLED", "FAIRPLAY", "FEVER", "FFM", "FFNDVD", "FHD", "FiCO", "Fidelio", "FiDO",
30 | "FiHTV", "FilmHD", "FIXIT", "FKKHD", "FLAiR", "FLAiTE", "FLAME", "FliK", "FmE", "FoA", "FORBiDDEN",
31 | "ForceBlue", "FoV", "FQM", "FRAGMENT", "FSiHD", "FTC", "FTP", "FUA", "FULLSiZE", "FUTURiSTiC",
32 | "FUtV", "FZERO",
33 | "GAMETiME", "GAYGAY", "GDR", "GECKOS", "GeiWoYIZhangPiAOBA", "GENESIDE", "GENUiNE", "GERUDO",
34 | "GETiT", "GFW", "GHOULS", "GiMBAP", "GiMCHi", "GL", "GM4F", "GMA", "GMB", "Goatlove", "GOOGLE", "GORE",
35 | "GreenBlade", "GUACAMOLE", "GUYLiAN", "GZP", "GGWP", "GGEZ", "GLHF",
36 | "HACO", "HAGGiS", "HAFVCD", "HALCYON", "HANNIBAL", "HCA", "HD_Leaks", "HD1080", "HD4U",
37 | "HDCLASSiCS", "HDCP", "HDEX", "HDi", "HDMI", "HDpure", "HiFi", "HLS", "HooKah", "HiVE",
38 | "HYGGE", "HTO", "hV", "HYBRiS", "HyDe", "IAMABLE",
39 | "iFN", "iFPD", "iGNiTE", "iGNiTiON", "IGUANA", "iHATE", "iKA", "iLG", "iLLUSiON", "iLS", "iMBT",
40 | "IMMERSE", "iMMORTALS", "iMOVANE", "iNCiTE", "iNCLUSION", "iNFAMOUS", "iNGOT", "iNjECTiON",
41 | "INSECTS", "iNSPiRE", "iNSPiRED", "iNTEGRUM", "iNTiMiD", "iNVANDRAREN", "INVEST", "iSG", "iTWASNTME", "iVL",
42 | "Jackal", "JACKVID", "JAG", "Japhson", "JAR", "JFKXVID", "JKR", "JMT", "JoKeR", "JoLLyRoGeR",
43 | "JUMANJi", "JustWatch",
44 | "KAFFEREP", "KaKa", "KAMERA", "KART3LDVD", "KEG", "KILLERS", "KJS", "KLiNGON", "KNOCK", "KON",
45 | "KSi", "KuDoS", "KYR", "KOGi", "KURRAGOMMA",
46 | "LAJ", "LAP", "Larceny", "LAZERS", "LCHD", "LD", "LEON", "LEViTY", "LiBRARiANS", "LiGHTNiNG",
47 | "LiNE", "LMG", "LOGiES", "LOL", "LOST", "LRC", "LUSO", "LPD", "LUSEKOFTA",
48 | "M14CH0", "MAGiC", "MainEvent", "MAJiKNiNJAZ", "MANGACiTY", "MATCH", "MaxHD", "MEDDY",
49 | "MEDiAMANiACS", "MELBA", "MELiTE", "MenInTights", "METCON", "METiS", "MHQ", "MHT", "MIDDLE",
50 | "MiMiC", "MiNDTHEGAP", "MoA", "MODERN", "MoF", "MoH", "MOMENTUM", "monstermash", "MOTION", "MOVEiT", "MSD",
51 | "MTB", "MuXHD", "MVM", "MVN", "MVS", "M0RETV", "NAISU",
52 | "NANO", "NaWaK", "nDn", "NDRT", "NeDiVx", "NEPTUNE", "NERDHD", "NERV", "NewMov", "NGB", "NGR",
53 | "NHH", "NiCEBD", "NJKV", "NLSGDVDr", "NODLABS", "NoLiMiT", "NORDiCHD", "NORiTE", "NORKiDS",
54 | "NORPiLT", "NOSCREENS", "NoSence", "NrZ", "NAISU", "NOMA", "NORDCUP", "NAISU", "NOMA", "NORDCUP",
55 | "NORUSH",
56 | "o0o", "OCULAR", "OEM", "OLDHAM", "OMERTA", "OMiCRON", "OohLaLa", "OPiUM", "OPTiC", "ORC",
57 | "ORCDVD", "ORENJi", "ORGANiC", "ORiGiNAL", "OSiRiS", "OSiTV", "OUIJA", "OLDTiME", "OLLONBORRE",
58 | "OPUS",
59 | "P0W4", "P0W4DVD", "PARASiTE", "PARTiCLE", "PaTHe", "PCH", "PFa", "PHASE", "PiRATE", "PiX",
60 | "PKPTRS", "BLACKPANTERS", "PiNKPANTERS", "PQPTRS", "PLUTONiUM", "PostX", "PoT", "PRECELL", "PREMiER",
61 | "PRiDEHD", "PRiNCE", "PROGRESS", "PROMiSE", "ProPL", "PROVOKE", "PROXY", "PSYCHD", "PTi",
62 | "PTWINNER", "PEGASUS", "PVR",
63 | "QCF", "QiM", "QiX", "QPEL", "QRUS",
64 | "R3QU3ST", "R4Z3R", "RCDiVX", "RedBlade", "REFiNED", "REGEXP", "REiGN", "RELEASE", "Replica",
65 | "REPRiS", "Republic", "REQ", "RESISTANCE", "RetailBD", "RETRO", "REVEiLLE", "REVOLTE", "REWARD",
66 | "RF", "RFtA", "RiFF", "RiTALiN", "RiTALiX", "RiVER", "ROOR", "ROUGH", "ROUNDROBIN", "ROVERS",
67 | "RRH", "RTA", "RUBY", "RUNNER", "RUSTED", "RUSTLE", "Ryotox", "RADiOACTiVE",
68 | "SADPANDA", "SAiMORNY", "SAiNTS", "SAPHiRE", "SATIVA", "SBC", "SCARED", "SChiZO", "SCREAM",
69 | "SD6", "SECRETOS", "SECTOR7", "SEiGHT", "SEMTEX", "SEPTiC", "SERUM", "SFM", "SharpHD",
70 | "SHOCKWAVE", "SHORTBREHD", "SiNNERS", "SKA", "SKGTV", "SLeTDiVX", "SomeTV", "SONiDO", "SORNY",
71 | "SPARKS", "SPLiNTER", "SPOOKS", "SPRiNTER", "SQUEAK", "SQUEEZE", "SRiZ", "SRP", "ss", "SSF",
72 | "SPAMnEGGS", "STRINGERBELL", "STRiFE", "STRONG", "StyleZz", "SUBTiTLES", "SUM", "SUPERiER",
73 | "SUPERSIZE", "SuPReME", "SURCODE", "SVA", "SVD", "SVENNE", "SKRiTT", "SKYFiRE", "SLOT", "SCONES", "SENPAI",
74 | "TAPAS", "TARGET", "TASTE", "TBS", "TBZ", "TDF", "TEKATE", "TELEViSiON", "TENEIGHTY", "TERMiNAL",
75 | "TFE", "TGP", "TheBatman", "ThEdEaDLiVe", "TheFrail", "THENiGHTMAREiNHD", "TheWretched", "TiDE",
76 | "Tiggzz", "TiiX", "TLA", "tlf", "TNAN", "ToF", "TOPAZ", "TORO", "TRG", "TrickorTreat", "TRiPS",
77 | "TRUEDEF", "TrV", "TUBE", "TURBO", "TURKiSO", "TUSAHD", "TVP", "TWCiSO", "TWiST", "TWiZTED",
78 | "TXF", "TxxZ", "TOOSA", "TABULARiA", "TOOSA", "UNDERTAKERS", "WATCHABLE", "xD2V",
79 | "UAV", "UBiK", "UKDHD", "ULF", "ULSHD", "UltraHD", "UMF", "UNiQUE", "UNiVERSUM", "UNRELiABLE",
80 | "UNSKiLLED", "USELESS", "USURY", "UNTOUCHABLES", "UNTOUCHED", "UNVEiL", "URTV", "USi",
81 | "VALiOMEDiA", "VALUE", "VAMPS", "VCDVaULT", "Vcore", "VeDeTT", "VERUM", "VETO", "VEXHD",
82 | "VH-PROD", "VideoCD", "ViLD", "ViRA", "ViTE", "VoMiT", "vRs", "VST", "VxTXviD",
83 | "W4F", "WAF", "WaLMaRT", "WastedTime", "WAT", "watchHD", "WAVEY", "waznewz", "WEBSTER",
84 | "WEBTiFUL", "Wednesday29th", "WHEELS", "WHiSKEY", "WhiteRhino", "WHiZZ", "WhoKnow", "WiDE",
85 | "WiKeD", "WiNNiNG", "Wizeguys", "WPi", "WRD", "WATCHABLE", "STRINGERBELL",
86 | "x0DuS", "XanaX", "XANOR", "xCZ", "XMF", "XORBiTANT", "XPERT_HD", "XPRESS", "XR5", "xSCR",
87 | "XSTREEM", "XTV", "xV", "XviK", "xD2V",
88 | "YCDV", "YELLOWBiRD", "YesTV",
89 | "ZEST", "ZZGtv"
90 | ];
91 |
92 | if (typeof window !== 'undefined') {
93 | window.sceneGroups = sceneGroups;
94 | }
--------------------------------------------------------------------------------