TorrentFilter

Filter and highlight torrents with conditions

// ==UserScript==
// @name         TorrentFilter
// @description  Filter and highlight torrents with conditions
// @version      1.0.3
// @author       Anonymous
// @match        *.nexushd.org/*
// @match        uhdbits.org/*
// @match        totheglory.im/*
// @match        kp.m-team.cc/*
// @match        filelist.io/*
// @match        greatposterwall.com/*
// @match        pterclub.com/*
// @match        pt.sjtu.edu.cn/*
// @require      https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
// @require      https://code.jquery.com/jquery-migrate-1.0.0.js
// @icon         http://www.nexushd.org/favicon.ico
// @namespace    d8e7078b-abee-407d-bcb6-096b59eeac17
// @license      MIT
// ==/UserScript==
const $ = window.jQuery;
////////////////////////////////////////////////////////////////////////////////////////////////
// Settings
// 黑名单,一定会被过滤
const blackList = [
  '', 'CMCT', 'ADE', 'FRDS', 'beAst', 'TLF', 'CHD', 'NYPAD'
].map(team => team.toLowerCase());
// 白名单,一定不被过滤
const whiteList = [
  // BluRay
  '4EVERHD','(C)Z','AE','AJ8','AJP','antsy','Arucard','AURiNKO','AW','Ayaku','BBW','BG','BMF','BoK','BS','Cache',
  'CALiGARi','Chotab','CRiME','Crow','D4','DiGG','DiR','DiRTY','disc','DBO','DoNOLi','E76','ECI','EML-HDTEAM','ESiR',
  'ETH','EucHD','FANDANGO','FaP','fLAMEhd','FPG','FSK','Ft4U','FTO','fty','Funner','Geek','GMoRK','GoLDSToNE','GOS',
  'GrapeHD','GRiND','GrupoHDS','H@M','H2','h264iRMU','HaB','HANDJOB','HDB','HDC','HDBiRD','HDEncX','HDL','HDxT','HiFi',
  'HR','hymen','HZ','iCO','IDE','IMDTHS','incarnation','iNFLiKTED','iNK','iON','iOZO','Ivandro','IY','J4F','JAVLiU',
  'JCH','k2','k4n0','kaBOOM','KalorZ','KiNGS','KiTTeN','KTN','KweeK','LP','LSHD','lulz','M794','MAGiC','MC','MCR','MdM',
  'MMI','Mojo','momosas','Mondo','Moshy','NaRB','NCmt','NFHD','NiP','NiX','NorTV','NoVA','NWO','OAS','OB1','OmertaHD',
  'ONYX','ORiGEN','PeeWee','PerfectionHD','PetaHD','PHiN','PiNG','PRESTiGE','Prime','PXE','QDP','QXE','RANDi','REDJOHN',
  'Redµx','REPTiLE','RightSiZE','RuDE','RZF','S26','SA89','SFH','sJR','SK','Slappy','SLO','SLO4U','SMoKeR','SPeSHaL',
  'SrS','SURFER','TAiCHi','THORA','TjHD','tK','TM','toho','ToK','tRuEHD','TSE','TsH','UioP','V','VanRay','Viet3X','(pr0n)',
  'ViNYL','ViSUM','Vroom','wAm','XSHD','YanY','Z','Zim','D','ZMB','Z-XCV','CRiSC','CtrlHD','DON','EA','EbP','LolHD','NTb',
  'SbR','TayTo','VietHD','de[42]','FoRM','NiBuRu','SaNcTi','Penumbra','Positive','SHeNTo','decibeL','D-Z0N3','FTW-HD',
  'OISTiLe','TDD','ZQ','PTer','WiKi','c0kE','dps','EDPH','HDMaNiAcS','HDVN','HiDt','iFT','JKP','JM','KnG','LorD','playHD',
  'prldm','PuTal','Q0S','RightSIZE','rttr','SaL','Skazhutin','TayTO','TBB','ZoroSenpai','147','Atomic','BARC0DE','BTN',
  'BV','BYRHD','BdC','CHAOS','CNZ','CREATiVE','CRX','CarpeDiem','Dariush','Dave','DiVULGED','DigitalIrony','Envi','EuReKA',
  'EwT','Friday','GALAXY','Japhson','KASHMiR','L9','LiNG','MGs','MKu','MaG','Narkyy','O2STK','ReQuEsT','Tron','VXS',
  'W4NK3R','WMD','WMING','Whales','WiHD','WiLDCAT','XTA','i9','iKA','nmd','nek','npuer','xander','uR','xvistos','SPHD',
  'eXterminator','PuTao', 'RiCO', 'TnP', 'SUPER',
  // Other BluRay
  'E.N.D', 'GALVANiZE', 'NyHD',
  // WEB / HDTV
  'FLUX', 'ADWeb', 'playWEB', 'TEPES', 'MZABI', 'AREY', 'CMRG', 'HDCTV', 'KHN', 'SMURF', 'ARiN'
].map(team => team.toLowerCase());
// 站名
const TTG = 'totheglory'; const PTERCLUB = 'pterclub'; const PUTAO = 'pt.sjtu'; const MTEAM = 'm-team'; const NHD = 'nexushd';
const BluRay = 'bluray'; const WEB = 'web'; const HDTV = 'hdtv';
const res1080p = '1080p'; const res720p = '720p'; const res2160p = '2160p'
const colorRecipeLight = {
  name: '',
  year: 'Blue',
  media: 'Navy',
  resolution: 'Green',
  codec: 'DimGray',
  team: 'Red',
  background: ''
}
const colorRecipeDark = {
  name: '',
  year: 'DeepSkyBlue',
  media: 'DodgerBlue',
  resolution: 'Green',
  codec: 'Gray',
  team: 'Red',
  background: ''
}
const siteInfoMap = {
  [TTG]: {
    // 主页
    hostName: 'totheglory.im',
    pages: [
      'browse.php'
    ],
    searchPage: /(?:&|\?)search_field=/i,
    // 过滤时是否移除条目。如果设置为true,被过滤的entry不会出现在页面中,否则只是不被高亮
    removeFiltered: true,
    // 字段配色
    colors: colorRecipeDark,
    watchType: {
      // 720p, 1080p, 2160p
      resolutions: [res1080p, res2160p],
      // 监测的media类型:BluRay, WEB,
      media: [BluRay, WEB, HDTV],
      // 白名单模式,设置为true时,既不在黑名单也不在白名单中的会被过滤
      teamsWhiteListMode: true
    }
  },
  [PTERCLUB]: {
    hostName: 'pterclub.com',
    pages: [
      'torrents.php',
      'officialgroup.php'
    ],
    searchPage: /(?:&|\?)search=/i,
    removeFiltered: true,
    colors: colorRecipeDark,
    watchType: {
      resolutions: [res720p, res1080p, res2160p],
      media: [BluRay, WEB, HDTV],
      teamsWhiteListMode: true
    }
  },
  [PUTAO]: {
    hostName: 'pt.sjtu.edu.cn',
    pages: [
      'torrents.php'
    ],
    searchPage: /(?:&|\?)search=/i,
    removeFiltered: true,
    colors: colorRecipeLight,
    watchType: {
      resolutions: [res1080p, res2160p],
      media: [BluRay, WEB, HDTV],
      teamsWhiteListMode: true
    }
  },
  [MTEAM]: {
    hostName: 'm-team.cc',
    pages: [
      'browse',
      'browse/movie'
    ],
    searchPage: /(?:&|\?)keyword=/i,
    waitForElement: 'div[class="flex flex-nowrap"]',
    removeFiltered: true,
    colors: colorRecipeDark,
    watchType: {
      resolutions: [res1080p, res2160p],
      media: [BluRay, WEB, HDTV],
      teamsWhiteListMode: true
    }
  },
  [NHD]: {
    hostName: 'nexushd.org',
    pages: [
      'torrents.php'
    ],
    searchPage: /(?:&|\?)search=/i,
    removeFiltered: false,
    colors: colorRecipeLight,
    watchType: {
      resolutions: [res720p, res1080p, res2160p],
      media: [BluRay, WEB, HDTV],
      teamsWhiteListMode: true
    }
  }
}
const retryInterval = 500
////////////////////////////////////////////////////////////////////////////////////////////////
// Functions
function escapeRegExp(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function decodeTorrentTags(torrent_name) {
  let start = ''
  let end = ''
  // 移除末尾无效内容(方便识别压制组)
  let matchEnd = torrent_name.match(/\.(mkv|mp4|avi|ts|wmv|mpg)$/, 'i')
  if (matchEnd) {
    end = matchEnd[0]
    torrent_name = torrent_name.substring(0, torrent_name.length - end.length)
  }
  // 移除开头无效内容并记录移除部分的长度(用于最终恢复真实字符索引)
  let matchStart = torrent_name.match(/^(「.*」|\[.*\]) */)
  if (matchStart) {
    start = matchStart[0]
    torrent_name = torrent_name.substring(start.length)
  }
  let tags = {
    name: [-1, 0],
    year: [-1, 0],
    media: [-1, 0],
    resolution: [-1, 0],
    codec: [-1, 0],
    team: [-1, 0]
  };
  let match_media = torrent_name.match(/\b(((UHD )?Blu-?Ray)|UHD|(BD|(HD)?DVD|WEB|HDTV)Rip|(HD)?DVD|HDTV|WEB(-?DL)?)\b/i);
  if (match_media) {
    tags.media = [match_media.index, match_media[0].length];
  }
  let match_team = torrent_name.match(/\b(D-Z0N3|[^\s-@]*(@[^\s-]+)?)$/);
  if (match_team) {
    tags.team = [match_team.index, match_team[0].length];
  }
  let match_codec = torrent_name.match(/\b(x26\d|h26\d|h\.?26\d|avc|hevc|xvid|divx|mpeg-\d|vc-1)\b/i);
  if (match_codec) {
    tags.codec = [match_codec.index, match_codec[0].length];
  }
  let match_resolution = torrent_name.match(/\b((480|720|1080|2160)[ip]|4k(?! ?remaster| ?restoration| ?restore))\b/i);
  if (match_resolution) {
    tags.resolution = [match_resolution.index, match_resolution[0].length];
  }
  // 注意符合年份regex的可能有多个,因为电影标题中可能有年份,所以要选择最后一个
  let match_year = torrent_name.match(/\b\d{4}\b/g);
  if (match_year) {
    let year = match_year[match_year.length - 1];
    tags.year = [torrent_name.lastIndexOf(year), year.length];
  }
  let name_length = torrent_name.length;
  for (var tag in tags) {
    if (tags[tag][0] < name_length && tags[tag][0] > 0) {
      name_length = tags[tag][0];
    }
  }
  tags.name = [0, name_length - 1];
  // 所有index加上头部长度
  for (const key in tags) {
    tags[key] = [tags[key][0] + start.length, tags[key][1]]
  }
  return tags
}
function whetherRemove(tags, title, siteName) {
  let site = siteInfoMap[siteName] || {}
  if (!site.watchType) {
    return false
  }
  let mediaToWatch = site.watchType.media
  let resolutionsToWatch = site.watchType.resolutions
  let teamsWhiteListMode = site.watchType.teamsWhiteListMode
  let team = '';
  if (tags.team[0] >= 0) {
    team = title.substring(tags.team[0], tags.team[0] + tags.team[1]).toLowerCase();
  }
  let resolution = '';
  if (tags.resolution[0] >= 0) {
    resolution = title.substring(tags.resolution[0], tags.resolution[0] + tags.resolution[1]).toLowerCase();
  }
  let media = '';
  if (tags.media[0] >= 0) {
    media = title.substring(tags.media[0], tags.media[0] + tags.media[1]).toLowerCase();
  }
  // 压制组过滤
  var remove = whiteList.includes(team)
    ? false
    : blackList.includes(team)
      ? true
      : teamsWhiteListMode;
  // 分辨率过滤
  if (!remove && resolutionsToWatch) {
    let res_ok = false;
    if (resolutionsToWatch.includes(resolution)) {
      res_ok = true;
    } else if (resolution.match(/4k/i) && resolutionsToWatch.includes('2160p')) {
      res_ok = true;
    }
    remove = !res_ok;
  }
  // 媒介过滤
  if (!remove && mediaToWatch) {
    let media_ok = false;
    if (mediaToWatch.includes(media)) {
      media_ok = true;
    } else if (media.match(/(UHD BluRay)|BluRay|UHD|Blu-ray|BDRip/i) && mediaToWatch.includes('bluray')) {
      media_ok = true;
    } else if (media.match(/WEB-DL|WEBRip|WEB/i) && mediaToWatch.includes('web')) {
      media_ok = true;
    } else if (media.match(/DVDRip|HDDVD|DVD/i) && mediaToWatch.includes('dvd')) {
      media_ok = true;
    }
    remove = !media_ok;
  }
  if (remove) {
    console.log(`remove ${title}`);
  } else {
    console.log(`keep ${title}`);
  }
  return remove;
}
// tags are the slices of the title stored in a dictionary, {'media': [4, 1], 'team': [8, 1]}
// renderFieldFunction renders a field, <color=red>text</color>
// renderTitleFunction renders a title
function renderTitle(originalText, tags, renderFieldFunction, renderTitleFunction, siteName) {
  let site = siteInfoMap[siteName] || {}
  let newText = '';
  let colors = site.colors || {}
  // sort the tags by starting index of the tag
  let sorted_keys = Object.keys(tags).map(k => ([k, tags[k][0]])).sort((a, b) => (a[1] - b[1]));
  let j = 0;
  for (let i = 0; i < sorted_keys.length; i++) {
    let key = sorted_keys[i][0];
    let idx_field = tags[key][0];
    let len_field = tags[key][1];
    let color_field = colors[key];
    if (len_field > 0) {
      let text_field = originalText.substring(idx_field, idx_field + len_field);
      if (j < idx_field) {
        newText += originalText.substring(j, idx_field);
      }
      if (color_field) {
        newText += renderFieldFunction(text_field, color_field);
      } else {
        newText += text_field;
      }
      j = idx_field + len_field;
    }
  }
  // possible extension
  if (j < originalText.length) {
    newText += originalText.substring(j);
  }
  let renderedTitle = renderTitleFunction(newText);
  return renderedTitle;
}
function runWhenReady(readySelector, callback) {
  var tryNow = function() {
    var elem = document.querySelector(readySelector)
    if (elem) {
      callback(elem)
    } else {
      console.log(`Page not ready yet, retrying in ${retryInterval/1000} seconds`)
      setTimeout(tryNow, retryInterval)
    }
  }
  tryNow()
}
function update(siteName, isSearchPage) {
  if (!siteName) {
    return
  }
  const site = siteInfoMap[siteName]
  if (siteName === PTERCLUB || siteName === PUTAO || siteName === NHD) {
    let tbody = $('tbody').closest('table.torrents');
    tbody.map(function() {
      let titles = $('a')
        .closest('table.torrentname')
        .find('a');
      $.each(titles, function(_, obj) {
        if ($(obj).attr("title")) {
          let title = $(obj).attr("title").trim();
          let tags = decodeTorrentTags(title);
          let remove = whetherRemove(tags, title, siteName);
          if (remove) {
            if (site.removeFiltered && !isSearchPage) {
              let row = obj.closest('table.torrentname').closest('tr');
              row.remove();
            }
          } else {
            let newText = renderTitle(title, tags,
              (text, color) => {
                return `<span style="color: ${color};">${text}</span>`;
              },
              nt => {
                return nt;
              },
              siteName
            );
            $(obj).find('b').html(newText);
            if (site.colors && site.colors.background) {
              $(obj).find('b').css('background-color', site.colors.background);
            }
          }
        }
      });
    });
  } else if (siteName === TTG) {
    let tbody = $('tbody').closest('#torrent_table');
    tbody.map(function() {
      let titles = $('a[class!="treport"]')
        .closest('div.name_left')
        .closest('tr.hover_hr')
        .find('a[class!="treport"][href^="/t/"] b');
      $.each(titles, function(_, obj) {
        if ($(obj).prop('innerHTML')) {
          let title = $(obj).prop('innerText').split(/\r?\n/)[0].trim();
          let tags = decodeTorrentTags(title);
          let remove = whetherRemove(tags, title, siteName);
          if (remove) {
            if (site.removeFiltered && !isSearchPage) {
              let row = obj.closest('tr.hover_hr');
              row.remove();
            }
          } else {
            let originalText = $(obj).prop('outerHTML');
            let newText = renderTitle(title, tags,
              (text, color) => {
                return `<span style="color: ${color};">${text}</span>`;
              },
              nt => {
                let regex = RegExp('(<b>.*)(' + escapeRegExp(title) + ')(.*<br>)', '');
                return originalText.replace(regex, '$1' + nt + '$3');
              },
              siteName
            );
            $(obj).html(newText);
            if (site.colors && site.colors.background) {
              $(obj).css('background-color', site.colors.background);
            }
          }
        }
      });
    })
  } else if (siteName === MTEAM) {
    let tbody = $('tbody').closest('#root').find('tbody')
    tbody.map(function() {
      let titles = $('strong')
        .closest('div[class="flex flex-nowrap"]')
        .find('strong')
      $.each(titles, function(_, obj) {
        if ($(obj).prop('textContent')) {
          let title = $(obj).prop('textContent').trim();
          let tags = decodeTorrentTags(title);
          let remove = whetherRemove(tags, title, siteName);
          if (remove) {
            if (site.removeFiltered && !isSearchPage) {
              let row = obj.closest('tr')
              row.remove()
            }
          } else {
            let newText = renderTitle(title, tags,
              (text, color) => {
                return `<span style="color: ${color};">${text}</span>`;
              },
              nt => {
                return nt
              },
              siteName
            );
            $(obj).html(`<strong>${newText}</strong>`);
            if (site.colors && site.colors.background) {
              $(obj).find('strong').css('background-color', site.colors.background);
            }
          }
        }
      });
    });
  }
}
(() => {
  'use strict';
  const siteName = Object.keys(siteInfoMap).find(sn => {
    let st = siteInfoMap[sn]
    return window.location.href.match(escapeRegExp(st.hostName))
  })
  let page = ''
  let site = {}
  if (siteName) {
    site = siteInfoMap[siteName]
    page = site.pages.find(pg => {
      let url = `${site.hostName}/${pg}`
      return window.location.href.match(escapeRegExp(url))
    })
  }
  if (!siteName || !page) {
    return
  }
  const isSearchPage = !!window.location.href.match(site.searchPage)
  console.log(`running in site ${siteName} and page ${page}.`)
  if (isSearchPage) {
    console.log(`running in search page`)
  }
  if (site.waitForElement) {
    // eslint-disable-next-line no-unused-vars
    runWhenReady(site.waitForElement, _ => {
      update(siteName, isSearchPage)
    })
  } else {
    update(siteName, isSearchPage)
  }
})();