NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name whatcd gazelle collapse duplicates // @include /https?://www\.empornium\.(me|sx)/torrents\.php.*/ // @exclude /https?://www\.empornium\.(me|sx)/torrents\.php\?id.*/ // @include /https?://www\.empornium\.(me|sx)/user\.php.*/ // @include /https?://femdomcult\.org/torrents\.php.*/ // @exclude /https?://femdomcult\.org/torrents\.php\?id.*/ // @include /https?://femdomcult\.org/user\.php.*/ // @include /https?://www\.cheggit\.me/torrents\.php.*/ // @exclude /https?://www\.cheggit\.me/torrents\.php\?id.*/ // @include /https?://www\.cheggit\.me/user\.php.*/ // @include /https?://pornbay\.org/torrents\.php.*/ // @exclude /https?://pornbay\.org/torrents\.php\?id.*/ // @include /https?://pornbay\.org/user\.php.*/ // @version 17 // @require http://code.jquery.com/jquery-2.1.1.js // @require https://raw.githubusercontent.com/jashkenas/underscore/1.8.3/underscore.js // @grant GM_addStyle // ==/UserScript== 'use strict'; // Changelog: // * version 17 // - added option for freeleach icon // - added option for warning icon // * version 16 // - added resolutions 240, 380, 960, 1440, 1600, 1920 // - added image resolutions 1600px, 2000px, 3000px // - added variations h.265, h265, hevc, uhd // - added variations Oculus, Playstation // - added variations "lower bitrate", "higher bitrate" // - added resolution suffix * // - added support for pornbay.org // - added support for cheggit.me // * version 15 // - added variation lq // - added variations 30 fps, 60 fps // - added variations Samsung, Smartphone // * version 14 // - improved detection of VR variations // - added resolution 416 // - added bitstream variations 1K, 2K // * version 13 // - removed leading space on empty version title (without symbols) // - added comments indicator (links directly to comments section) // - simplified version title css selector // - added variation Mps // - added variation "Oculus Rift" // - added variation "Virtual Reality" // - added variations "Desktop VR", "Smartphone VR", "Gear VR", "Oculus VR" // - added variations ultrahd, hi-res // - added variations splitsceces, split-scenes, "split sceces" // * version 12 // - added resolution 405 // - added support for notifications page // * version 11 // - added freeleach icon ∞ // - added warning icon ⚑ // - added option to add tags from collapsed duplicates // - added resolutions 272, 326, 392, 408, 450 // - moved mov and wmv to video_containers group // - added variations fhd, mkv // - added variations "images", "picture set" // - extracted Group and Version from main code // - fixed and improved sorting (720p > SD) // - small changes to css // * version 10 // - reworked and extracted matching algorithm to separate module with 3 specialized engines // - added precise trimmers to remove remaining delimiters (space , - / +) while collapsing // - added support for ** containers // - added support for {} containers // - added resolutions 352, 544, 558, 640, 1072 // - added variations hq, uhq, sd, hd // - added variations "w images" and "with images" // - normalization for multiple spaces // - replace left delimiter with space // - improved performance 125ms -> 87ms // - less hacky solution for [MP4]s // - versions and variations are now surrounded by parenthesis [] // - small adjustment to css // - new dependency underscore.js // * version 9 // - fix css for direct thumbnails (when replace_categories is off) // - added support for femdomcult.org // * version 8 // - symmetric variations (before or after resolution) // - support for new variations: web-dl, mov, wmv // - fixed version title being wrapped on user pages // - more reliable delimiter // * version 7 // - download icons for every resolutions // - checked by staff symbol for versions (replaces icon) // - bookmark symbol for versions (replaces icon) // - support for bitrate variation eg. [5Mbps] // - support for classic resolutions eg. [1920x1080] // * version 6 // - small changes to patterns // * version 5 // - small changes to patterns // * version 4 // - added support for bookmarks // - fixed duplicates with the same name // - added default thumbnails on hover // * version 3 // - added support for stream "[]s" variation // - added support for "H.265/HEVC" variation that appears after resolution // - greatly improved sorting for combined versions // * version 2 // - now it works on user pages // * version 1 // - initial version var comment_icon = [ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAz', 'klEQVQoU43QPysEABjH8YfJv8tg8wLEaJTBrhRlUYTFcK/gJpuSN6FkvE0mqywG+Rh1y0km', 'V1d3SbjHcC7uurv0HZ7h95meEMNrn0XnXqWU7pRMdINDXz9jp6qFX7DbM7Z7MtkBD31BOrB', 'mRYTmAHBsw5ulcDsAbCtJ12FVq89MQUVqhrCj0TPfmHUmpfsQplSkupp3VWWb5lxJqWU9hB', 'Nl+8YVzFtWdOlTSg17nU+2G7XlwosPNXXp0Vg3+Nu0I89OB4MQRswMByH+Ab4BEZhLmRFDo', 'vgAAAAASUVORK5CYII=' ].join(''); GM_addStyle([ '/* hide default icons */', '.torrent .cats_col + td > span, .torrent .cats_cols + td > span {', ' display: none;', '}', '.torrent.collapse-hidden {', ' display: none;', '}', '.torrent .tags {', ' padding-top: 5px;', ' padding-left: 0;', '}', '.torrent .icon {', ' float: none;', ' margin-left: 0;', ' margin-top: 1px;', '}', '.torrent a[href^="torrents.php?action=download"] {', ' position: absolute;', ' margin-left: 0;', '}', '.torrent .version .collapsed-title {', ' display: inline-block;', ' margin-left: 20px;', ' margin-bottom: 2px;', '}', '.collapsed-freeleach, .collapsed_warning {', ' position: absolute;', ' margin-left: 5px !important;', ' top: 2px;', '}', '.torrent .version {', ' white-space: nowrap;', ' position: relative;', '}', '.torrent .version:first-of-type {', ' padding-top: 3px;', '}', '.torrent .tags {', ' padding-top: 3px;', '}', '.torrent .version .comment {', ' position: absolute;', ' right: 0;', ' background-image: url("' + comment_icon + '");', ' background-repeat: no-repeat;', ' background-position: 0 1px;', ' padding-left: 19px;', ' text-align: right;', '', '}' ].join('\n')); this.jQuery = jQuery.noConflict(true); // very fast difference // can only work on sorted unique arrays var difference_fast = function (a, b, compare_function) { var a_idx = 0; var b_idx = 0; var a_len = a.length; var b_len = b.length; var result = []; while (a_idx < a_len && b_idx < b_len) { switch (compare_function(a[a_idx], b[b_idx])) { case 0: // common a_idx++; b_idx++; break; case 1: // only in b b_idx++; break; case -1: // only in a result.push(a[a_idx]); a_idx++; break; } } if (a_idx < a_len && b_idx == b_len) return result.concat(a.slice(a.idx)); return result; }; // begin TitleParser // like String.prototype.match(//g) but remembers matched groups var match_and_remember = function (content, pattern) { var matches = []; var match; while ((match = pattern.exec(content))) { matches.push(match); } // reset last index, so pattern can be used again pattern.lastIndex = 0; return matches; }; // slightly modified mutation of match_and_remember and collect_hits // because it works without containers, it must trim next part after // each match var match_remember_and_collect = function (content, patterns, trimmers) { var hits = []; // for each pattern var reduced_content = patterns.reduce(function (content, pattern) { var match; // for each match while ((match = pattern.regexp.exec(content))) { // add it to hits hits.push({ rank: pattern.rank, match: match[1] // store memorized match }); // and remove it from content var content_before = content.slice(0, match.index); var content_after = content.slice(match.index + match[0].length); var trimmed_before = content_before.replace(trimmers.right, ' '); var trimmed_after = content_after.replace(trimmers.left, ''); content = trimmed_before + trimmed_after; // reset last index, so pattern can be used again // match will always be removed from content // so infinite loop is not possible pattern.regexp.lastIndex = 0; } return content.replace(trimmers.both, ''); }, content); return { reduced_content: reduced_content, hits: hits }; }; function MirrorEngine(patterns, trimmers) { var self = this; this.open_container = function (match) { return { source: match[0], before: match[1], open: match[2], content: match[3], close: match[4], after: match[5] }; }; this.close_container = function (container, content) { if (content === undefined) content = container.content; return container.before + container.open + content + container.close + container.after; }; this.container_have_hits = function(container) { return !!container.hits.length; }; this.find_containers = function (title, container_pattern) { var containers = match_and_remember(title, container_pattern).map(self.open_container); containers.forEach(function (container) { var collected_hits = match_remember_and_collect(container.content, patterns, trimmers); container.reduced_content = collected_hits.reduced_content; container.hits = collected_hits.hits; container.before = container.before.replace(trimmers.right, ''); container.after = container.after.replace(trimmers.left, ''); }); return containers.filter(self.container_have_hits); }; this.trim_title = function (title, containers) { // for each container return containers.reduce(function (title, container) { // get content var content = container.reduced_content; // trim it // content = content.replace(trimmers, '') // is no longer needed, because was already trimmed in collect_hits // if not empty surround it with proper parenthesis if (content.length) { content = self.close_container(container, content); } // and exclude container from title return title.replace(container.source, content); }, title).trim(); }; // @deprecated this.join_hits = function (containers) { return containers.reduce(function (hits, container) { return hits.concat(container.hits); }, []); }; this.parse = function (title, container_pattern) { title = title.trim(); var containers = self.find_containers(title, container_pattern); title = self.trim_title(title, containers); return { title: title, containers: containers }; }; } function CuttingEngine(patterns, trimmers) { var self = this; this.open_container = function (content) { return { content: content, hits: [] }; }; this.split_title = function (title, container_pattern) { var containers = title.split(container_pattern).map(self.open_container); containers.forEach(function (container, index, containers) { if (index === 0 || index === containers.length - 1) { return; } var collected_hits = match_remember_and_collect(container.content, patterns, trimmers); container.reduced_content = collected_hits.reduced_content; container.hits = collected_hits.hits; }); return containers; }; this.join_title = function (containers) { return containers.map(function (container) { return container.hits.length ? container.reduced_content : container.content; }).join(''); }; this.trim_title = function (containers) { containers.forEach(function (container, index, containers) { if (container.hits.length && !container.reduced_content.length) { // remove container_pattern from previous and next container containers[index - 1].content = ''; containers[index + 1].content = ''; if (containers.length > index + 2) { // trim next container to prevent creation double delimiters in content // after joining containers containers[index + 2].content = containers[index + 2].content.replace(trimmers.left, ''); } } }); }; // @deprecated this.join_hits = function (containers) { return containers.reduce(function (hits, container) { return hits.concat(container.hits); }, []); }; this.parse = function (title, container_pattern) { title = title.trim(); var containers = self.split_title(title, container_pattern); self.trim_title(containers); title = self.join_title(containers); title = title.trim(); return { title: title, containers: containers.filter(function (container) {return container.hits.length;}) }; }; } function RawEngine(patterns, trimmers) { var self = this; this.container_from_hit = function (hit) { return { hits: [hit] }; }; this.parse = function (title) { var result = match_remember_and_collect(title, patterns, trimmers); return { title: result.reduced_content, containers: result.hits.map(self.container_from_hit) }; }; } function TitleParser() { var self = this; this.trimmers = { left: /^[-+,/ ]+/g, both: /^[-+,/ ]+|[-+,/ ]+$/g, right: /[-+,/ ]+$/g }; this.video_containers = {rank: 0, regexp: /\b(mp4|mkv|wmv|mov|avi)\b/ig}; this.resolutions = {rank: 1, regexp: /\b((?:240|270|272|326|352|360|368|380|384|392|396|400|404|405|406|408|416|420|432|450|480|540|544|558|576|640|674|720|960|1072|1080|1440|1600|1920|2160)(?:p|i)?)\b\*?/ig}; this.resolutions_images = {rank: 2, regexp: /\b((?:1600|2000|3000)px)\b/ig}; this.resolutions_classic = {rank: 3, regexp: /\b(\d+x\d+(?:p|i)?)\b/ig}; this.resolutions_additional = {rank: 4, regexp: /\b(4k)\b/ig}; this.variations = {rank: 5, regexp: /\b(web-?dl|h\.265\/hevc|hevc\/h\.265|h\.?265|hevc|split[- ]?scenes)\b/ig}; this.variations_common = {rank: 6, regexp: /\b(lq|sd|hd|ultrahd|fhd|uhd|hq|uhq|hi-res)\b/ig}; this.fps = {rank: 7, regexp: /\b((?:30|60) ?fps)\b/ig}; this.bitrate = {rank: 8, regexp: /\b(\d+(?:\.\d+)?Mb?ps)\b/ig}; this.bitrate_additional = {rank: 9, regexp: /\b(bts)\b/ig}; this.bitrate_additional2 = {rank: 9, regexp: /\b(1k|2k)\b/ig}; this.bitrate_additional3 = {rank: 10, regexp: /\b((?:lower|higher) bitrate)\b/ig}; this.picsets = {rank: 11, regexp: /\b(w images|with images|images|picture set|picsets?|imagesets?)\b/ig}; this.request = {rank: 12, regexp: /\b(req|request)\b/ig}; this.virtual_gear = {rank: 13, regexp: /\b((?:Desktop|Smartphone|Gear|Oculus|Playstation) ?VR|Oculus ?Rift)\b/ig}; this.virtual_gear2 = {rank: 13, regexp: /\b(Samsung|Smartphone|Oculus)\b/ig}; this.virtual_reality = {rank: 14, regexp: /\b(Virtual ?Reality)\b/ig}; this.patterns_default = [ this.video_containers, this.resolutions_classic, this.resolutions, this.resolutions_images, this.resolutions_additional, this.variations, this.variations_common, this.fps, this.bitrate, this.bitrate_additional, this.bitrate_additional2, this.bitrate_additional3, this.picsets, this.request, this.virtual_gear, this.virtual_gear2, this.virtual_reality ]; this.patterns_for_raw = _.without(this.patterns_default, this.virtual_reality); this.pack_mirror = { engine: MirrorEngine, container_patterns: [ /()(\[)([^\]]+?)(\])(s)$/g, /()(\[)(.+?)(\])([-, ]*)/g, /()(\()(.+?)(\))([-, ]*)/g, /()(\{)(.+?)(\})([-, ]*)/g ], patterns: self.patterns_default, trimmers: self.trimmers }; this.pack_cutting = { engine: CuttingEngine, container_patterns: [ /(\*)/ ], patterns: self.patterns_default, trimmers: self.trimmers }; this.pack_raw = { engine: RawEngine, container_patterns: [null], patterns: self.patterns_for_raw, trimmers: self.trimmers }; this.is_not_empty = function (container) { return container.hits.length; }; this.parse_pack = function (title, pack) { var engine = new pack.engine(pack.patterns, pack.trimmers); var containers = pack.container_patterns.map(function (container_pattern) { var result = engine.parse(title, container_pattern); title = result.title; return result.containers; }); containers = _.flatten(containers, true); containers = containers.filter(self.is_not_empty); return { title: title, containers: containers }; }; this.compare = function(a, b) { return a < b ? -1 : a > b ? 1 : 0; }; this.compare_hits = function (a, b) { return self.compare(a.rank, b.rank); }; this.add_rank_boundaries = function (container) { var ranks = _.pluck(container.hits, 'rank'); container.rank_min = Math.min.apply(null, ranks); }; this.parse = function (title) { title = title.trim(); var mirror_result = self.parse_pack(title, self.pack_mirror); title = mirror_result.title; var cutting_result = self.parse_pack(title, self.pack_cutting); title = cutting_result.title; var raw_result = self.parse_pack(title, self.pack_raw); title = raw_result.title; var containers = _.flatten([ mirror_result.containers, cutting_result.containers, raw_result.containers ], true); // console.log(containers); return { title: title, containers: containers }; }; this.simplify = function (result) { var hits = _.flatten(_.pluck(result.containers, 'hits'), true); var plain_hits = _.pluck(hits.sort(self.compare_hits), 'match'); return { title: result.title, hits: plain_hits }; }; } // end TitleParser function Version(title_parser, $row, use_freeleach_icon, use_warning_icon) { var self = this; this.symbol_check = '✓'; this.symbol_warning = '⚑'; this.symbol_bookmark = '★'; this.symbol_freeleach = '∞'; this.icon_freeleach = [ '<img src="static/common/symbols/freedownload.gif"', 'class="collapsed-freeleach"', 'alt="Freeleech"', 'title="Unlimited Freeleech">' ].join(' '); this.icon_reported = [ '<span title="This torrent will be automatically deleted unless the uploader fixes it"', 'class="icon icon_warning collapsed_warning">', '</span>' ].join(' '); this.use_freeleach_icon = use_freeleach_icon; this.use_warning_icon = use_warning_icon; this.$title = null; this.reduced_title = null; this._get_$checkbox = function () { return $row.find('input[type=checkbox]'); }; this._get_$title = function () { return $row.find('a[href^="torrents.php?id"]'); }; this._get_$comments = function () { return $row.find([ '.cats_col + td + td + td', '.cats_cols + td + td + td' ].join(', ')); }; this._get_$tags_container = function () { return $row.find('.tags'); }; this._get_$tags = function () { return $row.find('.tags a'); }; this._get_$check_icon = function () { return $row.find('span > span.icon_okay'); }; this._get_$warning_icon = function () { return $row.find('span > span.icon_warning'); }; this._get_$bookmark_icon = function () { return $row.find('span > img[alt=bookmarked]'); }; this._get_$freeleach_icon = function () { return $row.find('span > img[alt=Freeleech]'); }; this._get_$download_icon = function () { return $row.find('span > a[href^="torrents.php?action=download"]'); }; this.apply_mp4s_workaround = function (containers) { var mp4s = _.findWhere(containers, {after: 's'}); if (mp4s === undefined) return; var res = _.findWhere(containers, {rank_min: title_parser.resolutions.rank}); if (res === undefined) return; res.after = mp4s.after; mp4s.after = ''; }; this._get_name = function () { if (!self.containers.length) return ''; var containers = _.sortBy(self.containers, 'rank_min'); return _.pluck(containers, 'tag').join(' '); }; this._init = function () { self.$checkbox = self._get_$checkbox(); self.$title = self._get_$title(); self.$comments = self._get_$comments(); self.$tags = self._get_$tags(); self.$tags_container = self._get_$tags_container(); self.$check_icon = self._get_$check_icon(); self.$warning_icon = self._get_$warning_icon(); self.$bookmark_icon = self._get_$bookmark_icon(); self.$freeleach_icon = self._get_$freeleach_icon(); self.$download_icon = self._get_$download_icon(); var title = self.$title.text(); var result = title_parser.parse(title); // todo: to fix user typos // .toLowerCase() // .replace(/\s+/g, '') self.reduced_title = result.title.replace(/\s+/g, ' '); self.containers = result.containers; self.containers.forEach(title_parser.add_rank_boundaries); self.apply_mp4s_workaround(self.containers); self.containers.forEach(self._add_tag); self.name = self._get_name(); }; this.toggle_checkbox = function (value) { self.$checkbox.prop('checked', value); }; this._add_tag = function (container) { var hits = _.sortBy(container.hits, 'rank'); var content = _.pluck(hits, 'match').join(', '); container.tag = '[' + content + ']' + (container.after || ''); }; this._version_title = function () { var new_title = []; var new_icons = []; if (self.name) new_title.push(self.name); if (self.$check_icon.length) new_title.push(self.symbol_check); if (self.$warning_icon.length) { if (self.use_warning_icon) new_icons.push(self.icon_reported); else new_title.push(self.symbol_warning); } if (self.$bookmark_icon.length) new_title.push(self.symbol_bookmark); if (self.$freeleach_icon.length) { if (self.use_freeleach_icon) new_icons.push(self.icon_freeleach); else new_title.push(self.symbol_freeleach); } var $new_title = self.$title.clone(); $new_title.addClass('collapsed-title'); $new_title.text(new_title.join('\u00a0') || '\u00a0'); new_icons.forEach(function (icon) { $new_title.append(icon); }); return $new_title; }; this._comments_link = function () { var link = jQuery('<a class="comment"></a>'); link.text(self.$comments.text().trim()); link.attr('href', self.$title.attr('href') + '#thanksdiv'); return link; }; this.collapse = function () { self.collapse = null; var $el = jQuery('<div class="version"></div>'); if (self.$download_icon.length) $el.append(self.$download_icon.clone()); $el.append(self._version_title()); if (!self.$checkbox.length && parseInt(self.$comments.text()) > 0) $el.append(self._comments_link()); return $el; }; this.hide = function () { $row.addClass('collapse-hidden'); }; this._init(); } function Group(name) { var self = this; this.versions = []; this.add_version = function (version) { self.versions.push(version); }; this.compare = function(a, b) { return a < b ? -1 : a > b ? 1 : 0; }; this.compare_mixed = function (a_str, b_str) { var a_num = a_str.match(/\d+/g) || []; var b_num = b_str.match(/\d+/g) || []; if (a_num.length && b_num.length) { // number compare each pair of pattern matches var length = Math.min(a_num.length, b_num.length); for (var i = 0; i < length; i++) { var a_int = parseInt(a_num[i]); var b_int = parseInt(b_num[i]); if (isNaN(a_int) || isNaN(b_int)) break; var result = self.compare(a_int, b_int); if (result !== 0) return result; } return self.compare(a_str, b_str); } else if(a_num.length || b_num.length) return a_num.length ? 1 : -1; else return self.compare(a_str, b_str); }; this.compare_versions = function (a, b) { var a_str = a.name; var b_str = b.name; return self.compare_mixed(a_str, b_str); }; this.compare_tags = function (a, b) { return self.compare(a.name, b.name); }; this._convert_tag = function () { var $this = jQuery(this); return { name: $this.text(), elem: this }; }; this._convert_tags = function (tags) { return tags.map(self._convert_tag).get(); }; this._missing_tags = function (versions) { var visible_tags = self._convert_tags(versions[0].$tags); var hidden_tags = _.pluck(versions.slice(1), '$tags'); hidden_tags = hidden_tags.map(self._convert_tags); hidden_tags = _.flatten(hidden_tags, true); visible_tags.sort(self.compare_tags); hidden_tags.sort(self.compare_tags); hidden_tags = _.uniq(hidden_tags, true, function (tag) {return tag.name;}); return difference_fast(hidden_tags, visible_tags, self.compare_tags); }; this.collapse = function (add_missing_tags) { self.collapse = null; var versions = self.versions.sort(self.compare_versions).reverse(); _.invoke(versions.slice(1), 'hide'); var collapsed_versions = _.invoke(versions, 'collapse'); versions[0].$title.after(collapsed_versions); versions[0].$title.text(name); versions[0].$title.parent().find('br').remove(); versions[0].$checkbox.change(function(event) { var checked = event.currentTarget.checked; _.invoke(versions.slice(1), 'toggle_checkbox', checked); }); if (versions.length && add_missing_tags) { var missing_tags = self._missing_tags(versions); if (missing_tags.length) { var elements = _.pluck(missing_tags, 'elem'); elements.forEach(function (elem) { versions[0].$tags_container.append(' ', jQuery(elem).clone()); }); } } }; } function CollapseDuplicates(title_parser, add_missing_tags, freeleach_icon, warning_icon) { var self = this; this.groups = {}; this.get_group = function (name) { var group = self.groups[name]; if (group === undefined) { group = new Group(name); self.groups[name] = group; } return group; }; this.create_group = function (_, row) { var version = new Version(title_parser, jQuery(row), freeleach_icon, warning_icon); self.get_group(version.reduced_title).add_version(version); }; this.create_groups = function() { jQuery('.torrent_table').find('tr.torrent').each(self.create_group); }; this.collapse_group = function (group) { group.collapse(add_missing_tags); }; this.collapse_groups = function() { _.each(self.groups, self.collapse_group); }; this.create_groups(); this.collapse_groups(); } // jQuery('body').one('click', function() { // var add_missing_tags = false; // new CollapseDuplicates(new TitleParser, add_missing_tags); // }); (function () { var add_missing_tags = false; var freeleach_icon = false; var warning_icon = false; new CollapseDuplicates(new TitleParser, add_missing_tags, freeleach_icon, warning_icon); })();