├── README.md └── gazelle_collapse_duplicates.user.js /README.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | This is a userscript. You need a compatible browser with an extension to run this type of scripts. 3 | 4 | If you need help on setting up your browser, have a look at [greasyfork](https://greasyfork.org/en). 5 | 6 | # Installation 7 | Simply point your browser to the [raw](https://github.com/colligere/collapse_duplicates/raw/master/gazelle_collapse_duplicates.user.js) version of the script. Your browser add-on will automatically prompt you to install the script. 8 | -------------------------------------------------------------------------------- /gazelle_collapse_duplicates.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name gazelle collapse duplicates 3 | // @include /https?://www\.empornium\.(me|sx|is)/torrents\.php.*/ 4 | // @exclude /https?://www\.empornium\.(me|sx|is)/torrents\.php\?id.*/ 5 | // @include /https?://www\.empornium\.(me|sx|is)/user\.php.*/ 6 | // @include /https?://femdomcult\.org/torrents\.php.*/ 7 | // @exclude /https?://femdomcult\.org/torrents\.php\?id.*/ 8 | // @include /https?://femdomcult\.org/user\.php.*/ 9 | // @include /https?://www\.cheggit\.me/torrents\.php.*/ 10 | // @exclude /https?://www\.cheggit\.me/torrents\.php\?id.*/ 11 | // @include /https?://www\.cheggit\.me/user\.php.*/ 12 | // @include /https?://pornbay\.org/torrents\.php.*/ 13 | // @exclude /https?://pornbay\.org/torrents\.php\?id.*/ 14 | // @include /https?://pornbay\.org/user\.php.*/ 15 | // @include /https?://(www\.)?enthralled\.me/torrents\.php.*/ 16 | // @exclude /https?://(www\.)?enthralled\.me/torrents\.php\?id.*/ 17 | // @include /https?://(www\.)?enthralled\.me/user\.php.*/ 18 | // @version 27.0 19 | // @updateURL https://github.com/colligere/collapse_duplicates/raw/master/gazelle_collapse_duplicates.user.js 20 | // @require http://code.jquery.com/jquery-2.1.1.js 21 | // @require https://raw.githubusercontent.com/jashkenas/underscore/1.8.3/underscore.js 22 | // @grant GM.setValue 23 | // @grant GM.getValue 24 | // @grant GM_setValue 25 | // @grant GM_getValue 26 | // ==/UserScript== 27 | 28 | 'use strict'; 29 | 30 | // About 31 | // This userscript groups variations (different resolution, container, ...) of the same torrent together to unclutter the torrent list. 32 | // 33 | // The original version of this script was written by node998 but hasn't been maintained in a while. I have now forked the script on github to incorporate some recent fixes and additions. 34 | 35 | // Changelog: 36 | // * version 27.0 37 | // - Added an option to collapse chronologically (you'll see the group in the newest post's location) 38 | // * version 26.2 39 | // - Added support for enthralled.me 40 | // * version 26.1 41 | // - Added PSVR variant 42 | // * version 26.0 43 | // - support for pornbay 44 | // - new option to preserve original title 45 | // * version 25.7 46 | // - improved handling of x265 47 | // * version 25.6 48 | // - settings button now shows on the .is domain 49 | // * version 25.5 50 | // - modified regexes for VR (to support occulus go) and resolutions (to support non-standard resolutions) 51 | // * version 25.4 52 | // - added empornium.is domain 53 | // * version 25.3 54 | // - support all *K resolutions 55 | // * version 25.2 56 | // - workaround for violentmonkey bug https://github.com/violentmonkey/violentmonkey/issues/713 57 | // * version 25.1 58 | // - change to download URL 59 | // * version 25 60 | // - Updates for new emp icons 61 | // * version 24.4 62 | // - Added support for new bookmark icons 63 | // * version 24.3 64 | // - Some more compatibility fixes 65 | // * version 24.2 66 | // - Fixed compatibility with direct thumbnails script (at least in vertical layout) 67 | // * version 24.1 68 | // - Disabled the new config options for engines that don't support GM functions 69 | // * version 24 70 | // - Added a configuration dialog (accessible through the user menu on emp) 71 | // - Added options to bring back the emp freeleech/warned/checked icons (off by default) 72 | // - Added horizontal stacking of versions (off by default) 73 | // - Requires 2 extra grants (GM.setValue and GM.getValue) to store settings 74 | // * version 23 75 | // - Improved handling of VR tags 76 | // * version 22 77 | // - Removed absolute positioning for freeleech & warning icons 78 | // * version 21 79 | // - Further GM4 & FF57+ fixes 80 | // - fixed: The script would get executed twice when using the browser back/forward buttons (fix by sapphreak) 81 | // * version 20 82 | // - Compatibility with greasemonkey 4.0 83 | // - Replaced GM_addStyle 84 | // - Removed jQuery.noConflict (fix by sapphreak) 85 | // * version 19 86 | // - improvement: collapse patterns are now category-specific 87 | // * version 18.4 88 | // - improvement: added ipad variation 89 | // * version 18.3 90 | // - improvement: included vive 91 | // - improvement: added @updateURL metadata 92 | // * version 18.2 93 | // - improvement: included game platforms PC and MAC 94 | // * version 18.1 95 | // - fixed: PornBay compatibility (fix by Starbuck) 96 | // * version 18 97 | // - fixed: only one torrent is visible (fix by starbuck) 98 | // - improvement: include variations mobile-high,mobile-medium and mobile-low 99 | // - improvement: included video containers 3gp and mpeg4 100 | // - change: this script is now forked on github 101 | // * version 17 102 | // - added option for freeleech icon 103 | // - added option for warning icon 104 | // * version 16 105 | // - added resolutions 240, 380, 960, 1440, 1600, 1920 106 | // - added image resolutions 1600px, 2000px, 3000px 107 | // - added variations h.265, h265, hevc, uhd 108 | // - added variations Oculus, Playstation 109 | // - added variations "lower bitrate", "higher bitrate" 110 | // - added resolution suffix * 111 | // - added support for pornbay.org 112 | // - added support for cheggit.me 113 | // * version 15 114 | // - added variation lq 115 | // - added variations 30 fps, 60 fps 116 | // - added variations Samsung, Smartphone 117 | // * version 14 118 | // - improved detection of VR variations 119 | // - added resolution 416 120 | // - added bitstream variations 1K, 2K 121 | // * version 13 122 | // - removed leading space on empty version title (without symbols) 123 | // - added comments indicator (links directly to comments section) 124 | // - simplified version title css selector 125 | // - added variation Mps 126 | // - added variation "Oculus Rift" 127 | // - added variation "Virtual Reality" 128 | // - added variations "Desktop VR", "Smartphone VR", "Gear VR", "Oculus VR" 129 | // - added variations ultrahd, hi-res 130 | // - added variations splitsceces, split-scenes, "split sceces" 131 | // * version 12 132 | // - added resolution 405 133 | // - added support for notifications page 134 | // * version 11 135 | // - added freeleech icon ∞ 136 | // - added warning icon ⚑ 137 | // - added option to add tags from collapsed duplicates 138 | // - added resolutions 272, 326, 392, 408, 450 139 | // - moved mov and wmv to video_containers group 140 | // - added variations fhd, mkv 141 | // - added variations "images", "picture set" 142 | // - extracted Group and Version from main code 143 | // - fixed and improved sorting (720p > SD) 144 | // - small changes to css 145 | // * version 10 146 | // - reworked and extracted matching algorithm to separate module with 3 specialized engines 147 | // - added precise trimmers to remove remaining delimiters (space , - / +) while collapsing 148 | // - added support for ** containers 149 | // - added support for {} containers 150 | // - added resolutions 352, 544, 558, 640, 1072 151 | // - added variations hq, uhq, sd, hd 152 | // - added variations "w images" and "with images" 153 | // - normalization for multiple spaces 154 | // - replace left delimiter with space 155 | // - improved performance 125ms -> 87ms 156 | // - less hacky solution for [MP4]s 157 | // - versions and variations are now surrounded by parenthesis [] 158 | // - small adjustment to css 159 | // - new dependency underscore.js 160 | // * version 9 161 | // - fix css for direct thumbnails (when replace_categories is off) 162 | // - added support for femdomcult.org 163 | // * version 8 164 | // - symmetric variations (before or after resolution) 165 | // - support for new variations: web-dl, mov, wmv 166 | // - fixed version title being wrapped on user pages 167 | // - more reliable delimiter 168 | // * version 7 169 | // - download icons for every resolutions 170 | // - checked by staff symbol for versions (replaces icon) 171 | // - bookmark symbol for versions (replaces icon) 172 | // - support for bitrate variation eg. [5Mbps] 173 | // - support for classic resolutions eg. [1920x1080] 174 | // * version 6 175 | // - small changes to patterns 176 | // * version 5 177 | // - small changes to patterns 178 | // * version 4 179 | // - added support for bookmarks 180 | // - fixed duplicates with the same name 181 | // - added default thumbnails on hover 182 | // * version 3 183 | // - added support for stream "[]s" variation 184 | // - added support for "H.265/HEVC" variation that appears after resolution 185 | // - greatly improved sorting for combined versions 186 | // * version 2 187 | // - now it works on user pages 188 | // * version 1 189 | // - initial version 190 | 191 | var comment_icon = [ 192 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAz', 193 | 'klEQVQoU43QPysEABjH8YfJv8tg8wLEaJTBrhRlUYTFcK/gJpuSN6FkvE0mqywG+Rh1y0km', 194 | 'V1d3SbjHcC7uurv0HZ7h95meEMNrn0XnXqWU7pRMdINDXz9jp6qFX7DbM7Z7MtkBD31BOrB', 195 | 'mRYTmAHBsw5ulcDsAbCtJ12FVq89MQUVqhrCj0TPfmHUmpfsQplSkupp3VWWb5lxJqWU9hB', 196 | 'Nl+8YVzFtWdOlTSg17nU+2G7XlwosPNXXp0Vg3+Nu0I89OB4MQRswMByH+Ab4BEZhLmRFDo', 197 | 'vgAAAAASUVORK5CYII=' 198 | ].join(''); 199 | 200 | var css = [ 201 | '/* hide default icons */', 202 | '.torrent .cats_col + td > span, .torrent .cats_cols + td > span {', 203 | ' display: none;', 204 | '}', 205 | '/* exception to display .newtorrent flag */', 206 | '.torrent .cats_col + td > span.newtorrent, .torrent .cats_cols + td > span.newtorrent {', 207 | ' display: initial;', 208 | ' margin-right: 0;', 209 | '}', 210 | '.torrent.collapse-hidden {', 211 | ' display: none;', 212 | '}', 213 | '.torrent .icon {', 214 | ' float: none;', 215 | ' margin-left: 0;', 216 | ' margin-top: 0;', 217 | ' margin-right: 4px;', 218 | ' vertical-align: bottom;', 219 | '}', 220 | '.torrent a[href^="/torrents.php?action=download"] {', 221 | ' margin-left: 0;', 222 | '}', 223 | '.torrent a[href^="torrents.php?action=download"] {', 224 | ' vertical-align: bottom;', 225 | '}', 226 | '.torrent .version .collapsed-title {', 227 | ' display: inline-block;', 228 | ' padding-top: 3px;', 229 | ' vertical-align: top;', 230 | '}', 231 | '.collapsed-freeleech, .collapsed_warning, .collapsed_okay, .collapsed_bookmarked {', 232 | ' margin-left: 5px !important;', 233 | ' top: 2px;', 234 | '}', 235 | '.torrent .version {', 236 | ' white-space: nowrap;', 237 | ' position: relative;', 238 | '}', 239 | '.torrent .version:first-of-type {', 240 | ' padding-top: 3px;', 241 | '}', 242 | '.torrent .version_horizontal {', 243 | ' float: left;', 244 | '}', 245 | '.torrent .version .comment {', 246 | ' background-image: url("' + comment_icon + '");', 247 | ' background-repeat: no-repeat;', 248 | ' background-position: 0 -2px;', 249 | ' padding-left: 19px;', 250 | ' margin-left: 5px;', 251 | ' text-align: right;', 252 | '}', 253 | '.torrent .version .comment_vertical {', 254 | ' position: absolute;', 255 | ' right: 0;', 256 | ' background-position: 0 1px;', 257 | '}', 258 | '.torrent .horizontal_separator {', 259 | ' float: none;', 260 | ' margin-left: 11px;', 261 | ' margin-right: 10px;', 262 | ' vertical-align: super;', 263 | ' color: #004DC0;', 264 | ' font-weight: bold;', 265 | '}', 266 | '.torrent_icon_container {', 267 | ' display: none', 268 | '}', 269 | '.version .icon_stack {', 270 | ' width: 18px;', 271 | ' height: 22px;', 272 | ' animation-play-state: paused;', 273 | '}', 274 | '.version .icon_stack:hover {', 275 | ' animation-play-state: running;', 276 | '}', 277 | '.version img {', 278 | ' vertical-align: bottom;', 279 | ' margin-right: 3px;', 280 | '}', 281 | ].join('\n'); 282 | 283 | // Replacement for GM_addStyle, which isn't available on greasemonkey > v4.0 284 | function add_css(styles, id) { 285 | var params = {'type': "text/css"}; 286 | if (id) params.id = id; 287 | var element = jQuery("", params).html(styles); 288 | jQuery('head').append(element); 289 | } 290 | 291 | 292 | // very fast difference 293 | // can only work on sorted unique arrays 294 | var difference_fast = function (a, b, compare_function) { 295 | var a_idx = 0; 296 | var b_idx = 0; 297 | var a_len = a.length; 298 | var b_len = b.length; 299 | var result = []; 300 | 301 | while (a_idx < a_len && b_idx < b_len) { 302 | switch (compare_function(a[a_idx], b[b_idx])) { 303 | case 0: // common 304 | a_idx++; 305 | b_idx++; 306 | break; 307 | case 1: // only in b 308 | b_idx++; 309 | break; 310 | case -1: // only in a 311 | result.push(a[a_idx]); 312 | a_idx++; 313 | break; 314 | } 315 | } 316 | if (a_idx < a_len && b_idx == b_len) 317 | return result.concat(a.slice(a.idx)); 318 | return result; 319 | }; 320 | 321 | // begin TitleParser 322 | 323 | // like String.prototype.match(//g) but remembers matched groups 324 | var match_and_remember = function (content, pattern) { 325 | var matches = []; 326 | var match; 327 | while ((match = pattern.exec(content))) { 328 | matches.push(match); 329 | } 330 | // reset last index, so pattern can be used again 331 | pattern.lastIndex = 0; 332 | return matches; 333 | }; 334 | 335 | // slightly modified mutation of match_and_remember and collect_hits 336 | // because it works without containers, it must trim next part after 337 | // each match 338 | var match_remember_and_collect = function (content, patterns, trimmers) { 339 | var hits = []; 340 | // for each pattern 341 | var reduced_content = patterns.reduce(function (content, pattern) { 342 | var match; 343 | // for each match 344 | while ((match = pattern.regexp.exec(content))) { 345 | // add it to hits 346 | hits.push({ 347 | rank: pattern.rank, 348 | match: match[1] // store memorized match 349 | }); 350 | // and remove it from content 351 | var content_before = content.slice(0, match.index); 352 | var content_after = content.slice(match.index + match[0].length); 353 | var trimmed_before = content_before.replace(trimmers.right, ' '); 354 | var trimmed_after = content_after.replace(trimmers.left, ''); 355 | content = trimmed_before + trimmed_after; 356 | 357 | // reset last index, so pattern can be used again 358 | // match will always be removed from content 359 | // so infinite loop is not possible 360 | pattern.regexp.lastIndex = 0; 361 | } 362 | return content.replace(trimmers.both, ''); 363 | }, content); 364 | return { 365 | reduced_content: reduced_content, 366 | hits: hits 367 | }; 368 | }; 369 | 370 | function MirrorEngine(patterns, trimmers) { 371 | var self = this; 372 | 373 | this.open_container = function (match) { 374 | return { 375 | source: match[0], 376 | before: match[1], 377 | open: match[2], 378 | content: match[3], 379 | close: match[4], 380 | after: match[5] 381 | }; 382 | }; 383 | 384 | this.close_container = function (container, content) { 385 | if (content === undefined) 386 | content = container.content; 387 | return container.before + container.open + content + container.close + container.after; 388 | }; 389 | 390 | this.container_have_hits = function(container) { 391 | return !!container.hits.length; 392 | }; 393 | 394 | this.find_containers = function (title, container_pattern) { 395 | var containers = match_and_remember(title, container_pattern).map(self.open_container); 396 | 397 | containers.forEach(function (container) { 398 | var collected_hits = match_remember_and_collect(container.content, patterns, trimmers); 399 | container.reduced_content = collected_hits.reduced_content; 400 | container.hits = collected_hits.hits; 401 | container.before = container.before.replace(trimmers.right, ''); 402 | container.after = container.after.replace(trimmers.left, ''); 403 | }); 404 | return containers.filter(self.container_have_hits); 405 | }; 406 | 407 | this.trim_title = function (title, containers) { 408 | // for each container 409 | return containers.reduce(function (title, container) { 410 | // get content 411 | var content = container.reduced_content; 412 | // trim it 413 | // content = content.replace(trimmers, '') // is no longer needed, because was already trimmed in collect_hits 414 | // if not empty surround it with proper parenthesis 415 | if (content.length) { 416 | content = self.close_container(container, content); 417 | } 418 | // and exclude container from title 419 | return title.replace(container.source, content); 420 | }, title).trim(); 421 | }; 422 | 423 | // @deprecated 424 | this.join_hits = function (containers) { 425 | return containers.reduce(function (hits, container) { 426 | return hits.concat(container.hits); 427 | }, []); 428 | }; 429 | 430 | this.parse = function (title, container_pattern) { 431 | title = title.trim(); 432 | 433 | var containers = self.find_containers(title, container_pattern); 434 | title = self.trim_title(title, containers); 435 | 436 | return { 437 | title: title, 438 | containers: containers 439 | }; 440 | }; 441 | } 442 | 443 | function CuttingEngine(patterns, trimmers) { 444 | var self = this; 445 | 446 | this.open_container = function (content) { 447 | return { 448 | content: content, 449 | hits: [] 450 | }; 451 | }; 452 | 453 | this.split_title = function (title, container_pattern) { 454 | var containers = title.split(container_pattern).map(self.open_container); 455 | 456 | containers.forEach(function (container, index, containers) { 457 | if (index === 0 || index === containers.length - 1) { 458 | return; 459 | } 460 | var collected_hits = match_remember_and_collect(container.content, patterns, trimmers); 461 | container.reduced_content = collected_hits.reduced_content; 462 | container.hits = collected_hits.hits; 463 | }); 464 | return containers; 465 | }; 466 | 467 | this.join_title = function (containers) { 468 | return containers.map(function (container) { 469 | return container.hits.length ? container.reduced_content : container.content; 470 | }).join(''); 471 | }; 472 | 473 | this.trim_title = function (containers) { 474 | containers.forEach(function (container, index, containers) { 475 | if (container.hits.length && !container.reduced_content.length) { 476 | // remove container_pattern from previous and next container 477 | containers[index - 1].content = ''; 478 | containers[index + 1].content = ''; 479 | if (containers.length > index + 2) { 480 | // trim next container to prevent creation double delimiters in content 481 | // after joining containers 482 | containers[index + 2].content = containers[index + 2].content.replace(trimmers.left, ''); 483 | } 484 | } 485 | }); 486 | }; 487 | 488 | // @deprecated 489 | this.join_hits = function (containers) { 490 | return containers.reduce(function (hits, container) { 491 | return hits.concat(container.hits); 492 | }, []); 493 | }; 494 | 495 | this.parse = function (title, container_pattern) { 496 | title = title.trim(); 497 | 498 | var containers = self.split_title(title, container_pattern); 499 | self.trim_title(containers); 500 | title = self.join_title(containers); 501 | title = title.trim(); 502 | 503 | return { 504 | title: title, 505 | containers: containers.filter(function (container) {return container.hits.length;}) 506 | }; 507 | }; 508 | } 509 | 510 | function RawEngine(patterns, trimmers) { 511 | var self = this; 512 | 513 | this.container_from_hit = function (hit) { 514 | return { 515 | hits: [hit] 516 | }; 517 | }; 518 | 519 | this.parse = function (title) { 520 | var result = match_remember_and_collect(title, patterns, trimmers); 521 | return { 522 | title: result.reduced_content, 523 | containers: result.hits.map(self.container_from_hit) 524 | }; 525 | }; 526 | } 527 | 528 | function TitleParser() { 529 | var self = this; 530 | 531 | this.trimmers = { 532 | left: /^[-+,/ ]+/g, 533 | both: /^[-+,/ ]+|[-+,/ ]+$/g, 534 | right: /[-+,/ ]+$/g 535 | }; 536 | this.video_containers = {rank: 0, regexp: /\b(mp4|mkv|wmv|mov|avi|3pg|3gp|mpeg4)\b/ig}; 537 | this.resolutions = {rank: 1, regexp: /\b([0-9]+p(?!x))\b/ig}; 538 | this.resolutions_images = {rank: 2, regexp: /\b((?:1600|2000|3000)px)\b/ig}; 539 | this.resolutions_classic = {rank: 3, regexp: /\b(\d+x\d+(?:p|i)?)\b/ig}; 540 | this.resolutions_additional = {rank: 4, regexp: /\b([0-9]+k)\b/ig}; 541 | this.variations = {rank: 5, regexp: /\b(web-?dl|h\.26[45]\/hevc|hevc\/h\.265|h\.?26[45]|hevc|x\.?26[45]|split[- ]?scenes)\b/ig}; 542 | this.variations_common = {rank: 6, regexp: /\b(lq|sd|hd|ultrahd|fhd|uhd|hq|uhq|hi-res|mobile-high|mobile-medium|mobile-low|ipad)\b/ig}; 543 | this.fps = {rank: 7, regexp: /\b((?:30|60) ?fps)\b/ig}; 544 | this.bitrate = {rank: 8, regexp: /\b(\d+(?:\.\d+)?[Mk]b?ps)\b/ig}; 545 | this.bitrate_additional = {rank: 9, regexp: /\b(bts)\b/ig}; 546 | this.bitrate_additional2 = {rank: 9, regexp: /\b(1k|2k)\b/ig}; 547 | this.bitrate_additional3 = {rank: 10, regexp: /\b((?:lower|higher) bitrate)\b/ig}; 548 | this.picsets = {rank: 11, regexp: /\b(w images|with images|images|picture set|picsets?|imagesets?)\b/ig}; 549 | this.request = {rank: 12, regexp: /\b(req|request)\b/ig}; 550 | this.virtual_gear = {rank: 13, regexp: /\b(Oculus,? Go|Oculus|Oculus\/vive|Oculus\/Rift|PSVR)\b/ig}; 551 | this.virtual_gear2 = {rank: 13, regexp: /\b(Samsung|Smartphone|DayDream)\b/ig}; 552 | this.virtual_gear3 = {rank: 14, regexp: /\b((?:Desktop|Smartphone|Gear|Playstation) ?VR|Oculus|vive)\b/ig}; 553 | this.virtual_reality = {rank: 15, regexp: /\b(Virtual ?Reality)\b/ig}; 554 | this.games = {rank: 16, regexp: /\b(pc|mac)\b/ig}; 555 | 556 | this.patterns_video = [ 557 | this.video_containers, 558 | this.resolutions_classic, 559 | this.resolutions, 560 | this.resolutions_images, 561 | this.resolutions_additional, 562 | this.variations, 563 | this.variations_common, 564 | this.fps, 565 | this.bitrate, 566 | this.bitrate_additional, 567 | this.bitrate_additional2, 568 | this.bitrate_additional3, 569 | this.picsets, 570 | this.request, 571 | this.virtual_gear, 572 | this.virtual_gear2, 573 | this.virtual_gear3, 574 | this.virtual_reality 575 | ]; 576 | 577 | this.patterns_games = [ 578 | this.games 579 | ]; 580 | 581 | this.patterns_for_raw = _.without(this.patterns_video, this.virtual_reality); 582 | 583 | this.pack_mirror = { 584 | engine: MirrorEngine, 585 | container_patterns: [ 586 | /()(\[)([^\]]+?)(\])(s)$/g, 587 | /()(\[)(.+?)(\])([-, ]*)/g, 588 | /()(\()(.+?)(\))([-, ]*)/g, 589 | /()(\{)(.+?)(\})([-, ]*)/g 590 | ], 591 | patterns: { 592 | video: self.patterns_video, 593 | games: self.patterns_games 594 | }, 595 | trimmers: self.trimmers 596 | }; 597 | 598 | this.pack_cutting = { 599 | engine: CuttingEngine, 600 | container_patterns: [ 601 | /(\*)/ 602 | ], 603 | patterns: { 604 | video: self.patterns_video, 605 | games: self.patterns_games 606 | }, 607 | trimmers: self.trimmers 608 | }; 609 | 610 | this.pack_raw = { 611 | engine: RawEngine, 612 | container_patterns: [null], 613 | patterns: { 614 | video: self.patterns_for_raw, 615 | games: self.patterns_for_raw 616 | }, 617 | trimmers: self.trimmers 618 | }; 619 | 620 | this.is_not_empty = function (container) { 621 | return container.hits.length; 622 | }; 623 | 624 | this.parse_pack = function (title, pack, category) { 625 | var match_patterns; 626 | 627 | if (category == 'games.apps') { 628 | match_patterns = pack.patterns.games; 629 | } else { 630 | match_patterns = pack.patterns.video; 631 | } 632 | 633 | var engine = new pack.engine(match_patterns, pack.trimmers); 634 | var containers = pack.container_patterns.map(function (container_pattern) { 635 | var result = engine.parse(title, container_pattern); 636 | title = result.title; 637 | return result.containers; 638 | }); 639 | containers = _.flatten(containers, true); 640 | containers = containers.filter(self.is_not_empty); 641 | 642 | return { 643 | title: title, 644 | containers: containers 645 | }; 646 | }; 647 | 648 | this.compare = function(a, b) { 649 | return a < b ? -1 : a > b ? 1 : 0; 650 | }; 651 | 652 | this.compare_hits = function (a, b) { 653 | return self.compare(a.rank, b.rank); 654 | }; 655 | 656 | this.add_rank_boundaries = function (container) { 657 | var ranks = _.pluck(container.hits, 'rank'); 658 | container.rank_min = Math.min.apply(null, ranks); 659 | }; 660 | 661 | this.parse = function (title, category) { 662 | title = title.trim(); 663 | 664 | var mirror_result = self.parse_pack(title, self.pack_mirror, category); 665 | title = mirror_result.title; 666 | 667 | var cutting_result = self.parse_pack(title, self.pack_cutting, category); 668 | title = cutting_result.title; 669 | 670 | var raw_result = self.parse_pack(title, self.pack_raw, category); 671 | title = raw_result.title; 672 | 673 | var containers = _.flatten([ 674 | mirror_result.containers, 675 | cutting_result.containers, 676 | raw_result.containers 677 | ], true); 678 | 679 | return { 680 | title: title, 681 | containers: containers 682 | }; 683 | }; 684 | 685 | this.simplify = function (result) { 686 | var hits = _.flatten(_.pluck(result.containers, 'hits'), true); 687 | var plain_hits = _.pluck(hits.sort(self.compare_hits), 'match'); 688 | 689 | return { 690 | title: result.title, 691 | hits: plain_hits 692 | }; 693 | }; 694 | } 695 | 696 | // end TitleParser 697 | 698 | function Version(title_parser, $row, config) { 699 | var self = this; 700 | 701 | this.symbol_check = '✓'; 702 | this.symbol_warning = '⚑'; 703 | this.symbol_bookmark = '★'; 704 | this.symbol_freeleech = '∞'; 705 | this.$title = null; 706 | this.reduced_title = null; 707 | 708 | this.freeleech = false 709 | 710 | this._get_$checkbox = function () { 711 | return $row.find('input[type=checkbox]'); 712 | }; 713 | 714 | this._get_$title = function () { 715 | return $row.find('a[href^="torrents.php?id"], a[href^="/torrents.php?id"]'); 716 | }; 717 | 718 | this._get_$comments = function () { 719 | return $row.find([ 720 | '.cats_col + td + td + td', 721 | '.cats_cols + td + td + td' 722 | ].join(', ')); 723 | }; 724 | 725 | this._get_$tags_container = function () { 726 | return $row.find('.tags'); 727 | }; 728 | 729 | this._get_$tags = function () { 730 | return $row.find('.tags a'); 731 | }; 732 | 733 | this._get_$check_icon = function () { 734 | if ($row.find('.torrent_icon_container .icon_torrent_okay').length > 0) { 735 | return $row.find('.torrent_icon_container .icon_torrent_okay').parents('span'); 736 | } else if ($row.find('.icon_okay').length > 0) { 737 | return $row.find('.icon_okay'); 738 | } else { 739 | return 0; 740 | } 741 | }; 742 | 743 | this._get_$warning_icon = function () { 744 | if ($row.find('.torrent_icon_container .icon_torrent_warned').length > 0) { 745 | return $row.find('.torrent_icon_container .icon_torrent_warned').parents('span'); 746 | } else if ($row.find('.icon_warning').length > 0) { 747 | return $row.find('.icon_warning'); 748 | } else { 749 | return 0; 750 | } 751 | }; 752 | 753 | this._get_$bookmark_icon = function () { 754 | if ($row.find('.torrent_icon_container .icon_nav_bookmarks').length > 0) { 755 | return $row.find('.torrent_icon_container .icon_nav_bookmarks').parents('span'); 756 | } else if ($row.find('img[alt^=bookmarked]').length > 0) { 757 | return $row.find('img[alt^=bookmarked]'); 758 | } else { 759 | return 0; 760 | } 761 | }; 762 | 763 | this._get_$bookmarked = function () { 764 | return $row.find('.torrent_icon_container .bookmarked').parents('span'); 765 | }; 766 | 767 | this._get_$freeleech_icon = function () { 768 | var fl = $row.find('.torrent_icon_container .icon_torrent_bonus').parents('span'); 769 | 770 | if (fl.length > 0) { // emp style 771 | if (fl.find('.unlimited_leech').length) { 772 | self.freeleech = true; 773 | } 774 | return fl; 775 | } else { // vanilla Gazelle style 776 | if ($row.find('img[alt^=Freeleech]').length > 0) { 777 | self.freeleech = true; 778 | return $row.find('img[alt^=Freeleech]'); 779 | } 780 | } 781 | 782 | return 0; 783 | }; 784 | 785 | this._get_$download_icon = function () { 786 | if ($row.find('a[href^="/torrents.php?action=download"]').length > 0) { // emp style 787 | return $row.find('a[href^="/torrents.php?action=download"]').parent(); 788 | } else if ($row.find('a[href^="torrents.php?action=download"]').length > 0) { // vanilla Gazelle style 789 | return $row.find('a[href^="torrents.php?action=download"]'); 790 | } else { 791 | return 0; 792 | } 793 | }; 794 | 795 | this._get_$category = function () { 796 | return $row.find('.cats_col > div') 797 | } 798 | 799 | this._get_$timestamp = function () { 800 | return Date.parse($row.find('.time').attr('alt')) 801 | } 802 | 803 | this.apply_mp4s_workaround = function (containers) { 804 | var mp4s = _.findWhere(containers, {after: 's'}); 805 | if (mp4s === undefined) 806 | return; 807 | var res = _.findWhere(containers, {rank_min: title_parser.resolutions.rank}); 808 | if (res === undefined) 809 | return; 810 | res.after = mp4s.after; 811 | mp4s.after = ''; 812 | }; 813 | 814 | this._get_name = function () { 815 | if (!self.containers.length) 816 | return ''; 817 | var containers = _.sortBy(self.containers, 'rank_min'); 818 | return _.pluck(containers, 'tag').join(' '); 819 | }; 820 | 821 | this._init = function () { 822 | self.$checkbox = self._get_$checkbox(); 823 | self.$title = self._get_$title(); 824 | self.$comments = self._get_$comments(); 825 | self.$tags = self._get_$tags(); 826 | self.$tags_container = self._get_$tags_container(); 827 | self.$check_icon = self._get_$check_icon(); 828 | self.$warning_icon = self._get_$warning_icon(); 829 | self.$bookmark_icon = self._get_$bookmark_icon(); 830 | self.$bookmarked = self._get_$bookmarked(); 831 | self.$freeleech_icon = self._get_$freeleech_icon(); 832 | self.$download_icon = self._get_$download_icon(); 833 | self.$category = self._get_$category(); 834 | self.$timestamp = self._get_$timestamp(); 835 | 836 | var title = self.$title.text(); 837 | var category = self.$category.attr('title'); 838 | var result = title_parser.parse(title, category); 839 | 840 | self.reduced_title = result.title.replace(/\s+/g, ' '); 841 | // self.minimal_title = result.title.replace(/[^a-zA-Z0-9]/g, ''); 842 | 843 | self.containers = result.containers; 844 | 845 | self.containers.forEach(title_parser.add_rank_boundaries); 846 | self.apply_mp4s_workaround(self.containers); 847 | self.containers.forEach(self._add_tag); 848 | self.name = self._get_name(); 849 | }; 850 | 851 | this.toggle_checkbox = function (value) { 852 | self.$checkbox.prop('checked', value); 853 | }; 854 | 855 | this._add_tag = function (container) { 856 | var hits = _.sortBy(container.hits, 'rank'); 857 | var content = _.pluck(hits, 'match').join(', '); 858 | container.tag = '[' + content + ']' + (container.after || ''); 859 | }; 860 | 861 | this._version_title = function () { 862 | var new_title = []; 863 | var new_icons = []; 864 | if (self.name) 865 | new_title.push(self.name); 866 | 867 | if (self.$check_icon.length && !config.icon_checked) { 868 | new_title.push(self.symbol_check); 869 | } 870 | if (self.$warning_icon.length && !config.icon_warning) { 871 | new_title.push(self.symbol_warning); 872 | } 873 | if (self.$bookmarked.length && !config.icon_bookmarked) { 874 | new_title.push(self.symbol_bookmark); 875 | } 876 | if (self.freeleech === true && !config.icon_freeleech) { 877 | new_title.push(self.symbol_freeleech); 878 | } 879 | 880 | var $new_title = self.$title.clone(); 881 | $new_title.addClass('collapsed-title'); 882 | $new_title.text(new_title.join('\u00a0') || '\u00a0'); 883 | new_icons.forEach(function (icon) { 884 | $new_title.append(icon); 885 | }); 886 | 887 | return $new_title; 888 | }; 889 | 890 | this._comments_link = function () { 891 | var link = jQuery(''); 892 | link.text(self.$comments.text().trim()); 893 | link.attr('href', self.$title.attr('href') + '#thanksdiv'); 894 | 895 | if (config.show_vertical) { 896 | link.addClass('comment_vertical'); 897 | } 898 | 899 | return link; 900 | }; 901 | 902 | this.collapse = function () { 903 | self.collapse = null; 904 | 905 | var $el = jQuery('
'); 906 | 907 | if (self.$download_icon.length) { 908 | $el.append(self.$download_icon); 909 | } 910 | if (self.$freeleech_icon.length && config.icon_freeleech) { 911 | $el.append(self.$freeleech_icon); 912 | } 913 | if (self.$check_icon.length && config.icon_checked) { 914 | $el.append(self.$check_icon); 915 | } 916 | if (self.$warning_icon.length && config.icon_warning) { 917 | $el.append(self.$warning_icon); 918 | } 919 | if (self.$bookmark_icon.length && config.icon_bookmarked) { 920 | $el.append(self.$bookmark_icon); 921 | } 922 | 923 | $el.append(self._version_title()); 924 | 925 | 926 | if (!self.$checkbox.length && parseInt(self.$comments.text()) > 0) { 927 | $el.append(self._comments_link()); 928 | } 929 | 930 | // support for vertical and horizontal stacking of versions 931 | if (!config.show_vertical) { 932 | $el.addClass('version_horizontal'); 933 | } 934 | 935 | return $el; 936 | }; 937 | 938 | this.hide = function () { 939 | $row.addClass('collapse-hidden'); 940 | }; 941 | 942 | this._init(); 943 | } 944 | 945 | function Group(name) { 946 | var self = this; 947 | 948 | this.versions = []; 949 | 950 | this.add_version = function (version) { 951 | self.versions.push(version); 952 | }; 953 | 954 | this.compare = function(a, b) { 955 | return a < b ? -1 : a > b ? 1 : 0; 956 | }; 957 | 958 | this.compare_mixed = function (a_str, b_str) { 959 | var a_num = a_str.match(/\d+/g) || []; 960 | var b_num = b_str.match(/\d+/g) || []; 961 | 962 | if (a_num.length && b_num.length) { 963 | // number compare each pair of pattern matches 964 | var length = Math.min(a_num.length, b_num.length); 965 | for (var i = 0; i < length; i++) { 966 | var a_int = parseInt(a_num[i]); 967 | var b_int = parseInt(b_num[i]); 968 | 969 | if (isNaN(a_int) || isNaN(b_int)) 970 | break; 971 | 972 | var result = self.compare(a_int, b_int); 973 | if (result !== 0) 974 | return result; 975 | } 976 | return self.compare(a_str, b_str); 977 | } 978 | else if(a_num.length || b_num.length) 979 | return a_num.length ? 1 : -1; 980 | else 981 | return self.compare(a_str, b_str); 982 | }; 983 | 984 | this.compare_versions = function (a, b) { 985 | var a_str = a.name; 986 | var b_str = b.name; 987 | return self.compare_mixed(a_str, b_str); 988 | }; 989 | 990 | this.compare_tags = function (a, b) { 991 | return self.compare(a.name, b.name); 992 | }; 993 | 994 | this._convert_tag = function () { 995 | var $this = jQuery(this); 996 | return { 997 | name: $this.text(), 998 | elem: this 999 | }; 1000 | }; 1001 | 1002 | this._convert_tags = function (tags) { 1003 | return tags.map(self._convert_tag).get(); 1004 | }; 1005 | 1006 | this._missing_tags = function (versions) { 1007 | var visible_tags = self._convert_tags(versions[0].$tags); 1008 | var hidden_tags = _.pluck(versions.slice(1), '$tags'); 1009 | hidden_tags = hidden_tags.map(self._convert_tags); 1010 | hidden_tags = _.flatten(hidden_tags, true); 1011 | 1012 | visible_tags.sort(self.compare_tags); 1013 | hidden_tags.sort(self.compare_tags); 1014 | hidden_tags = _.uniq(hidden_tags, true, function (tag) {return tag.name;}); 1015 | 1016 | return difference_fast(hidden_tags, visible_tags, self.compare_tags); 1017 | }; 1018 | 1019 | this.collapse = function (config) { 1020 | self.collapse = null; 1021 | var versions = {}; 1022 | 1023 | if ( ! config.sort_chronological) { 1024 | versions = self.versions.sort(self.compare_versions).reverse(); 1025 | } else { 1026 | versions = self.versions 1027 | } 1028 | 1029 | _.invoke(versions.slice(1), 'hide'); 1030 | 1031 | var collapsed_versions = _.invoke(versions, 'collapse'); 1032 | versions[0].$title.after(collapsed_versions); 1033 | if ( ! config.preserve_title) { 1034 | versions[0].$title.text(name); 1035 | } 1036 | versions[0].$title.parent().find('br').remove(); 1037 | 1038 | if (!config.show_vertical) { 1039 | versions[0].$title.after(''); 1040 | } 1041 | 1042 | versions[0].$checkbox.change(function(event) { 1043 | var checked = event.currentTarget.checked; 1044 | _.invoke(versions.slice(1), 'toggle_checkbox', checked); 1045 | }); 1046 | 1047 | if (!config.show_vertical) { 1048 | jQuery.each(collapsed_versions, function(i, version) { 1049 | if (i > 0) { 1050 | collapsed_versions[i].prepend(' | '); 1051 | } 1052 | }); 1053 | } 1054 | 1055 | if (versions.length && config.add_missing_tags) { 1056 | var missing_tags = self._missing_tags(versions); 1057 | if (missing_tags.length) { 1058 | var elements = _.pluck(missing_tags, 'elem'); 1059 | elements.forEach(function (elem) { 1060 | versions[0].$tags_container.append(' ', jQuery(elem).clone()); 1061 | }); 1062 | } 1063 | } 1064 | }; 1065 | } 1066 | 1067 | function CollapseDuplicates(title_parser, config) { 1068 | var self = this; 1069 | 1070 | this.groups = {}; 1071 | 1072 | this.get_group = function (name) { 1073 | var group = self.groups[name]; 1074 | if (group === undefined) { 1075 | group = new Group(name); 1076 | self.groups[name] = group; 1077 | } 1078 | return group; 1079 | }; 1080 | 1081 | this.create_group = function (_, row) { 1082 | var version = new Version(title_parser, jQuery(row), config); 1083 | self.get_group(version.reduced_title).add_version(version); 1084 | }; 1085 | 1086 | this.create_groups = function() { 1087 | jQuery('.torrent_table').find('tr.torrent').each(self.create_group); 1088 | }; 1089 | 1090 | this.collapse_group = function (group) { 1091 | group.collapse(config); 1092 | }; 1093 | 1094 | this.collapse_groups = function() { 1095 | _.each(self.groups, self.collapse_group); 1096 | }; 1097 | 1098 | this.create_groups(); 1099 | this.collapse_groups(); 1100 | } 1101 | 1102 | function CreateConfigDialog(config) { 1103 | var css = [ 1104 | '.cdc_checkbox {', 1105 | ' margin-right: 4px', 1106 | '}' 1107 | ].join('\n'); 1108 | 1109 | // add general css 1110 | add_css(css, 'collapse_duplicates_config'); 1111 | 1112 | jQuery("body").prepend( 1113 | // config dialog background 1114 | jQuery('