Gazelle Music Tracker Snatched Tool

Mark snatched torrents across your favorite gazelle music trackers.

// ==UserScript==
// @name         Gazelle Music Tracker Snatched Tool
// @name:zh      GMTS:Gazelle 架构音乐 PT 种子状态标记工具
// @name:zh-CN   GMTS:Gazelle 架构音乐 PT 种子状态标记工具
// @name:zh-TW   GMTS:Gazelle 架构音樂 PT 種子狀態標記工具
// @namespace       Gazelle Snatched
// @description     Mark snatched torrents across your favorite gazelle music trackers.
// @description:zh      将你在 Gazelle 架构音乐 PT 上下载中、已完成等的种子用不同的颜色标记出来,选择隐藏保种的种子。
// @description:zh-CN   将你在 Gazelle 架构音乐 PT 上下载中、已完成等的种子用不同的颜色标记出来,选择隐藏保种的种子。
// @description:zh-TW   將你在 Gazelle 架构音樂 PT 上下載中、已完成等的種子用不同的顏色標記出來,選擇隱藏保種的種子。
// @author          Mordred
// @translator      ZexWoo
// @include         https://*redacted.ch/*
// @include         https://*orpheus.network/*
// @include         https://*dicmusic.club/*
// @include         https://*dicmusic.com/*
// @include         https://*lztr.me/*
// @require         https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js
// @grant           GM_xmlhttpRequest
// @grant           GM_registerMenuCommand
// @grant           GM_getResourceText
// @version         3.0.7
// @date            2023-11-01
// @license MIT
// ==/UserScript==

var snatched_groups = {};

(function () {
  "use strict";
  var start = new Date();

  if (typeof GM_registerMenuCommand == "undefined") {
    window["GM_registerMenuCommand"] = function (
      caption,
      commandFunc,
      accessKey
    ) {
      if (!document.body) {
        console.error("GM_registerMenuCommand got no body.");
        return;
      }
      let contextMenu = document.body.getAttribute("contextmenu");
      let menu = contextMenu
        ? document.querySelector("menu#" + contextMenu)
        : null;
      if (!menu) {
        menu = document.createElement("menu");
        menu.setAttribute("id", "gm-registered-menu");
        menu.setAttribute("type", "context");
        document.body.appendChild(menu);
        document.body.setAttribute("contextmenu", "gm-registered-menu");
      }
      let menuItem = document.createElement("menuitem");
      menuItem.textContent = caption;
      menuItem.addEventListener("click", commandFunc, true);
      menu.appendChild(menuItem);
    };
  }

  if (typeof GM_getResourceText == "undefined") {
    window["GM_getResourceText"] = function (aRes) {
      "use strict";
      return GM.getResourceUrl(aRes)
        .then((url) => fetch(url))
        .then((resp) => resp.text())
        .catch(function (error) {
          console.log("请求失败", error);
          return null;
        });
    };
  }

  if (typeof GM == "object") {
    Object.getOwnPropertyNames(GM).forEach(function (elem) {
      if (typeof GM[elem] == "function") {
        window["GM_" + elem] = function () {
          return GM[elem](arguments).then(function (res) {
            return res;
          });
        };
      }
    });
  }

  var chromeExtension = true;
  var manifest;
  var chromep;
  var storageObj = { gazelle_snatched: {} };
  if (!window.chrome || !chrome.extension) {
    // not on chrome so do FF specific things
    chromeExtension = false;
    // Not working: @require 		materialize_CSS https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css
    // var materialize_CSS = GM_getResourceText ("materialize_CSS");
    // addStyle(materialize_CSS);
    manifest = GM_info.script;
  } else {
    manifest = chrome.runtime.getManifest();

    chromep = new ChromePromise();
  }
  console.log(manifest.name + " v" + manifest.version + " by Mordred");

  var releaseTypes = [
    "Album",
    "EP",
    "Soundtrack",
    "Compilation",
    "Remix",
    "Anthology",
    "DJ Mix",
    "Single",
    "Live album",
    "Mixtape",
    "Unknown",
    "Bootleg",
    "Interview",
    "Demo",
  ];
  var releaseTypeRegex = new RegExp(
    "\\[(?:" + releaseTypes.join("|") + ")\\]$"
  );

  function GM_getLSValue(key, defaultValue) {
    var value = window.localStorage.getItem(key);
    if (value == null) value = defaultValue;
    // if (chromeExtension) {
    // 	chromep.storage.local.get('gazelle_snatched').then(function (data) {
    // 		console.log(key, data.gazelle_snatched[key]);
    // 	});
    // }
    return value;
  }

  function GM_setLSValue(key, value) {
    try {
      window.localStorage.setItem(key, value);
    } catch (e) {
      console.error(
        "错误:无法更新种子列表。你很可能需要提高 localStorage 的值。\
				请查看主帖以了解详细解决方案:https://redacted.ch/forums.php?action=viewthread&threadid=4082&page=4#post279935"
      );
    }
    // if (!chromeExtension) {
    // 	GM_setValue(key, value);
    // }
    // if (chromeExtension) {
    // 	storageObj.gazelle_snatched[key] = value;
    // 	chromep.storage.local.set(storageObj);
    // }
  }
  function GM_deleteLSValue(key) {
    window.localStorage.removeItem(key);
  }

  function addStyle(css) {
    $('<style type="text/css">' + css + "</style>").appendTo("head");
  }

  function GM_xmlhttpRequest(details) {
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function () {
      var responseState = {
        responseXML: xmlhttp.readyState == 4 ? xmlhttp.responseXML : "",
        responseText: xmlhttp.readyState == 4 ? xmlhttp.responseText : "",
        readyState: xmlhttp.readyState,
        responseHeaders:
          xmlhttp.readyState == 4 ? xmlhttp.getAllResponseHeaders() : "",
        status: xmlhttp.readyState == 4 ? xmlhttp.status : 0,
        statusText: xmlhttp.readyState == 4 ? xmlhttp.statusText : "",
      };
      if (details["onreadystatechange"]) {
        details["onreadystatechange"](responseState);
      }
      if (xmlhttp.readyState == 4) {
        if (
          details["onload"] &&
          xmlhttp.status >= 200 &&
          xmlhttp.status < 300
        ) {
          details["onload"](responseState);
        }
        if (
          details["onerror"] &&
          (xmlhttp.status < 200 || xmlhttp.status >= 300)
        ) {
          details["onerror"](responseState);
        }
      }
    };
    try {
      //cannot do cross domain
      xmlhttp.open(details.method, details.url);
    } catch (e) {
      if (details["onerror"]) {
        //simulate a real error
        details["onerror"]({
          responseXML: "",
          responseText: "",
          readyState: 4,
          responseHeaders: "",
          status: 403,
          statusText: "Forbidden",
        });
      }
      return;
    }
    if (details.headers) {
      for (var prop in details.headers) {
        xmlhttp.setRequestHeader(prop, details.headers[prop]);
      }
    }
    xmlhttp.send(typeof details.data != "undefined" ? details.data : null);
  }

  function getIconImageUrl(icon) {
    if (chromeExtension) {
      return chrome.extension.getURL("images/" + icon + ".png");
    } else {
      var url = "";
      switch (icon) {
        case "uploaded":
          url = "https://ptpimg.me/4i1y66.png";
          break;
        case "snatched":
          url = "https://ptpimg.me/13itg3.png";
          break;
        case "down":
          url = "https://ptpimg.me/8180y8.png";
          break;
        case "bookmark":
          url = "https://ptpimg.me/33z7ms.png";
          break;
        case "whatcd":
          url = "https://ptpimg.me/eo9003.png";
          break;
      }
      return url;
    }
  }
 // var hide_seeding = false;
  var GROUP_SNATCHED =
    "font-style:italic; font-weight:bolder; text-decoration:underline;";
  var T_SNATCHED =
    "color: #E5B244 !important; text-decoration:line-through !important; display:inline; background:url(" +
    getIconImageUrl("snatched") +
    ") top right no-repeat; padding:1px 18px 1px 0;";
  var UPLOADED =
    "color: #63b708 !important; text-decoration:line-through !important; display:inline; background:url(" +
    getIconImageUrl("uploaded") +
    ") top right no-repeat; padding:1px 18px 1px 0;";
  var LEECHING =
    "color: #F70000 !important; display:inline; background:url(" +
    getIconImageUrl("down") +
    ") top right no-repeat; padding:1px 18px 1px 0;";
  var SEEDING = "font-style:italic; text-decoration:none !important;";
  var BOOKMARKED =
    "background:url(" +
    getIconImageUrl("bookmark") +
    ") top right no-repeat; padding:1px 18px 1px 0;";
  var WHATCD_GROUP =
    "background:url(" +
    getIconImageUrl("whatcd") +
    ") top right no-repeat; padding:1px 22px 1px 0;";
  // var UPLOADED = 'color: #63b708 !important; text-decoration:line-through !important; display:inline; background:url(https://whatimg.com/i/8oux68.png) top right no-repeat; padding:1px 18px 1px 0;';
  // var LEECHING = 'color: #F70000 !important; display:inline; background:url(https://whatimg.com/i/ay3zvb.png) top right no-repeat; padding:1px 18px 1px 0;';
  // var SEEDING = 'font-style:italic; text-decoration:none !important;';
  // var BOOKMARKED = 'background:url(https://whatimg.com/i/4otnce.png) top right no-repeat; padding:1px 18px 1px 0;';

  var HEADER_STYLE =
    ".sBoxTitle { color: white; } .sBoxTitle:visited { color: white; } .sboxTitleVersion { color: red; } .sboxTitleVersion:visited { color: red; }";
  var AUTO_UPDATE_INTERVAL = 20; /* minutes */
  var STATUS_BOX_YOFFSET = 20; /* pixels */

  var domain_prefix = "gazelle_";
  var domain_abbr = "g";
  switch (location.hostname) {
    case "redacted.ch":
      domain_prefix = "redacted_";
      domain_abbr = "r";
      break;
    case "orpheus.network":
      domain_prefix = "orpheus_";
      domain_abbr = "o";
      break;
    case "dicmusic.club":
      domain_prefix = "dicmusic_";
      domain_abbr = "d";
      break;
  }

  var global_updateFreq = getDomainLSValue("update_freq", AUTO_UPDATE_INTERVAL);
  var global_hideStatusBox = getDomainLSValue("box_hidden", "false");
  var hide_seeding_torrent = getDomainLSValue("torrent_hide", "false");
  var global_SB_YOffset = getDomainLSValue("box_yoffset", STATUS_BOX_YOFFSET);
  /* Inject CSS style */
  var style_groupsnatched = getDomainLSValue(
    "style_groupsnatched",
    GROUP_SNATCHED
  );
  var style_tsnatched = getDomainLSValue("style_tsnatched", T_SNATCHED);
  var style_uploaded = getDomainLSValue("style_uploaded", UPLOADED);
  var style_leeching = getDomainLSValue("style_leeching", LEECHING);
  var style_seeding = getDomainLSValue("style_seeding", SEEDING);
  var style_bookmarked = getDomainLSValue("style_bookmarked", BOOKMARKED);
  var scriptVersion = GM_getLSValue("script_version", "0.0.0");
  var style_whatgroup = GM_getLSValue("style_whatgroup", WHATCD_GROUP);
  addStyle(".group_snatched { " + style_groupsnatched + " }");
  addStyle(".gazelle_snatched { " + style_tsnatched + " }");
  addStyle(".gazelle_uploaded { " + style_uploaded + " }");
  addStyle(".gazelle_leeching { " + style_leeching + " }");
  addStyle(".gazelle_seeding { " + style_seeding + " }");
  addStyle(".gazelle_bookmark { " + style_bookmarked + " }");
  addStyle(".whatcd_group { " + style_whatgroup + " }");
  addStyle(HEADER_STYLE);

  /** REMOVE THESE STYLES FOR CHROME */
  addStyle(
    ".gazelle_menu { background-color: rgba(40,40,40,0.96); position: fixed; z-index: 902; font-family: Arial, sans-serif; font-size: 11px !important; }"
  );
  addStyle(".pull-right { float: right; } ");
  addStyle(
    ".gazelle_btn { margin-right: 5px; text-decoration: none; color: #fff; background-color: #26a69a; text-align: center; letter-spacing: .5px; transition: .2s ease-out; cursor: pointer; border: none; border-radius: 2px; display: inline-block; height: 36px; line-height: 36px; padding: 0 2rem; text-transform: uppercase; vertical-align: middle; -webkit-tap-highlight-color: transparent; } "
  );
  addStyle(
    ".gazelle_btn:hover { background-color: #2bbbad; box-shadow: 0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2); } "
  );
  addStyle(
    ".gazelle_sm_btn { padding: 2px 5px; margin-top: -3px; line-height: 20px; height: 20px; }"
  );

  addStyle(".gazelle_subItem { margin: 0px 5px 0px 25px; }");
  addStyle(
    ".gazelle_numeric { padding: 2px !important; font-size: 9pt !important; }"
  );
  addStyle(".gazelle_header { color:#ffffff !important; font-size: 11pt; }");
  addStyle(
    ".gazelle_text { width: 68% !important; margin-right:10px; padding: 2px !important; font-size: 9pt !important; }"
  );
  addStyle(".gazelle_small_text { font-size: 10px; }");
  addStyle(".gazelle_link { margin-left:3px; margin-right:3px; }");
  addStyle(
    ".gazelle_class { display: inline-block; width:93px; margin-left:25px; margin-bottom:9px; font-size:8pt;}"
  );
  addStyle(
    ".gazelle_leftCol { width:50%; height:auto; display:table-cell; padding: 10px 0px 10px; }"
  );
  addStyle(
    ".gazelle_rightCol { width:auto; height:auto; display:table-cell; padding: 10px 0px 10px; }"
  );
  /** END FIREFOX STYLES */

  /* Throttled proxy */
  function ThrottledProxy(url_base, delay) {
    var last_req = new Date(0);
    var queue = [];
    var processing = false;

    return {
      get: function (req) {
        var now = new Date();
        queue.push(req);
        if (!processing) {
          /* Race condition: atomic test and set would be appropriate here, to ensure thread safety (is it a problem?) */
          processing = true;
          var diff = last_req.getTime() + delay - now.getTime();
          if (diff > 0) {
            var that = this;
            window.setTimeout(function () {
              that.process_queue();
            }, diff);
          } else {
            this.process_queue();
          }
        }
      },

      process_queue: function () {
        var req = queue.shift();
        this.do_request(req);
        processing = queue.length > 0;
        if (processing) {
          var that = this;
          window.setTimeout(function () {
            that.process_queue();
          }, delay);
        }
      },

      do_request: function (req) {
        last_req = new Date();
        var timer;
        var req_timed_out = false; /* We cannot abort a request, so we need keep track of whether it timed out */

        /* Create timeout handler */
        timer = window.setTimeout(function () {
          /* Race condition: what if the request returns successfully now? */
          req_timed_out = true;
          if (req.error) req.error(null, "网络超时");
        }, req.timeout || 20000);

        /* Do the actual request */
        GM_xmlhttpRequest({
          method: req.method || "GET",
          url: url_base + req.url,
          headers: {
            /*'User-Agent': navigator.userAgent,*/ Accept:
              req.accept || "text/xml",
          },
          onload: function (response) {
            window.clearTimeout(timer);
            if (!req_timed_out) req.callback(response);
          },
          onerror: function (response) {
            window.clearTimeout(timer);
            if (!req_timed_out && req.error)
              req.error(response, "GM_xmlhttpRequest error");
          },
        });
      },
    };
  }

  /* Global status area - feel free to reuse in your own scripts :)
	   Requires jQuery and the round extension above. */
  function StatusBox(title) {
    /* Setup status area */
    var status_area = $("#gazelle_greasemonkey_status_area").eq(0);
    if (status_area.length == 0) {
      var statWidth = "20%";
      if (window.innerWidth < 1340) statWidth = 268;
      status_area = $('<div id="gazelle_greasemonkey_status_area"></div>').css({
        position: "fixed",
        margin: global_SB_YOffset.toString() + "px 20px",
        width: statWidth,
        "z-index": 901,
      });
      var boxPos = getDomainLSValue("box_position", "top_right");
      if (boxPos == "bottom_right")
        status_area.css({ bottom: "0", right: "0" });
      else if (boxPos == "top_left") status_area.css({ top: "0", left: "0" });
      else if (boxPos == "bottom_left")
        status_area.css({ bottom: "0", left: "0" });
      /* top_right */ else status_area.css({ top: "0", right: "0" });
      $("body").append(status_area);
    }

    /* Create box */
    var box = $('<div id="status_content_area"></div>').hide();
    box.css({
      color: "white",
      "background-color": "black",
      opacity: 0.7,
      margin: "0 0 10px 0",
      padding: "10px 10px 20px 10px",
      "border-radius": "10px",
    });

    /* Create contents area */
    var contents = $("<div></div>");
    box.append(contents);

    var timer = null;
    var timeout = 0;
    var inhibit_fade = false;

    function set_visible(visible) {
      if (visible && box.is(":hidden")) box.fadeIn(500);
      else if (!visible && box.is(":visible")) box.fadeOut(500);
    }

    function clear_timer() {
      if (timer) {
        window.clearTimeout(timer);
        timer = null;
      }
    }

    function set_timer() {
      if (!timer && timeout > 0) {
        timer = window.setTimeout(function () {
          clear_timer();
          set_visible(false);
        }, timeout);
      }
    }

    function update_timer(t) {
      clear_timer();
      timeout = t;
      if (!inhibit_fade) set_timer();
    }

    function set_inhibit_fade(inhibit) {
      inhibit_fade = inhibit;
      if (!inhibit_fade) {
        set_timer();
      } else clear_timer();
    }

    /* Register event handlers */
    box.mouseenter(function (event) {
      set_inhibit_fade(true);
      $(this).fadeTo(500, 0.9);
    });

    box.mouseleave(function (event) {
      set_inhibit_fade(false);
      $(this).fadeTo(500, 0.7);
    });

    box.click(function (event) {
      clear_timer();
      $(this).unbind("mouseenter");
      $(this).unbind("mouseleave");
      set_visible(false);
    });

    /* Append to global status area */
    status_area.append(box);
    return {
      contents: function () {
        return contents;
      },

      show: function (t) {
        if (
          global_hideStatusBox != "true" ||
          /\/torrents\.php.type/.test(document.URL)
        ) {
          t = t || 0;
          update_timer(t);
          set_visible(true);
        }
      },

      hide: function () {
        clear_timer();
        set_visible(false);
      },
    };
  }

  function doOptionsMenu() {
    var options_menu = $("#gazelle_options_menu").eq(0);
    if (options_menu.length == 0) {
      var optHeight = 570;
      var optWidth = 820;
      options_menu = $(
        '<div id="gazelle_options_menu" class="gazelle_menu"></div>'
      )
        .css({
          top: window.innerHeight * 0.95,
          left: "50%",
          "margin-left": -optWidth * 0.5,
          width: optWidth,
          height: optHeight * 1.18,
          "border-radius": "10px",
          "z-index": 50000000,
          padding: "20px",
        })
        .hide();
      var css_div = $("<div></div>").css({
        width: "95%",
        height: "auto",
        margin: "0 20px 15px",
        color: "#ffffff", //,'overflow': 'hidden'
      });
      var refreshHeader = $('<h3 class="gazelle_header">更新频率</h3>');
      var refreshInput = $(
        '<input class="gazelle_subItem gazelle_numeric" type="text" name="interval" maxlength="3">更新间隔分钟数(最小值为 10)<br>'
      ).css({ "text-align": "right", width: "20px" });
      var columns_div = $("<div></div>").css({
        width: "100%",
        "margin-top": "-18px",
        display: "table",
      });
      var leftColumn = $('<div class="gazelle_leftCol"></div>');
      leftColumn.append(refreshHeader);
      leftColumn.append(refreshInput);

      var hideHeader = $('<h3 class="gazelle_header">可见性</h3>');
      var check_box_hide = $(
        '<input class="gazelle_subItem" type="checkbox" name="visibility">在所有页面展示状态栏<br>'
      );
      var explanation_div = $(
        "<div class=\"gazelle_small_text gazelle_subItem\">无论脚本更新是否可用,状态栏将始终显示在 '/torrents.php?type=...' 。</div>"
      );
      var hide_seeding = $(
        '<input class="gazelle_subItem" type="checkbox" name="hide_torrent">隐藏正在做种的种子<br>'
      );
      leftColumn.append(hideHeader);
      leftColumn.append(check_box_hide);
      leftColumn.append(explanation_div);
      leftColumn.append(hide_seeding);

      var positionHeader = $('<h3 class="gazelle_header">状态栏位置</h3>');
      var radio_button_tl = $(
        '<input class="gazelle_subItem" type="radio" name="location" id="top_left"/>左上角<br>'
      );
      var radio_button_tr = $(
        '<input class="gazelle_subItem" type="radio" name="location" id="top_right"/>右上角<br>'
      );
      var radio_button_bl = $(
        '<input class="gazelle_subItem" type="radio" name="location" id="bottom_left"/>左下角<br>'
      );
      var radio_button_br = $(
        '<input class="gazelle_subItem" type="radio" name="location" id="bottom_right"/>右下角<br>'
      );
      var rightColumn = $('<div class="gazelle_rightCol"></div>');
      rightColumn.append(positionHeader);
      rightColumn.append(radio_button_tl);
      rightColumn.append(radio_button_tr);
      rightColumn.append(radio_button_bl);
      rightColumn.append(radio_button_br);

      var offsetHeader = $('<h3 class="gazelle_header">状态栏纵向偏移</h3>');
      var offsetInput = $(
        '<input class="gazelle_subItem gazelle_numeric" type="text" name="yOffset" maxlength="3">相对窗口顶部或底部的偏移像素值<br>'
      ).css({ "text-align": "right", width: "20px" });
      rightColumn.append(offsetHeader);
      rightColumn.append(offsetInput);
      columns_div.append(leftColumn);
      columns_div.append(rightColumn);
      css_div.append(columns_div);

      var full_div = $("<div></div>");

      var styleHeader = $('<h3 class="gazelle_header">链接样式设置</h3>');
      full_div.append(styleHeader);
      var sampleText = $(
        '<span class="gazelle_class"></span><a href="#" id="sample_gsnatched">已完成种子组链接示例</a><br>'
      );
      sampleText.click(function () {
        return false;
      });
      snatchedInput = $(
        '<span class="gazelle_class">.group_snatched</span><input class="gazelle_text" type="text" id="input_gsnatched" value="' +
          style_groupsnatched +
          '">'
      );
      var applyLink = $('<span class="gazelle_btn gazelle_sm_btn">测试</span>');
      applyLink.click(function () {
        applyStyle("sample_gsnatched", "input_gsnatched");
        return false;
      });
      var defaultLink = $(
        '<span class="gazelle_btn gazelle_sm_btn">重置</span>'
      );
      defaultLink.click(function () {
        setStyle("sample_gsnatched", GROUP_SNATCHED);
        $("input[id='input_gsnatched']").val(GROUP_SNATCHED);
        return false;
      });
      full_div.append(sampleText);
      full_div.append(snatchedInput);
      full_div.append(applyLink);
      full_div.append(defaultLink);

      var sampleText = $(
        '<span class="gazelle_class"></span><a href="#" id="sample_tsnatched">已完成种子链接示例</a><br>'
      );
      sampleText.click(function () {
        return false;
      });
      var snatchedInput = $(
        '<span class="gazelle_class">.gazelle_snatched</span><input class="gazelle_text" type="text" id="input_tsnatched" value="' +
          style_tsnatched +
          '">'
      );
      applyLink = $('<span class="gazelle_btn gazelle_sm_btn">测试</span>');
      applyLink.click(function () {
        applyStyle("sample_tsnatched", "input_tsnatched");
        applyStyle("sample_seeding", "input_tsnatched", "input_seeding");
        return false;
      });
      defaultLink = $('<span class="gazelle_btn gazelle_sm_btn">重置</span>');
      defaultLink.click(function () {
        setStyle("sample_tsnatched", T_SNATCHED);
        setStyle(
          "sample_seeding",
          T_SNATCHED + $("input[id='input_seeding']").val()
        );
        $("input[id='input_tsnatched']").val(T_SNATCHED);
        return false;
      });
      full_div.append(sampleText);
      full_div.append(snatchedInput);
      full_div.append(applyLink);
      full_div.append(defaultLink);

      sampleText = $(
        '<span class="gazelle_class"></span><a href="#" id="sample_uploaded">已发布种子链接示例</a><br>'
      );
      sampleText.click(function () {
        return false;
      });
      snatchedInput = $(
        '<span class="gazelle_class">.gazelle_uploaded</span><input class="gazelle_text" type="text" id="input_uploaded" value="' +
          style_uploaded +
          '">'
      );
      applyLink = $('<span class="gazelle_btn gazelle_sm_btn">测试</span>');
      applyLink.click(function () {
        applyStyle("sample_uploaded", "input_uploaded");
        applyStyle("sample_ul_seed", "input_uploaded", "input_seeding");
        return false;
      });
      defaultLink = $('<span class="gazelle_btn gazelle_sm_btn">重置</span>');
      defaultLink.click(function () {
        setStyle("sample_uploaded", UPLOADED);
        setStyle(
          "sample_ul_seed",
          UPLOADED + $("input[id='input_seeding']").val()
        );
        $("input[id='input_uploaded']").val(UPLOADED);
        return false;
      });
      full_div.append(sampleText);
      full_div.append(snatchedInput);
      full_div.append(applyLink);
      full_div.append(defaultLink);

      //sampleText = $('<span class="gazelle_class"></span><a href="#" id="sample_seeding">Sample Seeding Snatched Torrent Link</a><span>&nbsp;&nbsp;(.gazelle_snatched style is also applied to this link)</span><br>');
      //sampleTxt2 = $('<span class="gazelle_class"></span><a href="#" id="sample_ul_seed">Sample Seeding Uploaded Torrent Link</a><span>&nbsp;&nbsp;(.gazelle_uploaded style is also applied to this link)</span><br>');
      sampleText = $(
        '<span class="gazelle_class"></span>做种中的链接<i>总是</i>会应用于 .gazelle_snatched 和 .gazelle_uploaded 样式,<br><span class="gazelle_class"></span>所以 .gazelle_seeding 通常用于覆盖上述基本样式。</br>'
      );
      var sampleTxt2 = $(
        '<span class="gazelle_class"></span><a href="#" id="sample_seeding">做种中已完成种子链接示例</a>&nbsp;&nbsp;<a href="#" id="sample_ul_seed">做种中已发布种子示例</a><br>'
      );
      //sampleText.click(function () { return false; });
      sampleTxt2.click(function () {
        return false;
      });
      snatchedInput = $(
        '<span class="gazelle_class">.gazelle_seeding</span><input class="gazelle_text" type="text" id="input_seeding" value="' +
          style_seeding +
          '">'
      );
      applyLink = $('<span class="gazelle_btn gazelle_sm_btn">测试</span>');
      applyLink.click(function () {
        applyStyle("sample_seeding", "input_tsnatched", "input_seeding");
        applyStyle("sample_ul_seed", "input_uploaded", "input_seeding");
        return false;
      });
      defaultLink = $('<span class="gazelle_btn gazelle_sm_btn">重置</span>');
      defaultLink.click(function () {
        setStyle(
          "sample_seeding",
          $("input[id='input_tsnatched']").val() + SEEDING
        );
        $("input[id='input_seeding']").val(SEEDING);
        setStyle(
          "sample_ul_seed",
          $("input[id='input_uploaded']").val() + SEEDING
        );
        return false;
      });
      full_div.append(sampleText);
      full_div.append(sampleTxt2);
      full_div.append(snatchedInput);
      full_div.append(applyLink);
      full_div.append(defaultLink);

      sampleText = $(
        '<span class="gazelle_class"></span><a href="#" id="sample_leeching">下载中种子链接示例</a><br>'
      );
      sampleText.click(function () {
        return false;
      });
      snatchedInput = $(
        '<span class="gazelle_class">.gazelle_leeching</span><input class="gazelle_text" type="text" id="input_leeching" value="' +
          style_leeching +
          '">'
      );
      applyLink = $('<span class="gazelle_btn gazelle_sm_btn">测试</span>');
      applyLink.click(function () {
        applyStyle("sample_leeching", "input_leeching");
        return false;
      });
      defaultLink = $('<span class="gazelle_btn gazelle_sm_btn">重置</span>');
      defaultLink.click(function () {
        setStyle("sample_leeching", LEECHING);
        $("input[id='input_leeching']").val(LEECHING);
        return false;
      });
      full_div.append(sampleText);
      full_div.append(snatchedInput);
      full_div.append(applyLink);
      full_div.append(defaultLink);

      sampleText = $(
        '<span class="gazelle_class"></span><a href="#" id="sample_bookmarked">已收藏种子链接示例</a><br>'
      );
      sampleText.click(function () {
        return false;
      });
      snatchedInput = $(
        '<span class="gazelle_class">.gazelle_bookmark</span><input class="gazelle_text" type="text" id="input_bookmarked" value="' +
          style_bookmarked +
          '">'
      );
      applyLink = $('<span class="gazelle_btn gazelle_sm_btn">测试</span>');
      applyLink.click(function () {
        applyStyle("sample_bookmarked", "input_bookmarked");
        return false;
      });
      defaultLink = $('<span class="gazelle_btn gazelle_sm_btn">重置</span>');
      defaultLink.click(function () {
        setStyle("sample_bookmarked", BOOKMARKED);
        $("input[id='input_bookmarked']").val(BOOKMARKED);
        return false;
      });
      full_div.append(sampleText);
      full_div.append(snatchedInput);
      full_div.append(applyLink);
      full_div.append(defaultLink);
      css_div.append(full_div);

      var okay_button = $(
        '<span id="js_ok_button" class="gazelle_btn pull-right">确认</span>'
      );
      okay_button.click(function () {
        CommitOptions();
        DisplaySlideMenu(false);
      });
      var cancel_button = $(
        '<span id="js_close_button" class="gazelle_btn pull-right">取消</span>'
      );
      cancel_button.click(function () {
        DisplaySlideMenu(false);
      });
      var button_div = $("<div></div>").css({
        width: "95%",
        margin: "15px",
        overflow: "hidden",
      });

      options_menu.append(css_div);
      button_div.append(cancel_button);
      button_div.append(okay_button);
      options_menu.append(button_div);
      $("body").append(options_menu);
    } else {
      // we already created the div
      var boxPos = getDomainLSValue("box_position", "top_right");
      $("input[name='location'][id='" + boxPos + "']").attr(
        "checked",
        "checked"
      );
      if (global_hideStatusBox != "true")
        $("input[name='visibility']").attr("checked", "checked");
      if (hide_seeding_torrent != "true")
        $("input[name='hide_torrent']").attr("checked", "checked");
      $("input[name='interval']").val(global_updateFreq);
      $("input[name='yOffset']").val(global_SB_YOffset);
      applyStyle("sample_gsnatched", "input_gsnatched");
      applyStyle("sample_tsnatched", "input_tsnatched");
      applyStyle("sample_uploaded", "input_uploaded");
      applyStyle("sample_leeching", "input_leeching");
      applyStyle("sample_seeding", "input_tsnatched", "input_seeding");
      applyStyle("sample_ul_seed", "input_uploaded", "input_seeding");
      applyStyle("sample_bookmarked", "input_bookmarked");
    }
  }

  function applyStyle(textControl, styleControl, styleControl2) {
    var css_style = $("input[id='" + styleControl + "']").val();
    if (styleControl2)
      css_style += $("input[id='" + styleControl2 + "']").val();
    setStyle(textControl, css_style);
  }

  function setStyle(textControl, css_style) {
    $("a[id='" + textControl + "']").removeAttr("style");
    $("a[id='" + textControl + "']").attr("style", css_style);
  }

  function CommitOptions() {
    var locRadio = $("input[name='location']:checked").attr("id");
    if (locRadio.length != 0) {
      setDomainLSValue("box_position", locRadio);
    }
    var boxHide = $("input[name='visibility']:checked");
    var seedHide = $("input[name='hide_torrent']:checked");
    if (boxHide.length != 0) {
      deleteDomainLSValue("box_hidden");
    } else {
      setDomainLSValue("box_hidden", "true");
      global_hideStatusBox = true;
      status.hide();
    }
    if (seedHide.length != 0) {
        deleteDomainLSValue("torrent_hide");
        $(".gazelle_seeding").parents("tr").remove();
       // console.log('选择隐藏');
    }else{
      hide_seeding_torrent=true;
      $(".gazelle_seeding").parents("tr").show();
      setDomainLSValue("torrent_hide", "true");
      //console.log('选择显示做种');
    }
    var updateFreq = $("input[name='interval']").val();
    if (jQuery.isNumeric(updateFreq)) {
      if (updateFreq != AUTO_UPDATE_INTERVAL) {
        if (updateFreq < 10) updateFreq = 10;
        setDomainLSValue("update_freq", updateFreq);
      } else deleteDomainLSValue("update_freq");
    }
    var offset = $("input[name='yOffset']").val();
    if (jQuery.isNumeric(offset) && offset >= 0) {
      if (offset != STATUS_BOX_YOFFSET) setDomainLSValue("box_yoffset", offset);
      else deleteDomainLSValue("box_yoffset");
    }
    AddOrDeleteCustomStyle(
      "input_gsnatched",
      GROUP_SNATCHED,
      "style_groupsnatched",
      ".group_snatched"
    );
    AddOrDeleteCustomStyle(
      "input_tsnatched",
      T_SNATCHED,
      "style_tsnatched",
      ".gazelle_snatched"
    );
    AddOrDeleteCustomStyle(
      "input_uploaded",
      UPLOADED,
      "style_uploaded",
      ".gazelle_uploaded"
    );
    AddOrDeleteCustomStyle(
      "input_leeching",
      LEECHING,
      "style_leeching",
      ".gazelle_leeching"
    );
    AddOrDeleteCustomStyle(
      "input_seeding",
      SEEDING,
      "style_seeding",
      ".gazelle_seeding"
    );
    AddOrDeleteCustomStyle(
      "input_bookmarked",
      BOOKMARKED,
      "style_bookmarked",
      ".gazelle_bookmark"
    );
  }

  function AddOrDeleteCustomStyle(inputName, def_css, storageVal, className) {
    var css = jQuery.trim($("input[id='" + inputName + "']").val());
    if (css == def_css) {
      // if the current css stripped of whitespace equals the default style, delete the custom style
      deleteDomainLSValue(storageVal);
      css = def_css;
    } else setDomainLSValue(storageVal, css);
    addStyle(className + "{" + css + "}"); // updates the page without reloading (at least on chrome)
  }

  function DisplaySlideMenu(showMenu) {
    if (showMenu) {
      if (!slideMenuShowing) {
        slideMenuShowing = 1;
        $("#gazelle_options_menu")
          .show()
          .animate({
            top: "-=" + ($("#gazelle_options_menu").innerHeight() - 10) + "px",
          });
      }
    } else {
      slideMenuShowing = 0;
      $("#gazelle_options_menu").animate(
        {
          top: "+=" + ($("#gazelle_options_menu").innerHeight() - 10) + "px",
        },
        function () {
          $("#gazelle_options_menu").hide();
        }
      );
    }
  }
  /*****************************/
  /*** END OPTIONS PAGE CODE ***/
  /*****************************/

  /* Cache */
  function Cache(name, def_value) {
    var cache;
    return {
      serialize: function () {
        setDomainLSValue(name, JSON.stringify(cache));
      },
      unserialize: function () {
        cache = jQuery.parseJSON(getDomainLSValue(name, "false"));
        if (!cache) cache = jQuery.extend({}, def_value); /* clone */
        return cache;
      },
      clear: function () {
        cache = jQuery.extend({}, def_value); /* clone */
        this.serialize();
      },
      name: domain_prefix + name,
    };
  }

  function registerMenuCommand(oText, oFunc) {
    if (/firefox/i.test(navigator.userAgent))
      GM_registerMenuCommand(oText, oFunc);
    MenuCommandArray[MenuCommandArray.length] = [
      oText.replace("Gazelle Snatched: ", ""),
      oFunc,
      oText.replace("Gazelle Snatched: ", "").replace(" ", "_"),
    ];
  }

  function upgradeSnatchCache(c) {
    var snatched = c.unserialize();

    if (!snatched.version) {
      snatched.version = 1;
    }
    switch (snatched.version) {
      // all upgrades should only go up one version at a time. No skipping versions or changing released upgrade code
      case 1:
        group_cache = Cache("snatched_groups", {
          version: currSnatchedGroupsVer,
          groups: snatched.groups,
        });
        group_cache.unserialize();
        group_cache.serialize();
        delete snatched.groups;
        snatched.version++;
        c.serialize();
        break;
      case 2:
        break;
      default:
        console.error(
          'not handling this version of "' +
            c.name +
            '" -- update the script or contact Mordred'
        );
        break;
    }
  }

  function buildSnatchedGroups(groups, siteIdentifier) {
    var snatchedGroups = {};
    for (var group in groups) {
      snatchedGroups[groups[group].nm.toLowerCase()] = {
        s: siteIdentifier,
        id: group,
      };
    }
    return snatchedGroups;
  }

  /************************************/
  /*** SCRIPT EXECUTION STARTS HERE ***/
  /************************************/

  /* Get gazelle base URL */
  var gazelle_url_base = location.protocol + "//" + location.hostname;

  /* Create proxy */
  var gazelle_proxy = ThrottledProxy(gazelle_url_base, 1000);

  /* Get user id of this user */
  var user_id = (function () {
    var m = $("#userinfo_username .username")
      .eq(0)
      .attr("href")
      .match(/user\.php\?id=(\d+)/);
    if (m) return m[1];
    return null;
  })();

  if (!user_id) return; /* Exceptional condition: User ID not found */

  /* Create status box */
  // var server_version = GM_getLSValue("serverVersion", CURRENT_VERSION);
  var status = StatusBox("Gazelle Snatched");
  var options = doOptionsMenu();
  var slideMenuShowing = 0;

  /* backup what.cd cache */ /* TODO: Remove this eventually? */
  var whatcd_cache = GM_getLSValue("snatch_cache", {});
  if (whatcd_cache.length > 5) {
    whatcd_cache = jQuery.parseJSON(whatcd_cache);
    delete whatcd_cache.torrents;
    GM_setLSValue("whatcd_snatched_groups", JSON.stringify(whatcd_cache));
    console.warn(
      "Saved what.cd snatched groups list for later use. -- You should not see this message again."
    );
    GM_deleteLSValue("snatch_cache");
  }

  var what_groups = GM_getLSValue("whatcd_snatched_groups", {});
  if (what_groups.length > 5) {
    what_groups = jQuery.parseJSON(what_groups);
    Object.assign(
      snatched_groups,
      buildSnatchedGroups(what_groups.groups, "w")
    );
    if (chromeExtension) {
      storageObj.gazelle_snatched["whatcd_snatched_groups"] = what_groups;
      chromep.storage.local.set(storageObj);
    }
  }

  var currSnatchedTorrentVer = 2;
  var currSnatchedGroupsVer = 1;

  /* Cache of snatched torrents */
  var snatch_cache = Cache("snatch_cache", {
    version: currSnatchedTorrentVer,
    torrents: {},
  });
  var bookmark_cache = Cache("bookmark_cache", { groups: {} });
  var group_cache = Cache("snatched_groups", {
    version: currSnatchedGroupsVer,
    groups: {},
  });

  var MenuCommandArray = [];
  var hasPageGMloaded = false;

  upgradeSnatchCache(snatch_cache);

  Object.assign(
    snatched_groups,
    buildSnatchedGroups(group_cache.unserialize().groups, domain_abbr)
  );
  // console.log(snatched_groups);

  /* Reset command */
  registerMenuCommand("Gazelle Snatched: 重新加载", function () {
    snatch_cache.clear();
    bookmark_cache.clear();
    setDomainLSValue("last_update", "0");
    setDomainLSValue("full_update", "1");
    setDomainLSValue("fullUpdateStarted", "1");
    location.reload();
  });
  /* Update w/o clear */
  registerMenuCommand("Gazelle Snatched: 更新", function () {
    setDomainLSValue("last_update", "0");
    setDomainLSValue("full_update", "1");
    setDomainLSValue("force_all", "1");
    setDomainLSValue("fullUpdateStarted", "1");
    location.reload();
  });
  registerMenuCommand("Gazelle Snatched: 设置", function () {
    DisplaySlideMenu(true);
  });

  doGMMenu();
  doOptionsMenu();

  /* Scan torrent table in doc and mark links as type in cache */
  function scan_torrent_page(doc, type) {
    var torrent_table = $(doc).find("#content > .thin > table").eq(0);
    if (torrent_table.length == 0) return 0;
    var found = 0;

    /* New version: {"groups":{"2417":{"nm":"pg.lost - Yes I Am"}}, "torrents":{941290:{ty:"snatched", sd:1}}} // this was changed to save space */
    var d = snatch_cache.unserialize();
    var g = group_cache.unserialize();
    torrent_table.find("div.group_info").each(function (i) {
      /* Find group and torrent ID */
      var group_id;
      var torrent_id;
      var link = $(this).children("a:last").eq(0);
      if (link.attr("href").includes("searchstr")) {
        link = $(this).children("a:last").prev("a");
      }
      var m = link
        .attr("href")
        .match(/torrents\.php\?id=(\d+)&torrentid=(\d+)/);
      if (m) {
        group_id = m[1];
        torrent_id = m[2];
      } else {
        /* I don't know if we can ever get here! */
        m = link.attr("href").match(/torrents\.php\?id=(\d+)/);
        if (m) {
          group_id = m[1];
          link = $(this).children("td").eq(1).find("span:first a:first").eq(0);
          m = link
            .attr("href")
            .match(/torrents\.php\?action=download&id=(\d+)/);
          if (m) torrent_id = m[1];
        }
        if (!m) {
          status
            .contents()
            .append(
              '<div><span style="color: red;">失败:</span> ' +
                $(this).children("td").eq(1).text() +
                "</div>"
            );
          z(); //purposely error out
        }
      }

      /* Save in cache */
      if (group_id && torrent_id) {
        // we are saving a type of "snatched" but when applying that class we have to apply it as "gazelle_snatched" due to gazelle having it's own .snatched style now
        if (
          !d.torrents[torrent_id] ||
          (type != "seeding" &&
            d.torrents[torrent_id].ty != type &&
            !(type != "uploaded" && d.torrents[torrent_id].ty == "uploaded")) || // we have issues if you've snatched a torrent you uploaded, so uploaded takes precendence
          (type == "seeding" &&
            (d.torrents[torrent_id].ty == "leeching" ||
              !d.torrents[torrent_id].sd))
        ) {
          var reg = $(this)
            .text()
            .match(/DL\s\|(?:\sFL\s\|)?\sRP\s+(.+)\[\d{4}\]\s(?:\[.+\]\s)?-/);
          if (!reg)
            reg = $(this)
              .text()
              .match(/DL\s\|(?:\sFL\s\|)?\sRP\s+(.*)\s(-\s.*eech)?/); // applications and books
          if (!reg)
            reg = $(this)
              .text()
              .match(/.*\s]\s+(.+)\s(\[\d{4}\])\s-/); // old way -- still good on non-redacted sites?
          if (!reg)
            reg = $(this)
              .text()
              .match(/.*\s]\s+(.+)\s-?/); // older way??
          if (!reg) {
            console.error("似乎未能找到种子组的名称,请联系开发者。");
            console.error("尝试分析种子组名称:", $(this).text());
          }
          var nm = reg[1].trim();

          g.groups[group_id] = { nm: nm.replace(/"/g, "'") };
          if (type == "seeding") {
            /* Special case seeding */
            if (d.torrents[torrent_id]) {
              if (d.torrents[torrent_id].ty == "leeching") {
                d.torrents[torrent_id].ty = "snatched";
              }
              d.torrents[torrent_id].sd = 1;
            } else {
              d.torrents[torrent_id] = { ty: "seeding", sd: 1 };
            }
          } else {
            if (d.torrents[torrent_id]) d.torrents[torrent_id].ty = type;
            else d.torrents[torrent_id] = { ty: type, sd: 0 };
          }
          //console.log ("adding:" + nm + " with group_id="+group_id+", torrent_id="+torrent_id);
          found += 1;
        }
      }
    });

    if (found !== 0) {
      // found something new so save
      snatch_cache.serialize();
      group_cache.serialize();
    }
    return found;
  }

  function scan_bookmark_page(doc) {
    //console.log ('scanning bookmark page');
    var torrent_table = $(doc).find("#torrent_table").eq(0);
    if (torrent_table.length == 0) return 0;
    var found = 0;

    bookmark_cache.clear(); // makes sense not to save bookmarks because they get added/removed a lot and it's just one page
    var b = bookmark_cache.unserialize();
    torrent_table.find("tr.group.discog").each(function (i) {
      /* Find group and torrent ID */
      var group_id;

      var link = $(this).find("strong a:last").eq(0);
      var m = link.attr("href").match(/torrents\.php\?id=(\d+)/);
      if (m) {
        group_id = m[1];
        b.groups[group_id] = 1;
        found++;
      }
      //console.log (found + ". group_id:" + group_id + " - " + link.text());
    });
    torrent_table.find("tr.torrent").each(function (i) {
      // single, non-music torrents show up not in a group
      /* Find group and torrent ID */
      var group_id;

      var link = $(this).find("strong a:last").eq(0);
      var m = link.attr("href").match(/torrents\.php\?id=(\d+)/);
      if (m) {
        group_id = m[1];
        b.groups[group_id] = 1;
        found++;
      }
      //console.log (found + ". group_id:" + group_id + " - " + link.text());
    });
    bookmark_cache.serialize();
    return found;
  }

  /* Fetch and scan all pages of type, call callback when done */
  function scan_all_torrent_pages(type, page_cb, finish_cb, forced_full) {
    var page = 1;
    var total = 0;
    var lastPage = 0;

    function request_url() {
      if (type == "bookmark") return "/bookmarks.php?type=torrents";
      else
        return (
          "/torrents.php?type=" + type + "&userid=" + user_id + "&page=" + page
        );
    }

    function error_handler(response, reason) {
      status
        .contents()
        .append(
          '<div><span style="color: red;">错误:</span> 无法获取 ' +
            type +
            " 页 " +
            page +
            " (" +
            reason +
            ")</div>"
        );
      status.show();
      finish_cb(total);
    }

    function page_handler(response) {
      if (response.status == 200) {
        var doc = document.implementation.createHTMLDocument("");
        doc.documentElement.innerHTML = response.responseText; //.replace(/<head>[\s\S]*<\/head>/,"<head><\/head>");

        page_cb(type, page);

        if (forced_full) {
          lastPage = 1;
          $(doc)
            .find("#content .linkbox")
            .eq(0)
            .find("a:last")
            .each(function (i) {
              var pgVal = $(this)
                .attr("href")
                .match(/torrents\.php\?page=(\d+)&type/);
              lastPage = pgVal[1];
            });
        }
        if (type == "bookmark") {
          var found = scan_bookmark_page(doc);
        } else {
          var found = scan_torrent_page(doc, type);
        }
        total += found;
        if (
          (!found && !forced_full) ||
          (forced_full && page >= lastPage) ||
          type == "bookmark"
        ) {
          finish_cb(type, total);
          return;
        } /* End of asynchronous chain */

        page += 1;
        gazelle_proxy.get({
          url: request_url(),
          callback: page_handler,
          error: error_handler,
        });
      } else {
        error_handler(response, "HTTP " + response.status);
      }
    }
    gazelle_proxy.get({
      url: request_url(),
      callback: page_handler,
      error: error_handler,
    });
  }

  function parse_json_api(type, page_cb, finish_cb) {
    var total = 0;
    function error_handler(response, reason) {
      status
        .contents()
        .append(
          '<div><span style="color: red;">错误:</span> 无法获取 ' +
            type +
            " (" +
            reason +
            ")</div>"
        );
      status.show();
      finish_cb(type, total);
    }

    function page_handler(data) {
      let resp = JSON.parse(data.responseText);
      bookmark_cache.clear(); // we don't need to save the old bookmarks
      var b = bookmark_cache.unserialize();
      jQuery.each(resp.response.bookmarks, function (key, val) {
        b.groups[val.id] = 1;
        //console.log("id:"+ val.id + " - name:" + val.name);
      });
      finish_cb(type, resp.response.bookmarks.length);
      bookmark_cache.serialize();
    }
    // if the API gets expanded to other types, we won't hard code the URL here
    gazelle_proxy.get({
      url: "/ajax.php?action=bookmarks&type=torrents",
      callback: page_handler,
      error: error_handler,
      accept: "application/json",
    });
  }

  /* Mark all links to torrents that are snatched/uploaded/leeching/seeding/bookmarked */
  function mark_snatched_links() {
    if (/\/user\.php/.test(document.URL)) return; // don't mark snatched on user profile
    var d = snatch_cache.unserialize();
    var g = group_cache.unserialize();
    var b = bookmark_cache.unserialize()
    /* Go through all links */
    $("#content a").each(function (i) {
      var href = $(this).attr("href");
      if (href) {
        var group_id;
        var torrent_id;

        /* Find and mark links to snatched torrents */
        var m = href.match(/torrents\.php\?id=(\d+)&torrentid=(\d+)/);
        if (m) {
          group_id = m[1];
          torrent_id = m[2];
        } else {
          m = href.match(/torrents\.php\?torrentid=(\d+)/);
          if (m) {
            torrent_id = m[1];
          } else {
            m = href.match(/torrents\.php\?id=(\d+)/);
            if (m) group_id = m[1];
          }
        }

        /* Add classes */
        if (
          group_id &&
          b.groups[group_id] &&
          !/\/bookmarks\.php/.test(document.URL) &&
          !/\/user\.php/.test(document.URL) &&
          (!torrent_id || !$(this).parent().parent().is(".group_torrent")) &&
          !$(this).is(".post_id")
        ) {
          $(this).addClass("gazelle_bookmark");
        }
        if (torrent_id && d.torrents[torrent_id]) {
          if (d.torrents[torrent_id].ty == "snatched")
            $(this).addClass("gazelle_snatched");
          // we can't use .snatched anymore because what has now added it's own .snatched class
          else if (d.torrents[torrent_id].ty == "uploaded")
            $(this).addClass("gazelle_uploaded");
          else if (d.torrents[torrent_id].ty == "leeching")
            $(this).addClass("gazelle_leeching");
          if (d.torrents[torrent_id].sd) {
            if (d.torrents[torrent_id].ty != "uploaded")
              $(this).addClass("gazelle_seeding gazelle_snatched");
            // we're really just marking seeding here, but you can't seed if you haven't snatched so adding that class as well
            else $(this).addClass("gazelle_seeding");
          }
        }

        /* Change text if text is url */
        if (
          "/" + $(this).text() == $(this).attr("href") &&
          group_id &&
          g.groups[group_id] &&
          g.groups[group_id].nm
        ) {
          $(this).text(g.groups[group_id].nm);
        }
      }
    });

    /* Mark links on album page in torrent table */
    if (/\/torrents\.php/.test(document.URL)) {
      /* Parse search */
      var search = {};
      var search_list = document.location.search.substring(1).split("&");
      for (var i = 0; i < search_list.length; i++) {
        var pair = search_list[i].split("=");
        search[pair[0]] = pair[1];
      }

      if (search.id) {
        /* Album page */
        $("#content .torrent_table:first tr.group_torrent").each(function (i) {
          /* Find torrent id */
          var torrent_id;
          $(this)
            .find("td:first span:first a")
            .each(function (i) {
              var href = $(this).attr("href");
              if (href) {
                var m = href.match(/torrents\.php\?torrentid=(\d+)/);
                if (m) {
                  // the permalink automatically gets the style applied to it, so we need to remove it here and then manually add it to the text below
                  torrent_id = m[1];
                  $(this).removeClass(
                    "group_snatched gazelle_snatched gazelle_uploaded gazelle_leeching gazelle_seeding"
                  );
                  return false;
                }
              }
            });

          if (torrent_id && d.torrents[torrent_id]) {
            var link = $(this).find("td:first a:last");
            if (d.torrents[torrent_id].ty == "snatched")
              link.addClass("gazelle_snatched");
            // we can't use .snatched anymore because what has now added it's own .snatched class
            else if (d.torrents[torrent_id].ty == "uploaded")
              link.addClass("gazelle_uploaded");
            else if (d.torrents[torrent_id].ty == "leeching")
              link.addClass("gazelle_leeching");
            if (d.torrents[torrent_id].sd) {
              if (d.torrents[torrent_id].ty != "uploaded")
                link.addClass("gazelle_seeding gazelle_snatched");
              // we're really just marking seeding here, but you can't seed if you haven't snatched so setting that class too
              else link.addClass("gazelle_seeding");
            }
          }
        });
      }
    }

    /* Show bookmark link on bookmarked album page */
    if (/\/torrents\.php\?id/.test(document.URL)) {
      var group_id;
      var albumName = $("#content > .thin > .header > h2 > span").eq(0);
      var mark_snatched;
      if (albumName) {
        var m = document.URL.match(/torrents\.php\?id=(\d+)/);
        if (m) {
          group_id = m[1];
          if (b.groups[group_id]) albumName.addClass("gazelle_bookmark");
        }
      }

      /* show mark/unmark snatched on album page */
      if (
        ($("a.add_bookmark").length || $("a.remove_bookmark").length) &&
        !$("#mark_snatched").length
      ) {
        if (g.groups[group_id])
          mark_snatched = $(
            '<a href="#" id="mark_snatched" class="brackets">不标记已完成种子</a>'
          );
        else
          mark_snatched = $(
            '<a href="#" id="mark_snatched" class="brackets">标记已完成种子</a>'
          );

        var header = $("#content .header > h2").text();
        var key = header
          .replace(releaseTypeRegex, "")
          .replace(/\[\d*\]/, "")
          .trim();

        mark_snatched.on("click", function () {
          var g = group_cache.unserialize();
          if (g.groups[group_id]) {
            delete g.groups[group_id];
            mark_snatched.text("Mark Snatched");
          } else {
            g.groups[group_id] = { nm: key.replace(/"/g, "'") };
            mark_snatched.text("Unmark Snatched");
          }
          group_cache.serialize();
        });
        mark_snatched.insertAfter(".add_bookmark");
        mark_snatched.insertAfter(".remove_bookmark"); // won't have both links on same page
      }
    }

    /* Mark previously snatched groups */
    if (/\/artist\.php\?id/.test(document.URL)) {
      var artist = $("#content .header h2").text();
      $('#content a[href^="torrents.php?id="].tooltip').each(function () {
        var album = $(this)[0].innerText;
        var key = (artist + " - " + album).toLowerCase();
        if (snatched_groups[key]) {
          addGroupSnatched(key, snatched_groups[key], this);
        }
      });
    }

    if (/\/bookmarks\.php\?type=torrents/.test(document.URL)) {
      $("tr.group.discog td:nth-of-type(3) strong").each(function () {
        var key = this.innerText
          .replace(/\[\d*\]/, "")
          .trim()
          .toLowerCase();
        if (snatched_groups[key]) {
          addGroupSnatched(
            key,
            snatched_groups[key],
            $(this).contents().filter("a.tooltip")[0]
          );
        }
      });
    }

    if (
      /\/top10\.php/.test(document.URL) ||
      /\/torrents\.php\?action=notify/.test(document.URL)
    ) {
      $("td div.group_info > strong").each(function () {
        var key = this.innerText
          .replace(releaseTypeRegex, "")
          .replace(/\[\d*\]/, "")
          .trim()
          .toLowerCase();
        if (snatched_groups[key]) {
          addGroupSnatched(
            key,
            snatched_groups[key],
            $(this).contents().filter("a.tooltip")[0]
          );
        }
      });
    }

    if (/\/torrents.php\?id=/.test(document.URL)) {
      var regex = document.URL.match(/\/torrents.php\?id=(\d*)/);
      var id = regex[1];
      var keys = Object.keys(snatched_groups).filter(
        (group) => snatched_groups[group].id == id
      );
      if (keys.length) {
        addGroupSnatched(
          keys[0],
          snatched_groups[keys[0]],
          $("#content .header h2 span")[0]
        );
      } else {
        var header = $("#content .header h2").text();
        var key = header
          .replace(releaseTypeRegex, "")
          .replace(/\[\d*\]/, "")
          .trim()
          .toLowerCase();
        if (snatched_groups[key]) {
          addGroupSnatched(
            key,
            snatched_groups[key],
            $("#content .header h2 span")[0]
          );
        }
      }
    }

    if (/\/collages?\.php\?id/.test(document.URL)) {
      $("tr.group.discog td:nth-of-type(3) strong").each(function () {
        var key = this.innerText
          .replace(/\[\d*\]/, "")
          .replace(/^\d+ - /, "")
          .trim()
          .toLowerCase();
        if (snatched_groups[key]) {
          addGroupSnatched(
            key,
            snatched_groups[key],
            $(this).contents().filter("a.tooltip")[0]
          );
        }
      });
    }
  }

  function addGroupSnatched(name, key, element) {
    switch (key.s) {
      case "w": // what.cd
        $(element).addClass("group_snatched whatcd_group");
        break;
      case "o": // orpheus
        $(element).addClass("group_snatched");
        break;
      case "r": // redacted
        $(element).addClass("group_snatched");
        break;
      case "n": // notwhat
        $(element).addClass("group_snatched");
        break;
    }
    // console.log(key.id, name);
  }

  /* Mark torrent as leeching when download link is clicked */
  function mark_download_links() {
    $("#content")
      .find("a")
      .each(function (i) {
        var href = $(this).attr("href");
        if (href) {
          /* Find download links */
          var m = href.match(/torrents\.php\?action=download&id=(\d+)/);
          if (m) {
            var torrent_id = m[1];
            $(this).click(function (event) {
              var d = snatch_cache.unserialize();
              d.torrents[torrent_id] = { ty: "leeching", sd: 0 };
              snatch_cache.serialize();
              mark_snatched_links();
            });
          }
        }
      });
  }

  function mark_bookmark_links() {
    $("#content")
      .find("a")
      .each(function (i) {
        var id = $(this).attr("id");
        if (id) {
          /* Find download links */
          var m = id.match(/bookmarklink_torrent_(\d+)/);
          if (m) {
            //console.log (m);
            var group_id = m[1];
            $(this).click(function (event) {
              if (
                !/remove/i.test($(this).text()) &&
                !/unbookmark/i.test($(this).text())
              ) {
                var b = bookmark_cache.unserialize();
                b.groups[group_id] = 1;
                bookmark_cache.serialize();
                mark_snatched_links();
              } else {
                var b = bookmark_cache.unserialize();
                delete b.groups[group_id];
                bookmark_cache.serialize();
                $("#content")
                  .find("a.gazelle_bookmark")
                  .each(function (i) {
                    var href = $(this).attr("href");
                    if (href && href == "torrents.php?id=" + group_id) {
                      $(this).removeClass("gazelle_bookmark");
                    }
                  });
                $("#content > .thin > .header > h2 > span")
                  .eq(0)
                  .removeClass("gazelle_bookmark");
              }
            });
          }
        }
      });
  }

  /* This function was hacked from a generic one and converted to jQuery to work better with Gazelle Snatched.
	   If you'd like to see that version it's here: http://userscripts.org/scripts/show/68559 */
  function doGMMenu() {
    // jQuery Version
    if (!MenuCommandArray.length) {
      return;
    }
    var mdiv = $("<div></div>");
    $.each(MenuCommandArray, function (i, value) {
      if (i + 1 < MenuCommandArray.length)
        var mEntry = $(
          '<span><a href="#" id="' +
            MenuCommandArray[i][2] +
            '">' +
            MenuCommandArray[i][0] +
            "</a>\u00A0\u00A0|\u00A0\u00A0</span>"
        );
      else
        var mEntry = $(
          '<a href="#" id="' +
            MenuCommandArray[i][2] +
            '">' +
            MenuCommandArray[i][0] +
            "</a>"
        );
      mEntry.click(function () {
        MenuCommandArray[i][1](arguments[0]);
        var e = arguments[0];
        e.stopPropagation();
        return false;
      });
      mdiv.append(mEntry);
    });
    status.contents().append(mdiv);
  }

  /* Scan current page */
  if (/\/torrents\.php/.test(document.URL)) {
    /* Parse search */
    var search = {};
    var search_list = document.location.search.substring(1).split("&");
    for (var i = 0; i < search_list.length; i++) {
      var pair = search_list[i].split("=");
      search[pair[0]] = pair[1];
    }

    var full_update = parseInt(getDomainLSValue("full_update", "0"))
      ? true
      : false;

    if (
      (search.type == "snatched" ||
        search.type == "uploaded" ||
        search.type == "seeding" ||
        search.type == "leeching") &&
      search.userid == user_id &&
      !full_update
    ) {
      var scan_status = $("<div>扫描当前页面……<span></span></div>");
      status.contents().append(scan_status);
      status.show();

      /* Scan current page */
      var found = scan_torrent_page(document, search.type);
      scan_status
        .children("span")
        .text(
          "完成 (" +
            (found > 0 ? "找到 " + found + " 个新增项" : "无新增项") +
            ")"
        );
      status.show(5000);
    }
  }

  if (/\/bookmarks\.php(?!.action=edit)/i.test(document.URL)) {
    var scan_status = $("<div>扫描当前页面……<span></span></div>");
    status.contents().append(scan_status);
    status.show();

    bookmark_cache.clear();
    var found = scan_bookmark_page(document);

    scan_status
      .children("span")
      .text(found > 0 ? "找到 " + found + " 个收藏" : "未找到收藏");
    status.show(5000);
  }

  /* Mark links */
  mark_download_links();
  mark_bookmark_links();
  mark_snatched_links();

  /*******************************/
  /*** AUTO-UPDATE STARTS HERE ***/
  /*******************************/
  var now = new Date();
  var just_updated = 0;
  var last_update = parseInt(getDomainLSValue("last_update", "0"));
  var next_update = last_update + global_updateFreq * 60 * 1000;
  var full_update = parseInt(getDomainLSValue("full_update", "0"))
    ? true
    : false;
  var forced_full = parseInt(getDomainLSValue("force_all", "0")) ? true : false;

  // if (scriptVersion != CURRENT_VERSION) {
  // 	console.log("Script was recently updated to " + CURRENT_VERSION);
  // 	// the script was recently updated
  // 	GM_setLSValue('script_version', CURRENT_VERSION);
  // 	//deleteDomainLSValue('snatch_cache');		// Had to reset this due to changes in the cache structure. Will remove in a version or two.
  // 	deleteDomainLSValue('serverVersion');		// we remove this just to make sure it will be properly retrieved in the future
  // 	deleteDomainLSValue('lastUpdateCheck');
  // 	deleteDomainLSValue('last_update');
  // 	just_updated = 1;						// location.reload is called after we reach the end of this function so we don't want the script to continue executing before reloading first
  // 	location.reload();
  // }
  if (full_update) {
    deleteDomainLSValue("full_update");
    deleteDomainLSValue("last_update");
    deleteDomainLSValue("force_all");
    next_update = 0;
    last_update = 0;
  }

  if (hide_seeding_torrent != "true"){
      //console.log('选择hide');
      $(".gazelle_seeding").parents("tr").remove();
    }

  if (next_update < now.getTime() && just_updated != 1) {
    setDomainLSValue("last_update", now.getTime().toString());
    var fullUpdateFinished = getDomainLSValue("fullUpdateStarted", "0");
    var jobs = 5;
    var totalFound = {};

    /* Show auto update status */
    last_update = 0;
    var update_status = {
      snatched: $("<div>更新已完成种子:<span>初始化……</span></div>"),
      uploaded: $("<div>更新已发布种子:<span>初始化……</span></div>"),
      leeching: $("<div>更新下载中种子:<span>初始化……</span></div>"),
      seeding: $("<div>更新做种中种子:<span>初始化……</span></div>"),
      bookmark: $("<div>更新已收藏种子:<span>初始化……</span></div>"),
    };
    for (var type in update_status)
      status.contents().append(update_status[type]);
    status.show();

    function scan_page_handler(type, page) {
      if (last_update == 0) {
        update_status[type].children("span").text("第 " + page + " 页……");
        status.show();
      }
    }

    function scan_finished_handler(type, found) {
      if (last_update == 0) {
        if (type != "bookmark")
          update_status[type]
            .children("span")
            .text(
              "完成 (" +
                (found > 0 ? "找到 " + found + " 个新增项" : "无新增项") +
                ")"
            );
        else
          update_status[type]
            .children("span")
            .text(
              "完成 (" +
                (found > 0 ? "找到 " + found + " 个收藏" : "未找到收藏") +
                ")"
            );
      }

      jobs -= 1;
      totalFound[type] = found;

      if (jobs == 0) {
        mark_snatched_links();
        if (last_update == 0) {
          var total = [];
          for (var type in totalFound)
            if (totalFound[type] > 0)
              total.push(type + ": " + totalFound[type]);
          status.contents().append("<div>已完成自动更新</div>");
          deleteDomainLSValue("fullUpdateStarted");
          status.show(5000);
        }
      }
    }

    /* Rescan all types of torrent lists */
    if (fullUpdateFinished == 1) {
      forced_full = true;
    }
    scan_all_torrent_pages(
      "snatched",
      scan_page_handler,
      scan_finished_handler,
      forced_full
    );
    scan_all_torrent_pages(
      "uploaded",
      scan_page_handler,
      scan_finished_handler,
      forced_full
    );
    scan_all_torrent_pages(
      "leeching",
      scan_page_handler,
      scan_finished_handler,
      forced_full
    );
    scan_all_torrent_pages(
      "seeding",
      scan_page_handler,
      scan_finished_handler,
      forced_full
    );
    //scan_all_torrent_pages('bookmark', scan_page_handler, scan_finished_handler, forced_full);

    parse_json_api("bookmark", scan_page_handler, scan_finished_handler);
  }

  /**********************************/
  /*** SCRIPT EXECUTION ENDS HERE ***/
  /**********************************/

  function getDomainLSValue(key, defaultValue) {
    return GM_getLSValue(domain_prefix + key, defaultValue);
  }
  function setDomainLSValue(key, value) {
    return GM_setLSValue(domain_prefix + key, value);
  }
  function deleteDomainLSValue(key) {
    return GM_deleteLSValue(domain_prefix + key);
  }
})();