├── .gitignore ├── README.md ├── config-sample.ini ├── dupapi.py ├── emby_client.py ├── humanbytes.py ├── juzhang.js ├── plexmove.py ├── pt.drawio ├── requirements.txt ├── torfilter.js └── torss.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.json 9 | .vscode/ 10 | config.ini 11 | 12 | myTMDbParser* 13 | tt.py 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | db.sqlite* 70 | 71 | .DS_Store 72 | *.txt 73 | *.sh 74 | 75 | 76 | # Flask stuff: 77 | instance/ 78 | instance*/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 111 | __pypackages__/ 112 | 113 | # Celery stuff 114 | celerybeat-schedule 115 | celerybeat.pid 116 | 117 | # SageMath parsed files 118 | *.sage.py 119 | 120 | # Environments 121 | .env 122 | .venv 123 | env/ 124 | venv/ 125 | ENV/ 126 | env.bak/ 127 | venv.bak/ 128 | 129 | # Spyder project settings 130 | .spyderproject 131 | .spyproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | 136 | # mkdocs documentation 137 | /site 138 | 139 | # mypy 140 | .mypy_cache/ 141 | .dmypy.json 142 | dmypy.json 143 | 144 | # Pyre type checker 145 | .pyre/ 146 | 147 | # pytype static type analyzer 148 | .pytype/ 149 | 150 | # Cython debug symbols 151 | cython_debug/ 152 | 153 | torcp/version.py 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # PT站过滤下载入库流程 3 | * 这是一套基于日常使用pt站点的下片入库方案,可以某些条件过滤站点列出的条目,如果有一个自建的Plex/Emby影音库,可以查询哪些影片是库中缺少的。 4 | 5 | 整体流程如下: 6 | 7 | ![dataflow](https://ptpimg.me/07ivzz.png) 8 | 9 | 1. 种子列表过滤脚本`torfilter.js` 是一个油猴脚本,在PT站页面添加一些辅助过滤的功能,选中的种子可以提交信息给`dupapi.py`进行处理 10 | 2. `dupapi.py` 是一个api服务,可对提交来的信息在本地的媒体库中查重,并将下载链接交给qBittorrent启动下载,同时打上imdb信息标签 11 | 3. qBittorrent下载完成调用脚本将文件和imdb信息标签交 `torcp` 进行目录重新组织,参考 [这里的详细文档](https://github.com/ccf-2012/torcp/blob/main/qb%E8%87%AA%E5%8A%A8%E5%85%A5%E5%BA%93.md) 12 | 4. 组织好的媒体文件,Plex可刮削入库 13 | 14 | 15 | ## Last update: 16 | * 2024.10.19: lemonhd 17 | * 2023.11.23: hddolby, hd4fans, hdfans, pthome 18 | * 2023.9.6: wtsakura, soulvoice, ptsbao, tlf 19 | * 2023.1.25: `--siteid-folder` for duapi, torss: 添加种子时建立 'Site_Id_IMDb' 目录,例如:aud_108375_tt1172571 20 | * 2023.1.17: lemonhd (gazella, animate分类无效) 21 | * 2023.1.14: torss 22 | * 2022.12.14: hdc, hds 23 | * 2022.12.13: 对在列表页没有IMDb信息的站点,取详情页获取IMDb信息再下载 24 | * 2022.12.12: frds,beitai等站点支持;未作种,未曾下,列表页仅查重,查&下 25 | * 2022.10.12: 支持Emby,支持pt站上种子详情页上查重和下载 26 | * 2022.10.9: 新增本地查重下载服务 **dupapi** ,网页将种子信息提交进行查重,符合条件的推送下载器 27 | * 2022.10.5: ob, ssd 在拷贝下载链接时,从控制面板中取passkey拼合形成下载链接 28 | * 2022.10.5: IMDb 或 豆瓣 大于输入值 29 | * 2022.10.4: 加入大小介于过滤,单位GB,使用`,`分隔; 30 | * 2022.10.4: 使用Cookie保存参数 31 | * 2022.9.28: 标题不含,描述不含,分别搜索,忽略大小写; 加入ob, ssd支持 32 | * 2022.9.25: 支持pter, chd, ade种子列表过滤: 过滤: 未作种, 无国语,有中字,标题不含,以及imdb大于输入值 的种子 33 | 34 | 35 | ----- 36 | # 种子列表过滤脚本 torfilter 37 | ![torfilterjs](https://ptpimg.me/d5l9yv.png) 38 | 39 | 40 | ## Greasy Fork 安装地址 41 | * https://greasyfork.org/zh-CN/scripts/451748 42 | 43 | ## 功能 44 | 油猴脚本,在种子列表页中: 45 | 1. 过滤: 未作种,无国语,有中字,标题不含,描述不含,大小介于,IMDb/豆瓣大于输入值 的种子 46 | * 当前支持pter, chd, aud, ob, ssd, frds, beitai, ttg, hdc, hds, hh,redleaves, hdh, wtsakura, soulvoice, ptsbao, tlf, hddolby, hd4fans, hdfans, pthome 47 | * 大小介于的输入框中,单位为GB,使用`,` 或 `-` 分隔。填写 `0,20` 表示小于20GB的种子 48 | 2. 新增一列快速认领,当前仅支持猫站 49 | 3. 配合 dupapi.py 实现查重下载入库 (since 2022.10.9) 50 | 51 | * 本脚本仅在打开的站点页面上进行过滤,对站点服务器无任何额外请求负担 52 | * 在cookie中会保存参数,以便翻页时持有设置的值,不影响原cookie 53 | * lhd中gazella模式和animate分类无效 54 | 55 | 56 | ### 列表页的查重与下载 57 | * “仅查重” 与 “查&下” 都是通过后台dupapi进行; 58 | * ob, ssd, aud, pter, ttg, hds 等站列表页有IMDb标识,torfilter会用IMDb来查重并在推送qbit下载时添加标签(tag) 59 | * frds, chd, beitai, hdc等站在列表页没有IMDb链接信息,在种子详情页可能会获取到,则: 60 | - 在查重时,torfilter会使用种子名称提交查重,后台dupapi会以torcp解析种子名称后查找TMDb再进行查重。这种依赖种子名称查TMDb有可能失误; 61 | - 在下载时,torfilter将去获取种子详情页,并提取IMDb信息,之后再提交后台dupapi进行下载,这样也会在qbit中对种子加上标签; 62 | - 如果先点选了“仅查重”,再点“查&下”,则在下载时,会仅对“仅查重”时标绿的种子进行详情页的获取及下载,对于上述列表页没有IMDb信息的站,推荐如此操作下载,否则直接“查&下”将对可见列表中每个条目去站点获取详情页; 63 | * 在“查&下”过程中,每个条目下载后有约2秒的间隔,在“仅查重”时则只有50ms; 64 | 65 | 66 | ### Note: 67 | * 部分站不支持国语,中字标签搜索 68 | * ~~ ob, chd, ssd, frds, hds 的下载链接无passkey,拼合usercp中的passkey构成下载链接 ~~ 69 | * 种子列表页面上无法取得imdb的(frds, chd, beitai, hdc),以及列表页上无法取得下载链接的(hds),会进入种子详情页获取信息,这会对站点发起拉取页面的操作,各站对拉取页面和种子不同的保护措施,请自行把握。 70 | 71 | 72 | ### 提交查重下载后,返回几种结果 73 | * 提交下载后,页面中种子背景会改为若干种可能的颜色 : 74 | ![3种结果](https://ptpimg.me/3cgnss.png) 75 | > ~~图中filterapi,已改名dupapi,since 2022.10.12~~ 76 | > 图中filterapi,已改名torll,since 2024.12.26 77 | 78 | 1. 库中没有,提交qBittorrent下载了 79 | 2. 库中已有,跳过,不下载 80 | 3. TMDb没有查到,当前也是跳过不下载 81 | 4. dupapi无法提交给下载器出错时,返回400,在页面上显示红色 82 | 5. since 2022.10.12, 新增一种颜色`darkturquoise`,在种子列表页无法取得下载链接时,表示未重复但不下载 83 | 84 | 85 | 以下已经失效,torfilter 现在配合 torll 使用 86 | 87 | ----- 88 | ~~ 89 | # 本地下载入库api服务 dupapi 90 | 91 | ## 前置准备 92 | 1. python环境 93 | ```sh 94 | # 下载 95 | git clone https://github.com/ccf-2012/torfilter.git 96 | cd torfilter 97 | pip install -r requirements.txt 98 | ``` 99 | 100 | 2. 填写 `config.ini` 信息 101 | ```sh 102 | cp config-sample.ini config.ini 103 | vi config.ini 104 | ``` 105 | 106 | * 参考注释和示例,编写其中的各项信息: 107 | ```ini 108 | [PLEX] 109 | server_url=http://192.168.5.6:32400 110 | ; 取得Plex token的步骤: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ 111 | server_token=E3-my-plex-token-CbVsY 112 | 113 | ; [EMBY] 114 | ; server_url=http://192.168.5.6:8096 115 | ; user=embyuser 116 | ; pass=embypass 117 | 118 | [TMDB] 119 | ; 取得TMDb api key步骤: https://kb.synology.cn/zh-cn/DSM/tutorial/How_to_apply_for_a_personal_API_key_to_get_video_info 120 | api_key=9e07s1bthetmdbapikey3c2674b093 121 | 122 | 123 | [QBIT] 124 | server_ip=192.168.5.199 125 | port=8091 126 | user=MyQbitUsername 127 | pass=MyQbitPassword 128 | 129 | ``` 130 | 131 | ## 建立本地Emby/plex条目数据库 132 | > 每一次查重都现场从plex服务器中查询条目代价过大,所以这里是在本地建立 **sqlite** 数据库 133 | 134 | * 运行初始化数据库命令,从自己的Plex/Emby服务器中获取所有条目,存储在本地 **sqlite** 数据库中,其间如果发现条目没有TMDb数据,将会现查并补全。 135 | ```sh 136 | python dupapi.py --init-library 137 | ``` 138 | * 初始化命令运行完成后将会退出 139 | * sqlite 数据库存在当前目录下的 `instance` 目录中,如果想要重新初始化,可直接 `rm -rf instance` 删除再重建 140 | * 如果以非空库运行 `--init-library` 则原有数据会清除,如果要保留现有数据库中的数据添加新数据,可加 `--append` 参数 141 | * 如果同时配置了Emby和Plex,则两个库内容都会添加 142 | 143 | ### 命令使用 144 | ``` 145 | python dupapi.py -h 146 | 147 | usage: dupapi.py [-h] [--init-library] [-a] [--fill-tmdb] [--siteid-folder] 148 | 149 | A torrent handler does library dupe check, add qbit with tag, etc. 150 | 151 | options: 152 | -h, --help show this help message and exit 153 | --init-library init database with plex query. 154 | -a, --append append to local database, without delete old data. 155 | --fill-tmdb fill tmdb field if it miss. 156 | --siteid-folder make Site_Id_Imdb parent folder. 157 | ``` 158 | 159 | ### 例子 160 | ```sh 161 | # 清空当前数据,读入Plex/Emby中的数据 162 | python dupapi.py --init-library 163 | 164 | # 读入Plex/Emby中的数据,添加到当前数据库 165 | python dupapi.py --init-library --append 166 | python dupapi.py --init-library -a 167 | 168 | # 对当前数据库,对TMDb缺失的进行查找补全 169 | python dupapi.py --fill-tmdb 170 | 171 | ``` 172 | 173 | ## 启动 dupapi 服务 174 | * 如果不带参数执行 `dupapi.py` 则将启动 dupapi 服务,这是一个web api服务。 175 | * 当前内置的监听端口为 `3006`,这个端口号是与 `torfilter.js` 中对应的。若要修改,则两边代码中对应查找修改。 176 | * 当前过滤脚本 `torfilter.js` 会连接 `localhost` 中的 `dupapi`服务,如果部署在不同机器上,则在 `torfilter.js` 中查找修改。 177 | 178 | * 启动 dupapi 服务: 179 | ```sh 180 | python dupapi.py 181 | ``` 182 | 183 | * 过滤脚本 `torfilter.js` 会发送的api请求相当于: 184 | ```sh 185 | curl -i -H "Content-Type: application/json" -X POST -d '{"torname" : "The Frozen Ground 2013 1080p BluRay x265 10bit DTS-ADE", "imdb": "tt2005374", "downloadlink": "https://audiences.me/download.php?id=71406&...."}' http://localhost:3000/p/api/v1.0/dupedownload 186 | ``` 187 | 188 | ### 注意: 189 | * dupapi 服务设计为本地临时启用用途,暂无密码防护 190 | * dupapi接受浏览器插件torfilter连接,会连themoviedb.org查TMDb,会连qbit下载器进行下载;所以需三方网络都通,特别地,连接TMDb查询,可能需配置 host 或 梯; 191 | * dupapi当前对于剧集,没有分辨季与集,只要有就判重,可在详情页手动点下载。 192 | 193 | ----- 194 | *** 一些原来的小程序,已经整合到新版 torcp 中,未来再考虑开放 195 | 196 | ~~ -------------------------------------------------------------------------------- /config-sample.ini: -------------------------------------------------------------------------------- 1 | [PLEX] 2 | server_url=http://192.168.5.6:32400 3 | ; 取得Plex token的步骤: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ 4 | server_token=E3-my-plex-token-CbVsY 5 | rootdir = /gd124/media/148/emby/ 6 | 7 | [PLEX_SECTION] 8 | 中文剧集 = TV/cn 9 | 日韩剧集 = TV/ja, TV/ko 10 | 剧集 = TV/other 11 | 中文电影 = Movie/cn 12 | 电影 = Movie 13 | 14 | 15 | ; [EMBY] 16 | ; server_url=http://192.168.5.6:8096 17 | ; user=embyuser 18 | ; pass=embypass 19 | 20 | [TORCP] 21 | linkdir = /home/username/emby 22 | bracket = plex-bracket 23 | tmdb_lang = en-US 24 | lang = cn,ja,ko 25 | 26 | 27 | [TMDB] 28 | ; 取得TMDb api key步骤: https://kb.synology.cn/zh-cn/DSM/tutorial/How_to_apply_for_a_personal_API_key_to_get_video_info 29 | api_key=9e07s1bthetmdbapikey3c2674b093 30 | 31 | 32 | [QBIT] 33 | server_ip=192.168.5.199 34 | port=8091 35 | user=MyQbitUsername 36 | pass=MyQbitPassword 37 | ; pause=true 38 | ; dryrun=true 39 | -------------------------------------------------------------------------------- /dupapi.py: -------------------------------------------------------------------------------- 1 | # curl -i -H "Content-Type: application/json" -X POST -d '{"torname" : "The Frozen Ground 2013 1080p BluRay x265 10bit DTS-ADE", "imdb": "tt2005374", "downloadlink": "https://audiences.me/download.php?id=71406&...."}' http://localhost:3000/p/api/v1.0/checkdupe 2 | from flask import Flask, jsonify 3 | from flask import abort 4 | from flask import make_response 5 | from flask import request 6 | # from flask_httpauth import HTTPBasicAuth 7 | from flask import Flask 8 | from flask_sqlalchemy import SQLAlchemy 9 | from datetime import datetime 10 | import re 11 | import os 12 | from torcp.tmdbparser import TMDbNameParser 13 | from plexapi.server import PlexServer 14 | from emby_client import EmbyClient 15 | import configparser 16 | import argparse 17 | import qbittorrentapi 18 | import time 19 | 20 | # auth = HTTPBasicAuth() 21 | 22 | # @auth.get_password 23 | # def get_password(username): 24 | # if username == 'miguel': 25 | # return 'python' 26 | # return None 27 | 28 | # @auth.error_handler 29 | # def unauthorized(): 30 | # return make_response(jsonify({'error': 'Unauthorized access'}), 401) 31 | 32 | app = Flask(__name__) 33 | app.config[ 34 | 'SQLALCHEMY_DATABASE_URI'] = 'sqlite:///medialist.db' 35 | db = SQLAlchemy(app) 36 | # db.init_app(app) 37 | 38 | MAX_RETRY = 5 39 | class configData(): 40 | interval = 3 41 | plexServer = '' 42 | plexToken = '' 43 | embyServer = '' 44 | embyUser = '' 45 | embyPass = '' 46 | qbServer = '' 47 | tmdb_api_key = '' 48 | qbPort = '' 49 | qbUser = '' 50 | qbPass = '' 51 | addPause = False 52 | dryrun = False 53 | 54 | 55 | CONFIG = configData() 56 | 57 | 58 | class MediaItem(db.Model): 59 | __tablename__ = 'media_list_table' 60 | id = db.Column(db.Integer, primary_key=True) 61 | # site = db.Column(db.String(64)) 62 | title = db.Column(db.String(255)) 63 | originalTitle = db.Column(db.String(255)) 64 | librarySectionID = db.Column(db.String(16)) 65 | audienceRating = db.Column(db.Float) 66 | guid = db.Column(db.String(64)) 67 | key = db.Column(db.String(64)) 68 | imdb = db.Column(db.String(32)) 69 | tmdb = db.Column(db.String(32)) 70 | tvdb = db.Column(db.String(32)) 71 | doubanid = db.Column(db.String(32)) 72 | site = db.Column(db.String(32)) 73 | linkid = db.Column(db.String(32)) 74 | location0 = db.Column(db.String(255)) 75 | locationdirname = db.Column(db.String(255)) 76 | 77 | def serialize(self): 78 | return { 79 | 'id': self.id, 80 | 'title': self.title, 81 | 'tmdb': self.tmdb, 82 | } 83 | 84 | def __repr__(self): 85 | return '' % self.title 86 | 87 | 88 | def validDownloadlink(downlink): 89 | keystr = ['passkey', 'downhash', 'totheglory.im/dl/', 'download.php?hash='] 90 | return any(x in downlink for x in keystr) 91 | 92 | 93 | @app.route('/p/api/v1.0/dupedownload', methods=['POST']) 94 | # @auth.login_required 95 | def checkDupAddTor(): 96 | if not request.json or 'torname' not in request.json: 97 | abort(400) 98 | 99 | if (not CONFIG.qbServer): 100 | print("qBittorrent not set, skip") 101 | abort(400) 102 | 103 | if (not CONFIG.tmdb_api_key): 104 | print("tmdb_api_key not set, skip") 105 | abort(400) 106 | 107 | p = TMDbNameParser(CONFIG.tmdb_api_key, '') 108 | 109 | imdbstr = '' 110 | if 'imdbid' in request.json and request.json['imdbid']: 111 | imdbstr = request.json['imdbid'].strip() 112 | torTMDb = searchTMDb(p, request.json['torname'], imdbstr) 113 | 114 | siteIdStr = '' 115 | if 'siteid' in request.json and request.json['siteid']: 116 | siteIdStr = request.json['siteid'].strip() 117 | 118 | forceDownload = False 119 | if 'force' in request.json: 120 | forceDownload = request.json['force'] 121 | 122 | if torTMDb > 0: 123 | # q = db.session.query(PlexItem).filter_by(tmdb=torTMDb).first() 124 | exists = db.session.query(db.exists().where( 125 | MediaItem.tmdb == torTMDb)).scalar() 126 | if (exists) and (not forceDownload): 127 | return jsonify({'Dupe': True}), 202 128 | else: 129 | # print("Download: " + request.json['torname'] + " "+request.json['downloadlink']) 130 | if 'downloadlink' in request.json: 131 | if not validDownloadlink(request.json['downloadlink']): 132 | print("Not valid torrent downlink: %s ( %s) " % (request.json['torname'], request.json['downloadlink'])) 133 | return jsonify({'no download link': True}), 205 134 | 135 | if not CONFIG.dryrun: 136 | print("Added: " + request.json['torname']) 137 | if not addQbitWithTag(request.json['downloadlink'].strip(), imdbstr, siteIdStr): 138 | abort(400) 139 | else: 140 | # print("DRYRUN: " + request.json['torname']) 141 | print("DRYRUN: " + request.json['torname'] + "\n" + request.json['downloadlink']) 142 | 143 | return jsonify({'Download': True}), 201 144 | else: 145 | # if CONFIG.download_no_imdb: 146 | # if not addQbitWithTag(request.json['downloadlink'].strip(), imdbstr): 147 | # abort(400) 148 | return jsonify({'TMDbNotFound': True}), 203 149 | 150 | 151 | @app.route('/p/api/v1.0/checkdupeonly', methods=['POST']) 152 | # @auth.login_required 153 | def detailCheckDupe(): 154 | if not request.json or 'torname' not in request.json: 155 | abort(400) 156 | 157 | if (not CONFIG.qbServer): 158 | print("qBittorrent not set, skip") 159 | abort(400) 160 | 161 | if (not CONFIG.tmdb_api_key): 162 | print("tmdb_api_key not set, skip") 163 | abort(400) 164 | 165 | p = TMDbNameParser(CONFIG.tmdb_api_key, '') 166 | 167 | imdbstr = '' 168 | if 'imdbid' in request.json: 169 | imdbstr = request.json['imdbid'].strip() 170 | # print("has IMDb: " + imdbstr) 171 | torTMDb = searchTMDb(p, request.json['torname'], imdbstr) 172 | 173 | if torTMDb > 0: 174 | # q = db.session.query(PlexItem).filter_by(tmdb=torTMDb).first() 175 | exists = db.session.query(db.exists().where( 176 | MediaItem.tmdb == torTMDb)).scalar() 177 | if (exists): 178 | return jsonify({'Dupe': True}), 202 179 | else: 180 | return jsonify({'No Dupe': True}), 201 181 | else: 182 | # if CONFIG.download_no_imdb: 183 | # if not addQbitWithTag(request.json['downloadlink'].strip(), imdbstr): 184 | # abort(400) 185 | return jsonify({'TMDbNotFound': True}), 203 186 | 187 | 188 | @app.route('/p/api/v1.0/delete/', methods=['DELETE']) 189 | # @auth.login_required 190 | def delete_torrent(tor_id): 191 | MediaItem.query.filter_by(tor_id).delete() 192 | db.session.commit() 193 | return jsonify({'OK': 200}) 194 | 195 | 196 | @app.errorhandler(404) 197 | def not_found(error): 198 | return make_response(jsonify({'error': 'Not found'}), 404) 199 | 200 | 201 | @app.route('/p/api/v1.0/get/', methods=['GET']) 202 | # @auth.login_required 203 | def get_torrent(tor_id): 204 | tor = MediaItem.query.get_or_404(tor_id) 205 | 206 | return jsonify({'tor': tor.title}) 207 | 208 | 209 | @app.route('/p/api/v1.0/list', methods=['GET']) 210 | # @auth.login_required 211 | def get_torrents(): 212 | torlist = MediaItem.query.limit(10).all() 213 | return jsonify(torrents=[e.serialize() for e in torlist]) 214 | 215 | 216 | @app.route('/p/api/v1.0/test', methods=['GET']) 217 | # @auth.login_required 218 | def test_tasks(): 219 | datestr = '2022-03-16 05:25:53' 220 | thetime = datetime.strptime(datestr, '%Y-%m-%d %H:%M:%S') 221 | return jsonify(thetime) 222 | 223 | 224 | def addQbitWithTag(downlink, imdbtag, siteIdStr=None): 225 | qbClient = qbittorrentapi.Client( 226 | host=CONFIG.qbServer, port=CONFIG.qbPort, username=CONFIG.qbUser, password=CONFIG.qbPass) 227 | 228 | try: 229 | qbClient.auth_log_in() 230 | except qbittorrentapi.LoginFailed as e: 231 | print(e) 232 | 233 | if not qbClient: 234 | return False 235 | 236 | try: 237 | # curr_added_on = time.time() 238 | if siteIdStr and ARGS.siteid_folder: 239 | result = qbClient.torrents_add( 240 | urls=downlink, 241 | is_paused=CONFIG.addPause, 242 | # save_path=download_location, 243 | # download_path=download_location, 244 | save_path=siteIdStr, 245 | # category=timestamp, 246 | tags=[imdbtag], 247 | use_auto_torrent_management=False) 248 | else: 249 | result = qbClient.torrents_add( 250 | urls=downlink, 251 | is_paused=CONFIG.addPause, 252 | tags=[imdbtag], 253 | use_auto_torrent_management=False) 254 | 255 | # breakpoint() 256 | if 'OK' in result.upper(): 257 | print('Torrent added.') 258 | else: 259 | print('Torrent not added! something wrong with qb api ...') 260 | except Exception as e: 261 | print('Torrent not added! Exception: '+str(e)) 262 | return False 263 | 264 | return True 265 | 266 | 267 | def tryFloat(fstr): 268 | try: 269 | f = float(fstr) 270 | except: 271 | f = 0.0 272 | return f 273 | 274 | 275 | def isMediaExt(path): 276 | fn, ext = os.path.splitext(path) 277 | return ext in ['.mkv', '.mp4', '.ts', '.m2ts'] 278 | 279 | 280 | def loadEmbyLibrary(): 281 | if not (CONFIG.embyServer and CONFIG.embyUser): 282 | print("Set the EMBY section in config.ini") 283 | return 284 | print("Create Database....") 285 | with app.app_context(): 286 | db.create_all() 287 | if not ARGS.append: 288 | emptyTable() 289 | 290 | print("Connect to the Emby server: " + CONFIG.embyServer) 291 | ec = EmbyClient(CONFIG.embyServer, CONFIG.embyUser, CONFIG.embyPass) 292 | p = TMDbNameParser(CONFIG.tmdb_api_key, 'en') 293 | 294 | r = ec.getMediaList() 295 | for item in r: 296 | print(">> " + item["Name"]) 297 | pi = MediaItem(title=item["Name"]) 298 | pi.key = item["ServerId"] 299 | pi.audienceRating = item["CommunityRating"] if "CommunityRating" in item else 0 300 | pi.locationdirname = os.path.basename(os.path.dirname(item["Path"])) 301 | 302 | guids = item["ProviderIds"] 303 | pi.imdb = guids['Imdb'] if 'Imdb' in guids else '' 304 | pi.tmdb = guids['Tmdb'] if 'Tmdb' in guids else '' 305 | pi.tvdb = guids['Tvdb'] if 'Tvdb' in guids else '' 306 | 307 | pd = item["PremiereDate"] if "PremiereDate" in item else "" 308 | if 'Tmdb' not in guids: 309 | print(" TMDb Not Found: %s (%s) " % (item["Name"], pd)) 310 | pi.tmdb = searchTMDb(p, item["Name"], pi.imdb) 311 | print(" %s: (TMDb: %s, IMDb: %s)\n %s" % (pi.title, pi.tmdb, pi.imdb, os.path.dirname(item["Path"]))) 312 | with app.app_context(): 313 | db.session.add(pi) 314 | db.session.commit() 315 | 316 | 317 | def emptyTable(): 318 | try: 319 | with app.app_context(): 320 | num_rows_deleted = db.session.query(MediaItem).delete() 321 | db.session.commit() 322 | return num_rows_deleted 323 | except: 324 | db.session.rollback() 325 | return 0 326 | 327 | 328 | def plexKeyExists(videokey): 329 | with app.app_context(): 330 | exists = db.session.query(db.exists().where(MediaItem.key == videokey)).scalar() 331 | # exists = db.session.query(MediaItem.id).filter_by(key=videokey).first() is not None 332 | 333 | return exists 334 | 335 | 336 | # @app.route('/sitetor/api/v1.0/init', methods=['GET']) 337 | def loadPlexLibrary(): 338 | if not (CONFIG.plexServer and CONFIG.plexToken): 339 | print("Set the 'server_token' and 'server_url' in config.ini") 340 | return 341 | print("Create Database....") 342 | with app.app_context(): 343 | db.create_all() 344 | 345 | if not ARGS.append: 346 | emptyTable() 347 | 348 | tstart = time.time() 349 | print("Connect to the Plex server: " + CONFIG.plexServer) 350 | baseurl = CONFIG.plexServer # 'http://{}:{}'.format(ip, port) 351 | plex = PlexServer(baseurl, CONFIG.plexToken) 352 | # movies = plex.library.section(sectionstr) 353 | p = TMDbNameParser(CONFIG.tmdb_api_key, 'en') 354 | for idx, video in enumerate(plex.library.all()): 355 | for n in range(MAX_RETRY): 356 | try: 357 | pi = MediaItem(title=video.title) 358 | pi.originalTitle = video.originalTitle 359 | pi.librarySectionID = video.librarySectionID 360 | pi.audienceRating = tryFloat(video.audienceRating) 361 | break 362 | except Exception as e: 363 | if n < MAX_RETRY: 364 | print('Fail to reload the video' + str(e)) 365 | print('retry %d time.' % (n+1)) 366 | time.sleep(2) 367 | else: 368 | print('Fail to reload the video MAX_RETRY(%d) times' % (MAX_RETRY)) 369 | os._exit(1) 370 | 371 | # pi.originalTitle = video.originalTitle 372 | if plexKeyExists(video.key): 373 | continue 374 | 375 | pi.guid = video.guid 376 | pi.key = video.key 377 | if len(video.locations) > 0: 378 | pi.location0 = ','.join(video.locations) 379 | if isMediaExt(video.locations[0]): 380 | pi.locationdirname = os.path.basename( 381 | os.path.dirname(video.locations[0])) 382 | else: 383 | pi.locationdirname = os.path.basename(video.locations[0]) 384 | else: 385 | print('No location: ', video.title) 386 | imdb = '' 387 | for guid in video.guids: 388 | imdbmatch = re.search(r'imdb://(tt\d+)', guid.id, re.I) 389 | if imdbmatch: 390 | pi.imdb = imdbmatch[1] 391 | imdb = imdbmatch[1] 392 | tmdbmatch = re.search(r'tmdb://(\d+)', guid.id, re.I) 393 | if tmdbmatch: 394 | pi.tmdb = tmdbmatch[1] 395 | tvdbmatch = re.search(r'tvdb://(\d+)', guid.id, re.I) 396 | if tvdbmatch: 397 | pi.tvdb = tvdbmatch[1] 398 | if not pi.tmdb: 399 | pi.tmdb = searchTMDb(p, video.title, imdb) 400 | with app.app_context(): 401 | db.session.add(pi) 402 | db.session.commit() 403 | print("%d : %s , %s , %s, %s" % (idx, video.title, 404 | video.originalTitle, video.locations, video.guids)) 405 | print(time.strftime("%H:%M:%S", time.gmtime(time.time()-tstart))) 406 | 407 | 408 | def readConfig(): 409 | config = configparser.ConfigParser() 410 | config.read('config.ini') 411 | 412 | # CONFIG.interval = config['PLEX'].getint('interval', 3) 413 | # 'http://{}:{}'.format(ip, port) 414 | if 'PLEX' in config: 415 | CONFIG.plexServer = config['PLEX'].get('server_url', '') 416 | # https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ 417 | CONFIG.plexToken = config['PLEX'].get('server_token', '') 418 | 419 | if 'EMBY' in config: 420 | CONFIG.embyServer = config['EMBY'].get('server_url', '') 421 | CONFIG.embyUser = config['EMBY'].get('user', '') 422 | CONFIG.embyPass = config['EMBY'].get('pass', '') 423 | 424 | if 'TMDB' in config: 425 | CONFIG.tmdb_api_key = config['TMDB'].get('api_key', '') 426 | 427 | if 'QBIT' in config: 428 | CONFIG.qbServer = config['QBIT'].get('server_ip', '') 429 | CONFIG.qbPort = config['QBIT'].get('port', '') 430 | CONFIG.qbUser = config['QBIT'].get('user', '') 431 | CONFIG.qbPass = config['QBIT'].get('pass') 432 | 433 | CONFIG.addPause = config['QBIT'].getboolean('pause', False) 434 | CONFIG.dryrun = config['QBIT'].getboolean('dryrun', False) 435 | 436 | 437 | def searchTMDb(TmdbParser, title, imdb): 438 | if imdb: 439 | TmdbParser.parse(title, useTMDb=True, hasIMDbId=imdb) 440 | else: 441 | TmdbParser.parse(title, useTMDb=True) 442 | return TmdbParser.tmdbid 443 | 444 | 445 | def fillTMDbListDb(): 446 | with app.app_context(): 447 | query = db.session.query(MediaItem).filter(MediaItem.tmdb == None) 448 | if not CONFIG.tmdb_api_key: 449 | print("Set the ['TMDB']['api_key'] in config.ini") 450 | return 451 | 452 | p = TMDbNameParser(CONFIG.tmdb_api_key, 'en') 453 | for row in query: 454 | row.tmdb = searchTMDb(p, row.title, row.imdb) 455 | # row.save() 456 | db.session.commit() 457 | 458 | 459 | def loadArgs(): 460 | global ARGS 461 | parser = argparse.ArgumentParser( 462 | description='A torrent handler does library dupe check, add qbit with tag, etc.' 463 | ) 464 | parser.add_argument('--init-library', action='store_true', 465 | help='init database with plex query.') 466 | parser.add_argument('-a', '--append', action='store_true', 467 | help='append to local database, without delete old data.') 468 | parser.add_argument('--fill-tmdb', action='store_true', 469 | help='fill tmdb field if it miss.') 470 | parser.add_argument('--siteid-folder', action='store_true', 471 | help='make Site_Id_Imdb parent folder.') 472 | ARGS = parser.parse_args() 473 | 474 | def initDatabase(): 475 | print("Init Database....") 476 | with app.app_context(): 477 | db.create_all() 478 | 479 | 480 | def main(): 481 | loadArgs() 482 | readConfig() 483 | if ARGS.init_library: 484 | loadPlexLibrary() 485 | loadEmbyLibrary() 486 | elif ARGS.fill_tmdb: 487 | fillTMDbListDb() 488 | else: 489 | initDatabase() 490 | app.run(host='0.0.0.0', port=3006, debug=True) 491 | 492 | 493 | if __name__ == '__main__': 494 | main() 495 | -------------------------------------------------------------------------------- /emby_client.py: -------------------------------------------------------------------------------- 1 | 2 | import hashlib 3 | import urllib 4 | import requests 5 | 6 | 7 | class EmbyClient: 8 | def __init__(self, serverUrl, username, password): 9 | self.serverUrl = serverUrl 10 | self.username = username 11 | self.password = password 12 | 13 | def authenticate(self): 14 | url = self.serverUrl + "/Users/AuthenticateByName?format=json" 15 | 16 | message_data = {} 17 | message_data["username"] = urllib.parse.quote(self.username) 18 | message_data["password"] = hashlib.sha1(self.password.encode('utf-8')).hexdigest() 19 | message_data["pw"] = urllib.parse.quote(self.password) 20 | 21 | headers = self.getHeaders() 22 | 23 | # print("Auth Url : %s" % url) 24 | # print("Auth Msg : %s" % message_data) 25 | # print("Auth Headers : %s" % headers) 26 | response = requests.post(url, data=message_data, headers=headers) 27 | # print(str(response.text)) 28 | if response: 29 | return response.json() 30 | return '' 31 | 32 | def getHeaders(self, user_info=None): 33 | 34 | auth_string = "MediaBrowser Client=\"TorFilter\",Device=\"FilterApi\",DeviceId=\"10\",Version=\"1\"" 35 | 36 | if user_info: 37 | auth_string += ",UserId=\"" + user_info["User"]["Id"] + "\"" 38 | 39 | headers = {} 40 | 41 | if user_info: 42 | headers["X-MediaBrowser-Token"] = user_info["AccessToken"] 43 | 44 | headers["Accept-encoding"] = "gzip" 45 | headers["Accept-Charset"] = "UTF-8,*" 46 | 47 | headers["X-Emby-Authorization"] = auth_string 48 | 49 | return headers 50 | 51 | def getUrlData(self, url, user_info): 52 | if url.find("{server}") != -1: 53 | url = url.replace("{server}", self.serverUrl) 54 | 55 | if url.find("{userid}") != -1: 56 | if "User" in user_info and "Id" in user_info["User"]: 57 | user_id = user_info["User"]["Id"] 58 | url = url.replace("{userid}", user_id) 59 | else: 60 | return "" 61 | 62 | headers = self.getHeaders(user_info) 63 | 64 | response = requests.get(url, headers=headers) 65 | return response.json() 66 | 67 | def getMediaList(self): 68 | user_info = self.authenticate() 69 | if not user_info: 70 | print("Authenticate Failed.") 71 | return [] 72 | 73 | url = ('{server}/emby/Users/{userid}/Items' + 74 | '?Recursive=true' + 75 | '&Fields=Path,PremiereDate,CommunityRating,ProviderIds' + 76 | '&IsMissing=False' + 77 | '&IncludeItemTypes=Movie,Series' + 78 | '&ImageTypeLimit=0') 79 | 80 | response_data = self.getUrlData(url, user_info) 81 | if not response_data: 82 | print("getUrlData Failed.") 83 | return [] 84 | 85 | item_list = response_data["Items"] 86 | return item_list 87 | 88 | -------------------------------------------------------------------------------- /humanbytes.py: -------------------------------------------------------------------------------- 1 | ## copy from github... 2 | ## 3 | from typing import List, Union 4 | 5 | class HumanBytes: 6 | METRIC_LABELS: List[str] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 7 | BINARY_LABELS: List[str] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] 8 | PRECISION_OFFSETS: List[float] = [0.5, 0.05, 0.005, 0.0005] # PREDEFINED FOR SPEED. 9 | PRECISION_FORMATS: List[str] = ["{}{:.0f} {}", "{}{:.1f} {}", "{}{:.2f} {}", "{}{:.3f} {}"] # PREDEFINED FOR SPEED. 10 | 11 | @staticmethod 12 | def format(num: Union[int, float], metric: bool=False, precision: int=1) -> str: 13 | """ 14 | Human-readable formatting of bytes, using binary (powers of 1024) 15 | or metric (powers of 1000) representation. 16 | """ 17 | 18 | assert isinstance(num, (int, float)), "num must be an int or float" 19 | assert isinstance(metric, bool), "metric must be a bool" 20 | assert isinstance(precision, int) and precision >= 0 and precision <= 3, "precision must be an int (range 0-3)" 21 | 22 | unit_labels = HumanBytes.METRIC_LABELS if metric else HumanBytes.BINARY_LABELS 23 | last_label = unit_labels[-1] 24 | unit_step = 1000 if metric else 1024 25 | unit_step_thresh = unit_step - HumanBytes.PRECISION_OFFSETS[precision] 26 | 27 | is_negative = num < 0 28 | if is_negative: # Faster than ternary assignment or always running abs(). 29 | num = abs(num) 30 | 31 | for unit in unit_labels: 32 | if num < unit_step_thresh: 33 | # VERY IMPORTANT: 34 | # Only accepts the CURRENT unit if we're BELOW the threshold where 35 | # float rounding behavior would place us into the NEXT unit: F.ex. 36 | # when rounding a float to 1 decimal, any number ">= 1023.95" will 37 | # be rounded to "1024.0". Obviously we don't want ugly output such 38 | # as "1024.0 KiB", since the proper term for that is "1.0 MiB". 39 | break 40 | if unit != last_label: 41 | # We only shrink the number if we HAVEN'T reached the last unit. 42 | # NOTE: These looped divisions accumulate floating point rounding 43 | # errors, but each new division pushes the rounding errors further 44 | # and further down in the decimals, so it doesn't matter at all. 45 | num /= unit_step 46 | 47 | return HumanBytes.PRECISION_FORMATS[precision].format("-" if is_negative else "", num, unit) 48 | 49 | -------------------------------------------------------------------------------- /juzhang.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 局长保种下载 3 | // @namespace https://greasyfork.org/zh-CN/scripts/ 4 | // @version 0.1 5 | // @license GPL-3.0 License 6 | // @description Love from juzhang 7 | // @author ccf2012 8 | // @icon https://pterclub.com/favicon.ico 9 | // @match https://pterclub.com/viewclaims.php* 10 | // @require https://code.jquery.com/jquery-3.6.0.min.js 11 | // @grant GM_setClipboard 12 | // @grant GM.xmlHttpRequest 13 | 14 | // ==/UserScript== 15 | 16 | function addDownloadPanel() { 17 | var torTable = $("#outer > table"); 18 | if (torTable.length <= 0) { 19 | return; 20 | } 21 | var donwnloadPanel = ` 22 | 23 | 28 | 29 | 30 |
24 | 27 |
31 | `; 32 | torTable.before(donwnloadPanel); 33 | } 34 | 35 | const pter_passkey = async () => { 36 | let html = await $.get("usercp.php"); 37 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 38 | if (passkeyRow.length > 0) { 39 | var key = passkeyRow.find("td:last").text(); 40 | return "&passkey=" + key.trim(); 41 | } 42 | return ""; 43 | }; 44 | 45 | function genDownloadLink(link, passKeyStr) { 46 | return link + passKeyStr; 47 | } 48 | 49 | var asyncCopyLink = async (html) => { 50 | $("#process-log").text("处理中..."); 51 | let passKeyStr = await pter_passkey(); 52 | // console.log(passKeyStr); 53 | 54 | let torlist = $(html).find( 55 | "#outer > table > tbody > tr > td > table > tbody > tr " 56 | ); 57 | var resulttext = ""; 58 | for (let index = 1; index < torlist.length; ++index) { 59 | if ($(torlist[index]).is(":visible")) { 60 | let hrefele = $(torlist[index]).find("td:nth-child(5) > a"); 61 | if (hrefele.length > 0) { 62 | let dllinktemp = hrefele 63 | .prop("href") 64 | .replace("details.php", "download.php"); 65 | let dllink = genDownloadLink(dllinktemp, passKeyStr); 66 | resulttext += dllink + "\n"; 67 | } 68 | } 69 | } 70 | GM_setClipboard(resulttext, "text"); 71 | $("#process-log").text("下载链接 已拷贝在剪贴板中"); 72 | }; 73 | 74 | function onClickCopyDownloadLink(html) { 75 | asyncCopyLink(html); 76 | } 77 | 78 | (function () { 79 | "use strict"; 80 | 81 | addDownloadPanel(document); 82 | $("#btn-copydllink").click(function () { 83 | onClickCopyDownloadLink(document); 84 | }); 85 | })(); 86 | -------------------------------------------------------------------------------- /plexmove.py: -------------------------------------------------------------------------------- 1 | from emby_client import EmbyClient 2 | from plexapi.server import PlexServer 3 | import configparser 4 | import argparse 5 | 6 | class configData(): 7 | interval = 3 8 | plexServer = '' 9 | plexToken = '' 10 | embyServer = '' 11 | embyUser = '' 12 | embyPass = '' 13 | qbServer = '' 14 | tmdb_api_key = '' 15 | qbPort = '' 16 | qbUser = '' 17 | qbPass = '' 18 | addPause = False 19 | dryrun = False 20 | 21 | 22 | CONFIG = configData() 23 | 24 | 25 | def readConfig(): 26 | config = configparser.ConfigParser() 27 | config.read('config.ini') 28 | 29 | # CONFIG.interval = config['PLEX'].getint('interval', 3) 30 | # 'http://{}:{}'.format(ip, port) 31 | if 'PLEX' in config: 32 | CONFIG.plexServer = config['PLEX'].get('server_url', '') 33 | # https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ 34 | CONFIG.plexToken = config['PLEX'].get('server_token', '') 35 | 36 | if 'EMBY' in config: 37 | CONFIG.embyServer = config['EMBY'].get('server_url', '') 38 | CONFIG.embyUser = config['EMBY'].get('user', '') 39 | CONFIG.embyPass = config['EMBY'].get('pass', '') 40 | 41 | if 'TMDB' in config: 42 | CONFIG.tmdb_api_key = config['TMDB'].get('api_key', '') 43 | 44 | 45 | 46 | def loadArgs(): 47 | global ARGS 48 | parser = argparse.ArgumentParser( 49 | description='A torrent handler does library dupe check, add qbit with tag, etc.' 50 | ) 51 | parser.add_argument('--plex-move', action='store_true', 52 | help='init database with plex query.') 53 | parser.add_argument('--emby-move', action='store_true', 54 | help='init database with plex query.') 55 | ARGS = parser.parse_args() 56 | 57 | 58 | def movePlexLibrary(): 59 | if not (CONFIG.plexServer and CONFIG.plexToken): 60 | print("Set the 'server_token' and 'server_url' in config.ini") 61 | return 62 | 63 | sectionstr = '剧集' 64 | print("Connect to the Plex server: " + CONFIG.plexServer) 65 | baseurl = CONFIG.plexServer # 'http://{}:{}'.format(ip, port) 66 | plex = PlexServer(baseurl, CONFIG.plexToken) 67 | medias = plex.library.section(sectionstr) 68 | # for idx, video in enumerate(plex.library.all()): 69 | docuCount = 0 70 | for idx, video in enumerate(medias.all()): 71 | # gtags = [g for g in video.genres if g.tag == '纪录'] 72 | hasDocu = next((g for g in video.genres if g.tag == '纪录'), None) 73 | if hasDocu: 74 | docuCount += 1 75 | print(str(docuCount) + ' ' + video.title) 76 | # for g in video.genres: 77 | # print(" >>" + g.tag) 78 | if len(video.locations) > 0: 79 | print(video.locations[0]) 80 | else: 81 | print('\033[33mNo location: %s \033[0m' % video.title) 82 | 83 | 84 | def main(): 85 | loadArgs() 86 | readConfig() 87 | # if ARGS.plex_move: 88 | movePlexLibrary() 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /pt.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sqlalchemy<2.0 2 | Flask-SQLAlchemy 3 | flask_httpauth 4 | torcp>=0.58 5 | plexapi 6 | flask 7 | qbittorrent-api 8 | tmdbv3api>=1.7.7 9 | requests 10 | urllib3 11 | feedparser 12 | wtforms -------------------------------------------------------------------------------- /torfilter.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 种子列表过滤 3 | // @namespace https://greasyfork.org/zh-CN/scripts/451748 4 | // @version 1.6.5 5 | // @license GPL-3.0 License 6 | // @description 在种子列表页中,过滤: 未作种,无国语,有中字,标题不含,描述不含,大小介于,IMDb/豆瓣大于输入值 的种子。配合dupapi可以实现Plex/Emby库查重。 7 | // @author ccf2012 8 | // @source https://github.com/ccf-2012/torfilter 9 | // @icon https://pterclub.com/favicon.ico 10 | // @grant GM_setClipboard 11 | // @grant GM.xmlHttpRequest 12 | // @connect 192.168.5.6 13 | // @require https://code.jquery.com/jquery-3.6.0.min.js 14 | // @match https://*pterclub.com/torrents.php* 15 | // @match https://*pterclub.com/officialgroup* 16 | // @match https://pterclub.com/details.php* 17 | // @match https://*chddiy.xyz/torrents.php* 18 | // @match https://*chddiy.xyz/details.php* 19 | // @match https://chdbits.co/details.php* 20 | // @match https://chdbits.co/torrents.php* 21 | // @match https://ptchdbits.co/torrents.php* 22 | // @match https://ptchdbits.co/details.php* 23 | // @match https://audiences.me/torrents.php* 24 | // @match https://audiences.me/details.php* 25 | // @match https://sunnypt.top/torrents.php* 26 | // @match https://sunnypt.top/details.php* 27 | // @match https://ourbits.club/torrents.php* 28 | // @match https://ourbits.club/details.php* 29 | // @match https://springsunday.net/torrents.php* 30 | // @match https://springsunday.net/details.php* 31 | // @match https://www.beitai.pt/torrents.php* 32 | // @match https://www.beitai.pt/details.php* 33 | // @match https://totheglory.im/browse.php?* 34 | // @match https://totheglory.im/t/* 35 | // @match https://pt.keepfrds.com/torrents.php* 36 | // @match https://pt.keepfrds.com/details.php* 37 | // @match https://*hdchina.org/torrents.php* 38 | // @match https://*hdchina.org/details.php* 39 | // @match https://hdsky.me/torrents.php* 40 | // @match https://hdsky.me/details.php* 41 | // @match https://hhanclub.top/torrents.php* 42 | // @match https://hhanclub.top/details.php* 43 | // @match https://leaves.red/torrents* 44 | // @match https://leaves.red/details* 45 | // @match https://hdhome.org/torrents.php* 46 | // @match https://hdhome.org/details.php* 47 | // @match https://wintersakura.net/torrents.php* 48 | // @match https://wintersakura.net/details.php* 49 | // @match https://pt.soulvoice.club/torrents.php* 50 | // @match https://pt.soulvoice.club/details.php* 51 | // @match https://ptsbao.club/torrents.php* 52 | // @match https://ptsbao.club/details.php* 53 | // @match https://pt.eastgame.org/torrents.php* 54 | // @match https://pt.eastgame.org/details.php* 55 | // @match https://www.hddolby.com/torrents.php* 56 | // @match https://www.hddolby.com/details.php* 57 | // @match https://pt.hd4fans.org/torrents.php* 58 | // @match https://pt.hd4fans.org/details.php* 59 | // @match https://hdfans.org/torrents.php* 60 | // @match https://hdfans.org/details.php* 61 | // @match https://pthome.net/torrents.php* 62 | // @match https://pthome.net/details.php* 63 | // @match https://kp.m-team.cc/torrents* 64 | // @match https://kp.m-team.cc/movie* 65 | // @match https://kp.m-team.cc/details* 66 | // @match https://piggo.me/torrents* 67 | // @match https://piggo.me/special* 68 | // @match https://piggo.me/details* 69 | // @match https://discfan.net/torrents* 70 | // @match https://discfan.net/details* 71 | // @match https://www.tjupt.org/torrents* 72 | // @match https://www.tjupt.org/details* 73 | // @match https://lemonhd.club/torrents* 74 | // @match https://lemonhd.club/details* 75 | 76 | // ==/UserScript== 77 | 78 | const API_SERVER = 'http://192.168.5.6:5006'; 79 | const API_AUTH_KEY = "some_api_key"; 80 | 81 | 82 | const API_CHECKDUP = API_SERVER + '/api/checkdupeonly'; 83 | const API_DUPDOWNLOAD = API_SERVER + '/api/dupedownload'; 84 | 85 | const not_supported = (element) => { 86 | return ""; 87 | }; 88 | 89 | const skip_passkey = async () => { 90 | return ""; 91 | }; 92 | 93 | // ====== pter 94 | const pter_imdbval = (element) => { 95 | var t = $(element).find( "a span[data-imdbid]" ); 96 | return t.text(); 97 | }; 98 | const pter_imdbid = (element) => { 99 | var t = $(element).find("a span[data-imdbid]") 100 | return (t && t.attr("data-imdbid")) ? 'tt'+t.attr("data-imdbid") : '' 101 | }; 102 | 103 | const pter_douban = (element) => { 104 | var d = $(element).find("a span[data-doubanid]" 105 | ); 106 | return d.text(); 107 | }; 108 | 109 | const pter_seeding = (element) => { 110 | // var d = $(element).find("img.progbargreen"); 111 | return ($(element).find("img.progbargreen").length > 0); 112 | }; 113 | 114 | const pter_downed = (element) => { 115 | // var d = $(element).find("img.progbargreen"); 116 | return ($(element).find("img.progbarred").length > 0); 117 | }; 118 | 119 | // ====== chd 120 | const chd_imdb = (element) => { 121 | var t = $(element).find( 122 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2)" 123 | ); 124 | return t.text(); 125 | }; 126 | 127 | const chd_seeding = (element) => { 128 | var d = $(element).find("td:nth-child(10)"); 129 | return (d.length > 0 && d.css("color") === 'rgb(0, 128, 0)') 130 | // return d.text() === "100%"; 131 | }; 132 | 133 | const chd_downed = (element) => { 134 | var d = $(element).find("td:nth-child(10)"); 135 | // return (d.length > 0 && d.css("color") === 'rgb(0, 128, 0)') 136 | return d.text() != "--"; 137 | }; 138 | 139 | 140 | const chd_passkey = async () => { 141 | let html = await $.get("usercp.php"); 142 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 143 | if (passkeyRow.length > 0) { 144 | var key = passkeyRow.find("td:last").text(); 145 | return "&passkey=" + key.trim(); 146 | } 147 | return ""; 148 | }; 149 | // ====== ade 150 | const ade_imdbval = (element) => { 151 | var t = $(element).find( 152 | "td.rowfollow.torrents-box > div.torrents-name > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(3)" 153 | ); 154 | return t.text(); 155 | }; 156 | const ade_imdbid = (element) => { 157 | var t = $(element) 158 | .find( 159 | "td.rowfollow.torrents-box > div.torrents-name > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(3)" 160 | ) 161 | .attr("href"); 162 | if (t) { 163 | var m = t.match(/title\/(tt\d+)/); 164 | } 165 | return m ? m[1] : ""; 166 | }; 167 | 168 | const ade_douban = (element) => { 169 | var d = $(element).find( 170 | "td.rowfollow.torrents-box > div.torrents-name > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(1)" 171 | ); 172 | return d.text(); 173 | }; 174 | 175 | const ade_seeding = (element) => { 176 | var d = $(element).find("div.torrents-progress"); 177 | 178 | return d.length > 0 && d.css("width") != "0px"; 179 | // return d.text() === "100%"; 180 | }; 181 | 182 | const ade_downed = (element) => { 183 | var d = $(element).find("div.torrents-progress2"); 184 | 185 | return d.length > 0 && d.css("width") != "0px"; 186 | }; 187 | 188 | 189 | const ade_passkey = async () => { 190 | let html = await $.get("usercp.php") 191 | // debugger; 192 | // $(html).find("#passkey").css("display", ""); 193 | let passkeyRow = $(html).find("#passkey"); 194 | if (passkeyRow.length > 0){ 195 | let key = passkeyRow.text().replace('(妥善保管,请勿泄露)', ''); 196 | return "&passkey=" + key.trim() + "&https=1" ; 197 | } 198 | return "" ; 199 | }; 200 | 201 | // ====== ob 202 | const ob_imdbval = (element) => { 203 | var t = $(element).find( 204 | "td:nth-child(2) > table > tbody > tr > td:nth-child(4) > div:nth-child(1) > em > label" 205 | ); 206 | return t.text(); 207 | }; 208 | const ob_imdbid = (element) => { 209 | var t = $(element) 210 | .find( 211 | "td:nth-child(2) > table > tbody > tr > td:nth-child(4) > div:nth-child(1) > em > label" 212 | ) 213 | .attr("data-imdbid"); 214 | 215 | return t ? "tt" + t : ""; 216 | }; 217 | 218 | const ob_douban = (element) => { 219 | var d = $(element).find( 220 | "td:nth-child(2) > table > tbody > tr > td:nth-child(4) > div:nth-child(2) > em > label" 221 | ); 222 | return d.text(); 223 | }; 224 | const ob_seeding = (element) => { 225 | var d = $(element).find("div.doing"); 226 | return d.length > 0 && d.attr("title").includes("100%"); 227 | }; 228 | const ob_downed = (element) => { 229 | var d = $(element).find("div.out"); 230 | return d.length > 0 && d.attr("title").includes("100%"); 231 | }; 232 | 233 | const ob_passkey = async () => { 234 | let html = await $.get("usercp.php"); 235 | 236 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 237 | if (passkeyRow.length <= 0) { 238 | passkeyRow = $(html).find('tr:contains("密匙"):last'); 239 | } 240 | if (passkeyRow.length <= 0) { 241 | passkeyRow = $(html).find('tr:contains("Passkey"):last'); 242 | } 243 | if (passkeyRow.length > 0) { 244 | let key = passkeyRow.find("td:last").text(); 245 | return "&passkey=" + key.trim() + "&https=1"; 246 | } 247 | return ""; 248 | }; 249 | 250 | // ====== redleaves 251 | const rl_imdbval = (element) => { 252 | var t = $(element).find("img[alt*='imdb']") 253 | return t.parent().text(); 254 | }; 255 | 256 | const rl_douban = (element) => { 257 | var d = $(element).find("img[alt*='douban']"); 258 | return d.parent().text(); 259 | }; 260 | 261 | const rl_seeding = (element) => { 262 | var d = $(element).find("div[title]"); 263 | return d.length > 0 && d.attr("title").includes("100"); 264 | }; 265 | const rl_downed = (element) => { 266 | var d = $(element).find("div[title]"); 267 | return d.length > 0 && d.attr("title").includes("100"); 268 | }; 269 | 270 | const rl_passkey = async () => { 271 | let html = await $.get("usercp.php"); 272 | 273 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 274 | if (passkeyRow.length > 0) { 275 | let key = passkeyRow.find("td:last").text(); 276 | return "&passkey=" + key.trim() + "&https=1"; 277 | } 278 | return ""; 279 | }; 280 | 281 | // ====== ssd 282 | const ssd_imdbval = (element) => { 283 | var t = $(element).find("span.torrent-rating"); 284 | if (t.length > 1){ 285 | return $(t[0]).text() 286 | } 287 | var s = t.parent().attr("href"); 288 | if (s && s.match(/search=(\d+)\b.*search_area=4/)) { 289 | return $(t[0]).text() 290 | } 291 | return "" 292 | // let imdb = ""; 293 | // if (t.parent().attr("href") && t.parent().attr("href").includes("imdb")) { 294 | // imdb = t.text(); 295 | // } 296 | // return imdb; 297 | }; 298 | 299 | const ssd_imdbid = (element) => { 300 | var t = $(element) 301 | .find("td:nth-child(2) > div:nth-child(2) > span > a:nth-child(1)") 302 | .attr("href"); 303 | if (t) { 304 | var m = t.match(/search=(\d+)\b.*search_area=4/); 305 | if (m) { 306 | return (m[1].length < 7) ? "tt" + m[1].padStart(7, '0') : "tt" + m[1]; 307 | } 308 | } 309 | return ""; 310 | }; 311 | 312 | const ssd_douban = (element) => { 313 | var t = $(element).find("span.torrent-rating"); 314 | if (t.length > 1){ 315 | return $(t[1]).text() 316 | } 317 | var s = t.parent().attr("href"); 318 | if (s && s.match(/search=(\d+)\b.*search_area=5/)) { 319 | return $(t[0]).text() 320 | } 321 | return "" 322 | 323 | // var d = $(element).find("td:nth-child(3) > div:nth-child(2) > a > span"); 324 | // if (d.length <= 0) { 325 | // d = $(element).find("td:nth-child(3) > div > a > span"); 326 | // } 327 | // let douban = ""; 328 | // if (d.parent().attr("href") && d.parent().attr("href").includes("douban")) { 329 | // douban = d.text(); 330 | // } 331 | // return douban; 332 | }; 333 | 334 | const ssd_seeding = (element) => { 335 | var d = $(element).find("div.p_seeding"); 336 | return d.length > 0; 337 | }; 338 | 339 | const ssd_downed = (element) => { 340 | var d = $(element).find("div.p_inactive"); 341 | return d.length > 0; 342 | }; 343 | 344 | const ssd_passkey = async () => { 345 | // site changed 2022.12 346 | // let html = await $.get("usercp.php"); 347 | // let passkeyRow = $(html).find('tr:contains("密钥"):last'); 348 | // if (passkeyRow.length > 0) { 349 | // var key = passkeyRow.find("td:last").text(); 350 | // return "&passkey=" + key.trim() + "&https=1"; 351 | // } 352 | return ""; 353 | }; 354 | 355 | const ssd_detailTable = (html) => { 356 | let downTr = $(html).find('tr:contains("下载"):first'); 357 | if (downTr) { 358 | return downTr.parent(); 359 | } else return null; 360 | }; 361 | 362 | 363 | // ====== ttg 364 | const ttg_imdbval = (element) => { 365 | var t = $(element).find("td:nth-child(2) > div.name_right > span.imdb_rate > a"); 366 | return t.text(); 367 | }; 368 | 369 | const ttg_imdbid = (element) => { 370 | var t = $(element).find("td:nth-child(2) > div.name_right > span.imdb_rate > a").attr("href"); 371 | if (t) { 372 | var m = t.match(/title\/(tt\d+)/); 373 | } 374 | return m ? m[1] : ""; 375 | }; 376 | 377 | const ttg_seeding = (element) => { 378 | var d = $(element).find("td:nth-child(2) > div.process.green > span"); 379 | return d.length > 0; 380 | }; 381 | 382 | const ttg_passkey = async () => { 383 | let html = await $.get("my.php"); 384 | let passkeyRow = $("td", $(html)).filter(function() { 385 | return $(this).text() == "Passkey"; 386 | }).closest("tr"); 387 | 388 | if (passkeyRow.length > 0) { 389 | var key = passkeyRow.find("td:last").text(); 390 | return key.trim(); 391 | } 392 | return ""; 393 | }; 394 | 395 | 396 | // beitai 397 | const beitai_seeding = (element) => { 398 | var d = $(element).find("td:nth-child(9)"); 399 | return d.text().includes("100%"); 400 | }; 401 | 402 | const beitai_passkey = async () => { 403 | let html = await $.get("usercp.php"); 404 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 405 | if (passkeyRow.length > 0) { 406 | var key = passkeyRow.find("td:last").text(); 407 | return "&passkey=" + key.trim() + "&https=1"; 408 | } 409 | return ""; 410 | }; 411 | 412 | // ====== frds 413 | const frds_imdbval = (element) => { 414 | var t = $(element).find("td:nth-child(2) > table > tbody > tr > td:nth-child(2) > div:nth-child(1) > img:nth-child(2)"); 415 | let imdb = ""; 416 | if (t.attr("src") && t.attr("src").includes("imdb")) { 417 | imdb = t.parent().text(); 418 | imdb = imdb.replace(/(-+|\d+\.\d*)\s*$/, '').trim() 419 | } 420 | return imdb; 421 | }; 422 | 423 | 424 | const frds_passkey = async () => { 425 | let html = await $.get("usercp.php"); 426 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 427 | if (passkeyRow.length > 0) { 428 | var key = passkeyRow.find("td:last").text(); 429 | return "&passkey=" + key.trim() + "&https=1"; 430 | } 431 | return ""; 432 | }; 433 | 434 | const frds_seeding = (element) => { 435 | // var d = $(element).find("td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > div:nth-child(1)"); 436 | var d = $("td:nth-child(2) > table > tbody > tr > td:nth-child(1)", element) 437 | return d.length > 0 && (/2s_up.gif/.exec(d.html())) 438 | }; 439 | 440 | const frds_downed = (element) => { 441 | // var d = $(element).find("td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > div:nth-child(1)"); 442 | var d = $("td:nth-child(2) > table > tbody > tr > td:nth-child(1)", element) 443 | return d.length > 0 && (/2s_dled.gif/.exec(d.html())) 444 | }; 445 | 446 | 447 | // ====== hdc 448 | const hdc_imdbval = (element) => { 449 | var t = $(element).find( 450 | "td.t_name > table > tbody > tr > td.act > a.imdb" 451 | ); 452 | return t.text(); 453 | }; 454 | 455 | const hdc_seeding = (element) => { 456 | var s = $(element).find("div.progress_seeding"); 457 | var d = $(element).find("div.progressarea"); 458 | return s.length > 0 || d.length > 0; 459 | }; 460 | const hdc_downed = (element) => { 461 | var d = $(element).find("div.progress_completed"); 462 | return d.length > 0; 463 | }; 464 | 465 | 466 | // ====== hds 467 | const sky_imdbval = (element) => { 468 | var t = $(element).find( 469 | "td > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(2)" 470 | ); 471 | return t.text(); 472 | }; 473 | const sky_imdbid = (element) => { 474 | var t = $(element) 475 | .find( 476 | "td > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(2)" 477 | ) 478 | .attr("href"); 479 | if (t) { 480 | var m = t.match(/title\/(tt\d+)/); 481 | } 482 | return m ? m[1] : ""; 483 | 484 | }; 485 | 486 | const sky_douban = (element) => { 487 | var d = $(element).find( 488 | "td > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(1)" 489 | ); 490 | return d.text(); 491 | }; 492 | 493 | const sky_seeding = (element) => { 494 | var s = $(element).find("div.progressseeding"); 495 | var d = $(element).find("div.progressdownloading"); 496 | return s.length > 0 || d.length > 0; 497 | }; 498 | 499 | const sky_downed = (element) => { 500 | var d = $(element).find("div.progressfinished"); 501 | 502 | return d && d.length > 0 ; 503 | }; 504 | 505 | const sky_passkey = async () => { 506 | // let html = await $.get("usercp.php"); 507 | // let passkeyRow = $(html).find('tr:contains("密钥"):last'); 508 | // if (passkeyRow.length > 0) { 509 | // var key = passkeyRow.find("td:last").text(); 510 | // return "&passkey=" + key.trim(); 511 | // } 512 | return ""; 513 | }; 514 | 515 | // ====== hhclub 516 | const hh_imdbval = (element) => { 517 | var t = $(element).find("img[title='imdb']"); 518 | return t.parent().text(); 519 | }; 520 | 521 | 522 | const hh_douban = (element) => { 523 | var d = $(element).find("img[title='douban']"); 524 | return d.parent().text(); 525 | }; 526 | 527 | const hh_seeding = (element) => { 528 | var s = $(element).find("[title*='leeching']"); 529 | var d = $(element).find("[title*='seeding']"); 530 | return s.length > 0 || d.length > 0; 531 | }; 532 | 533 | const hh_downed = (element) => { 534 | var d = $(element).find("[title*='inactivity']"); 535 | 536 | return d && d.length > 0 ; 537 | }; 538 | 539 | const hh_passkey = async () => { 540 | let html = await $.get("usercp.php"); 541 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 542 | if (passkeyRow.length > 0) { 543 | var key = passkeyRow.find("td:last").text(); 544 | return "&passkey=" + key.trim(); 545 | } 546 | return ""; 547 | }; 548 | 549 | // ====== lemonhd 550 | const lhd_imdbval = (element) => { 551 | var t = $(element).find( "a[href*='imdb']" ); 552 | return t.text(); 553 | }; 554 | const lhd_imdbid = (element) => { 555 | var t = $(element) 556 | .find("a[href*='imdb']" ) 557 | .attr("href"); 558 | 559 | if (t) { 560 | var m = t.match(/title\/(tt\d+)/); 561 | } 562 | return m ? m[1] : ""; 563 | }; 564 | 565 | const lhd_douban = (element) => { 566 | var d = $(element).find("a[href*='douban']"); 567 | return d.text(); 568 | }; 569 | const lhd_seeding = (element) => { 570 | var d = $(element).find("td.rowfollow.peer-active"); 571 | return d.length > 0 ; 572 | }; 573 | const lhd_downed = (element) => { 574 | var d = $(element).find("td:nth-child(10) > b"); 575 | return d.text() != "--";; 576 | }; 577 | 578 | const lhd_passkey = async () => { 579 | let html = await $.get("usercp.php"); 580 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 581 | if (passkeyRow.length > 0) { 582 | var key = passkeyRow.find("td:last").text(); 583 | return "&passkey=" + key.trim(); 584 | } 585 | return ""; 586 | }; 587 | 588 | // ====== hdh 589 | const hdh_imdbval = (element) => { 590 | var t = $(element).find( 591 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(3)" 592 | ); 593 | return t.text(); 594 | }; 595 | const hdh_imdbid = (element) => { 596 | var t = $(element) 597 | .find( 598 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(3)" 599 | ) 600 | .attr("href"); 601 | if (t) { 602 | var m = t.match(/title\/(tt\d+)/); 603 | } 604 | return m ? m[1] : ""; 605 | }; 606 | 607 | const hdh_douban = (element) => { 608 | var d = $(element).find( 609 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(1)" 610 | ); 611 | return d.text(); 612 | }; 613 | const hdh_seeding = (element) => { 614 | var d = $(element).find("td:nth-child(9)"); 615 | return d.text().includes("100%"); 616 | }; 617 | 618 | const hdh_downed = (element) => { 619 | var d = $(element).find("td:nth-child(9)"); 620 | // return (d.length > 0 && d.css("color") === 'rgb(0, 128, 0)') 621 | return d.text() != "--"; 622 | }; 623 | const hdh_passkey = async () => { 624 | let html = await $.get("usercp.php") 625 | // debugger; 626 | // $(html).find("#passkey").css("display", ""); 627 | let passkeyRow = $(html).find("#passkey"); 628 | if (passkeyRow.length > 0){ 629 | let key = passkeyRow.text().replace('(妥善保管,请勿泄露)', ''); 630 | return "&passkey=" + key.trim() + "&https=1" ; 631 | } 632 | return "" ; 633 | }; 634 | 635 | // ====== ptsbao 636 | const ptsbao_imdbval = (element) => { 637 | var t = $(element).find("img[title*='IMDb']") 638 | return t.parent().text(); 639 | }; 640 | 641 | const ptsbao_seeding = (element) => { 642 | var d = $(element).find("div[title]"); 643 | return d.length > 0 && d.attr("title").includes("100"); 644 | }; 645 | const ptsbao_downed = (element) => { 646 | var d = $(element).find("div[title]"); 647 | return d.length > 0 && d.attr("title").includes("100"); 648 | }; 649 | 650 | // ====== hddolby 651 | const hddolby_imdbval = (element) => { 652 | var t = $(element).find("img[src*='imdb.png']") 653 | return t.parent().text(); 654 | }; 655 | 656 | const hddolby_douban = (element) => { 657 | var d = $(element).find("img[src='douban.png']"); 658 | return d.parent().text(); 659 | }; 660 | 661 | const hddolby_imdbid = (element) => { 662 | var t = $(element) 663 | .find( 664 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(3)" 665 | ) 666 | .attr("href"); 667 | if (t) { 668 | var m = t.match(/title\/(tt\d+)/); 669 | } 670 | return m ? m[1] : ""; 671 | }; 672 | 673 | 674 | const hddolby_seeding = (element) => { 675 | var d = $(element).find("td:nth-child(9)"); 676 | return d.text().includes("100%"); 677 | }; 678 | const hddolby_downed = (element) => { 679 | var d = $(element).find("td:nth-child(9)"); 680 | // return (d.length > 0 && d.css("color") === 'rgb(0, 128, 0)') 681 | return d.text().includes("Noseed"); 682 | }; 683 | 684 | const hddolby_passkey = async () => { 685 | let html = await $.get("usercp.php"); 686 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 687 | if (passkeyRow.length > 0) { 688 | var key = passkeyRow.find("td:last").text(); 689 | return "&passkey=" + key.trim(); 690 | } 691 | return ""; 692 | }; 693 | 694 | //===== hd4fans 695 | 696 | const hd4fans_seeding = (element) => { 697 | var s = $(element).find("div.progress_seeding"); 698 | var d = $(element).find("div.progressarea"); 699 | return s.length > 0 || d.length > 0; 700 | }; 701 | const hd4fans_downed = (element) => { 702 | var d = $(element).find("div.progressarea"); 703 | return d.length > 0; 704 | }; 705 | 706 | const hd4fans_passkey = async () => { 707 | let html = await $.get("usercp.php"); 708 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 709 | if (passkeyRow.length > 0) { 710 | var key = passkeyRow.find("td:last").text(); 711 | return "&passkey=" + key.trim(); 712 | } 713 | return ""; 714 | }; 715 | 716 | //==== hdfans 717 | const hdfans_imdbval = (element) => { 718 | var t = $(element).find("img[title*='imdb']") 719 | return t.parent().text(); 720 | }; 721 | 722 | const hdfans_douban = (element) => { 723 | var d = $(element).find("img[title='douban']"); 724 | return d.parent().text(); 725 | }; 726 | 727 | 728 | const hdfans_seeding = (element) => { 729 | var s = $(element).find("[title*='leeching']"); 730 | var d = $(element).find("[title*='seeding']"); 731 | return s.length > 0 || d.length > 0; 732 | }; 733 | const hdfans_downed = (element) => { 734 | var d = $(element).find("[title*='inactivity']"); 735 | 736 | return d && d.length > 0 ; 737 | }; 738 | 739 | const hdfans_passkey = async () => { 740 | let html = await $.get("usercp.php"); 741 | let passkeyRow = $(html).find('tr:contains("密钥"):last'); 742 | if (passkeyRow.length > 0) { 743 | var key = passkeyRow.find("td:last").text(); 744 | return "&passkey=" + key.trim(); 745 | } 746 | return ""; 747 | }; 748 | 749 | //===== pthome 750 | const pthome_imdbval = (element) => { 751 | var t = $(element).find("img[src*='imdb']") 752 | return t.parent().text(); 753 | }; 754 | 755 | const pthome_douban = (element) => { 756 | var d = $(element).find("img[src='douban']"); 757 | return d.parent().text(); 758 | }; 759 | 760 | 761 | const pthome_imdbid = (element) => { 762 | var t = $(element) 763 | .find( 764 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(1) > div > a:nth-child(3)" 765 | ) 766 | .attr("href"); 767 | if (t) { 768 | var m = t.match(/title\/(tt\d+)/); 769 | } 770 | return m ? m[1] : ""; 771 | }; 772 | 773 | const pthome_seeding = (element) => { 774 | var d = $(element).find("td:nth-child(9)"); 775 | return (d.length > 0 && d.css("color") === 'rgb(0, 128, 0)') 776 | }; 777 | const pthome_downed = (element) => { 778 | var d = $(element).find("td:nth-child(9)"); 779 | return d.text().includes("100%"); 780 | }; 781 | 782 | // m-team 783 | const mteam_imdbval = (element) => { 784 | var t = $(element).find( "a[href*='imdb']" ); 785 | return t.text(); 786 | }; 787 | const mteam_imdbid = (element) => { 788 | var t = $(element) 789 | .find("a[href*='imdb']" ) 790 | .attr("href"); 791 | 792 | if (t) { 793 | var m = t.match(/title\/(tt\d+)/); 794 | } 795 | return m ? m[1] : ""; 796 | }; 797 | 798 | const mteam_douban = (element) => { 799 | var d = $(element).find("a[href*='douban']"); 800 | return d.text(); 801 | }; 802 | 803 | 804 | var config = [ 805 | { 806 | host: "pterclub.com", 807 | abbrev: "pter", 808 | eleTorTable: "#torrenttable", 809 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(4) > font", 810 | eleTorList: "#torrenttable > tbody > tr", 811 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) a", 812 | // eleTorItem: " table > tbody > tr > td > div > div:nth-child(1) > a", 813 | eleTorItemDesc: "table > tbody > tr > td > div > div:nth-child(2) > span", 814 | eleTorItemSize: "> td:nth-child(5)", 815 | eleTorItemSeednum: "> td:nth-child(6)", 816 | eleTorItemAdded: "> td:nth-child(4) > span", 817 | useTitleName: 1, 818 | eleIntnTag: "a.chs_tag-gf", 819 | eleCnLangTag: "a.chs_tag-gy", 820 | eleCnSubTag: "a.chs_tag-sub", 821 | // eleCHNAreaTag: "img.chn", 822 | eleDownLink: "td:nth-child(2) > table > tbody > tr > td > a:first", 823 | eleCatImg: "td:nth-child(1) > a:nth-child(1) > img", 824 | eleDetailTitle: "#top", 825 | filterGY: true, 826 | filterZZ: true, 827 | funcIMDb: pter_imdbval, 828 | funcIMDbId: pter_imdbid, 829 | funcDouban: pter_douban, 830 | funcSeeding: pter_seeding, 831 | funcDownloaded: pter_downed, 832 | funcGetPasskey: skip_passkey, 833 | }, 834 | { 835 | host: "chddiy.xyz", 836 | abbrev: "chd", 837 | eleTorTable: "#outer > table > tbody > tr > td > table", 838 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(3) > font", 839 | eleTorList: "#outer > table > tbody > tr > td > table > tbody > tr", 840 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 841 | eleTorItemDesc: 842 | "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > font", 843 | eleTorItemSize: "> td:nth-child(5)", 844 | eleTorItemSeednum: "> td:nth-child(6)", 845 | eleTorItemAdded: "td:nth-child(4) > span", 846 | useTitleName: 1, 847 | eleIntnTag: "div.tag-gf", 848 | eleCnLangTag: "div.tag-gy", 849 | eleCnSubTag: "div.tag-sub", 850 | eleDownLink: 851 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > a:nth-child(1)", 852 | eleCatImg: "td:nth-child(1) > a:nth-child(1) > img", 853 | eleDetailTitle: "#top", 854 | filterGY: true, 855 | filterZZ: true, 856 | funcIMDb: chd_imdb, 857 | funcIMDbId: not_supported, 858 | funcDouban: not_supported, 859 | funcSeeding: chd_seeding, 860 | funcDownloaded: chd_downed, 861 | funcGetPasskey: chd_passkey, 862 | }, 863 | { 864 | host: "chdbits.co", 865 | abbrev: "chd", 866 | eleTorTable: "#outer > table > tbody > tr > td > table", 867 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(3) > font", 868 | eleTorList: "#outer > table > tbody > tr > td > table > tbody > tr", 869 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 870 | eleTorItemDesc: 871 | "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > font", 872 | eleTorItemSize: "> td:nth-child(5)", 873 | eleTorItemSeednum: "> td:nth-child(6)", 874 | eleTorItemAdded: "td:nth-child(4) > span", 875 | useTitleName: 1, 876 | eleIntnTag: "div.tag-gf", 877 | eleCnLangTag: "div.tag-gy", 878 | eleCnSubTag: "div.tag-sub", 879 | eleDownLink: 880 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > a:nth-child(1)", 881 | eleCatImg: "td:nth-child(1) > a:nth-child(1) > img", 882 | eleDetailTitle: "#top", 883 | filterGY: true, 884 | filterZZ: true, 885 | funcIMDb: chd_imdb, 886 | funcIMDbId: not_supported, 887 | funcDouban: not_supported, 888 | funcSeeding: chd_seeding, 889 | funcDownloaded: chd_downed, 890 | funcGetPasskey: chd_passkey, 891 | }, 892 | { 893 | host: "audiences.me", 894 | abbrev: "aud", 895 | eleTorTable: "#torrenttable", 896 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(2) > font", 897 | eleTorList: "#torrenttable > tbody > tr", 898 | eleTorItem: 899 | "td.rowfollow.torrents-box > div.torrents-name > table > tbody > tr > td:nth-child(1) > a", 900 | eleTorItemDesc: 901 | "td > div.torrents-name > table > tbody > tr > td:nth-child(1) > span", 902 | eleTorItemSize: "> td:nth-child(5)", 903 | eleTorItemSeednum: "> td:nth-child(6)", 904 | eleTorItemAdded: "> td:nth-child(4) > span", 905 | useTitleName: 1, 906 | eleIntnTag: "span.tgf", 907 | eleCnLangTag: "span.tgy", 908 | eleCnSubTag: "span.tzz", 909 | eleDownLink: 910 | "td > div.torrents-name > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(2) > a:nth-child(1)", 911 | eleCatImg: "td:nth-child(1) > a > img", 912 | eleDetailTitle: "#top", 913 | filterGY: true, 914 | filterZZ: true, 915 | funcIMDb: ade_imdbval, 916 | funcIMDbId: ade_imdbid, 917 | funcDouban: ade_douban, 918 | funcSeeding: ade_seeding, 919 | funcDownloaded: ade_downed, 920 | funcGetPasskey: ade_passkey, 921 | // eleTorDetailTable: "tr:contains('副标题'):last", 922 | }, 923 | 924 | { 925 | host: "sunnypt.top", 926 | abbrev: "sunny", 927 | eleTorTable: "table.torrents", 928 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(4) > font:nth-child(4) > b", 929 | eleTorList: "table.torrents > tbody > tr", 930 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(2) > a", 931 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(2)", 932 | eleTorItemSize: "> td:nth-child(5)", 933 | eleTorItemSeednum: "> td:nth-child(6)", 934 | eleTorItemAdded: "td:nth-child(4) > span", 935 | useTitleName: 1, 936 | eleIntnTag: 'span:contains("官组")', 937 | eleCnLangTag: 'span:contains("国语")', 938 | eleCnSubTag: 'span:contains("中字")', 939 | eleDownLink: 940 | "table.torrentname > tbody > tr > td:nth-child(4) > a:nth-child(1)", 941 | eleCatImg: "td:nth-child(1) > a > img", 942 | eleDetailTitle: "#top", 943 | filterGY: true, 944 | filterZZ: true, 945 | funcIMDb: rl_imdbval, 946 | funcIMDbId: not_supported, 947 | funcDouban: rl_douban, 948 | funcSeeding: rl_seeding, 949 | funcDownloaded: rl_downed, 950 | funcGetPasskey: rl_passkey, 951 | }, 952 | { 953 | host: "ourbits.club", 954 | abbrev: "ob", 955 | eleTorTable: "#torrenttable", 956 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(7) > font", 957 | eleTorList: "#torrenttable > tbody > tr", 958 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 959 | eleTorItemDesc: "td:nth-child(2) > table > tbody > tr > td:nth-child(1)", 960 | eleTorItemSize: "> td:nth-child(5)", 961 | eleTorItemSeednum: "> td:nth-child(6)", 962 | eleTorItemAdded: "td:nth-child(4) > span", 963 | useTitleName: 1, 964 | eleIntnTag: "span.tag-gf", 965 | eleCnLangTag: "span.tag-gy", 966 | eleCnSubTag: "span.tag-zz", 967 | eleDownLink: 968 | "td:nth-child(2) > table > tbody > tr > td:nth-child(5) > a:nth-child(1)", 969 | eleCatImg: "td:nth-child(1) > a:nth-child(1) > img", 970 | eleDetailTitle: "#top", 971 | filterGY: true, 972 | filterZZ: true, 973 | funcIMDb: ob_imdbval, 974 | funcIMDbId: ob_imdbid, 975 | funcDouban: ob_douban, 976 | funcSeeding: ob_seeding, 977 | funcDownloaded: ob_downed, 978 | funcGetPasskey: ob_passkey, 979 | }, 980 | { 981 | host: "leaves.red", 982 | abbrev: "rl", 983 | eleTorTable: "table.torrents", 984 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(4) > font:nth-child(4) > b", 985 | eleTorList: "table.torrents > tbody > tr", 986 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(2) > a", 987 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(2)", 988 | eleTorItemSize: "> td:nth-child(5)", 989 | eleTorItemSeednum: "> td:nth-child(6)", 990 | eleTorItemAdded: "td:nth-child(4) > span", 991 | useTitleName: 1, 992 | eleIntnTag: 'span:contains("官组")', 993 | eleCnLangTag: 'span:contains("国语")', 994 | eleCnSubTag: 'span:contains("中字")', 995 | eleDownLink: 996 | "table.torrentname > tbody > tr > td:nth-child(4) > a:nth-child(1)", 997 | eleCatImg: "td:nth-child(1) > a > img", 998 | eleDetailTitle: "#top", 999 | filterGY: true, 1000 | filterZZ: true, 1001 | funcIMDb: rl_imdbval, 1002 | funcIMDbId: not_supported, 1003 | funcDouban: rl_douban, 1004 | funcSeeding: rl_seeding, 1005 | funcDownloaded: rl_downed, 1006 | funcGetPasskey: rl_passkey, 1007 | }, 1008 | { 1009 | host: "springsunday.net", 1010 | abbrev: "ssd", 1011 | eleTorTable: "table.torrents", 1012 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(3) > font", 1013 | eleTorList: "table.torrents > tbody > tr", 1014 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) a", 1015 | eleTorItemDesc: "div.torrent-smalldescr", 1016 | eleTorItemSize: "> td:nth-child(5)", 1017 | eleTorItemSeednum: "> td:nth-child(6)", 1018 | eleTorItemAdded: "td:nth-child(4) > span", 1019 | useTitleName: 1, 1020 | eleIntnTag: 'span:contains("CMCT")', 1021 | eleCnLangTag: 'span:contains("国配")', 1022 | eleCnSubTag: 'span:contains("中字")', 1023 | eleDownLink: 1024 | "table.torrentname > tbody > tr > td:nth-child(2) a", 1025 | eleCatImg: "td:nth-child(1) > a > img", 1026 | eleDetailTitle: "#top", 1027 | filterGY: true, 1028 | filterZZ: true, 1029 | funcIMDb: ssd_imdbval, 1030 | funcIMDbId: ssd_imdbid, 1031 | funcDouban: ssd_douban, 1032 | funcSeeding: ssd_seeding, 1033 | funcDownloaded: ssd_downed, 1034 | funcGetPasskey: ssd_passkey, 1035 | }, 1036 | { 1037 | host: "totheglory.im", 1038 | abbrev: "ttg", 1039 | eleTorTable: "#torrent_table", 1040 | eleCurPage: "#main_table > tbody > tr:nth-child(1) > td > p:nth-child(9) > a:nth-child(5) > b", 1041 | eleTorList: "#torrent_table > tbody > tr", 1042 | eleTorItem: "div.name_left > a", 1043 | eleTorItemDesc: "td:nth-child(2) > div.name_left > a > b > span", 1044 | eleTorItemSize: "td:nth-child(7)", 1045 | eleTorItemSeednum: "td:nth-child(9) > b:nth-child(1)", 1046 | eleTorItemAdded: "td:nth-child(5) > nobr", 1047 | useTitleName: 3, 1048 | eleIntnTag: "", 1049 | eleCnLangTag: "", 1050 | eleCnSubTag: "", 1051 | eleDownLink: 1052 | "td:nth-child(2) > div.name_right > span:nth-child(1) > a.dl_a", 1053 | eleCatImg: "td:nth-child(1) > a > img", 1054 | eleDetailTitle: "#main_table td > h1", 1055 | filterGY: false, 1056 | filterZZ: false, 1057 | funcIMDb: ttg_imdbval, 1058 | funcIMDbId: ttg_imdbid, 1059 | funcDouban: not_supported, 1060 | funcSeeding: ttg_seeding, 1061 | funcDownloaded: not_supported, 1062 | funcGetPasskey: ttg_passkey, 1063 | }, 1064 | { 1065 | host: "pt.keepfrds.com", 1066 | abbrev: "frds", 1067 | eleTorTable: "#form_torrent > table", 1068 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(2) > font", 1069 | eleTorList: "#form_torrent > table > tbody > tr", 1070 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 1071 | eleTorItemDesc: "td:nth-child(2) > table > tbody > tr > td:nth-child(1)", 1072 | eleTorItemSize: "> td:nth-child(5)", 1073 | eleTorItemSeednum: "td:nth-child(6) > b > a", 1074 | eleTorItemAdded: "td:nth-child(4) > span", 1075 | useTitleName: 2, 1076 | eleIntnTag: "div.tag-gf", 1077 | eleCnLangTag: "div.tag-gy", 1078 | eleCnSubTag: "div.tag-zz", 1079 | eleDownLink: 1080 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > div:nth-child(1) > a", 1081 | eleCatImg: "td:nth-child(1) > a > img", 1082 | eleDetailTitle: "#outer > table:nth-child(2) > tbody > tr:nth-child(2) > td.rowfollow", 1083 | filterGY: false, 1084 | filterZZ: false, 1085 | funcIMDb: frds_imdbval, 1086 | funcIMDbId: not_supported, 1087 | funcDouban: not_supported, 1088 | funcSeeding: frds_seeding, 1089 | funcDownloaded: frds_downed, 1090 | funcGetPasskey: frds_passkey, 1091 | }, 1092 | { 1093 | host: "www.beitai.pt", 1094 | abbrev: "beitai", 1095 | eleTorTable: "table.torrents", 1096 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(3) > font", 1097 | eleTorList: "table.torrents > tbody > tr", 1098 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 1099 | eleTorItemDesc: "td:nth-child(2) > table > tbody > tr > td:nth-child(1)", 1100 | eleTorItemSize: "> td:nth-child(5)", 1101 | eleTorItemSeednum: "> td:nth-child(6)", 1102 | eleTorItemAdded: "td:nth-child(4) > span", 1103 | useTitleName: 1, 1104 | eleIntnTag: "div.tag-gf", 1105 | eleCnLangTag: "div.tag-gy", 1106 | eleCnSubTag: "div.tag-zz", 1107 | eleDownLink: 1108 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > a:nth-child(1)", 1109 | eleCatImg: "td:nth-child(1) > a > img", 1110 | eleDetailTitle: "#top", 1111 | filterGY: false, 1112 | filterZZ: false, 1113 | funcIMDb: not_supported, 1114 | funcIMDbId: not_supported, 1115 | funcDouban: not_supported, 1116 | funcSeeding: beitai_seeding, 1117 | funcDownloaded: not_supported, 1118 | funcGetPasskey: beitai_passkey, 1119 | }, 1120 | { 1121 | host: "hdchina.org", 1122 | abbrev: "hdc", 1123 | eleTorTable: "#form_torrent > table", 1124 | eleCurPage: "#site_content > div > div.pagenav_part > div > ul > li.active", 1125 | eleTorList: "#form_torrent > table > tbody > tr", 1126 | eleTorItem: "td.t_name > table > tbody > tr > td:nth-child(2) > h3 > a", 1127 | eleTorItemDesc: "td.t_name > table > tbody > tr > td:nth-child(2) > h4", 1128 | eleTorItemSize: "td.t_size", 1129 | eleTorItemSeednum: "td.t_torrents > a", 1130 | eleTorItemAdded: "td.t_time > span", 1131 | useTitleName: 1, 1132 | eleIntnTag: "div.tag-gf", 1133 | eleCnLangTag: "div.tag-gy", 1134 | eleCnSubTag: "div.tag-zz", 1135 | eleDownLink: "td.t_name > table > tbody > tr > td:nth-child(2) > h3 > a", 1136 | eleCatImg: "td.t_cat > a > img", 1137 | eleDetailTitle: "#top", 1138 | filterGY: false, 1139 | filterZZ: false, 1140 | funcIMDb: hdc_imdbval, 1141 | funcIMDbId: not_supported, 1142 | funcDouban: not_supported, 1143 | funcSeeding: hdc_seeding, 1144 | funcDownloaded: hdc_downed, 1145 | funcGetPasskey: not_supported, 1146 | }, 1147 | { 1148 | host: "hdsky.me", 1149 | abbrev: "hds", 1150 | eleTorTable: "#outer > table > tbody > tr > td > table", 1151 | eleCurPage: "#outer > table > tbody > tr > td > form:nth-child(8) > p > font", 1152 | eleTorList: "#outer > table > tbody > tr > td > table > tbody > tr", 1153 | eleTorItem: "td > table > tbody > tr > td:nth-child(1) > a", 1154 | eleTorItemDesc: "td > table > tbody > tr > td:nth-child(1)", 1155 | eleTorItemSize: "> td:nth-child(5)", 1156 | eleTorItemSeednum: "td:nth-child(6) > b > a", 1157 | eleTorItemAdded: "td:nth-child(4) > span", 1158 | useTitleName: 1, 1159 | eleIntnTag: 'span:contains("官组")', 1160 | eleCnLangTag: 'span:contains("国语")', 1161 | eleCnSubTag: 'span:contains("中字")', 1162 | eleDownLink: 1163 | "td > table > tbody > tr > td:nth-child(1) > a", 1164 | eleCatImg: "td.t_cat > a > img", 1165 | eleDetailTitle: "#top", 1166 | filterGY: true, 1167 | filterZZ: true, 1168 | funcIMDb: sky_imdbval, 1169 | funcIMDbId: sky_imdbid, 1170 | funcDouban: sky_douban, 1171 | funcSeeding: sky_seeding, 1172 | funcDownloaded: sky_downed, 1173 | funcGetPasskey: sky_passkey, 1174 | }, 1175 | { 1176 | host: "hhanclub.top", 1177 | abbrev: "hh", 1178 | eleTorTable: "div.torrent-table-for-spider", 1179 | // eleTorTable: "#mainContent > div > div > div.flex.w-\[95\%\].bg-\[\#4F5879\]\/\[0\.7\].opacity-\[0\.7\].h-\[30px\].m-auto.z-20.\!rounded-\[3px\]", 1180 | eleCurPage: "#mainContent > div > > div:nth-child(1) > div > a ", 1181 | eleTorList: "div.torrent-table-for-spider > div", 1182 | eleTorItem: "div.torrent-title> div > a", 1183 | eleTorItemDesc: "div.torrent-title> div > div", 1184 | eleTorItemSize: "div.torrent-info > div.torrent-info-text-size", 1185 | eleTorItemSeednum: "div.torrent-info > div.torrent-info-text-seeders > a", 1186 | eleTorItemAdded: "div.torrent-info > torrent-info-text-added > span", 1187 | useTitleName: 0, 1188 | eleIntnTag: "span:contains('官方')", 1189 | eleCnLangTag: "span:contains('国语')", 1190 | eleCnSubTag: "span:contains('中字')", 1191 | eleDownLink: "div.torrent-manage > div > a", 1192 | eleCatImg: "div.torrent-cat > a > img", 1193 | eleDetailTitle: "#mainContent > div > div > div > div:nth-child(4)", 1194 | filterGY: true, 1195 | filterZZ: true, 1196 | funcIMDb: hh_imdbval, 1197 | funcIMDbId: not_supported, 1198 | funcDouban: hh_douban, 1199 | funcSeeding: hh_seeding, 1200 | funcDownloaded: hh_downed, 1201 | funcGetPasskey: not_supported, 1202 | }, 1203 | // TODO: gazella, animate cat fails 1204 | { 1205 | host: "lemonhd.club", 1206 | abbrev: "lhd", 1207 | eleTorTable: "table.torrents", 1208 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1209 | eleTorList: "table.torrents > tbody > tr", 1210 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) a", 1211 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1212 | eleTorItemSize: "> td:nth-child(5)", 1213 | eleTorItemSeednum: "> td:nth-child(6)", 1214 | eleTorItemAdded: "td:nth-child(4) > span", 1215 | useTitleName: 0, 1216 | eleIntnTag: "span.tag_gf", 1217 | eleCnLangTag: "span.tag_gy", 1218 | eleCnSubTag: "span.tag_zz", 1219 | eleDownLink: 1220 | "table > tbody > tr > td:nth-child(2) > div > div:nth-child(1) > a", 1221 | eleCatImg: "td:nth-child(1) > a > img", 1222 | eleDetailTitle: "#top", 1223 | filterGY: true, 1224 | filterZZ: true, 1225 | funcIMDb: lhd_imdbval, 1226 | funcIMDbId: lhd_imdbid, 1227 | funcDouban: lhd_douban, 1228 | funcSeeding: lhd_seeding, 1229 | funcDownloaded: lhd_downed, 1230 | funcGetPasskey: lhd_passkey, 1231 | }, 1232 | { 1233 | host: "hdhome.org", 1234 | abbrev: "hdh", 1235 | eleTorTable: "#torrenttable", 1236 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(2) > font", 1237 | eleTorList: "#torrenttable > tbody > tr", 1238 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 1239 | eleTorItemDesc: "td:nth-child(2) > table > tbody > tr > td:nth-child(1)", 1240 | eleTorItemSize: "> td:nth-child(5)", 1241 | eleTorItemSeednum: "> td:nth-child(6)", 1242 | eleTorItemAdded: "td:nth-child(4) > span", 1243 | useTitleName: 1, 1244 | eleIntnTag: "span.tgf", 1245 | eleCnLangTag: "span.tgy", 1246 | eleCnSubTag: 'span:contains("中字"), span:contains("官字")', 1247 | eleDownLink: 1248 | "td:nth-child(2) > table > tbody > tr > td:nth-child(2) > table > tbody > tr > td:nth-child(2) > a:nth-child(1)", 1249 | eleCatImg: "td:nth-child(1) > a > img", 1250 | eleDetailTitle: "#top", 1251 | filterGY: true, 1252 | filterZZ: true, 1253 | funcIMDb: hdh_imdbval, 1254 | funcIMDbId: hdh_imdbid, 1255 | funcDouban: hdh_douban, 1256 | funcSeeding: hdh_seeding, 1257 | funcDownloaded: hdh_downed, 1258 | funcGetPasskey: hdh_passkey, 1259 | }, 1260 | { 1261 | host: "wintersakura.net", 1262 | abbrev: "wintersakura", 1263 | eleTorTable: "table.torrents", 1264 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1265 | eleTorList: "table.torrents > tbody > tr", 1266 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(2) > a", 1267 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(2)", 1268 | eleTorItemSize: "> td:nth-child(5)", 1269 | eleTorItemSeednum: "> td:nth-child(6)", 1270 | eleTorItemAdded: "td:nth-child(4) > span", 1271 | useTitleName: 1, 1272 | eleIntnTag: 'span:contains("官方")', 1273 | eleCnLangTag: 'span:contains("国语")', 1274 | eleCnSubTag: 'span:contains("中字")', 1275 | eleDownLink: 1276 | "table.torrentname > tbody > tr > td:nth-child(4) > a:nth-child(1)", 1277 | eleCatImg: "td:nth-child(1) > a > img", 1278 | eleDetailTitle: "#top", 1279 | filterGY: true, 1280 | filterZZ: true, 1281 | funcIMDb: rl_imdbval, 1282 | funcIMDbId: not_supported, 1283 | funcDouban: rl_douban, 1284 | funcSeeding: rl_seeding, 1285 | funcDownloaded: rl_downed, 1286 | funcGetPasskey: rl_passkey, 1287 | }, 1288 | { 1289 | host: "pt.soulvoice.club", 1290 | abbrev: "soulvoice", 1291 | eleTorTable: "table.torrents", 1292 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1293 | eleTorList: "table.torrents > tbody > tr", 1294 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(2) > a", 1295 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(2)", 1296 | eleTorItemSize: "> td:nth-child(5)", 1297 | eleTorItemSeednum: "> td:nth-child(6)", 1298 | eleTorItemAdded: "td:nth-child(4) > span", 1299 | useTitleName: 1, 1300 | eleIntnTag: 'span:contains("官方")', 1301 | eleCnLangTag: 'span:contains("国语")', 1302 | eleCnSubTag: 'span:contains("中字")', 1303 | eleDownLink: 1304 | "table.torrentname > tbody > tr > td:nth-child(4) > a:nth-child(1)", 1305 | eleCatImg: "td:nth-child(1) > a > img", 1306 | eleDetailTitle: "#top", 1307 | filterGY: true, 1308 | filterZZ: true, 1309 | funcIMDb: rl_imdbval, 1310 | funcIMDbId: not_supported, 1311 | funcDouban: rl_douban, 1312 | funcSeeding: rl_seeding, 1313 | funcDownloaded: rl_downed, 1314 | funcGetPasskey: rl_passkey, 1315 | }, 1316 | { 1317 | host: "ptsbao.club", 1318 | abbrev: "ptsbao", 1319 | eleTorTable: "table.torrents", 1320 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1321 | eleTorList: "table.torrents > tbody > tr", 1322 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) > a", 1323 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1324 | eleTorItemSize: "> td:nth-child(6)", 1325 | eleTorItemSeednum: "td:nth-child(7)", 1326 | eleTorItemAdded: "td:nth-child(5) > span", 1327 | useTitleName: 1, 1328 | eleIntnTag: 'span:contains("官方")', 1329 | eleCnLangTag: 'span:contains("国语")', 1330 | eleCnSubTag: 'span:contains("中字")', 1331 | eleDownLink: 1332 | '#torrents_td > table > tbody > tr > td a[href*="passkey"]', 1333 | eleCatImg: "td:nth-child(1) > a > img", 1334 | eleDetailTitle: "#top", 1335 | filterGY: true, 1336 | filterZZ: true, 1337 | funcIMDb: ptsbao_imdbval, 1338 | funcIMDbId: not_supported, 1339 | funcDouban: not_supported, 1340 | funcSeeding: ptsbao_seeding, 1341 | funcDownloaded: ptsbao_downed, 1342 | funcGetPasskey: rl_passkey, 1343 | }, 1344 | { 1345 | host: "pt.eastgame.org", 1346 | abbrev: "tlf", 1347 | eleTorTable: "table.torrents", 1348 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(3) > font", 1349 | eleTorList: "table.torrents > tbody > tr", 1350 | eleTorItem: "td:nth-child(2) > table > tbody > tr > td:nth-child(1) > a", 1351 | eleTorItemDesc: "td:nth-child(2) > table > tbody > tr > td:nth-child(1)", 1352 | eleTorItemSize: "> td:nth-child(5)", 1353 | eleTorItemSeednum: "> td:nth-child(6)", 1354 | eleTorItemAdded: "td:nth-child(4) > span", 1355 | useTitleName: 1, 1356 | eleIntnTag: "div.tag-gf", 1357 | eleCnLangTag: "div.tag-gy", 1358 | eleCnSubTag: "div.tag-zz", 1359 | eleDownLink: 1360 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1361 | eleCatImg: "td:nth-child(1) > a > img", 1362 | eleDetailTitle: "#top", 1363 | filterGY: false, 1364 | filterZZ: false, 1365 | funcIMDb: rl_imdbval, 1366 | funcIMDbId: not_supported, 1367 | funcDouban: rl_douban, 1368 | funcSeeding: rl_seeding, 1369 | funcDownloaded: rl_downed, 1370 | funcGetPasskey: rl_passkey, 1371 | }, 1372 | { 1373 | host: "www.hddolby.com", 1374 | abbrev: "hddolby", 1375 | eleTorTable: "table.torrents", 1376 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1377 | eleTorList: "table.torrents > tbody > tr", 1378 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) > a", 1379 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1380 | eleTorItemSize: "> td:nth-child(5)", 1381 | eleTorItemSeednum: "> td:nth-child(6)", 1382 | eleTorItemAdded: "td:nth-child(4) > span", 1383 | useTitleName: 1, 1384 | eleIntnTag: "span.tgf", 1385 | eleCnLangTag: "span.tgy", 1386 | eleCnSubTag: "span.tzz", 1387 | eleDownLink: 1388 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1389 | eleCatImg: "td:nth-child(1) > a > img", 1390 | eleDetailTitle: "#top", 1391 | filterGY: true, 1392 | filterZZ: true, 1393 | funcIMDb: hddolby_imdbval, 1394 | funcIMDbId: hddolby_imdbid, 1395 | funcDouban: hddolby_douban, 1396 | funcSeeding: hddolby_seeding, 1397 | funcDownloaded: hddolby_downed, 1398 | funcGetPasskey: hddolby_passkey, 1399 | }, 1400 | { 1401 | host: "pt.hd4fans.org", 1402 | abbrev: "hd4fans", 1403 | eleTorTable: "table.torrents", 1404 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1405 | eleTorList: "table.torrents > tbody > tr", 1406 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) > a", 1407 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1408 | eleTorItemSize: "> td:nth-child(5)", 1409 | eleTorItemSeednum: "> td:nth-child(6)", 1410 | eleTorItemAdded: "td:nth-child(4) > span", 1411 | useTitleName: 1, 1412 | eleIntnTag: 'span:contains("官组")', 1413 | eleCnLangTag: 'span:contains("国语")', 1414 | eleCnSubTag: 'span:contains("中字")', 1415 | eleDownLink: 1416 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1417 | eleCatImg: "td:nth-child(1) > a > img", 1418 | eleDetailTitle: "#top", 1419 | filterGY: false, 1420 | filterZZ: false, 1421 | funcIMDb: not_supported, 1422 | funcIMDbId: not_supported, 1423 | funcDouban: not_supported, 1424 | funcSeeding: hd4fans_seeding, 1425 | funcDownloaded: hd4fans_downed, 1426 | funcGetPasskey: hd4fans_passkey, 1427 | }, 1428 | { 1429 | host: "hdfans.org", 1430 | abbrev: "hdfans", 1431 | eleTorTable: "table.torrents", 1432 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1433 | eleTorList: "table.torrents > tbody > tr", 1434 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) > a", 1435 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1436 | eleTorItemSize: "> td:nth-child(5)", 1437 | eleTorItemSeednum: "> td:nth-child(6)", 1438 | eleTorItemAdded: "td:nth-child(4) > span", 1439 | useTitleName: 1, 1440 | eleIntnTag: 'span:contains("官组")', 1441 | eleCnLangTag: 'span:contains("国语")', 1442 | eleCnSubTag: 'span:contains("中字")', 1443 | eleDownLink: 1444 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1445 | eleCatImg: "td:nth-child(1) > a > img", 1446 | eleDetailTitle: "#top", 1447 | filterGY: true, 1448 | filterZZ: true, 1449 | funcIMDb: hdfans_imdbval, 1450 | funcDouban: hdfans_douban, 1451 | funcIMDbId: not_supported, 1452 | funcSeeding: hdfans_seeding, 1453 | funcDownloaded: hdfans_downed, 1454 | funcGetPasskey: hdfans_passkey, 1455 | }, 1456 | { 1457 | host: "pthome.net", 1458 | abbrev: "pthome", 1459 | eleTorTable: "#torrenttable", 1460 | eleCurPage: "#outer > table > tbody > tr > td > p:nth-child(2) > font", 1461 | eleTorList: "table.torrents > tbody > tr", 1462 | eleTorItem: "table.torrentname > tbody > tr > td:nth-child(1) > a", 1463 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1464 | eleTorItemSize: "> td:nth-child(5)", 1465 | eleTorItemSeednum: "> td:nth-child(6)", 1466 | eleTorItemAdded: "td:nth-child(4) > span", 1467 | useTitleName: 1, 1468 | eleIntnTag: "span.tgf", 1469 | eleCnLangTag: "span.tgy", 1470 | eleCnSubTag: 'span:contains("中字"), span:contains("官字")', 1471 | eleDownLink: 1472 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1473 | eleCatImg: "td:nth-child(1) > a > img", 1474 | eleDetailTitle: "#top", 1475 | filterGY: true, 1476 | filterZZ: true, 1477 | funcIMDb: pthome_imdbval, 1478 | funcDouban: pthome_douban, 1479 | funcIMDbId: hddolby_imdbid, 1480 | funcSeeding: pthome_seeding, 1481 | funcDownloaded: pthome_downed, 1482 | funcGetPasskey: hddolby_passkey, 1483 | }, 1484 | { 1485 | host: "discfan.net", 1486 | abbrev: "discfan", 1487 | eleTorTable: "table.torrents", 1488 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1489 | eleTorList: "table.torrents > tbody > tr", 1490 | eleTorItem: "table.torrentname > tbody > tr a[href*='details.php']", 1491 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1492 | eleTorItemSize: "> td:nth-child(5)", 1493 | eleTorItemSeednum: "> td:nth-child(6)", 1494 | eleTorItemAdded: "td:nth-child(4) > span", 1495 | useTitleName: 1, 1496 | eleIntnTag: 'span:contains("官组")', 1497 | eleCnLangTag: 'span:contains("国语")', 1498 | eleCnSubTag: 'span:contains("中字")', 1499 | eleDownLink: 1500 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1501 | eleCatImg: "td:nth-child(1) > a > img", 1502 | eleDetailTitle: "#top", 1503 | filterGY: true, 1504 | filterZZ: true, 1505 | funcIMDb: mteam_imdbval, 1506 | funcDouban: mteam_douban, 1507 | funcIMDbId: mteam_imdbid, 1508 | funcSeeding: hdfans_seeding, 1509 | funcDownloaded: hdfans_downed, 1510 | funcGetPasskey: hdfans_passkey, 1511 | }, 1512 | { 1513 | host: "www.tjupt.org", 1514 | abbrev: "tjupt", 1515 | eleTorTable: "table.torrents", 1516 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1517 | eleTorList: "table.torrents > tbody > tr", 1518 | eleTorItem: "table.torrentname > tbody > tr a[href*='details.php']", 1519 | eleTorItemDesc: "table.torrentname > tbody > tr a[href*='details.php']", 1520 | eleTorItemSize: "> td:nth-child(5)", 1521 | eleTorItemSeednum: "> td:nth-child(6)", 1522 | eleTorItemAdded: "td:nth-child(4) > span", 1523 | useTitleName: 1, 1524 | eleIntnTag: 'span:contains("官方")', 1525 | eleCnLangTag: 'span:contains("国语")', 1526 | eleCnSubTag: 'span:contains("中字")', 1527 | eleDownLink: 1528 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1529 | eleCatImg: "td:nth-child(1) > a > img", 1530 | eleDetailTitle: "#top", 1531 | filterGY: true, 1532 | filterZZ: true, 1533 | funcIMDb: hdfans_imdbval, 1534 | funcDouban: hdfans_douban, 1535 | funcIMDbId: not_supported, 1536 | funcSeeding: hdfans_seeding, 1537 | funcDownloaded: hdfans_downed, 1538 | funcGetPasskey: hdfans_passkey, 1539 | }, 1540 | { 1541 | host: "piggo.me", 1542 | abbrev: "piggo", 1543 | eleTorTable: "table.torrents", 1544 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1545 | eleTorList: "table.torrents > tbody > tr", 1546 | eleTorItem: "table.torrentname > tbody > tr a[href*='details.php']", 1547 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1548 | eleTorItemSize: "> td:nth-child(5)", 1549 | eleTorItemSeednum: "> td:nth-child(6)", 1550 | eleTorItemAdded: "td:nth-child(4) > span", 1551 | useTitleName: 1, 1552 | eleIntnTag: 'span:contains("官组")', 1553 | eleCnLangTag: 'span:contains("国语")', 1554 | eleCnSubTag: 'span:contains("中字")', 1555 | eleDownLink: 1556 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1557 | eleCatImg: "td:nth-child(1) > a > img", 1558 | eleDetailTitle: "#top", 1559 | filterGY: true, 1560 | filterZZ: true, 1561 | funcIMDb: hdfans_imdbval, 1562 | funcDouban: hdfans_douban, 1563 | funcIMDbId: not_supported, 1564 | funcSeeding: hdfans_seeding, 1565 | funcDownloaded: hdfans_downed, 1566 | funcGetPasskey: hdfans_passkey, 1567 | }, 1568 | { 1569 | host: "kp.m-team.cc", 1570 | abbrev: "m-team", 1571 | eleTorTable: "table.torrents", 1572 | eleCurPage: "#outer > div > table > tbody > tr > td > p:nth-child(3) > font:nth-child(4)", 1573 | eleTorList: "table.torrents > tbody > tr", 1574 | eleTorItem: "table.torrentname > tbody > tr a[href*='details.php']", 1575 | eleTorItemDesc: "table.torrentname > tbody > tr > td:nth-child(1)", 1576 | eleTorItemSize: "> td:nth-child(5)", 1577 | eleTorItemSeednum: "> td:nth-child(6)", 1578 | eleTorItemAdded: "td:nth-child(4) > span", 1579 | useTitleName: 1, 1580 | eleIntnTag: '', 1581 | eleCnLangTag: 'img.label_dub', 1582 | eleCnSubTag: 'img.label_sub', 1583 | eleDownLink: 1584 | 'table.torrentname > tbody > tr a[href*="download.php"]', 1585 | eleCatImg: "td:nth-child(1) > a > img", 1586 | eleDetailTitle: "#top", 1587 | filterGY: true, 1588 | filterZZ: true, 1589 | funcIMDb: mteam_imdbval, 1590 | funcDouban: mteam_douban, 1591 | funcIMDbId: mteam_imdbid, 1592 | funcSeeding: hdfans_seeding, 1593 | funcDownloaded: hdfans_downed, 1594 | funcGetPasskey: hdfans_passkey, 1595 | }, 1596 | ]; 1597 | 1598 | var THISCONFIG = config.find((cc) => window.location.host.includes(cc.host)); 1599 | 1600 | function addFilterPanel() { 1601 | var torTable = $(THISCONFIG.eleTorTable); 1602 | if (torTable.length <= 0) { 1603 | return; 1604 | } 1605 | 1606 | var donwnloadPanel = ` 1607 | 1608 | 1622 | 1636 | 1637 | 1652 | 1653 | 1657 | 1658 | 1662 | 1667 | 1672 | 1677 | 1682 | 1683 | 1684 |
1609 | 1610 | 1611 | 1614 | 1615 | 1616 | 1619 | 1620 |
1612 | 1613 |
1617 | 1618 |
1621 |
1623 | 1624 | 1625 | 1628 | 1629 | 1630 | 1633 | 1634 |
1626 | 1627 |
1631 | 1632 |
1635 |
1638 | 1639 | 1640 | 1644 | 1645 | 1649 | 1650 |
1641 |
标题不含 1642 |
1643 |
1646 |
描述不含 1647 |
1648 |
1651 |
1654 |
大小介于 1655 |
1656 |
1659 |
IMDb/豆瓣 > 1660 |
1661 |
1663 | 1666 | 1668 | 1671 | 1673 | 1676 | 1678 | 1681 |
1685 | 1686 | 1687 | 1688 | `; 1689 | torTable.before(donwnloadPanel); 1690 | 1691 | if (!THISCONFIG.filterGY) { 1692 | $("#chnsub").parent().hide(); 1693 | } 1694 | if (!THISCONFIG.filterZZ) { 1695 | $("#nochnlang").parent().hide(); 1696 | } 1697 | } 1698 | 1699 | function addDetailPagePanel() { 1700 | $(` 1701 |
1702 | 1703 | 1708 | 1709 | 1710 | 1715 | 1716 | 1717 | 1722 | 1723 | 1724 | 1725 | 1726 |
1704 | 1707 |
1711 | 1714 |
1718 | 1721 |
1727 |
1728 | ` 1729 | ).appendTo("body"); 1730 | } 1731 | 1732 | function sizeStrToGB(sizeStr) { 1733 | var regex = /[+-]?\d+(\.\d+)?/g; 1734 | var sizeStr2 = sizeStr.replace(/,/g, ""); 1735 | var num = sizeStr2.match(regex).map(function (v) { 1736 | return parseFloat(v); 1737 | }); 1738 | var size = 0; 1739 | if (sizeStr.match(/(KB|KiB)/i)) { 1740 | size = num / 1024.0 / 1024.0; 1741 | } else if (sizeStr.match(/(MB|MiB)/i)) { 1742 | size = num / 1024.0; 1743 | } else if (sizeStr.match(/(GB|GiB)/i)) { 1744 | size = num; 1745 | } else if (sizeStr.match(/(TB|TiB)/i)) { 1746 | size = num * 1024.0; 1747 | } else { 1748 | size = num / 1024.0 / 1024.0 / 1024.0; 1749 | } 1750 | 1751 | return size; 1752 | } 1753 | 1754 | function sizeStrToBytes(sizeStr) { 1755 | var regex = /[+-]?\d+(\.\d+)?/g; 1756 | var sizeStr2 = sizeStr.replace(/,/g, ""); 1757 | var num = sizeStr2.match(regex).map(function (v) { 1758 | return parseFloat(v); 1759 | }); 1760 | var size = 0; 1761 | if (sizeStr.match(/(KB|KiB)/i)) { 1762 | size = num * 2**10; 1763 | } else if (sizeStr.match(/(MB|MiB)/i)) { 1764 | size = num * 2**20; 1765 | } else if (sizeStr.match(/(GB|GiB)/i)) { 1766 | size = num * 2**30; 1767 | } else if (sizeStr.match(/(TB|TiB)/i)) { 1768 | size = num * 2**40; 1769 | } else { 1770 | size = num; 1771 | } 1772 | 1773 | return Math.trunc(size); 1774 | } 1775 | 1776 | function getTorSizeRange(rangestr) { 1777 | let m = rangestr.match(/(\d+)([,,-]\s*(\d+))?/); 1778 | if (m) { 1779 | return [parseInt(m[1]) || 0, parseInt(m[3]) || 0]; 1780 | } 1781 | return [0, 0]; 1782 | } 1783 | 1784 | function saveToCookie(filterParam) { 1785 | var cookie_name = "filterParam"; 1786 | var cookie_value = filterParam; 1787 | var d = new Date(); 1788 | // change expire time here, 60 * 1000 for 1 minute 1789 | // d.setTime(d.getTime() + ( 60 * 1000)); 1790 | // this is 3 days 1791 | d.setTime(d.getTime() + 300 * 24 * 60 * 60 * 1000); 1792 | var expires = "expires=" + d.toUTCString(); 1793 | document.cookie = 1794 | cookie_name + "=" + cookie_value + ";" + expires + ";path=/"; 1795 | } 1796 | 1797 | function saveParamToCookie() { 1798 | let paramStr = 1799 | "minimdb=" + 1800 | $("#minimdb").val() + 1801 | "&sizerange=" + 1802 | $("#sizerange").val() + 1803 | "&titleregex=" + 1804 | $("#titleregex").val() + 1805 | "&descregex=" + 1806 | $("#titledescregex").val() + 1807 | "&seeding=" + 1808 | $("#seeding").is(":checked") + 1809 | "&downloaded=" + 1810 | $("#downloaded").is(":checked") + 1811 | "&chnsub=" + 1812 | $("#chnsub").is(":checked") + 1813 | "&nochnlang=" + 1814 | $("#nochnlang").is(":checked"); 1815 | saveToCookie(paramStr); 1816 | } 1817 | 1818 | const getCookieValue = (name) => 1819 | document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")?.pop() || ""; 1820 | 1821 | function loadParamFromCookie() { 1822 | var cc = getCookieValue("filterParam"); 1823 | if (cc) { 1824 | fillParam(cc); 1825 | } 1826 | } 1827 | 1828 | function fillParam(filterParam) { 1829 | var paraList = filterParam.split("&"); 1830 | for (var i = 0; i < paraList.length; i++) { 1831 | var m = paraList[i].match(/(\w+)\=(.*)/); 1832 | if (m) { 1833 | if (m[1] == "minimdb") { 1834 | $("#minimdb").val(m[2]); 1835 | } 1836 | if (m[1] == "sizerange") { 1837 | $("#sizerange").val(m[2]); 1838 | } 1839 | if (m[1] == "titleregex") { 1840 | $("#titleregex").val(m[2]); 1841 | } 1842 | if (m[1] == "descregex") { 1843 | $("#titledescregex").val(m[2]); 1844 | } 1845 | if (m[1] == "seeding") { 1846 | $("#seeding").prop("checked", m[2] == "true"); 1847 | } 1848 | if (m[1] == "downloaded") { 1849 | $("#downloaded").prop("checked", m[2] == "true"); 1850 | } 1851 | if (m[1] == "chnsub") { 1852 | $("#chnsub").prop("checked", m[2] == "true"); 1853 | } 1854 | if (m[1] == "nochnlang") { 1855 | $("#nochnlang").prop("checked", m[2] == "true"); 1856 | } 1857 | } 1858 | } 1859 | } 1860 | 1861 | function getItemTitle(item) { 1862 | let titlestr = ""; 1863 | 1864 | if (THISCONFIG.useTitleName == 1) { 1865 | titlestr = item.attr("title"); 1866 | } else if (THISCONFIG.useTitleName == 0) { 1867 | titlestr = item.text(); 1868 | } else if (THISCONFIG.useTitleName == 2) { 1869 | var elebr = item.parent().children("br").get(0); 1870 | if (elebr) { 1871 | var eletitle = elebr.nextSibling; 1872 | if (eletitle.data) titlestr = eletitle.data; 1873 | else if (eletitle.firstChild.data) titlestr = eletitle.firstChild.data; 1874 | } 1875 | } else if (THISCONFIG.useTitleName == 3) { 1876 | let titlehtml = item.html() 1877 | titlestr = titlehtml.substring(0, titlehtml.indexOf('
{ 1887 | $("#process-log").text("处理中..."); 1888 | let torlist = $(html).find(THISCONFIG.eleTorList); 1889 | let imdbMinVal = parseFloat($("#minimdb").val()) || 0.0; 1890 | let sizerange = getTorSizeRange($("#sizerange").val()); 1891 | saveParamToCookie(); 1892 | let filterCount = 0; 1893 | for (let index = 0; index < torlist.length; ++index) { 1894 | let element = torlist[index]; 1895 | let item = $(element).find(THISCONFIG.eleTorItem); 1896 | if (item.length <= 0) { 1897 | continue; 1898 | } 1899 | 1900 | let titlestr = getItemTitle(item); 1901 | let keepShow = true; 1902 | 1903 | if (sizerange[0] || sizerange[1]) { 1904 | let sizestr = $(element).find(THISCONFIG.eleTorItemSize).text().trim(); 1905 | let torsize = 0; 1906 | if (sizestr) { 1907 | torsize = sizeStrToGB(sizestr); 1908 | } 1909 | if (sizerange[0] && torsize < sizerange[0]) { 1910 | keepShow = false; 1911 | } 1912 | if (sizerange[1] && torsize > sizerange[1]) { 1913 | keepShow = false; 1914 | } 1915 | } 1916 | 1917 | var seednum = $(element).find(THISCONFIG.eleTorItemSeednum).text().trim(); 1918 | seednum = seednum.replace(/\,/g, ""); 1919 | if (!seednum) { 1920 | seednum = " "; 1921 | } 1922 | 1923 | var tortime; 1924 | if ($(element).find(THISCONFIG.eleTorItemAdded)[0]) { 1925 | tortime = $(element).find(THISCONFIG.eleTorItemAdded)[0].title; 1926 | } 1927 | if (!tortime) { 1928 | tortime = " "; 1929 | } 1930 | 1931 | if (imdbMinVal > 0.1) { 1932 | let imdbval = parseFloat(THISCONFIG.funcIMDb(element)) || 0.0; 1933 | let doubanval = parseFloat(THISCONFIG.funcDouban(element)) || 0.0; 1934 | if ((imdbval > 0.1 && imdbval < imdbMinVal) || (doubanval > 0.1 && doubanval < imdbMinVal)) { 1935 | keepShow = false; 1936 | } 1937 | } 1938 | if ($("#titleregex").val()) { 1939 | let regex = new RegExp($("#titleregex").val(), "gi"); 1940 | if (titlestr.match(regex)) { 1941 | keepShow = false; 1942 | } 1943 | } 1944 | let titledesc = "" 1945 | if ($("#titledescregex").val()) { 1946 | let regex = new RegExp($("#titledescregex").val(), "gi"); 1947 | let titleele = $(element).find(THISCONFIG.eleTorItemDesc); 1948 | if (titleele){ 1949 | titledesc = titleele.text() 1950 | titledesc = titledesc.replace(/[\n\r]+/g, ''); 1951 | titledesc = titledesc.replace(/\s{2,10}/g, ' '); 1952 | } 1953 | if (titledesc.match(regex)) { 1954 | keepShow = false; 1955 | } 1956 | } 1957 | 1958 | // if ($("#intn_tor").is(":checked") && $(torlist[index]).find(THISCONFIG.eleIntnTag).length <= 0) { 1959 | // keepShow = false; 1960 | // } 1961 | if ($("#seeding").is(":checked") && THISCONFIG.funcSeeding(element)) { 1962 | keepShow = false; 1963 | } 1964 | if ($("#downloaded").is(":checked") && THISCONFIG.funcDownloaded(element)) { 1965 | keepShow = false; 1966 | } 1967 | if ( 1968 | THISCONFIG.filterZZ && 1969 | $("#chnsub").is(":checked") && 1970 | $(torlist[index]).find(THISCONFIG.eleCnSubTag).length <= 0 1971 | ) { 1972 | keepShow = false; 1973 | } 1974 | if ( 1975 | THISCONFIG.filterGY && 1976 | $("#nochnlang").is(":checked") && 1977 | $(torlist[index]).find(THISCONFIG.eleCnLangTag).length > 0 1978 | ) { 1979 | keepShow = false; 1980 | } 1981 | 1982 | if (keepShow) { 1983 | $(element).show(); 1984 | } else { 1985 | $(element).hide(); 1986 | console.log("Filtered: "+ titlestr+" : "+titledesc) 1987 | filterCount++; 1988 | } 1989 | } 1990 | $("#process-log").text("过滤了:" + filterCount); 1991 | }; 1992 | 1993 | var asyncCopyLink = async (html) => { 1994 | $("#process-log").text("处理中..."); 1995 | let passKeyStr = await THISCONFIG.funcGetPasskey(); 1996 | // console.log(passKeyStr); 1997 | 1998 | let torlist = $(html).find(THISCONFIG.eleTorList); 1999 | var resulttext = ""; 2000 | for (let index = 1; index < torlist.length; ++index) { 2001 | if ($(torlist[index]).is(":visible")) { 2002 | let dllink = getDownloadLink(torlist[index], passKeyStr); 2003 | if (dllink) { 2004 | resulttext += dllink + "\n"; 2005 | } 2006 | } 2007 | } 2008 | GM_setClipboard(resulttext, "text"); 2009 | $("#process-log").text("下载链接 已拷贝在剪贴板中"); 2010 | }; 2011 | 2012 | function onClickCopyDownloadLink(html) { 2013 | asyncCopyLink(html); 2014 | } 2015 | 2016 | var SUM_SIZE; 2017 | 2018 | var postToFilterDownloadApi = async (tordata, doDownload, ele) => { 2019 | var apiUrl = doDownload ? API_DUPDOWNLOAD : API_CHECKDUP 2020 | let sizestr = $(ele).find(THISCONFIG.eleTorItemSize).text().trim(); 2021 | let torsize = 0; 2022 | if (sizestr) { 2023 | torsize = sizeStrToGB(sizestr); 2024 | } 2025 | var resp = GM.xmlHttpRequest({ 2026 | method: "POST", 2027 | url: apiUrl, 2028 | data: JSON.stringify(tordata), 2029 | headers: { 2030 | "Content-Type": "application/json", 2031 | "X-API-Key": API_AUTH_KEY 2032 | }, 2033 | onload: function (response) { 2034 | if (response.status == 202) { 2035 | $(ele).css("background-color", "lightgray"); 2036 | // console.log("Dupe: " + tordata.torname); 2037 | } else if (response.status == 201) { 2038 | let p = doDownload ? "下载 " : "无重 " 2039 | SUM_SIZE += (parseFloat(torsize) || 0.0); 2040 | $("#process-log").text(p + SUM_SIZE.toFixed(1) +" GB"); 2041 | if (doDownload){ 2042 | $(ele).css("background-color", "darkseagreen"); 2043 | } 2044 | else { 2045 | $(ele).css("background-color", "LightBlue"); // CadetBlue, CornflowerBlue DodgerBlue DarkTurquoise 2046 | } 2047 | // console.log("Add download: " + tordata.torname); 2048 | } else if (response.status == 205) { 2049 | $(ele).css("background-color", "darkturquoise"); 2050 | // console.log("no dupe but no download: " + tordata.torname); 2051 | } else if (response.status == 203) { 2052 | $(ele).css("background-color", "lightpink"); 2053 | // console.log("TMDbNotFound: " + tordata.torname); 2054 | } else { 2055 | $(ele).css("background-color", "red"); 2056 | console.log(response); 2057 | } 2058 | }, 2059 | onerror: function (reponse) { 2060 | //alert('error'); 2061 | console.log("error: ", reponse); 2062 | }, 2063 | }); 2064 | }; 2065 | 2066 | function sleep(ms) { 2067 | return new Promise((resolve) => setTimeout(resolve, ms)); 2068 | } 2069 | 2070 | // modified from PT-Plugin-Plus/resource/schemas/NexusPHP/details.js 2071 | function _getDownloadUrlByPossibleHrefs(pagehtml) { 2072 | const possibleHrefs = [ 2073 | // pthome 2074 | "a[href*='downhash'][class!='forward_a']", 2075 | // hdchina 2076 | "a[href*='hash'][href*='https'][class!='forward_a']", 2077 | // misc 2078 | "a[href*='passkey'][href*='https'][class!='forward_a']", 2079 | "a[href*='passkey'][class!='forward_a']", 2080 | "a[href*='https://totheglory.im/dl/']", 2081 | ]; 2082 | 2083 | 2084 | var dllink = null; 2085 | for (const href of possibleHrefs) { 2086 | const query = $(href, pagehtml); 2087 | if (query.length) { 2088 | dllink = query.prop("href"); 2089 | } 2090 | } 2091 | if (!dllink) { 2092 | dllink = 2093 | $("input[value*='passkey']").prop("value") || 2094 | $("td.rowfollow:contains('&passkey='):last").text() || 2095 | $("a[href*='download'][href*='?id']:first").attr("href") || 2096 | $("a[href*='download.php?']:first").attr("href"); 2097 | } 2098 | return dllink; 2099 | } 2100 | 2101 | 2102 | function getHDCuid() { 2103 | let bodytext = $("body").html(); 2104 | let datas = /userdetails\.php\?id=(\d+)/.exec( bodytext ); 2105 | if (datas && datas.length > 1) { 2106 | return datas[1]; 2107 | } 2108 | return ""; 2109 | } 2110 | 2111 | 2112 | function getCurrentPageIMDb() { 2113 | let bodytext = $("body").html(); 2114 | let datas = /www\.imdb\.com\/title\/(tt\d+)/.exec( bodytext ); 2115 | if (datas && datas.length > 1) { 2116 | return datas[1]; 2117 | } 2118 | return ""; 2119 | } 2120 | 2121 | var postToDetailCheckDupeApi = async (apiurl, tordata) => { 2122 | let logele = "#detail-log"; 2123 | let down = apiurl.indexOf('download')>0 ? true : false 2124 | var resp = GM.xmlHttpRequest({ 2125 | method: "POST", 2126 | url: apiurl, 2127 | data: JSON.stringify(tordata), 2128 | headers: { 2129 | "Content-Type": "application/json", 2130 | "X-API-Key": API_AUTH_KEY 2131 | }, 2132 | onload: function (response) { 2133 | if (response.status == 202) { 2134 | $(logele).parent().parent().css("background-color", "lightgray"); 2135 | $(logele).text("重复."); 2136 | } else if (response.status == 201) { 2137 | $(logele).parent().parent().css("background-color", "darkseagreen"); 2138 | $(logele).text(down? "添加下载": "无重复."); 2139 | } else if (response.status == 205) { 2140 | $(logele).parent().parent().css("background-color", "darkturquoise"); 2141 | $(logele).text("无下载链接."); 2142 | } else if (response.status == 203) { 2143 | $(logele).parent().parent().css("background-color", "lightpink"); 2144 | $(logele).text("TMDbNotFound."); 2145 | } else { 2146 | $(logele).parent().parent().css("background-color", "red"); 2147 | $(logele).text("出错."); 2148 | } 2149 | }, 2150 | onerror: function (reponse) { 2151 | //alert('error'); 2152 | console.log("error: ", reponse); 2153 | }, 2154 | }); 2155 | }; 2156 | 2157 | 2158 | function getSubtitle(){ 2159 | let subtitle = $("td:contains('副标题') + td") 2160 | return subtitle.text() 2161 | } 2162 | 2163 | function getPageTorSize(){ 2164 | let infotext = $("td:contains('基本信息') + td") 2165 | let datas = /大小[^0-9]*([\d\.]+\s*[MGT]B)/.exec( infotext.text() ); 2166 | if (datas && datas.length > 1) { 2167 | return sizeStrToBytes(datas[1]); 2168 | } 2169 | return 0 2170 | } 2171 | 2172 | var asyncDetailApiDownload = async (html, forcedl) => { 2173 | $("#detail-log").text("处理中..."); 2174 | // dllink = $("#torrent_dl_url > a").href() 2175 | // TODO: 2176 | let titlestr = getDetailTitle(); 2177 | 2178 | let dllink = _getDownloadUrlByPossibleHrefs(html); 2179 | if (dllink) { 2180 | let imdbid = getCurrentPageIMDb(); 2181 | let siteId = getSiteId(document.URL, imdbid); 2182 | let torsizeint = getPageTorSize(); 2183 | // console.log(torsizeint); 2184 | var tordata = { 2185 | torname: titlestr, 2186 | torsize: torsizeint, 2187 | imdbid: imdbid, 2188 | downloadlink: dllink, 2189 | siteid: siteId, 2190 | force: forcedl 2191 | }; 2192 | await postToDetailCheckDupeApi( 2193 | API_DUPDOWNLOAD, 2194 | tordata 2195 | ); 2196 | } 2197 | }; 2198 | 2199 | 2200 | var fetchDetailPageGetIMDbAndDlink = async (detailLink, downloadLink) => { 2201 | // let m = downloadLink.match(/\?id=(\d+)/); 2202 | // if (!m) { 2203 | // return ["", downloadLink]; 2204 | // } 2205 | // var torrentId = m[1]; 2206 | if (!detailLink) { 2207 | return ["", downloadLink]; 2208 | } 2209 | 2210 | let detailhtml = await $.get(detailLink); 2211 | let datas = /www\.imdb\.com\/title\/(tt\d+)/.exec( detailhtml ); 2212 | let dllink = _getDownloadUrlByPossibleHrefs(detailhtml); 2213 | if (!dllink) { 2214 | dllink = downloadLink; 2215 | } 2216 | if (datas && datas.length > 1) { 2217 | return [datas[1], dllink]; 2218 | } 2219 | else { 2220 | return ["", dllink]; 2221 | } 2222 | } 2223 | 2224 | 2225 | function linkPasskey(link, passKeyStr) { 2226 | if (THISCONFIG.host == "totheglory.im") 2227 | { 2228 | return link.replace(/\d+$/, "") + passKeyStr; 2229 | } 2230 | else{ 2231 | return link + passKeyStr; 2232 | } 2233 | } 2234 | 2235 | 2236 | function getDownloadLink(element, passKeyStr){ 2237 | let hrefele = $(element).find(THISCONFIG.eleDownLink); 2238 | if (hrefele.length > 0) { 2239 | let url = hrefele.prop("href"); 2240 | let dllink = ""; 2241 | if (THISCONFIG.host == "hdchina.org"){ 2242 | let uidstr = getHDCuid(); 2243 | dllink = linkPasskey(url, passKeyStr) + "&uid=" + uidstr; 2244 | } 2245 | else { 2246 | dllink = linkPasskey(url, passKeyStr); 2247 | } 2248 | // let dllink = linkPasskey(url, passKeyStr); 2249 | 2250 | return dllink 2251 | } 2252 | return "" 2253 | } 2254 | 2255 | function getSiteId(detailLink, imdbstr){ 2256 | var m = null 2257 | if (THISCONFIG.host == "totheglory.im") 2258 | { 2259 | m = detailLink.match(/t\/(\d+)/); 2260 | } 2261 | else { 2262 | m = detailLink.match(/\?id=(\d+)/); 2263 | } 2264 | let sid = m ? m[1] : ""; 2265 | if (imdbstr){ 2266 | sid = sid + "_" + imdbstr 2267 | } 2268 | return THISCONFIG.abbrev + "_" + sid 2269 | } 2270 | 2271 | 2272 | 2273 | var DUPECHECKED = false; 2274 | var asyncApiDownload = async (html, doDownload) => { 2275 | let dupeChecked = DUPECHECKED 2276 | $("#process-log").text("处理中..."); 2277 | let passKeyStr = await THISCONFIG.funcGetPasskey(); 2278 | SUM_SIZE = 0; 2279 | let torlist = $(html).find(THISCONFIG.eleTorList); 2280 | for (let index = 1; index < torlist.length; ++index) { 2281 | if ($(torlist[index]).is(":visible")) { 2282 | let element = torlist[index]; 2283 | let item = $(element).find(THISCONFIG.eleTorItem); 2284 | let detailLink = item.prop("href"); 2285 | let seednum = parseInt($(element).find(THISCONFIG.eleTorItemSeednum).text().trim()) || 0; 2286 | 2287 | if (item.length == 0) { continue} 2288 | // refer to Line 978: 2289 | // } else if (response.status == 201) { 2290 | // $(ele).css("background-color", "darkseagreen"); 'rgb(143, 188, 143)' 2291 | if (doDownload && dupeChecked && $(element).css('background-color') != 'rgb(173, 216, 230)') { 2292 | continue; 2293 | } 2294 | 2295 | let titlestr = getItemTitle(item); 2296 | let imdbid = THISCONFIG.funcIMDbId(element); 2297 | let dllink = getDownloadLink(torlist[index], passKeyStr); 2298 | let siteId = getSiteId(detailLink, imdbid); 2299 | 2300 | // if (dllink && seednum > 0) { 2301 | if (dllink ) { 2302 | // check detal page imdb only when doDownload 2303 | // hdsky exception: need fetch detail page 2304 | if (doDownload && (!imdbid || THISCONFIG.host == "hdsky.me")) { 2305 | let res = await fetchDetailPageGetIMDbAndDlink(detailLink, dllink); 2306 | imdbid = res[0]; 2307 | dllink = res[1]; 2308 | console.log("DETAIL_PAGE: ", titlestr, imdbid); 2309 | } 2310 | let sizestr = $(element).find(THISCONFIG.eleTorItemSize).text().trim(); 2311 | 2312 | var tordata = { 2313 | torname: titlestr, 2314 | torsize: sizeStrToBytes(sizestr), 2315 | imdbid: imdbid, 2316 | downloadlink: dllink, 2317 | siteid: siteId, 2318 | }; 2319 | await postToFilterDownloadApi(tordata, doDownload, element); 2320 | if (doDownload){ 2321 | await sleep(2000); 2322 | } 2323 | else { 2324 | await sleep(200); 2325 | } 2326 | } 2327 | } 2328 | } 2329 | if (!doDownload) DUPECHECKED = true; 2330 | if ($("#process-log").text() == '处理中...'){ 2331 | let msg = (doDownload) ? "查重下载已提交" : "查重已提交"; 2332 | $("#process-log").text(msg); 2333 | } 2334 | }; 2335 | 2336 | function onClickApiDownload(html) { 2337 | asyncApiDownload(html, true); 2338 | } 2339 | 2340 | function onClickApiCheckDupe(html) { 2341 | asyncApiDownload(html, false); 2342 | } 2343 | 2344 | function onClickDetailDownload(html) { 2345 | asyncDetailApiDownload(html, false); 2346 | } 2347 | 2348 | function onClickDetailForceDownload(html) { 2349 | asyncDetailApiDownload(html, true); 2350 | } 2351 | 2352 | function getDetailTitle() { 2353 | let titlestr = $(THISCONFIG.eleDetailTitle).text().trim(); 2354 | titlestr = titlestr.replace(/\[?禁转\s*/, "") 2355 | titlestr = titlestr.replace(/\[[^\]]+\]\s*$/, "") 2356 | titlestr = titlestr.replace(/\s*\[(50%|30%|(2X)?免费)\].*$/, "") 2357 | titlestr = titlestr.replace(/\s*\(限时.*$/, "").trim() 2358 | return titlestr; 2359 | } 2360 | 2361 | var asyncDetailCheckDupe = async (html) => { 2362 | $("#detail-log").text("处理中.."); 2363 | // dllink = $("#torrent_dl_url > a").href() 2364 | // let titlestr = $("#top").text(); 2365 | let titlestr = getDetailTitle(); 2366 | let dllink = _getDownloadUrlByPossibleHrefs(html); 2367 | if (dllink) { 2368 | let imdbid = getCurrentPageIMDb(); 2369 | var tordata = { 2370 | torname: titlestr, 2371 | imdbid: imdbid, 2372 | downloadlink: dllink, 2373 | }; 2374 | await postToDetailCheckDupeApi( 2375 | API_CHECKDUP, 2376 | tordata 2377 | ); 2378 | } 2379 | else { 2380 | console.log("download link not found.") 2381 | $("#detail-log").text("无下载链接"); 2382 | } 2383 | }; 2384 | 2385 | function onClickDetailCheckDup(html) { 2386 | asyncDetailCheckDupe(html); 2387 | } 2388 | 2389 | 2390 | function addAdoptColumn(html) { 2391 | // const torTable = $(THISCONFIG.eleTorTable); 2392 | if (THISCONFIG.host != "pterclub.com") { 2393 | return; 2394 | } 2395 | const idregex = /id=(\d+)/; 2396 | 2397 | var torlist = $(html).find(THISCONFIG.eleTorList); 2398 | for (let index = 0; index < torlist.length; ++index) { 2399 | let element = torlist[index]; 2400 | let item = $(element).find(THISCONFIG.eleTorItem); 2401 | let href = item.attr("href"); 2402 | if (href) { 2403 | let torid = href.match(idregex); 2404 | if (torid) { 2405 | let sizeele = $(element).find(THISCONFIG.eleTorItemSize); 2406 | $(element).append( 2407 | " 认领" 2410 | ); 2411 | } 2412 | } else { 2413 | $(element).append(' 认领种子 '); 2414 | } 2415 | } 2416 | } 2417 | 2418 | (function () { 2419 | "use strict"; 2420 | if (THISCONFIG) { 2421 | if (window.location.href.match(/details/) || 2422 | window.location.href.match(/totheglory.im\/t/) ) { 2423 | addDetailPagePanel(); 2424 | $("#btn-detail-checkdupe").click(function () { 2425 | onClickDetailCheckDup(document); 2426 | }); 2427 | $("#btn-detail-apidownload").click(function () { 2428 | onClickDetailDownload(document); 2429 | }); 2430 | $("#btn-detail-forceapidownload").click(function () { 2431 | onClickDetailForceDownload(document); 2432 | }); 2433 | } else { 2434 | addAdoptColumn(document); 2435 | addFilterPanel(); 2436 | loadParamFromCookie(); 2437 | $("#btn-filterlist").click(function () { 2438 | onClickFilterList(document); 2439 | }); 2440 | $("#btn-copydllink").click(function () { 2441 | onClickCopyDownloadLink(document); 2442 | }); 2443 | $("#btn-apidownload").click(function () { 2444 | onClickApiDownload(document); 2445 | }); 2446 | $("#btn-apicheckdupe").click(function () { 2447 | onClickApiCheckDupe(document); 2448 | }); 2449 | } 2450 | } 2451 | })(); 2452 | -------------------------------------------------------------------------------- /torss.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A script to rss and parse the info page of a torrent to get IMDb id, 4 | add torrent to the qbit client with this IMDb id as a tag. 5 | """ 6 | import re 7 | import argparse 8 | import requests 9 | from http.cookies import SimpleCookie 10 | from torcp.tmdbparser import TMDbNameParser 11 | import qbittorrentapi 12 | import configparser 13 | import feedparser 14 | import datetime 15 | import time 16 | from sqlalchemy import Column, String, Integer, Float, create_engine 17 | from sqlalchemy.orm import sessionmaker 18 | from sqlalchemy.ext.declarative import declarative_base 19 | from urllib.parse import urlparse 20 | 21 | from humanbytes import HumanBytes 22 | # from lxml import etree 23 | 24 | DOWNLOAD_URL_RE = [ 25 | r'download\.php\?id=(\d+)&downhash=(\w+)', 26 | r'download\.php\?id=(\d+)&passkey=(\w+)', 27 | ] 28 | 29 | 30 | class configData(): 31 | interval = 3 32 | qbServer = '' 33 | tmdb_api_key = '' 34 | qbPort = '' 35 | qbUser = '' 36 | qbPass = '' 37 | addPause = False 38 | dryrun = False 39 | 40 | 41 | CONFIG = configData() 42 | 43 | 44 | def readConfig(): 45 | config = configparser.ConfigParser() 46 | config.read('config.ini') 47 | 48 | if 'TMDB' in config: 49 | CONFIG.tmdb_api_key = config['TMDB'].get('api_key', '') 50 | 51 | if 'QBIT' in config: 52 | CONFIG.qbServer = config['QBIT'].get('server_ip', '') 53 | CONFIG.qbPort = config['QBIT'].get('port', '') 54 | CONFIG.qbUser = config['QBIT'].get('user', '') 55 | CONFIG.qbPass = config['QBIT'].get('pass') 56 | 57 | CONFIG.addPause = config['QBIT'].getboolean('pause', False) 58 | CONFIG.dryrun = config['QBIT'].getboolean('dryrun', False) 59 | 60 | 61 | ModelBase = declarative_base() 62 | 63 | 64 | class MediaItem(ModelBase): 65 | __tablename__ = 'media_list_table' 66 | id = Column(Integer, primary_key=True) 67 | # site = Column(String(64)) 68 | title = Column(String(255)) 69 | originalTitle = Column(String(255)) 70 | librarySectionID = Column(String(16)) 71 | audienceRating = Column(Float) 72 | guid = Column(String(64)) 73 | key = Column(String(64)) 74 | imdb = Column(String(32)) 75 | tmdb = Column(String(32)) 76 | tvdb = Column(String(32)) 77 | doubanid = Column(String(32)) 78 | site = Column(String(32)) 79 | linkid = Column(String(32)) 80 | location0 = Column(String(255)) 81 | locationdirname = Column(String(255)) 82 | 83 | def __repr__(self): 84 | return '' % self.title 85 | 86 | 87 | class RSSHistory(ModelBase): 88 | __tablename__ = 'rss_history_table' 89 | id = Column(Integer, primary_key=True) 90 | # site = Column(String(64)) 91 | title = Column(String(255)) 92 | # imdbstr = Column(String(32)) 93 | 94 | 95 | # 初始化数据库连接: 96 | engine = create_engine('sqlite:///instance/medialist.db') 97 | # 创建DBSession类型: 98 | DBSession = sessionmaker(bind=engine) 99 | SESSION = DBSession() 100 | ModelBase.metadata.create_all(engine) 101 | 102 | 103 | def rssGetDetailAndDownload(rsslink): 104 | feed = feedparser.parse(rsslink) 105 | rssFeedSum = 0 106 | rssAccept = 0 107 | for item in feed.entries: 108 | rssFeedSum += 1 109 | if not hasattr(item, 'id'): 110 | print('!! No id') 111 | continue 112 | if not hasattr(item, 'title'): 113 | print('!! No title') 114 | continue 115 | 116 | if existsRssHistory(item.title): 117 | # print(" >> exists in rss history, skip") 118 | continue 119 | 120 | print("%d: %s (%s)" % (rssFeedSum, item.title, 121 | datetime.datetime.now().strftime("%H:%M:%S"))) 122 | 123 | saveRssHistory(item.title) 124 | 125 | if ARGS.title_regex: 126 | if not re.search(ARGS.title_regex, item.title, re.I): 127 | print(' >> TITLE_REGEX not match.') 128 | continue 129 | 130 | if ARGS.title_not_regex: 131 | if re.search(ARGS.title_not_regex, item.title, re.I): 132 | print(' >> TITLE_NOT_REGEX not match.') 133 | continue 134 | 135 | imdbstr = '' 136 | if ARGS.cookie: 137 | if hasattr(item, 'link'): 138 | match, imdbstr, downlink, title = parseDetailPage( 139 | item.link, ARGS.cookie) 140 | if not match: 141 | # print(' >> Info page regex not match.') 142 | continue 143 | if ARGS.exclude_no_imdb and (not imdbstr): 144 | print(' >> Without IMDb') 145 | continue 146 | siteIdStr = getSiteId(item.link, imdbstr) 147 | 148 | if hasattr(item, 'links') and len(item.links) > 1: 149 | rssDownloadLink = item.links[1]['href'] 150 | rssSize = item.links[1]['length'] 151 | print(' %s (%s), %s' % 152 | (imdbstr, HumanBytes.format(int(rssSize)), rssDownloadLink)) 153 | r = checkDupAddTor(item.title, rssDownloadLink, 154 | imdbstr, siteIdStr, forceDownload=False) 155 | print(' >> %d ' % r) 156 | if r == 201: 157 | # Download 158 | rssAccept += 1 159 | # print('Sleeping for %d seconds' % ARGS.sleep) 160 | time.sleep(ARGS.sleep) 161 | 162 | print('Total: %d, Accepted: %d (%s)' % 163 | (rssFeedSum, rssAccept, datetime.datetime.now().strftime("%H:%M:%S"))) 164 | 165 | 166 | def validDownloadlink(downlink): 167 | keystr = ['passkey', 'downhash', 'totheglory.im/dl/', 168 | 'totheglory.im/rssdd.php', 'download.php?hash='] 169 | return any(x in downlink for x in keystr) 170 | 171 | 172 | def searchTMDb(TmdbParser, title, imdb): 173 | if imdb: 174 | TmdbParser.parse(title, useTMDb=True, hasIMDbId=imdb) 175 | else: 176 | TmdbParser.parse(title, useTMDb=True) 177 | return TmdbParser.tmdbid 178 | 179 | 180 | def existsRssHistory(torname): 181 | q = SESSION.query(RSSHistory.id).filter(RSSHistory.title == torname) 182 | exists = SESSION.query(q.exists()).scalar() 183 | return exists 184 | 185 | 186 | def saveRssHistory(torname): 187 | SESSION.add(RSSHistory(title=torname)) 188 | SESSION.commit() 189 | 190 | 191 | def checkDatabaseExists(torTMDb): 192 | q = SESSION.query(MediaItem.id).filter(MediaItem.tmdb == torTMDb) 193 | exists = SESSION.query(q.exists()).scalar() 194 | return exists 195 | 196 | 197 | def checkDupAddTor(torname, downloadLink, imdbstr, siteIdStr, forceDownload=False): 198 | if not torname: 199 | return 400 200 | 201 | if (not CONFIG.qbServer): 202 | print("qBittorrent not set, skip") 203 | return 400 204 | 205 | if (not CONFIG.tmdb_api_key): 206 | print("tmdb_api_key not set, skip") 207 | return 400 208 | 209 | p = TMDbNameParser(CONFIG.tmdb_api_key, '') 210 | 211 | torTMDb = searchTMDb(p, torname, imdbstr) 212 | 213 | if torTMDb > 0: 214 | exists = checkDatabaseExists(torTMDb) 215 | # exists = session.query(exists().where( 216 | # MediaItem.tmdb == torTMDb)).scalar() 217 | if (exists) and (not forceDownload): 218 | return 202 219 | else: 220 | # print("Download: " + request.json['torname'] + " "+request.json['downloadlink']) 221 | if downloadLink: 222 | if not validDownloadlink(downloadLink): 223 | print(" >> Not valid torrent downlink: %s ( %s) " % 224 | (torname, downloadLink)) 225 | return 205 226 | 227 | if not CONFIG.dryrun: 228 | print(" >> Added: " + torname) 229 | if not addQbitWithTag(downloadLink.strip(), imdbstr, siteIdStr): 230 | return 400 231 | else: 232 | print(" >> DRYRUN: " + torname + 233 | "\n >> " + downloadLink) 234 | 235 | return 201 236 | else: 237 | # if CONFIG.download_no_imdb: 238 | # if not addQbitWithTag(request.json['downloadlink'].strip(), imdbstr): 239 | # abort(400) 240 | return 203 241 | 242 | 243 | def addQbitWithTag(downlink, imdbtag, siteIdStr=None): 244 | qbClient = qbittorrentapi.Client( 245 | host=CONFIG.qbServer, port=CONFIG.qbPort, username=CONFIG.qbUser, password=CONFIG.qbPass) 246 | 247 | try: 248 | qbClient.auth_log_in() 249 | except qbittorrentapi.LoginFailed as e: 250 | print(e) 251 | 252 | if not qbClient: 253 | return False 254 | 255 | try: 256 | # curr_added_on = time.time() 257 | if siteIdStr and ARGS.siteid_folder: 258 | result = qbClient.torrents_add( 259 | urls=downlink, 260 | is_paused=CONFIG.addPause, 261 | save_path=siteIdStr, 262 | # download_path=download_location, 263 | # category=timestamp, 264 | tags=[imdbtag], 265 | use_auto_torrent_management=False) 266 | else: 267 | result = qbClient.torrents_add( 268 | urls=downlink, 269 | is_paused=CONFIG.addPause, 270 | tags=[imdbtag], 271 | use_auto_torrent_management=False) 272 | # breakpoint() 273 | if 'OK' in result.upper(): 274 | pass 275 | # print(' >> Torrent added.') 276 | else: 277 | print(' >> Torrent not added! something wrong with qb api ...') 278 | except Exception as e: 279 | print(' >> Torrent not added! Exception: '+str(e)) 280 | return False 281 | 282 | return True 283 | 284 | 285 | # def findConfig(infoUrl): 286 | # hostnameList = urllib.parse.urlparse(infoUrl).netloc.split('.') 287 | # abbrev = hostnameList[-2] if len(hostnameList) >= 2 else '' 288 | # return next(filter(lambda ele: ele['host'] == abbrev, SITE_CONFIGS), None) 289 | 290 | def tryFloat(fstr): 291 | try: 292 | f = float(fstr) 293 | except: 294 | f = 0.0 295 | return f 296 | 297 | 298 | def abbrevHostloc(url): 299 | hostnameList = urlparse(url).netloc.split('.') 300 | if len(hostnameList) == 2: 301 | sitename = hostnameList[0] 302 | elif len(hostnameList) == 3: 303 | sitename = hostnameList[1] 304 | else: 305 | sitename = '' 306 | SITE_ABBRES = [('chdbits', 'chd'), ('pterclub', 'pter'), ('audiences', 'aud'), 307 | ('lemonhd', 'lhd'), ('keepfrds', 'frds'), ('ourbits', 'ob'), ('springsunday', 'ssd')] 308 | # result = next((i for i, v in enumerate(SITE_ABBRES) if v[0] == sitename), "") 309 | abbrev = [x for x in SITE_ABBRES if x[0] == sitename] 310 | return abbrev[0][1] if abbrev else sitename 311 | 312 | 313 | def getSiteId(detailLink, imdbstr): 314 | siteAbbrev = abbrevHostloc(detailLink) 315 | if (siteAbbrev == "ttg"): 316 | m = re.search(r"t\/(\d+)", detailLink, flags=re.A) 317 | else: 318 | m = re.search(r"id=(\d+)", detailLink, flags=re.A ) 319 | sid = m[1] if m else "" 320 | if imdbstr: 321 | sid = sid + "_" + imdbstr 322 | return siteAbbrev + "_" + sid 323 | 324 | 325 | def parseDetailPage(pageUrl, pageCookie): 326 | cookie = SimpleCookie() 327 | cookie.load(pageCookie) 328 | cookies = {k: v.value for k, v in cookie.items()} 329 | headers = { 330 | 'User-Agent': 331 | 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 332 | 'Content-Type': 'text/html; charset=UTF-8' 333 | } 334 | 335 | r = requests.get(pageUrl, headers=headers, cookies=cookies) 336 | r.encoding = r.apparent_encoding 337 | doc = r.text 338 | topTitle = '' 339 | mt = re.search(r'id=\"top\"[^>]*>([^<\n]*)', doc, flags=re.A) 340 | if mt: 341 | topTitle = mt[1].replace(" ", "") 342 | print("Detail page title: " + topTitle) 343 | 344 | if ARGS.info_regex: 345 | if not re.search(ARGS.info_regex, doc, flags=re.A): 346 | print(' >> INFO_REGEX not match.') 347 | return False, '', '', '' 348 | if ARGS.info_not_regex: 349 | if re.search(ARGS.info_not_regex, doc, flags=re.A): 350 | print(' >> INFO_NOT_REGEX not match.') 351 | return False, '', '', '' 352 | if ARGS.min_imdb: 353 | imdbval = 0 354 | m1 = re.search(r'IMDb.*?([0-9.]+)\s*/\s*10', doc, flags=re.A) 355 | if m1: 356 | imdbval = tryFloat(m1[1]) 357 | doubanval = 0 358 | m2 = re.search(r'豆瓣评分.*?([0-9.]+)/10', doc, flags=re.A) 359 | if m2: 360 | doubanval = tryFloat(m2[1]) 361 | if imdbval < 1 and doubanval < 1: 362 | ratelist = [x[1] for x in re.finditer( 363 | r'Rating:.*?([0-9.]+)\s*/\s*10\s*from', doc, flags=re.A)] 364 | if len(ratelist) >= 2: 365 | doubanval = tryFloat(ratelist[0]) 366 | imdbval = tryFloat(ratelist[1]) 367 | elif len(ratelist) == 1: 368 | #TODO: 不分辨douban/imdb了 369 | doubanval = tryFloat(ratelist[0]) 370 | imdbval = doubanval 371 | # rate1 = re.search(r'Rating:.*?([0-9.]+)\s*/\s*10\s*from', doc, flags=re.A) 372 | # if rate1: 373 | # imdbval = tryFloat(rate1[1]) 374 | 375 | print(" >> IMDb: %s, douban: %s" % (imdbval, doubanval)) 376 | 377 | if (imdbval < ARGS.min_imdb) and (doubanval < ARGS.min_imdb): 378 | print(" >> MIN_IMDb not match") 379 | return False, '', '', '' 380 | 381 | imdbstr = '' 382 | # imdbRe = r'IMDb(链接)\s*(\<.[!>]*\>)?.*https://www\.imdb\.com/title/tt(\d+)' 383 | imdbRe = r'www\.imdb\.com\/title\/(tt\d+)' 384 | m1 = re.search(imdbRe, doc, flags=re.A) 385 | if m1: 386 | imdbstr = m1[1] 387 | 388 | for reUrl in DOWNLOAD_URL_RE: 389 | if re.search(reUrl, doc, flags=re.A): 390 | break 391 | downlink = '' 392 | m2 = re.search(reUrl, doc, flags=re.A) 393 | if m2: 394 | downlink = m2[0] 395 | 396 | if downlink: 397 | parsed_uri = urlparse(pageUrl) 398 | downlink = '{uri.scheme}://{uri.netloc}/{relink}'.format( 399 | uri=parsed_uri, relink=downlink) 400 | 401 | return True, imdbstr, downlink, topTitle 402 | 403 | 404 | def loadArgs(): 405 | parser = argparse.ArgumentParser( 406 | description='A script to rss pt site, add torrent to qbit with IMDb id as a tag.' 407 | ) 408 | parser.add_argument('-R', '--rss', help='the rss link.') 409 | parser.add_argument( 410 | '-s', '--single-page', help='fetch single torrent in detail page.') 411 | parser.add_argument( 412 | '-c', '--cookie', help='the cookie to the detail page.') 413 | parser.add_argument('--title-regex', help='regex to match the rss title.') 414 | parser.add_argument('--title-not-regex', 415 | help='regex to not match the rss title.') 416 | parser.add_argument( 417 | '--info-regex', help='regex to match the info/detail page.') 418 | parser.add_argument('--info-not-regex', 419 | help='regex to not match the info/detail page.') 420 | parser.add_argument('--sleep', type=int, 421 | help='sleep between each request of info page.') 422 | parser.add_argument('--add-pause', 423 | action='store_true', 424 | help='Add torrent in PAUSE state.') 425 | parser.add_argument('--exclude-no-imdb', 426 | action='store_true', 427 | help='Do not download without IMDb') 428 | parser.add_argument('--min-imdb', type=int, 429 | help='filter imdb greater than .') 430 | parser.add_argument('--siteid-folder', action='store_true', 431 | help='make Site_Id_Imdb parent folder.') 432 | parser.add_argument('--init-rss-history', action='store_true', 433 | help='Init/Empty rss history table.') 434 | global ARGS 435 | ARGS = parser.parse_args() 436 | if not ARGS.sleep: 437 | ARGS.sleep = 5 438 | 439 | 440 | def initRssHistory(): 441 | print("Init Database....") 442 | num_rows_deleted = SESSION.query(RSSHistory).delete() 443 | SESSION.commit() 444 | print("%d rows deleted" % num_rows_deleted) 445 | return num_rows_deleted 446 | 447 | 448 | def main(): 449 | loadArgs() 450 | readConfig() 451 | if ARGS.init_rss_history: 452 | initRssHistory() 453 | if ARGS.rss: 454 | rssGetDetailAndDownload(ARGS.rss) 455 | 456 | elif ARGS.single_page: 457 | if ARGS.cookie: 458 | match, imdbstr, downlink, title = parseDetailPage( 459 | ARGS.single_page, ARGS.cookie) 460 | if not downlink: 461 | print("Error: download link not found") 462 | return 463 | siteIdStr = getSiteId(ARGS.single_page, imdbstr) 464 | print(imdbstr, downlink) 465 | r = checkDupAddTor(title, downlink, imdbstr, siteIdStr, forceDownload=False) 466 | print(' >> %d ' % r) 467 | # r = addQbitWithTag(downlink, imdbstr) 468 | 469 | 470 | if __name__ == '__main__': 471 | main() 472 | --------------------------------------------------------------------------------