├── .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 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 |
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}${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 | } --------------------------------------------------------------------------------