Repository: pt-plugins/PT-Plugin-Plus Branch: master Commit: f5a21686b770 Files: 478 Total size: 2.2 MB Directory structure: gitextract_s_ss06tw/ ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report-cn.md │ │ ├── bug-report.md │ │ ├── feature-request-cn.md │ │ ├── feature-request.md │ │ ├── new-tracker-request-cn.md │ │ ├── new-tracker-request.md │ │ └── suggestions-or-comments.md │ ├── issue-close-app.yml │ ├── pull_request_template.md │ ├── stale.yml │ └── workflows/ │ ├── build_action.yml │ └── build_canary.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── babel.config.js ├── debug/ │ ├── config/ │ │ └── config.json │ ├── data/ │ │ └── beforeSearching.json │ ├── package.json │ ├── src/ │ │ ├── App.ts │ │ ├── BuildPlugin.ts │ │ ├── SearchData.ts │ │ ├── buildResource.ts │ │ └── index.ts │ ├── tsconfig.json │ └── typings.d.ts ├── package.json ├── privacy-statement.md ├── public/ │ ├── _locales/ │ │ ├── en/ │ │ │ └── messages.json │ │ └── zh_CN/ │ │ └── messages.json │ ├── assets/ │ │ ├── base.css │ │ └── options.css │ ├── changelog.html │ ├── index.html │ ├── libs/ │ │ ├── Base64.js │ │ ├── drag.js │ │ ├── materialIcons/ │ │ │ ├── content_style.css │ │ │ └── style.css │ │ ├── notice/ │ │ │ ├── notice.js │ │ │ └── noticejs.css │ │ └── types.expand.js │ ├── manifest.json │ └── popup.html ├── resource/ │ ├── clients/ │ │ ├── README.md │ │ ├── deluge/ │ │ │ ├── config.json │ │ │ └── init.js │ │ ├── flood/ │ │ │ ├── config.json │ │ │ └── init.js │ │ ├── qbittorrent/ │ │ │ ├── config.json │ │ │ └── init.js │ │ ├── ruTorrent/ │ │ │ ├── config.json │ │ │ └── init.js │ │ ├── synologyDownloadStation/ │ │ │ ├── config.json │ │ │ └── init.js │ │ ├── transmission/ │ │ │ ├── config.json │ │ │ └── init.js │ │ └── utorrent/ │ │ ├── config.json │ │ └── init.js │ ├── i18n/ │ │ ├── README.md │ │ ├── en.json │ │ └── zh-CN.json │ ├── libs/ │ │ └── album/ │ │ ├── album.js │ │ └── style.css │ ├── publicSites/ │ │ ├── douban.com/ │ │ │ ├── common.js │ │ │ ├── config.json │ │ │ ├── doulist.js │ │ │ ├── explore.js │ │ │ ├── subject.js │ │ │ └── top250.js │ │ ├── goodmovieslist.com/ │ │ │ ├── best-movies.js │ │ │ └── config.json │ │ ├── imdb.com/ │ │ │ ├── config.json │ │ │ ├── subject.js │ │ │ └── top.js │ │ └── reseed.tongyifan.me/ │ │ ├── config.json │ │ └── reseed.js │ ├── schemas/ │ │ ├── Common/ │ │ │ ├── common.js │ │ │ ├── config.json │ │ │ ├── details.js │ │ │ ├── getSearchResult.js │ │ │ └── torrents.js │ │ ├── Discuz/ │ │ │ ├── config.json │ │ │ ├── details.js │ │ │ ├── getSearchResult.js │ │ │ └── torrents.js │ │ ├── Gazelle/ │ │ │ ├── config.json │ │ │ ├── getSearchResult.js │ │ │ └── torrents.js │ │ ├── GazelleJSONAPI/ │ │ │ ├── config.json │ │ │ └── getSearchResult.js │ │ ├── NexusPHP/ │ │ │ ├── common.js │ │ │ ├── config.json │ │ │ ├── details.js │ │ │ ├── getSearchResult.js │ │ │ ├── parser/ │ │ │ │ └── downloadURL.js │ │ │ └── torrents.js │ │ ├── README.md │ │ ├── TNode/ │ │ │ ├── common.js │ │ │ ├── config.json │ │ │ ├── details.js │ │ │ ├── getSearchResult.js │ │ │ └── torrents.js │ │ └── UNIT3D/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ ├── torrents.js │ │ └── userTorrents.js │ └── sites/ │ ├── 1ptba.com/ │ │ └── config.json │ ├── 52pt.site/ │ │ └── config.json │ ├── README.md │ ├── aidoru-online.me/ │ │ └── config.json │ ├── aither.cc/ │ │ └── config.json │ ├── alpharatio.cc/ │ │ └── config.json │ ├── animebytes.tv/ │ │ ├── config.json │ │ ├── getSearchResult.js │ │ └── userTorrents.js │ ├── anthelion.me/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── asiancinema.me/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── audiences.me/ │ │ └── config.json │ ├── azusa.wiki/ │ │ └── config.json │ ├── baconbits.org/ │ │ └── config.json │ ├── bemaniso.ws/ │ │ └── config.json │ ├── beyond-hd.me/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── bibliotik.me/ │ │ ├── config.json │ │ └── getUserSeedingTorrents.js │ ├── bitbr/ │ │ └── config.json │ ├── bitpt.cn/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── torrents.js │ ├── blutopia.cc/ │ │ └── config.json │ ├── broadcasthe.net/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── brokenstones.is/ │ │ └── config.json │ ├── bt.neu6.edu.cn/ │ │ └── config.json │ ├── bwtorrents.tv/ │ │ └── config.json │ ├── byr.pt/ │ │ └── config.json │ ├── carpt.net/ │ │ └── config.json │ ├── ccfbits.org/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── chdbits.co/ │ │ └── config.json │ ├── cinemageddon.net/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── club.hares.top/ │ │ └── config.json │ ├── cnlang.org/ │ │ ├── config.json │ │ └── getUserSeedingTorrents.js │ ├── concertos.live/ │ │ └── config.json │ ├── cyanbug.net/ │ │ └── config.json │ ├── dajiao.cyou/ │ │ └── config.json │ ├── dicmusic.com/ │ │ └── config.json │ ├── discfan.net/ │ │ └── config.json │ ├── et8.org/ │ │ └── config.json │ ├── extremlymtorrents.ws/ │ │ └── config.json │ ├── femdomcult.org/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── filelist.io/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── fsm.name/ │ │ └── config.json │ ├── gainbound.net/ │ │ └── config.json │ ├── gay-torrents.org/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── gazellegames.net/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── gfxpeers.net/ │ │ └── config.json │ ├── greatposterwall.com/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── hawke.uno/ │ │ └── config.json │ ├── hd-space.org/ │ │ ├── config.json │ │ └── details.js │ ├── hd-torrents.org/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ ├── getUserSeedingTorrents.js │ │ └── torrents.js │ ├── hdatmos.club/ │ │ └── config.json │ ├── hdbits.org/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── hdchina.org/ │ │ └── config.json │ ├── hdcity.city/ │ │ └── config.json │ ├── hddolby.com/ │ │ └── config.json │ ├── hdf.world/ │ │ └── config.json │ ├── hdfans.org/ │ │ └── config.json │ ├── hdhome.org/ │ │ └── config.json │ ├── hdmayi.com/ │ │ └── config.json │ ├── hdpt.xyz/ │ │ └── config.json │ ├── hdroute.org/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── torrents.js │ ├── hdsky.me/ │ │ └── config.json │ ├── hdtime.org/ │ │ └── config.json │ ├── hdvideo.one/ │ │ └── config.json │ ├── hdzone.me/ │ │ └── config.json │ ├── hhanclub.top/ │ │ └── config.json │ ├── htpt.cc/ │ │ └── config.json │ ├── hudbt.hust.edu.cn/ │ │ └── config.json │ ├── ihdbits.me/ │ │ └── config.json │ ├── iptorrents.com/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── torrents.js │ ├── joyhd.net/ │ │ └── config.json │ ├── jpopsuki.eu/ │ │ ├── config.json │ │ ├── getSearchResult.js │ │ ├── getUserSeedingTorrents.js │ │ └── torrents.js │ ├── jptv.club/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── jptvts.us/ │ │ └── config.json │ ├── kamept.com/ │ │ └── config.json │ ├── karagarga.in/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── kp.m-team.cc/ │ │ ├── config.json │ │ ├── getUserSeedingTorrents.js │ │ └── torrents.js │ ├── learnflakes.net/ │ │ └── config.json │ ├── leaves.red/ │ │ └── config.json │ ├── lztr.me/ │ │ └── config.json │ ├── monikadesign.uk/ │ │ └── config.json │ ├── nanyangpt.com/ │ │ └── config.json │ ├── nebulance.io/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── nicept.net/ │ │ └── config.json │ ├── npupt.com/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── oldtoons.world/ │ │ └── config.json │ ├── open.cd/ │ │ └── config.json │ ├── orpheus.network/ │ │ └── config.json │ ├── ourbits.club/ │ │ └── config.json │ ├── passthepopcorn.me/ │ │ ├── config.json │ │ ├── getSearchResult.js │ │ ├── getUserSeedingTorrents.js │ │ └── torrents.js │ ├── piggo.me/ │ │ └── config.json │ ├── pt.0ff.cc/ │ │ └── config.json │ ├── pt.2xfree.org/ │ │ └── config.json │ ├── pt.btschool.club/ │ │ └── config.json │ ├── pt.eastgame.org/ │ │ └── config.json │ ├── pt.hd4fans.org/ │ │ └── config.json │ ├── pt.hdbd.us/ │ │ └── config.json │ ├── pt.hdpost.top/ │ │ └── config.json │ ├── pt.hdupt.com/ │ │ └── config.json │ ├── pt.keepfrds.com/ │ │ └── config.json │ ├── pt.newworld.plus/ │ │ └── config.json │ ├── pt.sjtu.edu.cn/ │ │ └── config.json │ ├── pt.soulvoice.club/ │ │ └── config.json │ ├── pt.xauat6.edu.cn/ │ │ └── config.json │ ├── pt.zhixing.bjtu.edu.cn/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── torrents.js │ ├── ptchina.org/ │ │ └── config.json │ ├── pterclub.com/ │ │ └── config.json │ ├── pthome.net/ │ │ └── config.json │ ├── ptsbao.club/ │ │ └── config.json │ ├── pussytorrents.org/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ ├── getUserSeedingTorrents.js │ │ └── torrents.js │ ├── redacted.ch/ │ │ └── config.json │ ├── resource.xidian.edu.cn/ │ │ └── config.json │ ├── sdbits.org/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── shadowthein.net/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── speedapp.io/ │ │ └── config.json │ ├── sportscult.org/ │ │ ├── config.json │ │ └── details.js │ ├── springsunday.net/ │ │ └── config.json │ ├── sugoimusic.me/ │ │ └── config.json │ ├── teamhd.org/ │ │ └── config.json │ ├── thegeeks.click/ │ │ └── config.json │ ├── tjupt.org/ │ │ └── config.json │ ├── totheglory.im/ │ │ ├── bookmarks.js │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── parser/ │ │ └── downloadURL.js │ ├── u2.dmhy.org/ │ │ └── config.json │ ├── ubits.club/ │ │ └── config.json │ ├── uhdbits.org/ │ │ ├── config.json │ │ ├── getSearchResult.js │ │ └── getUserSeedingTorrents.js │ ├── ultrahd.net/ │ │ └── config.json │ ├── wintersakura.net/ │ │ └── config.json │ ├── world-in-hd.net/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ └── getSearchResult.js │ ├── www.beitai.pt/ │ │ └── config.json │ ├── www.cgpeers.com/ │ │ └── config.json │ ├── www.cinematik.net/ │ │ ├── browse.js │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── getUserSeedingTorrents.js │ ├── www.empornium.sx/ │ │ └── config.json │ ├── www.filept.com/ │ │ └── config.json │ ├── www.gamegamept.com/ │ │ ├── config.json │ │ └── torrents.js │ ├── www.gaytor.rent/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── www.haidan.video/ │ │ ├── config.json │ │ └── getSearchResult.js │ ├── www.hdarea.co/ │ │ └── config.json │ ├── www.hitpt.com/ │ │ └── config.json │ ├── www.icc2022.com/ │ │ └── config.json │ ├── www.morethantv.me/ │ │ └── config.json │ ├── www.myanonamouse.net/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ ├── getUserSeedingTorrents.js │ │ └── torrents.js │ ├── www.okpt.net/ │ │ ├── config.json │ │ └── torrents.js │ ├── www.pttime.org/ │ │ └── config.json │ ├── www.ptzone.xyz/ │ │ └── config.json │ ├── www.skyey2.com/ │ │ ├── config.json │ │ └── getUserSeedingTorrents.js │ ├── www.torrentday.com/ │ │ └── config.json │ ├── www.torrentleech.org/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── torrents.js │ ├── www.torrentseeds.org/ │ │ ├── config.json │ │ ├── details.js │ │ ├── getSearchResult.js │ │ └── torrents.js │ ├── xingtan.one/ │ │ └── config.json │ ├── zhuque.in/ │ │ └── config.json │ └── zmpt.cc/ │ └── config.json ├── src/ │ ├── background/ │ │ ├── README.md │ │ ├── collection.ts │ │ ├── config.ts │ │ ├── contextMenus.ts │ │ ├── controller.ts │ │ ├── downloadHistory.ts │ │ ├── downloadQuene.ts │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── infoParser.ts │ │ ├── keepUploadTask.ts │ │ ├── omnibox.ts │ │ ├── pageParser.ts │ │ ├── plugins/ │ │ │ ├── OWSS.ts │ │ │ └── WebDAV.ts │ │ ├── searchResultSnapshot.ts │ │ ├── searcher.ts │ │ ├── service.ts │ │ ├── site.ts │ │ ├── syncStorage.ts │ │ ├── user.ts │ │ └── userData.ts │ ├── changelog/ │ │ ├── Index.vue │ │ └── index.ts │ ├── content/ │ │ ├── README.md │ │ └── index.ts │ ├── debugger/ │ │ ├── Index.vue │ │ └── index.ts │ ├── interface/ │ │ ├── common.ts │ │ ├── enum.ts │ │ └── types.expand.js │ ├── options/ │ │ ├── App.vue │ │ ├── assets/ │ │ │ └── contextMenu.scss │ │ ├── components/ │ │ │ ├── ColorSelector.vue │ │ │ ├── Content.vue │ │ │ ├── DownloadTo.vue │ │ │ ├── Footer.vue │ │ │ ├── MovieInfoCard.vue │ │ │ ├── Navigation.vue │ │ │ ├── Permissions.vue │ │ │ ├── SearchBox.vue │ │ │ ├── Topbar.vue │ │ │ ├── TorrentProgress.vue │ │ │ └── WorkingStatus.vue │ │ ├── i18n.ts │ │ ├── main.ts │ │ ├── plugins/ │ │ │ └── vuetify.ts │ │ ├── router.ts │ │ ├── shims-tsx.d.ts │ │ ├── shims-vue.d.ts │ │ ├── store.ts │ │ ├── typings.d.ts │ │ └── views/ │ │ ├── About.vue │ │ ├── AutoSignWarning.vue │ │ ├── Donate.vue │ │ ├── History.vue │ │ ├── Home.vue │ │ ├── SystemLogs.vue │ │ ├── Teams.vue │ │ ├── TechnologyStack.vue │ │ ├── UserDataTimeline.vue │ │ ├── collection/ │ │ │ ├── AddToGroup.vue │ │ │ ├── GroupCard.vue │ │ │ └── Index.vue │ │ ├── keepUpload/ │ │ │ └── KeepUploadTasks.vue │ │ ├── search/ │ │ │ ├── Actions.vue │ │ │ ├── AddToCollectionGroup.vue │ │ │ ├── KeepUpload.vue │ │ │ ├── SearchResultSnapshot.vue │ │ │ ├── SearchTorrent.scss │ │ │ ├── SearchTorrent.ts │ │ │ └── SearchTorrent.vue │ │ ├── settings/ │ │ │ ├── Backup/ │ │ │ │ ├── Index.vue │ │ │ │ └── Server/ │ │ │ │ ├── Add.vue │ │ │ │ ├── Edit.vue │ │ │ │ ├── Editor.vue │ │ │ │ └── List.vue │ │ │ ├── Base/ │ │ │ │ └── Index.vue │ │ │ ├── DownloadClients/ │ │ │ │ ├── Add.vue │ │ │ │ ├── Edit.vue │ │ │ │ ├── Editor.vue │ │ │ │ └── Index.vue │ │ │ ├── DownloadPaths/ │ │ │ │ ├── Add.vue │ │ │ │ ├── Edit.vue │ │ │ │ ├── Index.vue │ │ │ │ └── KeyDescription.vue │ │ │ ├── Language/ │ │ │ │ └── Index.vue │ │ │ ├── SearchSolution/ │ │ │ │ ├── Edit.vue │ │ │ │ ├── Editor.vue │ │ │ │ └── Index.vue │ │ │ ├── SitePlugins/ │ │ │ │ ├── Add.vue │ │ │ │ ├── Edit.vue │ │ │ │ ├── Editor.vue │ │ │ │ └── Index.vue │ │ │ ├── SiteSearchEntry/ │ │ │ │ ├── Add.vue │ │ │ │ ├── Edit.vue │ │ │ │ ├── Editor.vue │ │ │ │ └── Index.vue │ │ │ ├── Sites/ │ │ │ │ ├── Add.vue │ │ │ │ ├── Edit.vue │ │ │ │ ├── Editor.vue │ │ │ │ ├── Index.vue │ │ │ │ └── UserInfo.vue │ │ │ └── SupportSchema.vue │ │ └── statisticCharts/ │ │ └── SiteBase.vue │ ├── popup/ │ │ └── index.ts │ └── service/ │ ├── api.ts │ ├── backupFileParser.ts │ ├── clientController.ts │ ├── downloader.ts │ ├── extension.ts │ ├── favicon.ts │ ├── filters.ts │ ├── localStorage.ts │ ├── logger.ts │ ├── movieInfoService.ts │ ├── pathHandler.ts │ └── public.ts ├── tsconfig.json ├── update/ │ └── index.xml ├── vue.config.js └── webpack/ ├── common.js ├── dev-background.js ├── dev-content.js ├── prod-background.js └── prod-content.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "env": { "browser": true, "commonjs": true, "es6": true, "jquery": true } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report-cn.md ================================================ --- name: Bug 反馈 about: 发起一个Bug反馈 title: '' labels: bug assignees: '' --- **请确认你已经做过并了解如下步骤,在 `[]` 中填入 `x` 选中** - [ ] 仔细看并搜索过[Wiki](https://github.com/pt-plugins/PT-Plugin-Plus/wiki) - [ ] 请确认你的版本比当前[Pre-release](https://github.com/pt-plugins/PT-Plugin-Plus/tags)要新,我们建议使用crx版本作为标准 - [ ] 搜素过Issue,确认没有相关问题 - [ ] 不理解、询问问题或者使用上的问题请使用Discuissions - PT 助手版本: - PT 助手安装方式:(市场安装,或zip包安装) - 浏览器名称及版本: - 浏览器是否安装了其他插件: - 停用其他插件后是否正常工作: - 问题描述: - 相关截图: - 重现步骤: ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- - PT Plugin Plus version: - PT Plugin Plus installation method: (market installation, or zip package installation) - Browser name and version: - Whether the browser has other plugins installed: - Is it working properly after disabling other plugins: - Problem Description: - Related screenshots: - Reproduce steps: ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request-cn.md ================================================ --- name: 功能请求 about: 发起一个新功能请求 title: '' labels: enhancement assignees: '' --- ## 您的功能请求是否与问题有关? 请描述一下。 ## 描述你想要的解决方案 ## 描述您考虑过的替代方案 ## 其他附加信息 ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/new-tracker-request-cn.md ================================================ --- name: 新站点请求 about: 发起一个新站点支持请求 title: '' labels: 'New Tracker' assignees: '' --- - 站点名称: - 站点地址: - 站点描述: - 资源类型: - 开放注册:是/否 - 是否连坐:是/否 - 站点规则: ================================================ FILE: .github/ISSUE_TEMPLATE/new-tracker-request.md ================================================ --- name: New Tracker Request about: Initiate a new tracker support request title: '' labels: 'New Tracker' assignees: '' --- - Tracker Name: - Tracker Url: - Tracker Description: - Resource Type: - Open registration: Yes / No - Tracker rules: ================================================ FILE: .github/ISSUE_TEMPLATE/suggestions-or-comments.md ================================================ --- name: Suggestions or comments about: 其他建议或意见 title: '' labels: Suggestions assignees: '' --- - ================================================ FILE: .github/issue-close-app.yml ================================================ # Comment that will be sent if an issue is judged to be closed comment: "This issue is closed because it does not meet our issue template. Please read it.\n 您好,我是问题处理机器人,因为您没有按规定提交问题,所以被我自动关闭了,请按规定填写,谢谢!" issueConfigs: - content: # bug report - "PT Plugin Plus version" - "PT Plugin Plus installation method" - "Problem Description" - content: # bug report cn - "PT 助手版本" - "PT 助手安装方式" - "问题描述" - content: # feature request - "Is your feature request related to a problem" - "Describe the solution you'd like" - "Describe alternatives you've considered" - content: # feature request cn - "您的功能请求是否与问题有关" - "描述你想要的解决方案" - "描述您考虑过的替代方案" - content: # 建议和意见 - "请在上面描述您的建议或意见,并且不要删除这段内容" - "Please describe your suggestions or comments" - content: # 新站点请求 - "站点名称" - "站点地址" - "站点描述" - "资源类型" - "开放注册" - "是否连坐" - "站点规则" - content: # 新站点请求 - "Tracker Name" - "Tracker Url" - "Tracker Description" - "Resource Type" - "Open registration" - "Tracker rules" ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 90 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 7 # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - pinned - security - enhancement - Development - bug # Set to true to ignore issues in a project (defaults to false) exemptProjects: true # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: true # Set to true to ignore issues with an assignee (defaults to false) exemptAssignees: true # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. 此问题已自动标记为陈旧,因为它近期没有活动。如果没有进一步的活动,它将被自动关闭。感谢您的贡献。 # Comment to post when removing the stale label. # unmarkComment: > # Your comment here. # Comment to post when closing a stale Issue or Pull Request. closeComment: | This issue has been automatically closed due to no further feedback. If you need to continue processing, please submit a new issue. Thank you for your contributions. 此问题因没有进一步反馈已被自动关闭,如需继续处理,请提交新的 issue ,感谢您的贡献。 # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Limit to only `issues` or `pulls` # only: issues # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': # pulls: # daysUntilStale: 30 # markComment: > # This pull request has been automatically marked as stale because it has not had # recent activity. It will be closed if no further activity occurs. Thank you # for your contributions. # issues: # exemptLabels: # - confirmed ================================================ FILE: .github/workflows/build_action.yml ================================================ name: Build Action Release on: push: branches: [ dev ] pull_request: branches: [ dev ] workflow_call: inputs: ref: default: 'dev' type: string auto_update_file: default: 'canary.xml' type: string build_xpi: default: false type: boolean outputs: version: value: ${{ jobs.zip.outputs.version }} buildXPI: value: ${{ jobs.xpi.outputs.exist_xpi_file }} secrets: CRX_PRIVATE_KEY: required: true jobs: build: name: Build dist runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' cache: yarn - run: yarn - run: yarn build continue-on-error: true - name: Upload Built to action uses: actions/upload-artifact@v4 with: name: build-dist-folder path: dist zip: name: Build Zip runs-on: ubuntu-latest needs: build outputs: version: ${{ steps.zip.outputs.extensionVersion }} steps: - uses: actions/download-artifact@v4 with: name: build-dist-folder path: dist - id: zip name: Build Zip uses: cardinalby/webext-buildtools-pack-extension-dir-action@v1 with: extensionDir: 'dist' zipFilePath: 'artifact/extension.zip' - name: Upload Built Zip to action uses: actions/upload-artifact@v4 with: name: dev-build-${{ steps.zip.outputs.extensionVersion }}-zip path: artifact/* crx: name: Build Crx (Chromium) runs-on: ubuntu-latest needs: build steps: - name: Set name output id: auto_update_file env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} if: ${{ env.CRX_PRIVATE_KEY != '' }} run: | USER_INPUT=${{ inputs.auto_update_file }} echo "value=${USER_INPUT:-"stable.xml"}" >> $GITHUB_OUTPUT - uses: actions/download-artifact@v4 env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} if: ${{ env.CRX_PRIVATE_KEY != '' }} with: name: build-dist-folder path: dist - name: Add update_url env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} if: ${{ env.CRX_PRIVATE_KEY != '' }} run: echo $(jq '. |= .+ {"update_url":"https://pt-plugins.github.io/PT-Plugin-Plus/update/${{ steps.auto_update_file.outputs.value }}"}' dist/manifest.json) > dist/manifest.json - id: zip name: Build Zip With update_url uses: cardinalby/webext-buildtools-pack-extension-dir-action@v1 env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} if: ${{ env.CRX_PRIVATE_KEY != '' }} with: extensionDir: 'dist' zipFilePath: 'extension.zip' - name: Build Crx env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} if: ${{ env.CRX_PRIVATE_KEY != '' }} uses: cardinalby/webext-buildtools-chrome-crx-action@v2 with: zipFilePath: 'extension.zip' crxFilePath: 'artifact/extension.crx' privateKey: ${{ secrets.CRX_PRIVATE_KEY }} updateXmlPath: artifact/${{ steps.auto_update_file.outputs.value }} updateXmlCodebaseUrl: https://github.com/pt-plugins/PT-Plugin-Plus/releases/download/v${{ steps.zip.outputs.extensionVersion }}/PT-Plugin-Plus-${{ steps.zip.outputs.extensionVersion }}.crx - name: Upload Built Crx to action env: CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }} if: ${{ env.CRX_PRIVATE_KEY != '' }} uses: actions/upload-artifact@v4 with: name: dev-build-${{ steps.zip.outputs.extensionVersion }}-crx path: artifact/* xpi: name: Build Xpi (Firefox) runs-on: ubuntu-latest needs: zip if: ${{ inputs.build_xpi }} outputs: exist_xpi_file: ${{ steps.addonsDeploy.outcome }} steps: - uses: actions/download-artifact@v4 with: name: dev-build-${{ needs.zip.outputs.version }}-zip - uses: cardinalby/webext-buildtools-firefox-sign-xpi-action@1.0.6 id: addonsDeploy if: ${{ env.FF_EXT_UUID != '' }} env: FF_EXT_UUID: ${{ secrets.FF_EXT_UUID }} continue-on-error: true with: timeoutMs: 600000 zipFilePath: extension.zip xpiFilePath: artifact/extension.signed.xpi extensionId: ${{ secrets.FF_EXT_UUID }} jwtIssuer: ${{ secrets.FF_JWT_ISSUER }} jwtSecret: ${{ secrets.FF_JWT_SECRET }} - name: Upload Built Xpi to action uses: actions/upload-artifact@v4 if: ${{ steps.addonsDeploy.outcome == 'success' }} with: name: dev-build-${{ needs.zip.outputs.version }}-xpi path: artifact/* ================================================ FILE: .github/workflows/build_canary.yml ================================================ name: Build Canary Release on: workflow_dispatch: schedule: - cron: '0 20 1,15 * *' jobs: action: uses: ./.github/workflows/build_action.yml with: ref: 'dev' build_xpi: true secrets: inherit canary: runs-on: ubuntu-latest needs: action permissions: contents: write steps: - name: Checkout uses: actions/checkout@master with: ref: 'dev' fetch-depth: 0 - name: Checkout gh-pages uses: actions/checkout@master with: ref: 'gh-pages' path: 'pages' - name: Prepare Release run: mkdir build - name: Get And rename Zip build uses: actions/download-artifact@v4 with: name: dev-build-${{ needs.action.outputs.version }}-zip - run: mv extension.zip build/PT-Plugin-Plus-${{ needs.action.outputs.version }}.zip - name: Get And remove Crx Build uses: actions/download-artifact@v4 with: name: dev-build-${{ needs.action.outputs.version }}-crx - run: | mv canary.xml pages/update/canary.xml -f mv extension.crx build/PT-Plugin-Plus-${{ needs.action.outputs.version }}.crx - name: Get And move Xpi Build uses: actions/download-artifact@v4 if: ${{ needs.action.outputs.buildXPI == 'success' }} with: name: dev-build-${{ needs.action.outputs.version }}-xpi - if: ${{ needs.action.outputs.buildXPI == 'success' }} run: | mv extension.signed.xpi build/PT-Plugin-Plus-${{ needs.action.outputs.version }}.xpi echo $(jq '.addons[].updates += [{"version": "${{ needs.action.outputs.version }}", "update_link": "https://github.com/pt-plugins/PT-Plugin-Plus/releases/download/v${{ needs.action.outputs.version }}/PT-Plugin-Plus-${{ needs.action.outputs.version }}.xpi"}]' pages/update/firefox.json) > pages/update/firefox.json - name: Deploy update xml and json uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./pages commit_message: deploy ${{ github.ref }} force_orphan: true user_name: github-actions[bot] user_email: github-actions[bot]@users.noreply.github.com - uses: ncipollo/release-action@v1 with: name: v${{ needs.action.outputs.version }} tag: v${{ needs.action.outputs.version }} commit: 'dev' generateReleaseNotes: true prerelease: true artifacts: 'build/*' ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw* debug/dist # Chrome 插件文件 *.crx *.pem *.zip # 发布的二进制文件 /releases ================================================ FILE: .nvmrc ================================================ 16 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 栽培者 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ > [!WARNING] > PT-Plugin-Plus 项目已经进入停止维护期(具体说明见: https://github.com/pt-plugins/PT-Plugin-Plus/issues/2235 ) > > 我们推荐您使用全新的替代方案: **[PT-depiler](https://github.com/pt-plugins/PT-depiler)** 。 > PT-depiler 是 PT-Plugin-Plus 的继任者,保留了绝大多数核心功能并进行了全面优化,包括更好的兼容性、更稳定的性能和更丰富的功能支持。建议所有用户迁移至新项目以获得持续的更新和支持。 > [!TIP] > 如果你浏览器中使用的PTPP已经被禁用,你可以尝试以下方法 **重新恢复使用** 或 **导出数据以迁移至PT-depiler**: > > ① [使用浏览器 flags 临时启用](https://github.com/pt-plugins/PT-Plugin-Plus/wiki#%E6%88%91%E5%B7%B2%E7%BB%8F%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8%E4%BA%86%E5%BA%94%E8%AF%A5%E6%80%8E%E4%B9%88%E5%8A%9E) (需要 chrome < 139) > > ② [使用临时插件 PTPP Exporter](https://github.com/pt-plugins/PT-Plugin-Plus/discussions/2241) (仅适用于通过 crx 或者 zip 方法安装) > > ③ [使用低版本 chrome 抢救数据](https://github.com/pt-plugins/PT-Plugin-Plus/discussions/2242) (推荐!也可用于其他基于 chromium 但未移除 MV2 支持的浏览器)


Releases GitHub license Telegram

--- ## 关于 PT 助手 Plus,是一款浏览器插件(Web Extensions),一个可以提升 PT 站点使用效率的工具。 适用于各 PT 站,可使下载种子等各项操作变化更简单、快捷。配合下载服务器(如 Transmission、µTorrent 等),可一键下载指定的种子。 该版本是对原来的 [PT 助手](https://github.com/ronggang/PT-Plugin) 进行了重构,去掉了繁琐的配置,以获得更好的使用体验; > ~~注意:`1.0.0` 以下的配置不能直接用于该版本,请勿将 `1.0.0` 以下的版本配置进行导入操作。~~ 最新版本请登录后从[Pre-release](https://github.com/pt-plugins/PT-Plugin-Plus/releases?include_prereleases/latest)获取。如不会安装请参看Wiki **提Issue前请务必检查Dev版本、Pull Request以及之前的Issue** **M-Team 请于站点控制台 -> 实验室 获取 Token 填入后使用** ## 已支持的浏览器 - ![Google Chrome](https://img.shields.io/chrome-web-store/v/abkdiiddckphbigmakaojlnmakpllenb.svg?label=Google%20Chrome) (已下架,见[原因](https://github.com/pt-plugins/PT-Plugin-Plus/wiki#%E5%B7%B2%E8%A2%AB%E4%B8%8B%E6%9E%B6%E7%9A%84%E6%B5%8F%E8%A7%88%E5%99%A8)) - ![Mozilla Firefox](https://img.shields.io/amo/v/pt-plugin-plus.svg?label=Mozilla%20Firefox) (已下架,见[原因](https://github.com/pt-plugins/PT-Plugin-Plus/wiki#%E5%B7%B2%E8%A2%AB%E4%B8%8B%E6%9E%B6%E7%9A%84%E6%B5%8F%E8%A7%88%E5%99%A8)) - ![Microsoft Edge](https://img.shields.io/badge/dynamic/json?label=Edge%20Addons&prefix=v&query=%24.version&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2FAddons%2Fgetproductdetailsbycrxid%2Fekhingnlcjebipkdcgkkheigmljefepn) - 及其他基于 `Chromium` 内核的浏览器 ## 功能 - 一键发送指定的种子到下载服务器,目前已支持: - Transmission - Synology Download Station - µTorrent - Deluge - qBittorrent `v4.1+` - ruTorrent - Flood - 比 RSS 更灵活的下载方式: - 针对不同的站点发送到不同的下载服务器; - 针对不同的站点、下载服务器设置不同的保存路径; - 批量下载当前页所有种子; - 批量复制当前页面所有种子的下载链接(`部分站点需要设置 passkey`); - 显示默认下载服务器当前可用空间,目前已支持: - Transmission - 多站聚合搜索相同关键字的种子; - 查看 [已支持的站点列表](https://github.com/pt-plugins/PT-Plugin-Plus/wiki/supported-sites) - 根据当前站点显示专属功能,如: - 封面模式浏览种子页面; - 保存下载历史记录(默认关闭); - `豆瓣` 电影页面、[Top250](https://movie.douban.com/top250)、[选电影](https://movie.douban.com/explore) 一键搜索 PT 种子支持; - `IMDb` 电影页面、[Top250](https://www.imdb.com/chart/top?ref_=nv_mv_250) 一键搜索 PT 种子支持; - 更多功能请参考 [Wiki](https://github.com/pt-plugins/PT-Plugin-Plus/wiki) ; ## 安装及使用 - 如何安装和使用,请参考 [Wiki](https://github.com/pt-plugins/PT-Plugin-Plus/wiki) 的详细说明; - 常见问题可 [点这里](https://github.com/pt-plugins/PT-Plugin-Plus/wiki/frequently-asked-questions) 找到答案; ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ '@vue/app' ] } ================================================ FILE: debug/config/config.json ================================================ { "port": 8001, "from": "../../resource", "to": "/" } ================================================ FILE: debug/data/beforeSearching.json ================================================ { "count": 8, "start": 0, "total": 5608, "subjects": [ { "rating": { "max": 10, "average": 9.7, "stars": "50", "min": 0 }, "genres": [ "\u7eaa\u5f55\u7247" ], "title": "\u521b\u9020\u201c\u54c8\u5229\u00b7\u6ce2\u7279\u201d\u7684\u4e16\u754c\uff1a\u97f3\u4e50", "casts": [], "collect_count": 1184, "original_title": "Creating the World of Harry Potter, Part 4: Sound and Music", "subtype": "movie", "directors": [], "year": "2010", "images": { "small": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2493762829.webp", "large": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2493762829.webp", "medium": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2493762829.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/6718144\/", "id": "6718144" }, { "rating": { "max": 10, "average": 7.1, "stars": "35", "min": 0 }, "genres": [ "\u5267\u60c5" ], "title": "\u97f3\u4e50", "casts": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1342515\/", "avatars": { "small": "https://img1.doubanio.com\/f\/movie\/ca527386eb8c4e325611e22dfcb04cc116d6b423\/pics\/movie\/celebrity-default-small.png", "large": "https://img3.doubanio.com\/f\/movie\/63acc16ca6309ef191f0378faf793d1096a3e606\/pics\/movie\/celebrity-default-large.png", "medium": "https://img1.doubanio.com\/f\/movie\/8dd0c794499fe925ae2ae89ee30cd225750457b4\/pics\/movie\/celebrity-default-medium.png" }, "name": "\u9ed2\u6ca2\u306e\u308a\u5b50", "id": "1342515" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1323580\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1357565957.64.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1357565957.64.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1357565957.64.webp" }, "name": "\u7ec6\u5ddd\u4fca\u4e4b", "id": "1323580" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1039227\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1550212443.35.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1550212443.35.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1550212443.35.webp" }, "name": "\u9ad8\u6865\u957f\u82f1", "id": "1039227" } ], "collect_count": 117, "original_title": "\u97f3\u697d", "subtype": "movie", "directors": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1050508\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p17532.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p17532.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p17532.webp" }, "name": "\u589e\u6751\u4fdd\u9020", "id": "1050508" } ], "year": "1972", "images": { "small": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2509743913.webp", "large": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2509743913.webp", "medium": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2509743913.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/3169341\/", "id": "3169341" }, { "rating": { "max": 10, "average": 7.4, "stars": "40", "min": 0 }, "genres": [ "\u5267\u60c5" ], "title": "\u97f3\u4e50", "casts": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1000594\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p45463.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p45463.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p45463.webp" }, "name": "\u5fb7\u83f2\u56e0\u00b7\u585e\u91cc\u683c", "id": "1000594" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1016766\/", "avatars": { "small": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p14457.webp", "large": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p14457.webp", "medium": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p14457.webp" }, "name": "\u7f57\u8d1d\u5c14\u00b7\u4faf\u8d5b\u56e0", "id": "1016766" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1085159\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1471197409.54.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1471197409.54.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1471197409.54.webp" }, "name": "Julie Dassin", "id": "1085159" } ], "collect_count": 84, "original_title": "La Musica", "subtype": "movie", "directors": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1000758\/", "avatars": { "small": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p12067.webp", "large": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p12067.webp", "medium": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p12067.webp" }, "name": "\u739b\u683c\u4e3d\u7279\u00b7\u675c\u62c9\u65af", "id": "1000758" }, { "alt": null, "avatars": null, "name": "Paul Seban", "id": null } ], "year": "1967", "images": { "small": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2264840662.webp", "large": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2264840662.webp", "medium": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2264840662.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/3223707\/", "id": "3223707" }, { "rating": { "max": 10, "average": 0, "stars": "00", "min": 0 }, "genres": [ "\u559c\u5267", "\u5267\u60c5" ], "title": "\u97f3\u4e50", "casts": [], "collect_count": 1, "original_title": "Muzika", "subtype": "movie", "directors": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1034597\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1391190217.73.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1391190217.73.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1391190217.73.webp" }, "name": "Juraj Nvota", "id": "1034597" } ], "year": "2008", "images": { "small": "https://img3.doubanio.com\/view\/subject\/s\/public\/s3678106.jpg", "large": "https://img3.doubanio.com\/view\/subject\/l\/public\/s3678106.jpg", "medium": "https://img3.doubanio.com\/view\/subject\/m\/public\/s3678106.jpg" }, "alt": "https:\/\/movie.douban.com\/subject\/3617774\/", "id": "3617774" }, { "rating": { "max": 10, "average": 8.1, "stars": "40", "min": 0 }, "genres": [ "\u5267\u60c5", "\u7231\u60c5" ], "title": "\u542c\u8bf4", "casts": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1013782\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1368156632.65.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1368156632.65.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1368156632.65.webp" }, "name": "\u5f6d\u4e8e\u664f", "id": "1013782" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1274316\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p31663.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p31663.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p31663.webp" }, "name": "\u9648\u610f\u6db5", "id": "1274316" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1313303\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p37554.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p37554.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p37554.webp" }, "name": "\u9648\u598d\u5e0c", "id": "1313303" } ], "collect_count": 435902, "original_title": "\u807d\u8aaa", "subtype": "movie", "directors": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1276077\/", "avatars": { "small": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p13508.webp", "large": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p13508.webp", "medium": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p13508.webp" }, "name": "\u90d1\u82ac\u82ac", "id": "1276077" } ], "year": "2009", "images": { "small": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250788664.webp", "large": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250788664.webp", "medium": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250788664.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/3824672\/", "id": "3824672" }, { "rating": { "max": 10, "average": 9.0, "stars": "45", "min": 0 }, "genres": [ "\u5267\u60c5", "\u4f20\u8bb0", "\u7231\u60c5" ], "title": "\u97f3\u4e50\u4e4b\u58f0", "casts": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1041081\/", "avatars": { "small": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p4777.webp", "large": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p4777.webp", "medium": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p4777.webp" }, "name": "\u6731\u8389\u00b7\u5b89\u5fb7\u9c81\u65af", "id": "1041081" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1036321\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p42033.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p42033.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p42033.webp" }, "name": "\u514b\u91cc\u65af\u6258\u5f17\u00b7\u666e\u5362\u9ed8", "id": "1036321" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1010671\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1355087417.43.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1355087417.43.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1355087417.43.webp" }, "name": "\u57c3\u7433\u8bfa\u00b7\u5e15\u514b", "id": "1010671" } ], "collect_count": 453324, "original_title": "The Sound of Music", "subtype": "movie", "directors": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1049929\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1485851955.3.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1485851955.3.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1485851955.3.webp" }, "name": "\u7f57\u4f2f\u7279\u00b7\u6000\u65af", "id": "1049929" } ], "year": "1965", "images": { "small": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p453788577.webp", "large": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p453788577.webp", "medium": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p453788577.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/1294408\/", "id": "1294408" }, { "rating": { "max": 10, "average": 9.2, "stars": "50", "min": 0 }, "genres": [ "\u8131\u53e3\u79c0" ], "title": "\u542c\u8bf4 \u7b2c\u4e00\u5b63", "casts": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1350153\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1434704950.63.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1434704950.63.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1434704950.63.webp" }, "name": "\u9a6c\u4e16\u82b3", "id": "1350153" } ], "collect_count": 3941, "original_title": "\u542c\u8bf4 \u7b2c\u4e00\u5b63", "subtype": "tv", "directors": [ { "alt": null, "avatars": null, "name": "\u9648\u6021\u5206", "id": null } ], "year": "2015", "images": { "small": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250357437.webp", "large": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250357437.webp", "medium": "https://img1.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250357437.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/26425523\/", "id": "26425523" }, { "rating": { "max": 10, "average": 8.5, "stars": "45", "min": 0 }, "genres": [ "\u559c\u5267", "\u7231\u60c5", "\u97f3\u4e50" ], "title": "\u518d\u6b21\u51fa\u53d1\u4e4b\u7ebd\u7ea6\u9047\u89c1\u4f60", "casts": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1054448\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p10192.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p10192.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p10192.webp" }, "name": "\u51ef\u62c9\u00b7\u5948\u7279\u8389", "id": "1054448" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1040505\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p15885.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p15885.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p15885.webp" }, "name": "\u9a6c\u514b\u00b7\u9c81\u5f17\u6d1b", "id": "1040505" }, { "alt": "https:\/\/movie.douban.com\/celebrity\/1174312\/", "avatars": { "small": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1380782810.86.webp", "large": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1380782810.86.webp", "medium": "https://img3.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1380782810.86.webp" }, "name": "\u4e9a\u5f53\u00b7\u83b1\u6587", "id": "1174312" } ], "collect_count": 299015, "original_title": "Begin Again", "subtype": "movie", "directors": [ { "alt": "https:\/\/movie.douban.com\/celebrity\/1280127\/", "avatars": { "small": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1470662353.8.webp", "large": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1470662353.8.webp", "medium": "https://img1.doubanio.com\/view\/celebrity\/s_ratio_celebrity\/public\/p1470662353.8.webp" }, "name": "\u7ea6\u7ff0\u00b7\u5361\u5c3c", "id": "1280127" } ], "year": "2013", "images": { "small": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250287733.webp", "large": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250287733.webp", "medium": "https://img3.doubanio.com\/view\/photo\/s_ratio_poster\/public\/p2250287733.webp" }, "alt": "https:\/\/movie.douban.com\/subject\/6874403\/", "id": "6874403" } ], "title": "\u641c\u7d22 \"\u97f3\u4e50\" \u7684\u7ed3\u679c" } ================================================ FILE: debug/package.json ================================================ { "name": "pt-plugin-plus-test", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "body-parser": "^1.18.3", "cors": "^2.8.5", "express": "^4.16.4", "lodash": "^4.17.19" }, "devDependencies": { "@types/express": "^4.17.2", "@types/lodash": "^4.14.117", "@types/node": "^10.12.0", "ts-node": "^7.0.1", "typescript": "^3.1.3" } } ================================================ FILE: debug/src/App.ts ================================================ // 导入基础库 import * as Express from "express"; import * as cors from "cors"; import * as BodyParser from "body-parser"; import * as PATH from "path"; import * as FS from "fs"; import { BuildPlugin } from "./BuildPlugin"; import { SearchData } from "./SearchData"; /** * 默认APP */ class App { public express = Express(); public options; public systemConfig; public i18n; constructor(options) { this.options = options || { port: 80, from: "../resource", to: "/" }; let buildPlugin = new BuildPlugin("../../resource"); this.systemConfig = JSON.stringify(buildPlugin.getSystemConfig()); this.i18n = JSON.stringify(buildPlugin.geti18n()); this.useModules(); this.mountRoutes(); } /** * 使用一些模块 */ private useModules() { const from = PATH.join(__dirname, this.options.from); this.express.use(cors()); // 启用静态文件目录 this.express.use(this.options.to, Express.static(from)); this.express.use(BodyParser.json()); // for parsing application/json this.express.use(BodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded } /** * 挂载路由 */ private mountRoutes(): void { this.express.get("/systemConfig.json", (req, res) => { res.send(this.systemConfig); }); this.express.get("/i18n.json", (req, res) => { res.send(this.i18n); }); const config = JSON.parse(this.systemConfig); this.express.get("/test/searchData.json", (req, res) => { res.send(new SearchData(config).generate()); }); this.express.get("/test/*.*", (req, res) => { console.log(req.url); let fileName = (req.url as any).match(/test\/(.[^\?]+)/)[1]; console.log(fileName); let path = PATH.resolve(__dirname, "../data/"); let file = PATH.join(path, fileName); if (FS.existsSync(file)) { let content = FS.readFileSync(PATH.join(path, fileName), "utf-8"); if (fileName.substr(-5) === ".json") { res.send(JSON.parse(content)); } else { console.log(PATH.join(path, fileName)); res.sendFile(PATH.join(path, fileName)); // res.send(content); } } else { res.send("no file."); } }); } /** * 启动服务 */ public start() { this.express.listen(this.options.port, err => { if (err) { return console.log(err); } return console.log(`Base Web Service Run at ${this.options.port}`); }); } } export default App; ================================================ FILE: debug/src/BuildPlugin.ts ================================================ import * as FS from "fs"; import * as PATH from "path"; export type Dictionary = { [key: string]: T }; /** * 构建过程辅助工具 */ export class BuildPlugin { public resourcePath: string = ""; private resourceMap = ["sites", "schemas", "clients", "publicSites"]; constructor(rootPaht: string = "../../dist/resource") { this.resourcePath = PATH.resolve(__dirname, rootPaht); console.log(this.resourcePath); } /** * 创建资源文件列表 */ public buildResource() { let fileName = PATH.join(this.resourcePath, `systemConfig.json`); FS.writeFileSync(fileName, JSON.stringify(this.getSystemConfig())); fileName = PATH.join(this.resourcePath, `i18n.json`); FS.writeFileSync(fileName, JSON.stringify(this.geti18n())); } /** * 获取系统配置信息 */ public getSystemConfig() { let result = {}; this.resourceMap.forEach((name: string) => { result[name] = this.getResourceConfig(name); }); return result; } /** * 获取指定的资源配置信息 * @param name */ private getResourceConfig(name: string): any { let parentFolder = PATH.join(this.resourcePath, name); let list = FS.readdirSync(parentFolder); let results: any[] = []; list.forEach((path: string) => { let _path = PATH.join(parentFolder, path); var stat = FS.statSync(_path); // 仅获取目录 if (stat && stat.isDirectory()) { let file = PATH.join(_path, `config.json`); if (FS.existsSync(file)) { let content = JSON.parse(FS.readFileSync(file, "utf-8")); // 获取解析器 let parser = this.getParser(PATH.join(_path, "parser")); if (parser) { content["parser"] = parser; } results.push(content); } } }); return results; } /** * 创建架构和站点的解析器 */ private makeParser(name: string) { let parentFolder = PATH.join(this.resourcePath, name); let list = FS.readdirSync(parentFolder); list.forEach((path: string) => { let _path = PATH.join(parentFolder, path); var stat = FS.statSync(_path); // 仅获取目录 if (stat && stat.isDirectory()) { let parser = this.getParser(PATH.join(_path, "parser")); if (parser) { let fileName = PATH.join(_path, `config.json`); let content = JSON.parse(FS.readFileSync(fileName, "utf-8")); content["parser"] = parser; FS.writeFileSync(fileName, JSON.stringify(content)); } } }); } /** * 获取解析器 * @param parentFolder */ private getParser(parentFolder): any { if (!FS.existsSync(parentFolder)) { return null; } let list = FS.readdirSync(parentFolder); let results: any = {}; list.forEach((path: string) => { let _path = PATH.join(parentFolder, path); var stat = FS.statSync(_path); // 仅获取目录 if (stat && stat.isFile() && PATH.extname(_path) == ".js") { results[PATH.basename(_path, ".js")] = FS.readFileSync(_path, "utf-8"); } }); return results; } /** * 获取已支持站点列表 */ public getSupportedSites() { let schemaFolder = PATH.join(this.resourcePath, "schemas"); let schemaList = FS.readdirSync(schemaFolder); let schemas: any = {}; schemaList.forEach((path: string) => { let file = PATH.join(schemaFolder, path); var stat = FS.statSync(file); // 仅获取目录 if (stat && stat.isDirectory()) { schemas[path] = []; } }); schemas["其他架构"] = []; let parentFolder = PATH.join(this.resourcePath, "sites"); let list = FS.readdirSync(parentFolder); let itemTemplate = "| $schema$ | $name$ | $search$ | $imdbSearch$ | $userData$ | $sendTorrent$ | $torrentProgress$ | $collaborator$ |"; list.forEach((path: string) => { let file = PATH.join(parentFolder, path); var stat = FS.statSync(file); // 仅获取目录 if (stat && stat.isDirectory()) { let fileName = PATH.join(file, `config.json`); let content = JSON.parse(FS.readFileSync(fileName, "utf-8")); let schema = content.schema; if (!schemas[schema]) { schema = "其他架构"; } let supportedFeatures = { search: true, imdbSearch: true, userData: true, sendTorrent: true }; if (content.supportedFeatures) { supportedFeatures = Object.assign( supportedFeatures, content.supportedFeatures ); } // 判断是否有跳过 IMDb 选项,有则定为不支持 IMDb if (content.searchEntryConfig) { if (content.searchEntryConfig.skipIMDbId === true) { supportedFeatures.imdbSearch = false; } } let count = schemas[schema].length; let item = this.replaceKeys(itemTemplate, { schema: count == 0 ? schema : "", name: content.name, search: supportedFeatures.search === true ? "√" : "", imdbSearch: supportedFeatures.imdbSearch === true ? "√" : "", userData: supportedFeatures.userData === true ? "√" : supportedFeatures.userData === false ? "" : supportedFeatures.userData, sendTorrent: supportedFeatures.sendTorrent === true ? "√" : "", torrentProgress: content.searchEntryConfig && content.searchEntryConfig.fieldSelector && content.searchEntryConfig.fieldSelector.progress ? "√" : "", collaborator: this.getCollaborator(content.collaborator) }); schemas[schema].push(item); } }); console.log("\n"); for (const key in schemas) { if (schemas.hasOwnProperty(key)) { const items: Array = schemas[key]; // console.log(`\n## ${key}`); items.forEach((item: string) => { console.log(item); }); } } console.log("\n"); // console.log(results); } public getCollaborator(source: string | Array): string { if (!source) { return ""; } if (typeof source == "string") { return source; } else if (source.length > 0) { let result: Array = []; source.forEach((item: string) => { result.push(item); }); return result.join(", "); } return ""; } /** * 获取语言配置信息 */ public geti18n() { let parentFolder = PATH.join(this.resourcePath, "i18n"); let list = FS.readdirSync(parentFolder); let results: Array = []; list.forEach((path: string) => { let file = PATH.join(parentFolder, path); var stat = FS.statSync(file); // 获取语言配置文件 if (stat && stat.isFile() && PATH.extname(file) == ".json") { let content = JSON.parse(FS.readFileSync(file, "utf-8")); if (content && content.code && content.name) { console.log(path, content.name); results.push({ name: content.name, code: content.code }); } } }); return results; } /** * 替换指定的字符串列表 * @param source * @param keys */ replaceKeys( source: string, keys: Dictionary, prefix: string = "" ): string { let result: string = source; for (const key in keys) { if (keys.hasOwnProperty(key)) { const value = keys[key]; let search = "$" + key + "$"; if (prefix) { search = `$${prefix}.${key}$`; } result = result.replace(search, value); } } return result; } } ================================================ FILE: debug/src/SearchData.ts ================================================ /** * 生成搜索测试数据 */ export class SearchData { constructor(public config: any = {}) {} public generate() { let results: any[] = []; let count = Math.floor(Math.random() * 100); const status = [1, 2, 255, undefined]; for (let i = 0; i < count; i++) { let host = this.getHost(); let data = { title: this.getTitle(), subTitle: this.getSubTitle(), link: `https://${host}/details.php?id=${i}`, url: `https://${host}/download.php?id=${i}`, size: this.getSize(), time: Math.floor( new Date().getTime() / 1000 - Math.floor(Math.random() * 10000000) ), author: "匿名", seeders: Math.floor(Math.random() * 1000), leechers: Math.floor(Math.random() * 1000), completed: Math.floor(Math.random() * 1000), comments: Math.floor(Math.random() * 1000), host: host, tags: this.getTags(), entryName: "全部", progress: Math.floor(Math.random() * 100), status: status[Math.floor(Math.random() * status.length)] }; results.push(data); } return JSON.stringify(results); } private getTitle() { let title: string[] = ["The Shawshank Redemption 1994"]; let datas = ["BluRay", "720p", "1080p", "x265", "10bit"]; const count = Math.floor(Math.random() * datas.length) - 1; if (count <= 0) { return title.join(" "); } for (let i = 0; i < count; i++) { let index = Math.floor(Math.random() * datas.length); title.push(datas[index]); datas.splice(index, 1); } return title.join(" "); } private getSubTitle() { let title: string[] = ["肖申克的救赎"]; let datas = [" / 刺激1995(台)", " / 月黑高飞(港)", "英简繁特效"]; const count = Math.floor(Math.random() * datas.length) - 1; if (count <= 0) { return title.join(" "); } for (let i = 0; i < count; i++) { let index = Math.floor(Math.random() * datas.length); title.push(datas[index]); datas.splice(index, 1); } return title.join(" "); } private getHost() { let index = Math.floor(Math.random() * this.config.sites.length); return this.config.sites[index].host; } private getTags() { let datas = [ { name: "Free", color: "blue" }, { name: "2xFree", color: "green" }, { name: "2xUp", color: "lime" }, { name: "2x50%", color: "light-green" }, { name: "30%", color: "indigo" }, { name: "50%", color: "orange" } ]; const index = Math.floor(Math.random() * datas.length) - 1; if (index <= 0) { return []; } return [datas[index]]; } private getSize() { const units = ["MB", "GB"]; return ( (Math.random() * 1000).toFixed(2) + units[Math.floor(Math.random() * units.length)] ); } } ================================================ FILE: debug/src/buildResource.ts ================================================ import { BuildPlugin } from "./BuildPlugin"; let buildPlugin = new BuildPlugin(); buildPlugin.buildResource(); buildPlugin.getSupportedSites(); console.log("编译完成于:%s \n", new Date().toLocaleString()); ================================================ FILE: debug/src/index.ts ================================================ import App from "./App"; import * as config from "../config/config.json"; new App(config).start(); ================================================ FILE: debug/tsconfig.json ================================================ // { // "compilerOptions": { // /* Basic Options */ // "target": "es5", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ // "module": "commonjs", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // // "lib": [], /* Specify library files to be included in the compilation. */ // // "allowJs": true, /* Allow javascript files to be compiled. */ // // "checkJs": true, /* Report errors in .js files. */ // // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // // "declaration": true, /* Generates corresponding '.d.ts' file. */ // // "sourceMap": true, /* Generates corresponding '.map' file. */ // // "outFile": "transmission.js", // /* Concatenate and emit output to single file. */ // // "outDir": "dist", // /* Redirect output structure to the directory. */ // // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // // "removeComments": true, /* Do not emit comments to output. */ // // "noEmit": true, /* Do not emit outputs. */ // // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ // /* Strict Type-Checking Options */ // "strict": true, // /* Enable all strict type-checking options. */ // // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // // "strictNullChecks": true, /* Enable strict null checks. */ // // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // /* Additional Checks */ // // "noUnusedLocals": true, /* Report errors on unused locals. */ // // "noUnusedParameters": true, /* Report errors on unused parameters. */ // // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // /* Module Resolution Options */ // // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // // "typeRoots": [], /* List of folders to include type definitions from. */ // // "types": [], /* Type declaration files to be included in compilation. */ // // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // /* Source Map Options */ // // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ // // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ // /* Experimental Options */ // // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // }, // "include": [ // "src/**/*" // ], // "exclude": [ // "node_modules", // "typings/main", // "typings/main.d.ts" // ] // } { // tsconfig 所在的根目录, 则是一个project "compilerOptions": { "module": "commonjs", // 模块系统 "target": "es2015", // 生成目标, 一般选择ES6,因为不是客户端环境,没必要还编译成 ES5 "outDir": "dist", // 一组严苛的编译选项 "noImplicitAny": false, "strictNullChecks": true, "strict": true, "alwaysStrict": true, "sourceMap": false, "noImplicitReturns": true, "noImplicitThis": true, "pretty": true, "listFiles": true, // 包含了哪些库,这个必要的时候还是很有用的 "listEmittedFiles": true, "lib": [ // 要那些 lib,按需选择即可 "es2016" ], // "noUnusedLocals": true, // "noUnusedParameters": true, // "noFallthroughCasesInSwitch": true, // 指定库的搜索路径,这个比较有用,一般会指定 @types,还可以按需添加 "typeRoots": [ "./node_modules/@types" ] // 库搜索路径下, 仅使用哪些库, 一般没啥用 // "types": [ // ] }, // file include会算出一个交集, 指明哪些是项目的 ts 文件 "include": [ "./**/*" ], // 排除项目下面不符合要求的文件,这个按需设定即可,可以放心排除乱七八糟的文件 "exclude": [ "node_modules", "**/*.spec.ts", "*.js" ] } ================================================ FILE: debug/typings.d.ts ================================================ declare module "*.json" { const value: any; export default value; } ================================================ FILE: package.json ================================================ { "name": "pt-plugin-plus", "version": "1.6.1", "packageManager": "yarn@1.19.1", "author": { "name": "ronggang", "url": "https://github.com/ronggang" }, "archiverName": "PT-Plugin-Plus", "displayName": "PT 助手 Plus", "homepage": "https://github.com/pt-plugins/PT-Plugin-Plus", "scripts": { "serve": "set NODE_OPTIONS=--openssl-legacy-provider & vue-cli-service serve --mode=test", "build": "set NODE_OPTIONS=--openssl-legacy-provider & yarn build:index && yarn build:background && yarn build:content && yarn resource", "lint": "vue-cli-service lint", "background": "webpack --config webpack/prod-background.js && webpack --config webpack/prod-content.js", "dev": "yarn dev:index && yarn dev:background && yarn dev:content && yarn resource", "dev-s": "cd debug && yarn install && tsc && node ./dist/index.js", "dev:index": "yarn install && vue-cli-service build --mode=development", "dev:background": "webpack --config webpack/dev-background.js --progress", "dev:content": "webpack --config webpack/dev-content.js --progress", "dev:bc": "yarn dev:background && yarn dev:content", "resource": "cd debug && yarn install && tsc && node ./dist/buildResource.js", "build:index": "yarn install && vue-cli-service build", "build:background": "webpack --config webpack/prod-background.js --progress", "build:content": "webpack --config webpack/prod-content.js --progress" }, "dependencies": { "@typescript-eslint/eslint-plugin": "^4.4.0", "@typescript-eslint/parser": "^4.4.0", "basiccontext": "^3.5.1", "blueimp-md5": "^2.19.0", "crypto-js": "^3.1.9-1", "dayjs": "^1.11.5", "dom-to-image": "^2.6.0", "dotenv": "^8.2.0", "extend": "^3.0.2", "file-saver": "^2.0.5", "github-markdown-css": "^5.1.0", "highcharts": "^10.2.1", "highcharts-vue": "^1.4.0", "i18next": "^21.9.1", "jszip": "^3.10.1", "marked": "^4.2.4", "parse-torrent": "^7.0.1", "ua-parser-js": "^1.0.2", "url-parse": "^1.5.10", "vue": "~2.6.14", "vue-class-component": "^6.3.2", "vue-i18n": "^8.11.2", "vue-property-decorator": "^7.0.0", "vue-router": "~3.5.4", "vuetify": "^1.3.0", "vuex": "^3.0.1", "webdav": "^3.6.2" }, "devDependencies": { "@types/blueimp-md5": "^2.18.0", "@types/chrome": "^0.0.75", "@types/crypto-js": "^3.1.43", "@types/dom-to-image": "^2.6.4", "@types/extend": "^3.0.1", "@types/file-saver": "^2.0.5", "@types/jquery": "^3.5.14", "@types/marked": "^4.0.8", "@types/parse-torrent": "^5.8.4", "@types/ua-parser-js": "^0.7.36", "@types/url-parse": "^1.4.8", "@vue/cli-plugin-babel": "^3.0.5", "@vue/cli-plugin-eslint": "^5.0.0", "@vue/cli-plugin-typescript": "^3.2.0", "@vue/cli-service": "^3.0.5", "@vue/eslint-config-typescript": "^11.0.0", "babel-eslint": "^10.0.1", "copy-webpack-plugin": "^4.6.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^9.0.0", "git-rev-sync": "^3.0.2", "sass": "^1.54.8", "sass-loader": "~7.3.1", "stylus": "^0.54.5", "stylus-loader": "^3.0.1", "terser-webpack-plugin": "^2.2.1", "ts-loader": "^5.3.1", "ts-node": "^8.5.2", "typescript": "^3.0.0", "uglifyjs-webpack-plugin": "~2.1.3", "vue-cli-plugin-vuetify": "^0.4.6", "vue-template-compiler": "~2.6.14", "vuetify-loader": "~1.7.3", "webpack": "^4.46.0", "webpack-cli": "^3.3.12", "webpack-merge": "^4.2.2" }, "resolutions": { "@types/node": "~18.11.9" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended", "@vue/typescript" ], "rules": { "no-console": 0 }, "parserOptions": { "parser": "@typescript-eslint/parser" } }, "postcss": { "plugins": { "autoprefixer": {} } }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] } ================================================ FILE: privacy-statement.md ================================================ 感谢使用PT助手(下称『助手』),为了让您能够安心的使用助手,特此向您说明助手的隐私权保护政策,以保障您的权益,请您详阅下列内容: ## 一、隐私权保护政策的适用范围 - 隐私权保护政策仅适用于助手,不适用于助手以外的相关网站; ## 二、个人信息的收集、处理及利用方式 - 在您使用助手的过程中,助手会使用您的个人信息进行展示,展示内容包括: - 您在已配置站点里的个人信息; - 已保存的个人信息历史记录生成图表; - 个人信息仅用于在助手里展示,不会用于其他用途; ## 三、信息存储和交换 - 在您使用助手的过程中,会产生一些配置数据和历史记录,这些信息全部存储于当前浏览器; - 当您已配置了备份服务器,这些信息会由您决定是否上传至这些服务器; - 除此之外,助手不会将这些数据上传至任何第三方服务器; - 如果您选择了使用备份服务器,我们强烈建议您使用以下方式进行配置: - 使用 `https` 的方式配置服务器地址; - 启用备份数据加密(采用 [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard))功能后再进行备份操作; ## 四、与第三人共用个人信息之政策 - 助手不会收集任何相关的个人信息,所以助手绝不会提供、交换、出租或出售任何您的个人信息给其他个人、团体、私人企业或公务机关; ## 五、Cookie之使用 - 当您进行搜索操作时,助手会访问您已配置的站点,Cookie 由浏览器提供,助手无权访问也不会对内容进行探测和修改,一切内容由浏览器和站点进行相互验证; - 当您授权助手访问 Cookie 信息时,这些信息仅用于备份和恢复操作,助手不会在除此之外的任何其他操作中使用它们; ## 六、隐私权保护政策之修正 - 助手隐私权保护政策将按用户需求变更而随时进行修正,修正后的条款将在本页面显示。 ================================================ FILE: public/_locales/en/messages.json ================================================ { "manifest_appName": { "message": "PT Plugin Plus" }, "manifest_shortName": { "message": "PT Plugin Plus" }, "manifest_appDescription": { "message": "Just a tools for Private Tracker." } } ================================================ FILE: public/_locales/zh_CN/messages.json ================================================ { "manifest_appName": { "message": "PT Plugin Plus" }, "manifest_shortName": { "message": "PTPP" }, "manifest_appDescription": { "message": "PT 助手,一个可以提升 PT 站点使用效率的工具。" } } ================================================ FILE: public/assets/base.css ================================================ .pt-plugin-body { background-color: aliceblue; border-radius: 8px; padding-bottom: 5px; width: 80px; position: fixed; right: 5px; opacity: .3; z-index: 10000; font-size: 12px; } .pt-plugin-body .pt-plugin-drag-title { cursor: move; width: 100%; border-top-left-radius: 5px; border-top-right-radius: 5px; background-color: #ffc107; height: 7px; } .pt-plugin-body:hover, .pt-plugin-body-over { opacity: 0.9 } .pt-plugin-body hr { height: 2px; border: 0; border-bottom: 1px dotted #ccc; margin: 4px 5px 5px 5px; } .pt-plugin-body .pt-plugin-logo { background-image: url('chrome-extension://__MSG_@@extension_id__/assets/icon-64.png'); background-size: cover; height: 32px; width: 32px; margin: 5px auto; opacity: 0.7; cursor: pointer; } @-moz-document url-prefix() { .pt-plugin-body .pt-plugin-logo { background-image: url('moz-extension://__MSG_@@extension_id__/assets/icon-64.png'); } } .pt-plugin-body .pt-plugin-droper { height: 45px; width: 100%; background-color: #1976d2; opacity: 0.2; border-radius: 5px; position: absolute; top: 0; left: 0; z-index: 100; } .pt-plugin-body .pt-plugin-button { height: 50px; display: flex; justify-content: center; align-items: center; color: #1976d2; text-decoration: none; } .pt-plugin-body a.pt-plugin-button { cursor: pointer; text-decoration: none; color: #1976d2; } .pt-plugin-body a.pt-plugin-button:hover { background-color: rgba(77, 154, 231, 0.226) } .pt-plugin-body .pt-plugin-button .pt-plugin-button-inner { text-align: center; } .pt-plugin-body .pt-plugin-onLoading { animation: onLoading 1.9s linear infinite running; } /* 正在执行动画 */ .pt-plugin-body .pt-plugin-loading { width: 40px; height: 40px; border-radius: 50%; border-width: 5px; border-style: solid; border-color: #1976d2; border-left-style: dashed; border-left-color: rgba(77, 154, 231, 0.226); animation: onLoading 1.9s linear infinite running; display: none; } @keyframes onLoading { 100% { transform: rotate(360deg); } } .action-success { width: 40px; height: 40px; position: relative; /* background: #4caf50; */ background-color: transparent; border-radius: 50%; display: flex; justify-content: center; align-items: center; display: none; } .action-success-ani { width: 100%; height: 50%; margin: 10% auto; transform: rotate(-45deg); transform: rotate(-45deg); overflow: hidden; } .action-success-ani:before, .action-success-ani:after { content: ""; position: absolute; background: #4caf50; border-radius: 2px } .action-success-ani:before { width: 4px; height: 100%; left: 0; animation: onActionSuccessLeft 0.2s linear 0.2s 1 both; animation: onActionSuccessLeft 0.2s linear 0.2s 1 both } .action-success-ani:after { width: 100%; height: 4px; bottom: 0; animation: onActionSuccessRight 0.2s linear 0.4s 1 both; animation: onActionSuccessRight 0.2s linear 0.4s 1 both } @keyframes onActionSuccessLeft { 0% { top: -100% } 100% { top: 0% } } @keyframes onActionSuccessLeft { 0% { top: -100% } 100% { top: 0% } } @keyframes onActionSuccessRight { 0% { left: -100% } 100% { left: 0% } } @keyframes onActionSuccessRight { 0% { left: -100% } 100% { left: 0% } } ================================================ FILE: public/assets/options.css ================================================ .btn-mini { width: 20px !important; height: 20px !important; font-size: 12px !important; } .btn-mini .v-btn__content i { font-size: 12px !important; } ================================================ FILE: public/changelog.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: public/index.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: public/libs/Base64.js ================================================ /** * * Base64 encode / decode * * @author haitao.tu * @date 2010-04-26 * @email tuhaitao@foxmail.com * */ function Base64() { // private property _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; // public method for encoding this.encode = function (input) { var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = _utf8_encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4); } return output; } // public method for decoding this.decode = function (input) { var output = ""; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); while (i < input.length) { enc1 = _keyStr.indexOf(input.charAt(i++)); enc2 = _keyStr.indexOf(input.charAt(i++)); enc3 = _keyStr.indexOf(input.charAt(i++)); enc4 = _keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } } output = _utf8_decode(output); return output; } // private method for UTF-8 encoding _utf8_encode = function (string) { string = string.replace(/\r\n/g,"\n"); var utftext = ""; for (var n = 0; n < string.length; n++) { var c = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if((c > 127) && (c < 2048)) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } return utftext; } // private method for UTF-8 decoding _utf8_decode = function (utftext) { var string = ""; var i = 0; var c = c1 = c2 = 0; while ( i < utftext.length ) { c = utftext.charCodeAt(i); if (c < 128) { string += String.fromCharCode(c); i++; } else if((c > 191) && (c < 224)) { c2 = utftext.charCodeAt(i+1); string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; } else { c2 = utftext.charCodeAt(i+1); c3 = utftext.charCodeAt(i+2); string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; } } return string; } } ================================================ FILE: public/libs/drag.js ================================================ /** * 拖放功能 * @see http://demo.jb51.net/js/2015/js-mxdx-draw-plug-codes/ */ function Drag() { // 初始化 this.initialize.apply(this, arguments); } Drag.prototype = { // 参数设置 setOptions: function(options) { this.options = { // 事件对象 handle: this.drag, // 锁定范围 limit: true, // 锁定位置 lock: false, // 锁定水平位置 lockX: false, // 锁定垂直位置 lockY: false, // 指定限制容器 maxContainer: document.documentElement || document.body, // 开始时回调函数 onStart: function() {}, // 拖拽时回调函数 onMove: function() {}, // 停止时回调函数 onStop: function() {} }; for (var p in options) this.options[p] = options[p]; }, // 初始化 initialize: function(drag, options) { this.drag = this.$(drag); this._x = this._y = this.left = this.top = 0; this._moveDrag = this.bind(this, this.moveDrag); this._stopDrag = this.bind(this, this.stopDrag); this.setOptions(options); this.handle = this.$(this.options.handle); this.maxContainer = this.$(this.options.maxContainer); this.maxTop = Math.max(this.maxContainer.clientHeight, this.maxContainer.scrollHeight) - this.drag.offsetHeight; this.maxLeft = Math.max(this.maxContainer.clientWidth, this.maxContainer.scrollWidth) - this.drag.offsetWidth; this.limit = this.options.limit; this.lockX = this.options.lockX; this.lockY = this.options.lockY; this.lock = this.options.lock; this.onStart = this.options.onStart; this.onMove = this.options.onMove; this.onStop = this.options.onStop; this.handle.style.cursor = "move"; this.addHandler(this.handle, "mousedown", this.bind(this, this.startDrag)); }, startDrag: function(event) { var event = event || window.event; this._x = event.clientX - this.drag.offsetLeft; this._y = event.clientY - this.drag.offsetTop; this.addHandler(document, "mousemove", this._moveDrag); this.addHandler(document, "mouseup", this._stopDrag); event.preventDefault && event.preventDefault(); this.handle.setCapture && this.handle.setCapture(); this.onStart(); }, moveDrag: function(event) { var event = event || window.event; var iTop = event.clientY - this._y; var iLeft = event.clientX - this._x; if (this.lock) return; this.limit && (iTop < 0 && (iTop = 0), iLeft < 0 && (iLeft = 0), iTop > this.maxTop && (iTop = this.maxTop), iLeft > this.maxLeft && (iLeft = this.maxLeft)); this.lockY || (this.drag.style.top = iTop + "px"); this.lockX || (this.drag.style.left = iLeft + "px"); event.preventDefault && event.preventDefault(); this.left = iLeft; this.top = iTop; this.onMove(); }, stopDrag: function() { this.removeHandler(document, "mousemove", this._moveDrag); this.removeHandler(document, "mouseup", this._stopDrag); this.handle.releaseCapture && this.handle.releaseCapture(); this.onStop({ left: this.left, top: this.top }); }, // 获取id $: function(id) { return typeof id === "string" ? document.getElementById(id) : id; }, // 添加绑定事件 addHandler: function(oElement, sEventType, fnHandler) { return oElement.addEventListener ? oElement.addEventListener(sEventType, fnHandler, false) : oElement.attachEvent("on" + sEventType, fnHandler); }, // 删除绑定事件 removeHandler: function(oElement, sEventType, fnHandler) { return oElement.removeEventListener ? oElement.removeEventListener(sEventType, fnHandler, false) : oElement.detachEvent("on" + sEventType, fnHandler); }, // 绑定事件到对象 bind: function(object, fnHandler) { return function() { return fnHandler.apply(object, arguments); }; } }; ================================================ FILE: public/libs/materialIcons/content_style.css ================================================ /* fallback */ @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url('chrome-extension://__MSG_@@extension_id__/libs/materialIcons/font.woff2') format('woff2'); } /*This will work for firefox*/ @-moz-document url-prefix() { @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url('moz-extension://__MSG_@@extension_id__/libs/materialIcons/font.woff2') format('woff2'); } } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; } ================================================ FILE: public/libs/materialIcons/style.css ================================================ /* fallback */ @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; src: url(font.woff2) format('woff2'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; word-wrap: normal; direction: ltr; -webkit-font-smoothing: antialiased; } ================================================ FILE: public/libs/notice/notice.js ================================================ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("NoticeJs", [], factory); else if(typeof exports === 'object') exports["NoticeJs"] = factory(); else root["NoticeJs"] = factory(); })(typeof self !== 'undefined' ? self : this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = "dist/"; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 2); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var noticeJsModalClassName = exports.noticeJsModalClassName = 'noticejs-modal'; var closeAnimation = exports.closeAnimation = 'noticejs-fadeOut'; var Defaults = exports.Defaults = { title: '', text: '', type: 'success', position: 'topRight', newestOnTop: false, timeout: 30, progressBar: true, indeterminate: false, closeWith: ['button'], animation: null, modal: false, width: 320, scroll: { maxHeightContent: 300, showOnHover: true }, rtl: false, callbacks: { beforeShow: [], onShow: [], afterShow: [], onClose: [], afterClose: [], onClick: [], onHover: [], onTemplate: [] } }; /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.appendNoticeJs = exports.addListener = exports.CloseItem = exports.AddModal = undefined; exports.getCallback = getCallback; var _api = __webpack_require__(0); var API = _interopRequireWildcard(_api); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } var options = API.Defaults; /** * @param {NoticeJs} ref * @param {string} eventName * @return {void} */ function getCallback(ref, eventName) { if (ref.callbacks.hasOwnProperty(eventName)) { ref.callbacks[eventName].forEach(function (cb) { if (typeof cb === 'function') { cb.apply(ref); } }); } } var AddModal = exports.AddModal = function AddModal() { if (document.getElementsByClassName(API.noticeJsModalClassName).length <= 0) { var element = document.createElement('div'); element.classList.add(API.noticeJsModalClassName); element.classList.add('noticejs-modal-open'); document.body.appendChild(element); // Remove class noticejs-modal-open setTimeout(function () { element.className = API.noticeJsModalClassName; }, 200); } }; var CloseItem = exports.CloseItem = function CloseItem(item) { getCallback(options, 'onClose'); // Set animation to close notification item if (options.animation !== null && options.animation.close !== null) { item.className += ' ' + options.animation.close; } setTimeout(function () { item.remove(); }, 200); // Close modal if (options.modal === true && document.querySelectorAll("[noticejs-modal='true']").length >= 1) { document.querySelector('.noticejs-modal').className += ' noticejs-modal-close'; setTimeout(function () { document.querySelector('.noticejs-modal').remove(); }, 500); } // Remove container var position = '.' + item.closest('.noticejs').className.replace('noticejs', '').trim(); setTimeout(function () { if (document.querySelectorAll(position + ' .item').length <= 0) { document.querySelector(position) && document.querySelector(position).remove(); } }, 500); }; var addListener = exports.addListener = function addListener(item) { // Add close button Event if (options.closeWith.includes('button')) { item.querySelector('.close').addEventListener('click', function () { CloseItem(item); }); } // Add close by click Event if (options.closeWith.includes('click')) { item.style.cursor = 'pointer'; item.addEventListener('click', function (e) { if (e.target.className !== 'close') { getCallback(options, 'onClick'); CloseItem(item); } }); } else { item.addEventListener('click', function (e) { if (e.target.className !== 'close') { getCallback(options, 'onClick'); } }); } item.addEventListener('mouseover', function () { getCallback(options, 'onHover'); }); }; var appendNoticeJs = exports.appendNoticeJs = function appendNoticeJs(noticeJsHeader, noticeJsBody, noticeJsProgressBar) { var target_class = '.noticejs-' + options.position; // Create NoticeJs item var noticeJsItem = document.createElement('div'); noticeJsItem.classList.add('item'); noticeJsItem.classList.add(options.type); if (options.rtl === true) { noticeJsItem.classList.add('noticejs-rtl'); } if (options.width !== '' && Number.isInteger(options.width)) { noticeJsItem.style.width = options.width + 'px'; } // Add Header if (noticeJsHeader && noticeJsHeader !== '') { noticeJsItem.appendChild(noticeJsHeader); } // Add body noticeJsItem.appendChild(noticeJsBody); // Add progress bar if (noticeJsProgressBar && noticeJsProgressBar !== '') { noticeJsItem.appendChild(noticeJsProgressBar); } // Empty top and bottom container if (['top', 'bottom'].includes(options.position)) { document.querySelector(target_class).innerHTML = ''; } // Add open animation if (options.animation !== null && options.animation.open !== null) { noticeJsItem.className += ' ' + options.animation.open; } // Add Modal if (options.modal === true) { noticeJsItem.setAttribute('noticejs-modal', 'true'); AddModal(); } // Add Listener addListener(noticeJsItem, options.closeWith); getCallback(options, 'beforeShow'); getCallback(options, 'onShow'); if (options.newestOnTop === true) { document.querySelector(target_class).insertAdjacentElement('afterbegin', noticeJsItem); } else { document.querySelector(target_class).appendChild(noticeJsItem); } getCallback(options, 'afterShow'); return noticeJsItem; }; /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _noticejs = __webpack_require__(3); var _noticejs2 = _interopRequireDefault(_noticejs); var _api = __webpack_require__(0); var API = _interopRequireWildcard(_api); var _components = __webpack_require__(4); var _helpers = __webpack_require__(1); var helper = _interopRequireWildcard(_helpers); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var NoticeJs = function () { /** * @param {object} options * @returns {Noty} */ function NoticeJs() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, NoticeJs); this.options = Object.assign(API.Defaults, options); this.component = new _components.Components(); this.id = "noticejs-" + Math.random(); this.on('beforeShow', this.options.callbacks.beforeShow); this.on('onShow', this.options.callbacks.onShow); this.on('afterShow', this.options.callbacks.afterShow); this.on('onClose', this.options.callbacks.onClose); this.on('afterClose', this.options.callbacks.afterClose); this.on('onClick', this.options.callbacks.onClick); this.on('onHover', this.options.callbacks.onHover); return this; } /** * @returns {NoticeJs} */ _createClass(NoticeJs, [{ key: 'show', value: function show() { var container = this.component.createContainer(); if (document.querySelector('.noticejs-' + this.options.position) === null) { document.body.appendChild(container); } var noticeJsHeader = void 0; var noticeJsBody = void 0; var noticeJsProgressBar = void 0; // Create NoticeJs header noticeJsHeader = this.component.createHeader(this.options.title, this.options.closeWith); // Create NoticeJs body noticeJsBody = this.component.createBody(this.options.text); // Create NoticeJs progressBar if (this.options.progressBar === true) { noticeJsProgressBar = this.component.createProgressBar(); } //Append NoticeJs var noticeJs = helper.appendNoticeJs(noticeJsHeader, noticeJsBody, noticeJsProgressBar); noticeJs.setAttribute("id", this.id); this.dom = noticeJs; return noticeJs; } /** * @param {string} eventName * @param {function} cb * @return {NoticeJs} */ }, { key: 'on', value: function on(eventName) { var cb = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; if (typeof cb === 'function' && this.options.callbacks.hasOwnProperty(eventName)) { this.options.callbacks[eventName].push(cb); } return this; } /** * @param {Object} options * @return {Notice} */ }, { key: 'close', /** * close */ value: function close() { helper.CloseItem(this.dom); } }], [{ key: 'overrideDefaults', value: function overrideDefaults(options) { this.options = Object.assign(API.Defaults, options); return this; } }]); return NoticeJs; }(); exports.default = NoticeJs; module.exports = exports['default']; /***/ }), /* 3 */ /***/ (function(module, exports) { // removed by extract-text-webpack-plugin /***/ }), /* 4 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Components = undefined; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _api = __webpack_require__(0); var API = _interopRequireWildcard(_api); var _helpers = __webpack_require__(1); var helper = _interopRequireWildcard(_helpers); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var options = API.Defaults; var Components = exports.Components = function () { function Components() { _classCallCheck(this, Components); } _createClass(Components, [{ key: 'createContainer', value: function createContainer() { var element_class = 'noticejs-' + options.position; var element = document.createElement('div'); element.classList.add('noticejs'); element.classList.add(element_class); return element; } }, { key: 'createHeader', value: function createHeader() { var element = void 0; if (options.title && options.title !== '') { element = document.createElement('div'); element.setAttribute('class', 'noticejs-heading'); element.textContent = options.title; } // Add close button if (options.closeWith.includes('button')) { var close = document.createElement('div'); close.setAttribute('class', 'close'); close.innerHTML = '×'; if (element) { element.appendChild(close); } else { element = close; } } return element; } }, { key: 'createBody', value: function createBody() { var element = document.createElement('div'); element.setAttribute('class', 'noticejs-body'); var content = document.createElement('div'); content.setAttribute('class', 'noticejs-content'); content.innerHTML = options.text; element.appendChild(content); if (options.scroll !== null && options.scroll.maxHeight !== '') { element.style.overflowY = 'auto'; element.style.maxHeight = options.scroll.maxHeight + 'px'; if (options.scroll.showOnHover === true) { element.style.visibility = 'hidden'; } } return element; } }, { key: 'createProgressBar', value: function createProgressBar() { var element = document.createElement('div'); element.setAttribute('class', 'noticejs-progressbar'); var bar = document.createElement('div'); bar.setAttribute('class', 'noticejs-bar'); element.appendChild(bar); // Progress bar animation if (options.progressBar === true && typeof options.timeout !== 'boolean' && options.timeout !== false) { var _frame = function _frame() { if (width <= 0) { clearInterval(id); var item = element.closest('div.item'); // Add close animation if (options.animation !== null && options.animation.close !== null) { // Remove open animation class item.className = item.className.replace(new RegExp('(?:^|\\s)' + options.animation.open + '(?:\\s|$)'), ' '); // Add close animation class item.className += ' ' + options.animation.close; // Close notification after 0.5s + timeout var close_time = parseInt(options.timeout) + 500; setTimeout(function () { helper.CloseItem(item); }, close_time); } else { // Close notification when progress bar completed helper.CloseItem(item); } } else { width--; bar.style.width = width + '%'; } }; var _indeterminateFrame = function _indeterminateFrame() { if (progressDirection === 0) { width--; if (width === 0) { progressDirection = 1; } } else { width++; if (width === 100) { progressDirection = 0; } } if (document.getElementById(domId) == null) { clearInterval(id); } else { bar.style.width = width + '%'; } }; var width = 100; var progressDirection = 0; var id = 0; var domId = ""; if (options.indeterminate === true) { id = setInterval(_indeterminateFrame, options.timeout); domId = "noticejs-progressbar-" + id; element.setAttribute("id", domId); } else { id = setInterval(_frame, options.timeout); } } return element; } }]); return Components; }(); /***/ }) /******/ ]); }); ================================================ FILE: public/libs/notice/noticejs.css ================================================ .noticejs-top{top:0;width:100%!important}.noticejs-top .item{border-radius:0!important;margin:0!important}.noticejs-topRight{top:10px;right:10px}.noticejs-topLeft{top:10px;left:10px}.noticejs-topCenter{top:10px;left:50%;transform:translate(-50%)}.noticejs-middleLeft,.noticejs-middleRight{right:10px;top:50%;transform:translateY(-50%)}.noticejs-middleLeft{left:10px}.noticejs-middleCenter{top:50%;left:50%;transform:translate(-50%,-50%)}.noticejs-bottom{bottom:0;width:100%!important}.noticejs-bottom .item{border-radius:0!important;margin:0!important}.noticejs-bottomRight{bottom:10px;right:10px}.noticejs-bottomLeft{bottom:10px;left:10px}.noticejs-bottomCenter{bottom:10px;left:50%;transform:translate(-50%)}.noticejs{font-family:Helvetica Neue,Helvetica,Arial,sans-serif}.noticejs .item{margin:0 0 10px;border-radius:3px;overflow:hidden}.noticejs .item .close{float:right;font-size:18px;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 0 #fff;opacity:1;margin-right:7px}.noticejs .item .close:hover{opacity:.5;color:#000}.noticejs .item a{color:#fff;border-bottom:1px dashed #fff}.noticejs .item a,.noticejs .item a:hover{text-decoration:none}.noticejs .success{background-color:#64ce83}.noticejs .success .noticejs-heading{background-color:#3da95c;color:#fff;padding:10px}.noticejs .success .noticejs-body{color:#fff;padding:10px}.noticejs .success .noticejs-body:hover{visibility:visible!important}.noticejs .success .noticejs-content{visibility:visible}.noticejs .info{background-color:#3ea2ff}.noticejs .info .noticejs-heading{background-color:#067cea;color:#fff;padding:10px}.noticejs .info .noticejs-body{color:#fff;padding:10px}.noticejs .info .noticejs-body:hover{visibility:visible!important}.noticejs .info .noticejs-content{visibility:visible}.noticejs .warning{background-color:#ff7f48}.noticejs .warning .noticejs-heading{background-color:#f44e06;color:#fff;padding:10px}.noticejs .warning .noticejs-body{color:#fff;padding:10px}.noticejs .warning .noticejs-body:hover{visibility:visible!important}.noticejs .warning .noticejs-content{visibility:visible}.noticejs .error{background-color:#e74c3c}.noticejs .error .noticejs-heading{background-color:#ba2c1d;color:#fff;padding:10px}.noticejs .error .noticejs-body{color:#fff;padding:10px}.noticejs .error .noticejs-body:hover{visibility:visible!important}.noticejs .error .noticejs-content{visibility:visible}.noticejs .progressbar{width:100%}.noticejs .progressbar .bar{width:1%;height:30px;background-color:#4caf50}.noticejs .success .noticejs-progressbar{width:100%;background-color:#64ce83;margin-top:-1px}.noticejs .success .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#3da95c}.noticejs .info .noticejs-progressbar{width:100%;background-color:#3ea2ff;margin-top:-1px}.noticejs .info .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#067cea}.noticejs .warning .noticejs-progressbar{width:100%;background-color:#ff7f48;margin-top:-1px}.noticejs .warning .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#f44e06}.noticejs .error .noticejs-progressbar{width:100%;background-color:#e74c3c;margin-top:-1px}.noticejs .error .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#ba2c1d}@keyframes noticejs-fadeOut{0%{opacity:1}to{opacity:0}}.noticejs-fadeOut{animation-name:noticejs-fadeOut}@keyframes noticejs-modal-in{to{opacity:.3}}@keyframes noticejs-modal-out{to{opacity:0}}.noticejs-rtl .noticejs-heading{direction:rtl}.noticejs-rtl .close{float:left!important;margin-left:7px;margin-right:0!important}.noticejs-rtl .noticejs-content{direction:rtl}.noticejs{position:fixed;z-index:10050}.noticejs ::-webkit-scrollbar{width:8px}.noticejs ::-webkit-scrollbar-button{width:8px;height:5px}.noticejs ::-webkit-scrollbar-track{border-radius:10px}.noticejs ::-webkit-scrollbar-thumb{background:hsla(0,0%,100%,.5);border-radius:10px}.noticejs ::-webkit-scrollbar-thumb:hover{background:#fff}.noticejs-modal{position:fixed;width:100%;height:100%;background-color:#000;z-index:10000;opacity:.3;left:0;top:0}.noticejs-modal-open{opacity:0;animation:noticejs-modal-in .3s ease-out}.noticejs-modal-close{animation:noticejs-modal-out .3s ease-out;animation-fill-mode:forwards} ================================================ FILE: public/libs/types.expand.js ================================================ String.prototype.getQueryString = function(name, split) { if (split == undefined) split = "&"; var rule = "(^|" + split + "|\\?)" + name + "=([^" + split + "#]*)(" + split + "|#|$)"; var reg = new RegExp(rule), r; if ((r = this.match(reg))) return decodeURI(r[2]); return null; }; /** * @return {number} */ String.prototype.sizeToNumber = function() { let _size_raw_match = this.match( /^(\d*\.?\d+)(.*[^ZEPTGMK])?([ZEPTGMK](B|iB))$/i ); if (_size_raw_match) { let _size_num = parseFloat(_size_raw_match[1]); let _size_type = _size_raw_match[3]; switch (true) { case /Zi?B/i.test(_size_type): return _size_num * Math.pow(2, 70); case /Ei?B/i.test(_size_type): return _size_num * Math.pow(2, 60); case /Pi?B/i.test(_size_type): return _size_num * Math.pow(2, 50); case /Ti?B/i.test(_size_type): return _size_num * Math.pow(2, 40); case /Gi?B/i.test(_size_type): return _size_num * Math.pow(2, 30); case /Mi?B/i.test(_size_type): return _size_num * Math.pow(2, 20); case /Ki?B/i.test(_size_type): return _size_num * Math.pow(2, 10); default: return _size_num; } } return 0; }; ================================================ FILE: public/manifest.json ================================================ { "name": "__MSG_manifest_appName__", "short_name": "__MSG_manifest_shortName__", "version": "1.6.1", "description": "__MSG_manifest_appDescription__", "manifest_version": 2, "default_locale": "zh_CN", "homepage_url": "https://github.com/pt-plugins/PT-Plugin-Plus", "browser_action": { "default_icon": "assets/icon-19.png", "default_title": "__MSG_manifest_appName__" }, "permissions": [ "activeTab", "clipboardRead", "clipboardWrite", "storage", "contextMenus", "notifications", "http://*/*", "https://*/*", "unlimitedStorage" ], "optional_permissions": ["downloads", "cookies"], "icons": { "16": "assets/icon.png", "19": "assets/icon-19.png", "64": "assets/icon-64.png", "128": "assets/icon-128.png" }, "options_ui": { "page": "index.html", "open_in_tab": true }, "background": { "scripts": [ "libs/types.expand.js", "libs/jquery/jquery-3.3.1.min.js", "libs/Base64.js", "js/background/libs.js", "js/background/background.js" ] }, "content_scripts": [{ "matches": [ "http://*/*", "https://*/*" ], "exclude_matches": [ "https://fonts.google.com/*" ], "css": [ "assets/base.css", "libs/materialIcons/content_style.css", "libs/notice/noticejs.css", "libs/basicContext/basicContext.min.css", "libs/basicContext/themes/default.min.css" ], "js": [ "libs/types.expand.js", "libs/jquery/jquery-3.3.1.min.js", "libs/Base64.js", "libs/notice/notice.js", "libs/basicContext/basicContext.min.js", "libs/drag.js", "js/content/libs.js", "js/content/content.js" ] }], "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "web_accessible_resources": [ "libs/materialIcons/*.woff2", "assets/*", "resource/*" ], "omnibox": { "keyword": "pt" }, "minimum_chrome_version": "64.0.3242", "browser_specific_settings": { "gecko": { "update_url": "https://pt-plugins.github.io/PT-Plugin-Plus/update/firefox.json" } } } ================================================ FILE: public/popup.html ================================================ PT 助手 ================================================ FILE: resource/clients/README.md ================================================ # 下载客户端定义说明 ## 目录说明 该目录存放所有支持的下载客户端,目录名为架构名称 ``` --目录名 ----config.json ----init.js ``` - 目录名为该客户端的类型 - config.json : 下载客户端定义文件 - init.js : 下载客户端初始化脚本 ## config.json 文件 示例: ```json { "name": "qBittorrent", "type": "qbittorrent", "ver": "0.0.1", "icon": "https://www.qbittorrent.org/favicon.ico", "scripts": ["init.js"], "description": "当前支持 qBittorrent v4.1+,由于浏览器限制,需要禁用 qBittorrent 的『启用跨站请求伪造(CSRF)保护』功能才能正常使用", "warning": "注意:由于 qBittorrent 验证机制限制,第一次测试连接成功后,后续测试无论密码正确与否都会提示成功。", "allowCustomPath": true, "pathDescription": "当前目录列表配置是指定硬盘上的绝对路径,如 /volume1/music/ 或 D:\\download\\music\\" } ``` - `name` : 客户端名称 - `type` : 客户端类型,必需和目录名相同 - `ver` : 当前定义的版本号,目前暂无特别用处 - `icon` : 用于显示客户端的图标 - `scripts`: <可选>数组,用于执行该客户端执行的脚本列表,目前暂无特别用处 - `description` : <可选>当前客户端描述 - `warning` : <可选>用于配置时显示的警告信息,要用于一些特殊提示 - `allowCustomPath` : <可选>是否允许自定义目录,默认为 false - `pathDescription` : <可选>自定义目录说明 ## init.js > 客户端初始化脚本文件 - 脚本最终需要将自身挂载到 `window` 对象下,挂载名称必需和 `type` 相同,且区分大小写! - 如:type='uTorrent' - 那么挂载名称为 `window.uTorrent` = xxx; * 客户端对象需对外暴露以下公用方法 - `init` : 用于初始化客户端,接收一个参数:`options` 表示客户端定义的相关参数 - 属性参考 [common.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/interface/common.ts) 的 `DownloadClient` - `call` : 用于方法调用,接收两个参数:`action`, `data`,返回一个 `Promise` 对象 - `action` : 字符串,需要执行的方法(动作)名称,方法说明: - `addTorrentFromURL` : 从指定的链接增加种子文件 - `testClientConnectivity` : 测试当前客户端是否可连接 - 更多方法参考 [common.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/interface/common.ts) 的 `EAction` - `data` : 任意类型,接收的数据,根据 `action` 不同,数据格式也不同 - 在脚本中可用的系统对象 - `PTBackgroundService` : 助手后台服务程序,详情参考 [service.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/background/service.ts) - `PTSevriceFilters` : 系统定义的过滤器,详情参考 [filters.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/service/filters.ts) ================================================ FILE: resource/clients/deluge/config.json ================================================ { "name": "Deluge", "type": "deluge", "ver": "0.0.1", "icon": "https://www.deluge-torrent.org/images/deluge-icon.png", "scripts": [ "init.js" ], "passwordOnly": true, "warning": "注意:由于 Deluge 验证机制限制,第一次测试连接成功后,后续测试无论密码正确与否都会提示成功。", "allowCustomPath": true, "pathDescription": "当前目录列表配置是指定硬盘上的绝对路径,如 /volume1/music/ 或 D:\\download\\music\\" } ================================================ FILE: resource/clients/deluge/init.js ================================================ /** * @see https://deluge.readthedocs.io/en/develop/reference/index.html */ (function ($) { //Deluge // id:1,method:auth.login,params:[url,null] // 380 web.download_torrent_from_url // 2 core.add_torrent_url class Deluge { /** * 初始化实例 * @param {*} options * loginName: 登录名 * loginPwd: 登录密码 * url: 服务器地址 */ init(options) { this.options = options; this.requestCount = -1; if (this.options.address.indexOf("/json") == -1) { let url = PTServiceFilters.parseURL(this.options.address); let address = [url.protocol, "://", url.host]; if (url.port) { address.push(`:${url.port}`); } address.push(url.path); if (url.path.substr(-1) != "/") { address.push("/"); } address.push("json"); this.options.address = address.join(""); } console.log("Deluge.init", this.options.address); } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("Deluge.call", action, data); return new Promise((resolve, reject) => { switch (action) { case "addTorrentFromURL": this.addTorrentFromUrl(data, result => { resolve(result); }); break; // 测试是否可连接 case "testClientConnectivity": this.getSessionId() .then(result => { resolve(result != ""); }) .catch(result => { reject(result); }); break; } }); } /** * 获取Session * @param {*} callback */ getSessionId(callback) { return new Promise((resolve, reject) => { var data = { id: ++this.requestCount, method: "auth.login", params: [this.options.loginPwd] }; $.ajax({ type: "POST", url: this.options.address, dataType: "json", contentType: "application/json", data: JSON.stringify(data), timeout: PTBackgroundService.options.connectClientTimeout }) .done((resultData, textStatus) => { this.isInitialized = true; if (callback) { callback(resultData); } resolve(this.token); }) .fail((jqXHR, textStatus) => { let result = { status: textStatus || "error", code: jqXHR.status, msg: textStatus === "timeout" ? i18n.t("downloadClient.timeout") : i18n.t("downloadClient.unknownError") //"连接超时" : "未知错误" }; switch (jqXHR.status) { case 0: result.msg = i18n.t("downloadClient.serverIsUnavailable") //"服务器不可用或网络错误" break; case 401: result.msg = i18n.t("downloadClient.serverConnectionFailed"); //"身份验证失败"; break; case 404: result.msg = i18n.t("downloadClient.notFound"); //"指定的地址未找到,服务器返回了 404"; break; } reject(result); }); }); } /** * 调用指定的RPC * @param {*} options * @param {*} callback * @param {*} tags */ exec(options, callback, tags) { var data = { id: ++this.requestCount }; $.extend(data, options); var settings = { type: "POST", url: this.options.address, data: JSON.stringify(data), contentType: "application/json", timeout: PTBackgroundService.options.connectClientTimeout, success: (resultData, textStatus) => { // 未认证 if (resultData && resultData.error && resultData.error.code == 1) { this.getSessionId() .then(() => { this.exec(options, callback, tags); }) .catch(result => { callback && callback(result); }); return; } if (callback) { callback(resultData, tags); } }, error: (request, event, page) => { console.log(request); this.getSessionId() .then(() => { this.exec(options, callback, tags); }) .catch(result => { callback && callback(result); }); } }; $.ajax(settings); } /** * 添加种子链接 * @param {*} data * @param {*} callback */ addTorrentFromUrl(data, callback) { let url = data.url; // 磁性连接(代码来自原版WEBUI) if (url.startsWith('magnet:')) { this.addTorrent({ method: "core.add_torrent_url", params: [ url, { download_location: data.savePath } ] }, callback ); return; } PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: url }) .then(result => { var fileReader = new FileReader(); fileReader.onload = e => { var contents = e.target.result; var key = "base64,"; var index = contents.indexOf(key); if (index == -1) { return; } var metainfo = contents.substring(index + key.length); this.addTorrent({ method: "core.add_torrent_file", params: [ "", metainfo, { download_location: data.savePath } ] }, callback ); }; fileReader.readAsDataURL(result); }) .catch(result => { callback && callback(result); }); } addTorrent(options, callback) { this.exec(options, resultData => { if (callback) { var result = resultData; if (!resultData.error && resultData.result) { result.status = "success"; result.msg = i18n.t("downloadClient.addURLSuccess", { name: this.options.name }); //"URL已添加至 Deluge 。"; } callback(result); } console.log(resultData); }); } } window.deluge = Deluge; })(jQuery, window); ================================================ FILE: resource/clients/flood/config.json ================================================ { "name": "Flood", "type": "flood", "ver": "0.0.1", "icon": "https://github.com/Flood-UI/flood/raw/master/flood.png", "scripts": [ "init.js" ], "description": "ruTorrent 的另一款基于Node的Web前端面板,界面美观,加载速度快。", "warning": "1.仅支持jesec/Flood,不支持原版Flood;2.如果当前已登录Flood面板,请退出登陆后再做连接性测试;3. 目前无法准确获得Flood添加种子是否成功。", "allowCustomPath": true } ================================================ FILE: resource/clients/flood/init.js ================================================ /** * @see https://github.com/jesec/flood/tree/master/server/routes/api */ (function ($) { // Flood class Client { /** * 初始化实例 * @param {*} options * loginName: 登录名 * loginPwd: 登录密码 * address: 服务器地址 */ init(options) { this.options = options; // 重写用户给的地址 let url = PTServiceFilters.parseURL(this.options.address); let address = [url.protocol, "://", url.host]; if (url.port) { address.push(`:${url.port}`) } address.push(url.path); if (url.path.substr(-1) !== "/") { address.push("/"); } this.options.address = address.join(""); console.log("Flood.init", this.options.address); } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("Flood.call", action, data); return new Promise((resolve, reject) => { switch (action) { case "addTorrentFromURL": this.addTorrentFromUrl(data, (result) => { if (result.status === "success") { resolve(result); } else { reject(result); } }); break; // 测试是否可连接 case "testClientConnectivity": this.testClientConnectivity().then(result => { resolve(true); }).catch((code, msg) => { reject({ status: "error", code, msg }); }); break; } }); } authenticate(callback) { // run the login first let options = this.options; $.ajax({ type: "POST", url: options.address + "api/auth/authenticate", contentType: 'application/json', data: JSON.stringify({ username: options.loginName, password: options.loginPwd, }), success: function (resultData, textStatus) { console.log(resultData); if (resultData.success) { console.log(resultData.token); if (callback && typeof callback === 'function') { callback(resultData); } } } }); } /** * 测试可连接性 * 接口返回 { isConnected: true } 时说明可连接 * * @see https://github.com/Flood-UI/flood/blob/master/server/routes/client.js#L16-L36 * * @param callback */ testClientConnectivity(callback) { let that = this; return new Promise((resolve, reject) => { this.authenticate(function (authData) { $.ajax({ type: 'GET', url: that.options.address + 'api/client/connection-test', // ping_addr timeout: PTBackgroundService.options.connectClientTimeout }).done((resultData, textStatus, request) => { if (resultData.isConnected) { that.isInitialized = true; if (callback) { callback(resultData); } resolve(); } }).fail((jqXHR, textStatus, errorThrown) => { reject(jqXHR.status, textStatus) }) }); }) } /** * 添加种子链接 * * @see https://github.com/Flood-UI/flood/blob/master/server/routes/client.js#L38-L44 * * @param {*} data * @param {*} callback */ addTorrentFromUrl(data, callback) { let url = data.url; let addTorrentData = { destination: data.savePath || '', /** isBasePath * @see https://github.com/Flood-UI/flood/blob/master/server/models/ClientRequest.js#L143-L149 * @see https://rtorrent-docs.readthedocs.io/en/latest/cmd-ref.html */ isBasePath: false, start: !data.autoStart, tag: [], } // 处理magent链接 if (url.startsWith('magnet:')) { addTorrentData.urls = [url]; this.addTorrentUrl(addTorrentData, callback); return; } // 种子文件 PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: url }) .then((result) => { var fileReader = new FileReader(); fileReader.onload = e => { var contents = e.target.result; var key = "base64,"; var index = contents.indexOf(key); if (index == -1) { return; } var metainfo = contents.substring(index + key.length); addTorrentData.files = [metainfo]; this.addTorrentFile(addTorrentData, callback); } }) .catch((result) => { callback && callback(result); }); } addTorrentUrl(data, callback) { this.addTorrent('api/torrents/add-urls', data, callback); } addTorrentFile(data, callback) { this.addTorrent('api/torrents/add-files', data, callback); } /** * * @param suffix * @param data * @param callback */ addTorrent(suffix, data, callback) { let options = this.options; this.authenticate(function () { $.ajax({ type: "POST", url: options.address + suffix, timeout: PTBackgroundService.options.connectClientTimeout, data: data, contentType: false, processData: false, success: (resultData, textStatus) => { if (callback) { var result = Object.assign({ status: "success", msg: i18n.t("downloadClient.addURLSuccess", { name: options.name }) }, resultData); callback(result); } }, }) }) } } // 添加到 window 对象,用于客户页面调用 window.flood = Client; })(jQuery, window); ================================================ FILE: resource/clients/qbittorrent/config.json ================================================ { "name": "qBittorrent", "type": "qbittorrent", "ver": "0.0.1", "icon": "https://www.qbittorrent.org/favicon.ico", "scripts": [ "init.js" ], "description": "当前支持 qBittorrent v4.1+,由于浏览器限制,需要禁用 qBittorrent 的『启用跨站请求伪造(CSRF)保护』功能才能正常使用", "warning": "注意:由于 qBittorrent 验证机制限制,第一次测试连接成功后,后续测试无论密码正确与否都会提示成功。", "allowCustomPath": true, "pathDescription": "当前目录列表配置是指定硬盘上的绝对路径,如 /volume1/music/ 或 D:\\download\\music\\" } ================================================ FILE: resource/clients/qbittorrent/init.js ================================================ /** * @see https://github.com/qbittorrent/qBittorrent/wiki/Web-API-Documentation */ (function($) { //qBittorrent class Client { /** * 初始化实例 * @param {*} options * loginName: 登录名 * loginPwd: 登录密码 * url: 服务器地址 */ init(options) { this.options = options; this.headers = {}; this.sessionId = ""; if (this.options.address.substr(-1) == "/") { this.options.address = this.options.address.substr( 0, this.options.address.length - 1 ); } console.log("qBittorrent.init", this.options.address); } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("qBittorrent.call", action, data); return new Promise((resolve, reject) => { switch (action) { case "addTorrentFromURL": this.addTorrentFromUrl(data, result => { if (result.status === "success") { resolve(result); } else { reject(result); } }); break; // 测试是否可连接 case "testClientConnectivity": this.getSessionId() .then(result => { resolve(true); }) .catch((code, msg) => { reject({ status: "error", code, msg }); }); break; } }); } /** * 获取Session * @param {*} callback */ getSessionId(callback) { return new Promise((resolve, reject) => { var data = { username: this.options.loginName, password: this.options.loginPwd }; // qb 需要禁用『启用跨站请求伪造保护』 var settings = { type: "POST", url: this.options.address + "/api/v2/auth/login", data: data, timeout: PTBackgroundService.options.connectClientTimeout }; $.ajax(settings) .done((resultData, textStatus, request) => { this.isInitialized = true; if (callback) { callback(resultData); } resolve(); console.log(this.sessionId); }) .fail((jqXHR, textStatus, errorThrown) => { reject(jqXHR.status, textStatus); }); }); } /** * 调用指定的RPC * @param {*} options * @param {*} callback * @param {*} tags */ exec(options, callback, tags) { var settings = { type: "POST", processData: false, contentType: false, method: "POST", url: this.options.address + options.method, data: options.params, timeout: PTBackgroundService.options.connectClientTimeout, success: (resultData, textStatus) => { if (callback) { callback(resultData, tags); } }, error: (jqXHR, textStatus, errorThrown) => { switch (jqXHR.status) { // Unsupported Media Type case 415: callback({ status: "error", code: jqXHR.status, msg: i18n.t("downloadClient.unsupportedMediaType") //"种子文件有误" }); return; default: break; } console.log(jqXHR); this.getSessionId() .then(() => { this.exec(options, callback, tags); }) .catch((code, msg) => { callback({ status: "error", code, msg: msg || code === 0 ? i18n.t("downloadClient.serverIsUnavailable") : i18n.t("downloadClient.unknownError") //"服务器不可用或网络错误" : "未知错误" }); }); } }; $.ajax(settings); } /** * 添加种子链接 * @param {*} data * @param {*} callback */ addTorrentFromUrl(data, callback) { let formData = new FormData(); if (data.savePath) { formData.append("savepath", data.savePath); // 禁用自动管理种子 formData.append("autoTMM", false); } if (data.autoStart != undefined) { formData.append("paused", !data.autoStart); } if (data.imdbId != undefined) { formData.append("tags", data.imdbId); } if (data.upLoadLimit && data.upLoadLimit > 0) { formData.append("upLimit", data.upLoadLimit * 1024); } let url = data.url; // 磁性连接 if (url.startsWith('magnet:')) { formData.append('urls', url); this.addTorrent(formData, callback); } else { PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: url }) .then(result => { formData.append("torrents", result, "file.torrent"); this.addTorrent(formData, callback); }) .catch(result => { callback && callback(result); }); } } addTorrent(params, callback) { this.exec( { method: "/api/v2/torrents/add", params: params }, resultData => { if (callback) { var result = Object.assign( { status: "", msg: "" }, resultData ); if ( (!resultData.error && resultData.result) || resultData == "Ok." ) { result.status = "success"; result.msg = i18n.t("downloadClient.addURLSuccess", { name: this.options.name }); //"URL已添加至 qBittorrent 。"; } callback(result); } console.log(resultData); } ); } } window.qbittorrent = Client; })(jQuery, window); ================================================ FILE: resource/clients/ruTorrent/config.json ================================================ { "name": "ruTorrent", "type": "ruTorrent", "ver": "0.0.1", "icon": "https://raw.githubusercontent.com/Novik/ruTorrent/master/images/favicon.ico", "scripts": [ "init.js" ], "description": "ruTorrent", "warning": "", "allowCustomPath": true, "pathDescription": "当前目录列表配置是指定硬盘上的绝对路径,如 /volume1/music/ 或 D:\\download\\music\\" } ================================================ FILE: resource/clients/ruTorrent/init.js ================================================ /** * @see https://github.com/Novik/ruTorrent/blob/master/php/addtorrent.php * @see https://github.com/Rhilip/PT-Plugin/blob/master/src/script/client.js#L477_L543 * */ (function($) { // ruTorrent class Client { /** * 初始化实例 * @param {*} options * loginName: 登录名 * loginPwd: 登录密码 * address: 服务器地址 */ init(options) { this.options = options; // 重写用户给的地址 let url = PTServiceFilters.parseURL(this.options.address); let address = [url.protocol, "://", url.host]; if (url.port) { address.push(`:${url.port}`); } address.push(url.path); if (url.path.substr(-1) !== "/") { address.push("/"); } this.options.address = address.join(""); console.log("ruTorrent.init", this.options.address); } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("ruTorrent.call", action, data); return new Promise((resolve, reject) => { switch (action) { case "addTorrentFromURL": this.addTorrentFromUrl(data, result => { if (result.result === "Success") { resolve(result); } else { reject(result); } }); break; // 测试是否可连接 case "testClientConnectivity": this.testClientConnectivity() .then(result => { resolve(true); }) .catch((code, msg) => { reject({ status: "error", code, msg }); }); break; } }); } /** * 测试可连接性 * 鉴于ruTorrent没有相关方法,则考虑请求 `/php/getsettings.php` 页面,如果返回json格式的信息 * 则说明可连接 * * @see https://github.com/Novik/ruTorrent/blob/master/php/getsettings.php * * @param callback */ testClientConnectivity(callback) { return new Promise((resolve, reject) => { $.ajax({ type: "GET", url: this.options.address + "php/getsettings.php", // ping_addr username: this.options.loginName, password: this.options.loginPwd, timeout: PTBackgroundService.options.connectClientTimeout }) .done((resultData, textStatus, request) => { this.isInitialized = true; if (callback) { callback(resultData); } resolve(); }) .fail((jqXHR, textStatus, errorThrown) => { reject(jqXHR.status, textStatus); }); }); } /** * 添加种子链接 * @param {*} data * @param {*} callback */ addTorrentFromUrl(data, callback) { let url = data.url; // 磁性连接 if (url.startsWith("magnet:")) { this.addTorrent( { dir_edit: data.savePath, paused: !data.autoStart, url: url, json: 1 // 输出json格式 }, callback ); return; } // 种子文件 PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: url }) .then(result => { let formData = new FormData(); formData.append("json", 1); // 输出json格式 // 如果有传值时,则设置路径参数 if (data.savePath) { formData.append("dir_edit", data.savePath); } formData.append("paused", !data.autoStart); formData.append("torrent_file", result, "file.torrent"); this.addTorrent(formData, callback); }) .catch(result => { callback && callback(result); }); } /** * POST完成后会被302到类似 /php/addtorrent.php?result[]=Success&name[]=file.torrent&json=1 * 得到类似 { "result" : "Success" } 的结果 * 其中result取值为 enum( * 'Success', // 添加成功 * 'Failed', // 添加失败(magnet,不存在种子文件,种子上传失败) * 'FailedURL', // 添加链接失败(ruT获取不到对应种子) * 'FailedFile' // 添加文件失败(rT返回错误) * ) * * * @param data * @param callback */ addTorrent(data, callback) { $.ajax({ type: "POST", url: this.options.address + "php/addtorrent.php", username: this.options.loginName, password: this.options.loginPwd, timeout: PTBackgroundService.options.connectClientTimeout, data: data, contentType: false, processData: false, dataType: 'json', success: (resultData, textStatus) => { if (callback) { callback(resultData); } } }); } } // 添加到 window 对象,用于客户页面调用 window.ruTorrent = Client; })(jQuery, window); ================================================ FILE: resource/clients/synologyDownloadStation/config.json ================================================ { "name": "Synology Download Station", "type": "synologyDownloadStation", "ver": "0.0.1", "icon": "https://www.synology.com/img/icon/favicon.png", "scripts": [ "init.js" ], "allowCustomPath": true, "pathDescription": "因 Synology Download Station API 接口限制,保存目录依赖于“暂存位置”,并且只允许使用相对路径;
如暂存位置为 /volume1/,期望存储目的地位置为 /volume1/music/,那么请在“目录列表”中填写:music" } ================================================ FILE: resource/clients/synologyDownloadStation/init.js ================================================ /** * @see https://global.download.synology.com/download/Document/DeveloperGuide/Synology_Download_Station_Web_API.pdf * @backport https://github.com/pt-plugins/PT-Plugin-Plus/blob/48c2d42a1d05c129c0abbbecf653b1b7d88a8a8e/src/resource/btClients/src/clients/synologyDownloadStation.ts */ (function ($, window) { class Client { init(options) { this.options = options; this.sessionId = ""; this.synoToken = ""; if (this.options.address.substr(-1) == "/") { this.options.address = this.options.address.substr(0, this.options.address.length - 1); } } /** * 获取 SID */ // FIXME 这个方法已经不止获取SID了,CSRFToken也是在此获得,该考虑换个名字了 getSessionId() { return new Promise((resolve, reject) => { let url = `${this.options.address}/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${encodeURIComponent(this.options.loginName)}&passwd=${encodeURIComponent(this.options.loginPwd)}&session=DownloadStation&format=sid&enable_syno_token=yes`; $.ajax({ url, timeout: PTBackgroundService.options.connectClientTimeout, dataType: "json" }).done((result) => { if (result && result.success) { this.sessionId = result.data.sid; this.synoToken = result.data.synotoken resolve(this.sessionId) } else { reject({ status: "error", code: result.error.code, msg: i18n.t("downloadClient.permissionDenied") //"身份验证失败" }) } /** 400 No such account or incorrect password 401 Account disabled 402 Permission denied 403 2-step verification code required 404 Failed to authenticate 2-step verification code */ }).fail(() => { reject() }) }) } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("synologyDownloadStation.call", action, data); return new Promise((resolve, reject) => { switch (action) { case "addTorrentFromURL": this.addTorrentFromUrl(data, (result) => { if (result && result.success) { resolve(result); } else { reject(result) } }); break; // 测试是否可连接 case "testClientConnectivity": this.getSessionId().then(result => { resolve(result != ""); }).catch(result => { reject(result); }) break; } }); } /** * 添加种子链接 * @param {*} options * @param {*} callback */ addTorrentFromUrl(options, callback) { if (!this.sessionId) { this.getSessionId().then((result) => { if (result) { this.addTorrentFromUrl(options, callback) } else { callback({ status: "error", msg: i18n.t("downloadClient.serverConnectionFailed") //"服务器连接失败" }) } }).catch((result) => { callback(result) }) return; } let postData = { api: 'SYNO.DownloadStation2.Task', method: 'create', version: 2, create_list: false, _sid: this.sessionId // fxxk, _sid 参数不能放在第一位,不然会直接 101 报错 } let headers = { 'X-SYNO-TOKEN': this.synoToken } // fxxk, 没有 destination 参数也会直接报错 let savePath = (options.savePath || "") + ""; if (savePath.substr(-1) === "/") { // 去除路径最后的 / ,以确保可以正常添加目录信息 savePath = savePath.substr(0, savePath.length - 1); } postData.destination = `"${savePath || ''}"`; if (options.url.startsWith('magnet:')) { postData.type = '"url"'; postData.url = [options.url]; this.addTorrent(postData, options, callback); } else { postData.type = '"file"'; postData.file = ['torrent']; let formData = new FormData(); Object.keys(postData).forEach((k) => { let v = postData[k]; if (v !== undefined) { if (Array.isArray(v)) { v = JSON.stringify(v); } formData.append(k, v); } }); PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: { url: options.url, parseTorrent: true } }) .then((result) => { formData.append("torrent", result.content, `${result.torrent.name}.torrent`) this.addTorrent(formData, headers, options, callback); }) .catch((result) => { callback && callback(result); }); } } addTorrent(formData, headers, options, callback) { $.ajax({ url: `${this.options.address}/webapi/entry.cgi`, headers, timeout: PTBackgroundService.options.connectClientTimeout, type: "POST", processData: false, contentType: false, data: formData, dataType: "json" }).done((result) => { console.log(result) if (result.error) { let errorMap = { 400: i18n.t("downloadClient.fileUploadFailed"), // "文件上传失败", 401: i18n.t("downloadClient.maxNumberOfTasksReached"), //"达到的最大任务数", 402: i18n.t("downloadClient.destinationDenied", { path: options.savePath }), //`指定的目录[${options.savePath}]不可用或无权限`, 403: i18n.t("downloadClient.destinationDoesNotExist", { path: options.savePath }) //`指定的目录[${options.savePath}]不存在` }; /** * 400 File upload failed 401 Max number of tasks reached 402 Destination denied 403 Destination does not exist 404 Invalid task id 405 Invalid task action 406 No default destination 407 Set destination failed 408 File does not exist */ if (result.error.code) { result.msg = errorMap[result.error.code]; } } callback(result) }).fail(() => { callback({ status: "error", msg: i18n.t("downloadClient.serverConnectionFailed") //"服务器连接失败" }) }) } } // 添加到 window 对象,用于客户页面调用 window.synologyDownloadStation = Client; })(jQuery, window) ================================================ FILE: resource/clients/transmission/config.json ================================================ { "name": "Transmission", "type": "transmission", "ver": "0.0.1", "icon": "https://raw.githubusercontent.com/transmission/transmission/master/web/images/favicon.png", "scripts": [ "init.js" ], "allowCustomPath": true, "pathDescription": "当前目录列表配置是指定硬盘上的绝对路径,如 /volume1/music/", "description": "默认情况下,系统会请求 http://ip:port/transmission/rpc 这个路径,如果无法连接,请确认 `settings.json` 文件的 `rpc-url` 值;详情可参考:https://github.com/pt-plugins/PT-Plugin-Plus/issues/32" } ================================================ FILE: resource/clients/transmission/init.js ================================================ /** * @see https://github.com/transmission/transmission/blob/master/extras/rpc-spec.txt */ (function ($, window) { const XHEADER = "X-Transmission-Session-Id"; class Transmission { /** * 初始化实例 * @param {*} options * loginName: 登录名 * loginPwd: 登录密码 * url: 服务器地址 */ init(options) { this.options = options; this.headers = []; if (options.loginName && options.loginPwd) { this.headers["Authorization"] = "Basic " + (new Base64()).encode(options.loginName + ":" + options.loginPwd); } if (this.options.address.indexOf("rpc") == -1) { let url = PTServiceFilters.parseURL(this.options.address); let address = [ url.protocol, "://", url.host ]; if (url.port) { address.push(`:${url.port}`) } address.push(url.path); if (url.path.substr(-1) != "/") { address.push("/"); } address.push("transmission/rpc"); this.options.address = address.join(""); } console.log("transmission.init", this.options.address); } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("transmission.call", action, data); return new Promise((resolve, reject) => { switch (action) { // 从指定的URL添加种子 case "addTorrentFromURL": this.addTorrentFromUrl(data.url, data.savePath, data.autoStart, (result) => { resolve(result); }, data.upLoadLimit); break; // 获取可用空间 case "getFreeSpace": this.getFreeSpace(data.path, (result) => { resolve(result); }); break; // 测试是否可连接 case "testClientConnectivity": this.sessionStats().then(result => { resolve(result.result == "success"); }).catch(result => { reject(result); }) break; } }); } /** * 调用指定的RPC * @param {*} options * @param {*} callback * @param {*} tags */ exec(options, callback, tags) { return new Promise((resolve, reject) => { var data = { method: "", arguments: {}, tag: "" }; let result = {}; $.extend(data, options); this.sendRequest({ type: "POST", url: this.options.address, dataType: 'json', data: JSON.stringify(data), timeout: PTBackgroundService.options.connectClientTimeout, headers: this.headers }, (resultData) => { if (callback) { callback(resultData, tags); } resolve(resultData); }, (request, event, page) => { switch (request.status) { case 0: result = { status: "error", code: request.status, msg: i18n.t("downloadClient.serverIsUnavailable") //"服务器不可用或网络错误" }; reject && reject(result) break; case 401: result = { status: "error", code: request.status, msg: i18n.t("downloadClient.permissionDenied") //"身份验证失败" }; reject && reject(result) break; default: result = { status: "error", code: request.status, msg: event || i18n.t("downloadClient.unknownError") //"未知错误" }; reject && reject(result) break; } }); }); } /** * 发送请求 * @param {*} options * @param {*} success * @param {*} error */ sendRequest(options, success, error) { $.ajax(options).done((resultData, textStatus) => { success && success(resultData, textStatus); }).fail((request, event, page) => { switch (request.status) { case 409: this.sessionId = request.getResponseHeader(XHEADER); this.headers[XHEADER] = this.sessionId; options.headers = this.headers; this.sendRequest(options, success, error); break; default: error && error(request, event, page); break; } }); } sessionStats() { return this.exec({ method: "session-stats" }); } /** * 添加种子 * @param string url 需要添加的地址 * @param string savePath 保存目录,如果不指定则以服务器配置为准 * @param bool autoStart 是否自动开始 * @param function callback 回调 */ addTorrentFromUrl(url, savePath, autoStart, callback, uploadLimit = 0) { var options = { method: "torrent-add", arguments: { filename: url, paused: (!autoStart) } }; if (savePath) { options.arguments["download-dir"] = savePath; } if (uploadLimit && uploadLimit > 0) { options.arguments["uploadLimit"] = uploadLimit; } // 磁性连接 if (url.startsWith('magnet:')) { options.arguments["filename"] = url; this.addTorrent(options, callback) } else { PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: url }) .then((result) => { var fileReader = new FileReader(); fileReader.onload = (e) => { var contents = e.target.result; var key = "base64,"; var index = contents.indexOf(key); if (index == -1) { return; } var metainfo = contents.substring(index + key.length); delete options.arguments["filename"]; options.arguments["metainfo"] = metainfo; this.addTorrent(options, callback); } fileReader.readAsDataURL(result); }) .catch((result) => { callback && callback(result); }); } } /** * 添加种子 * @param {*} options * @param {*} callback */ addTorrent(options, callback) { this.exec(options).then((data) => { switch (data.result) { // 添加成功 case "success": if (callback) { if (data.arguments["torrent-added"]) { callback(data.arguments["torrent-added"]); } // 重复的种子 else if (data.arguments["torrent-duplicate"]) { callback({ status: "duplicate", torrent: data.arguments["torrent-duplicate"] }); } } break; // 重复的种子 case "duplicate torrent": default: if (callback) { callback(data.result || data); } break; } }).catch((result) => { callback && callback(result); }); } /** * 獲取指定目錄的大小 * @param string path 需要获取的目录地址 * @param function callback 回调 */ getFreeSpace(path, callback) { this.exec({ method: "free-space", arguments: { "path": path } }).then((result) => { callback && callback(result); }).catch((result) => { callback && callback(result); }); } } // 添加到 window 对象,用于客户页面调用 window.transmission = Transmission; })(jQuery, window) ================================================ FILE: resource/clients/utorrent/config.json ================================================ { "name": "µTorrent", "type": "utorrent", "ver": "0.0.1", "icon": "https://www.utorrent.com/faviconUT.ico", "scripts": [ "init.js" ], "description": "由于 µTorrent Web API 接口不统一,当前仅支持 µTorrent Windows 版本,Mac 版本测试不可用,其他系统未知。使用前请确认 WebUI 已安装并开启", "allowCustomPath": true, "pathDescription": "注:目录功能仅支持 µTorrent 3.x.x 及以上版本;

1. 在 µTorrent 的 设置 -> 高级 -> 网页界面 添加一个下载目录,如:D:\\download\\
2. 在助手里添加目录列表(仅支持相对路径),如:music\\
3. 最终数据的保存目录为:D:\\download\\music\\" } ================================================ FILE: resource/clients/utorrent/init.js ================================================ /** * @see https://github.com/bittorrent/webui/blob/master/webui.js */ (function ($, window) { class uTorrent { /** * 初始化实例 * @param {*} options * loginName: 登录名 * loginPwd: 登录密码 * url: 服务器地址 */ init(options) { this.options = options; this.headers = []; this.token = ""; if (options.loginName && options.loginPwd) { this.headers["Authorization"] = "Basic " + new Base64().encode(options.loginName + ":" + options.loginPwd); } if (this.options.address.indexOf("gui") == -1) { let url = PTServiceFilters.parseURL(this.options.address); let address = [ url.protocol, "://", url.host ]; if (url.port) { address.push(`:${url.port}`) } address.push(url.path); if (url.path.substr(-1) != "/") { address.push("/"); } address.push("gui/"); this.options.address = address.join(""); } console.log("uTorrent.init", this.options.address); } /** * 执行指定的操作 * @param {*} action 需要执行的执令 * @param {*} data 附加数据 * @return Promise */ call(action, data) { console.log("uTorrent.call", action, data); return new Promise((resolve, reject) => { switch (action) { case "addTorrentFromURL": this.addTorrentFromUrl(data, result => { resolve(result); }); break; // 测试是否可连接 case "testClientConnectivity": this.getSessionId() .then(result => { resolve(result != ""); }) .catch(result => { reject(result); }); break; } }); } /** * 获取Session * @param {*} callback */ getSessionId(callback) { return new Promise((resolve, reject) => { $.ajax({ type: "GET", url: this.options.address + "token.html?t=", headers: this.headers, timeout: PTBackgroundService.options.connectClientTimeout }) .done(resultData => { console.log(resultData); this.token = $(resultData).html(); this.isInitialized = true; if (callback) { callback(this.token); } resolve(this.token); }) .fail((jqXHR, textStatus) => { let result = { status: textStatus || "error", code: jqXHR.status, msg: textStatus === "timeout" ? i18n.t("downloadClient.timeout") : i18n.t("downloadClient.unknownError") //"连接超时" : "未知错误" }; switch (jqXHR.status) { case 0: result.msg = i18n.t("downloadClient.serverIsUnavailable") //"服务器不可用或网络错误" break; case 401: result.msg = i18n.t("downloadClient.permissionDenied");//"身份验证失败"; break; case 404: result.msg = i18n.t("downloadClient.notFound");// "指定的地址未找到,服务器返回了 404"; break; } reject(result); }); }); } /** * 调用指定的RPC * @param {*} options * @param {*} callback * @param {*} tags */ exec(options, callback, tags) { if (!this.token) { this.getSessionId().then(() => { this.exec(options, callback, tags); }).catch((result) => { callback && callback(result); }); return; } var data = {}; var _settings = $.extend({ method: "GET", processData: undefined, contentType: undefined, queryString: "" }, options.settings); if (options.settings) { delete options.settings; } if (options.formData) { data = options.formData; } else { $.extend(data, options); } var settings = { type: _settings.method, url: this.options.address + "?token=" + this.token + _settings.queryString, dataType: "json", processData: _settings.processData, contentType: _settings.contentType, data: data, timeout: PTBackgroundService.options.connectClientTimeout, success: (resultData, textStatus) => { if (callback) { callback(resultData, tags); } }, error: (request, event, page) => { console.log(request); this.getSessionId().then(() => { this.exec(options, callback, tags); }).catch((result) => { callback && callback(result); }); }, headers: this.headers }; $.ajax(settings); } /** * 添加种子链接 * @param {*} data * @param {*} callback */ addTorrentFromUrl(data, callback) { let url = data.url; // 磁性连接 if (url.startsWith('magnet:')) { this.addTorrent({ action: "add-url", s: url, download_dir: 0, path: data.savePath ? data.savePath : "" }, callback); return; } PTBackgroundService.requestMessage({ action: "getTorrentDataFromURL", data: url }) .then((result) => { let formData = new FormData(); formData.append("torrent_file", result, "file.torrent") this.addTorrent({ settings: { method: "POST", processData: false, contentType: false, queryString: `&action=add-file&download_dir=0&path=` + (data.savePath ? data.savePath : "") }, formData }, callback); }) .catch((result) => { callback && callback(result); }); } addTorrent(options, callback) { this.exec(options, resultData => { if (callback) { var result = resultData; if (resultData.build) { result.status = "success"; result.msg = result.msg = i18n.t("downloadClient.addURLSuccess", { name: this.options.name });//"URL已添加至 µTorrent 。"; } callback(result); } console.log(resultData); } ); } } // 添加到 window 对象,用于客户页面调用 window.utorrent = uTorrent; })(jQuery, window); ================================================ FILE: resource/i18n/README.md ================================================ # 关于多语言 > 多语言环境正在构建中,现阶段正在努力将中文翻译为英文,需要各位英文达人参与翻译、复核,待英文文案可用后,将作为其他语言的源文件以供翻译。 ## 如何参与英文翻译? - 您可以通过以下方式来参与 - 通过在线翻译的方式来参与,欢迎加入:https://www.transifex.com/ronggang/pt-plugin-plus-dev - 直接以 `git` 方式更新本目录下的 `en.json` 文件; ## 多语言文案更新流程 - 中文词汇增加 -> 提交至 `transifex` 的 `dev` 项目 -> 翻译为英文 -> 复核 -> 转为正式文案 -> 翻译为其他语言 ## 如何测试已翻译的新语言 - 更新助手至 `v1.0.9` 之后的版本; - 进入助手配置页面; - 点击右下角的 `切换语言` -> `临时添加新语言` ; - 选择已经翻译好的语言文件,如:`zh-TW.json` ; - 如果格式正确,应该就可以看到新的语言内容了; - 如果是通过 `transifex` 在线翻译的,可以从 `transifex` 下载已翻译好的文件进行测试; - 使用该方式添加的语言文件仅本次生效,浏览器重启后恢复到默认状态; ================================================ FILE: resource/i18n/en.json ================================================ { "name": "English (Beta)", "code": "en", "authors": [ "ronggang", "ylxb2016", "xiongqiwei", "jackson008", "MewX" ], "words": { "app": { "initError": "The configuration information failed to be loaded. The system definition information was not obtained. Please try to refresh the current page.", "initializing": "The data is being prepared, please wait...", "author": "ronggang", "name": "PT Plugin Plus" }, "common": { "debugMode": "Debug mode active", "changeLanguage": "Switch language", "addLanguage": "Add a new language temporarily", "version": "Version", "systemLog": "System Log", "darkMode": "Invert Color", "haveNewReleases": "Update available", "add": "Add", "edit": "Edit", "copy": "Copy", "ok": "OK", "cancel": "Cancel", "remove": "Remove", "clear": "Clear", "removeConfirm": "Are you sure you want to delete this record?", "removeSelectedConfirm": "Are you sure you want to delete these {count} selected records?", "removeConfirmTitle": "Delete confirmation", "clearConfirm": "Are you sure you want to delete all records?", "id": "ID", "readyToStart": "Ready to start...", "help": "Help", "export": "Export", "import": "Import", "share": "Share", "actionConfirm": "Are you sure you want to do this?", "importFailed": "Import failed", "importSuccess": "Import success", "all": "All", "setDefault": "Set Default", "cancelDefault": "Cancel Default", "search": "Search", "color": "Color", "orderBy": "Order By", "orderMode": { "asc": "ASC", "desc": "DESC" }, "close": "Close", "copyed": "Copyed", "hot": "HOT", "loading": "Loading...", "lastUpdate": "last updated on {time}", "refresh": "Refresh" }, "topbar": { "title": "@:(app.name)", "navBarTip": "Click to show\/hide the navigation bar", "help": "Wiki", "github": "Github", "showNewTorrents": "Get the first page of each tracker", "showNewTorrentsTip": "Search the first page torrents of each tracker according to the current solution" }, "navigation": { "dashboard": { "title": "Overview", "userData": "My Data", "searchResults": "Search Results", "history": "Download History", "collection": "Collection", "searchResultSnapshot": "Search Snapshot", "keepUploadTask": "Reseed Task" }, "settings": { "title": "Settings", "base": "General", "sites": "Sites", "downloadClients": "Download Server", "downloadPaths": "Download Paths", "searchSolution": "Search Solution", "backup": "Backup & Restore", "permissions": "Permissions" }, "thanks": { "title": "Thanks", "reference": "Project Reference", "specialThanksTo": "Special Thanks" }, "support": { "title": "Support This Project", "bugReport": "Bug Report", "donate": "Donate", "debugger": "Debugger" } }, "permissions": { "title": "Thank you for choosing PT Plugin Plus", "subtitle": "In order to work properly, please authorize the required permission.", "authorize": "Authorization", "cancel": "Cancel", "cancelled": "Bye!", "details": { "allSites": "Access to all trackers for searching and get torrents data;", "tabs": "Read permission of the activity tab to display the PT Plugin Plus icon;", "downloads": "Download permission for batch download of torrents", "cookies": "Import/Export site cookies" }, "headers": { "title": "Permission description", "enabled": "Authorized" }, "request": { "default": "Are you sure granting this permission?", "cookies": "Are you sure granting cookies permission to read and write?" } }, "searchBox": { "searchTip": "Input keyword, IMDb numbers, then press to search", "default": "", "defaultTip": "Search only allowed sites", "all": "", "noSearchSolution": "No Search Solution, please add a solution", "noAllowSearchSites": "The site to search is not configured yet. Please configure it before.", "searchThisKey": "Search “{key}”", "doubanTip": "The above data comes from the Douban Movie API v2 ; if you do not want to display these results for pre-selection, you can close it in the General Settings", "toDouban": "View in Douban" }, "donate": { "title": "Thanks for the support" }, "history": { "title": "Download history", "remove": "Remove", "clear": "Clear", "removeConfirm": "Confirm to delete this record?", "removeConfirmTitle": "Delete confirmed", "clearConfirm": "Confirm to delete all download records?", "ok": "@:(common.ok)", "cancel": "@:(common.cancel)", "download": "Download again", "fail": "Failure", "success": "Success", "unknown": "N/A", "defaultPath": "Default Path", "seedingTorrent": "Sending torrents to download server...", "headers": { "site": "Source", "title": "Title", "status": "Status", "time": "Download time", "action": "Action" } }, "home": { "title": "My Data", "getInfos": "Refresh my data", "cancelRequest": "Cancel request", "requesting": "Requesting", "siteName": "Site name", "userName": "User name", "userLevel": "User level", "levelRequirements": "Level requirements", "seedingPoints": "Seeding Points", "showHnR": "H&R", "selectColumns": "Select Columns", "week": "Expressed in weeks", "timeline": "Time line", "settings": "Settings", "statistic": "Statistic", "newMessage": "New message", "startGetting": "Getting user profile...", "gettingForSite": "Getting {siteName} user profile", "requestCompleted": "Request completed, time: {second} seconds.", "getUserInfoError": "An error occurred", "getUserInfoAbort": "Get user profile request has been canceled. ({siteName})", "getUserInfoAbortError": "Cancellation failed to get user profile request. ({siteName})", "offline":"Offline", "headers": { "date": "Date", "site": "Site", "userName": "User name", "levelName": "Level", "activitiyData": "Activitiy data", "ratio": "Ratio", "seeding": "Seeding", "seedingSize": "Seeding size", "bonus": "Bonus", "seedingPoints": "Seeding Points", "bonusPerHour": "Bonus per hour", "joinTime": "Join time", "lastUpdateTime": "Update at", "status": "Status", "comments": "Comments", "uploads": "Uploaded", "trueDownloaded": "True Downloaded", "classPoints": "Class Points", "unsatisfieds": "Unsatisfieds", "prewarn": "H&R Prewarn" }, "levelRequirement": { "levelRequirements": "Level Requirements", "date": "Date", "site": "Site", "userName": "User name", "levelName": "Level", "activitiyData": "Activitiy data", "ratio": "Ratio", "seeding": "Seeding", "seedingSize": "Seeding size", "bonus": "Bonus", "seedingPoints": "Seeding Points", "seedingTime": "Seeding Time", "bonusPerHour": "Bonus per hour", "joinTime": "Join time", "lastUpdateTime": "Update at", "status": "Status", "comments": "Comments", "uploaded": "Uploaded", "downloaded":"Downloaded", "uploads": "Uploaded", "downloads": "Downloaded", "trueDownloaded": "True Downloaded", "classPoints": "Class Points", "uniqueGroups": "Unique Groups", "perfectFLAC": "\"Perfect\" FLAC", "alternative": "Alternative" }, "tip": "N/A means no support", "nodata": "You have not added a site, please go to [Sites] to add a site." }, "systemLog": { "title": "System Log", "save": "Save log", "headers": { "module": "Module", "event": "Events", "time": "Time", "msg": "Description", "action": "Action" } }, "reference": { "title": "This project uses or references the following items", "thanks": "The birth of 'PT Plugin Plus' is established on the base of these projects. Thanks all the participants of the project, thanks for your contribution!", "headers": { "name": "Name", "ver": "Version", "url": "URL" } }, "team": { "title": "Alphabetical Order", "contributors": "Contributors:", "issues": "Suggesters:" }, "timeline": { "share": "Generate a share image", "siteName": "Tracker name", "blurSiteIcon": "Blur site icon", "userName": "User name", "userLevel": "User level", "userId": "User UID", "showSites": "Show Sites", "close": "Close", "shareMessage": "Growth process", "time": { "year": " year(s) ", "month": " month(s) ", "day": " day(s) ", "hour": " hour(s) ", "mins": " minute(s) ", "week": " week(s) ", "ago": " ago", "lessThanAWeek": "Less than a week" }, "total": { "uploaded": "Total uploads: ", "downloaded": "Total download: ", "seedingSize": "Seeding size: ", "ratio": "Total ratio: ", "years": "PT ages:≈ {year} year(s)" }, "updateat": "Update at: ", "user": { "uploaded": "Uploaded: ", "downloaded": "Downloaded: ", "seedingSize": "Seeding size: ", "ratio": "Ratio: ", "bonus": "Bonus: ", "bonusPerHour": "Bonus per hour: " }, "inputDisplayName": "Please enter a name to display:", "inputShareMessage": "Please enter a message to display:" }, "searchTorrent": { "title": "Search Result", "download": "Download", "downloadFailed": "Re-download failed", "sendToClient": "Send to server", "sendToClientTip": "Send torrents to download server", "save": "Save", "saveTip": "Save torrents", "collection": "Collection", "searching": "Searching, please wait...", "cancelSearch": "Cancel search", "showCheckbox": "Multiple selection", "noTag": "No tag", "allSites": "All Sites", "multiDownloadConfirm": "The number of torrents currently downloaded exceeds one, and the browser may prompt multiple times to save. Do you want to continue?", "copyToClipboard": "Copy Links", "copyToClipboardTip": "Copy download links to clipboard", "reSearch": "Re-search", "showCategory": "Category", "filterSearchResults": "Filter search results", "noResultsSites": "Site with 0 result:", "failedSites": "Failed site:", "reSearchFailedSites": "Re-search failed site", "failUrl": "Invalid link", "headers": { "site": "Site", "title": "Title", "category": "Category\/Entrance", "size": "Size", "seeders": "S", "leechers": "L", "completed": "C", "comments": "Comments", "time": "Time(≈)", "action": "Action" }, "optionsIsMissing": "System parameter is missing", "sitesIsMissing": "Please set up the site first", "optionsIsMissingErrorMsg": "System parameters are lost, please re-open this page", "doubanIdConversionFailed": "Douban ID Conversion Failed", "skipSites": "Sites that do not support search at this time:", "noAllowSearchSites": "You have not configured a site that allows search. Please go to [Site Settings] to configure.", "searchStartMsg": "Ready to start searching, a total of {count} sites", "siteIsSearching": "[{siteName}] is searching.", "siteIsSearchDone": "{siteName} Search completed with {count} results.", "siteSearchAbort": "{host} Search request canceled", "siteSearchAbortError": "{host} Search request cancellation failed", "siteSearchTimeout": "{host} Connection timed out", "siteSearchError": "{host} Network or other error", "notLogged": "Not logged", "searchFinished": "Search completed, found {count} results, time: {second} seconds.", "searchProgress": "{count} results have been received, search is still in progress...", "seedingTorrent": "Sending torrent to download server...", "userCanceled": "User Canceled", "sendTorrentToClient": "Send torrent to the download server", "sendTorrentToClientSuccess": "Send torrent to download server successfully", "sendTorrentToClientError": "Send torrent to download server failed", "downloadSelectedError": "Failed to download torrent file: {name}", "copyLinkToClipboardSuccess": "Download link has been copied to the clipboard", "copyLinkToClipboardError": "Copying the download link failed!", "copySelectedToClipboardSuccess": "{count} download links have been copied to the clipboard", "downloadTo": "Download to: {path}", "noReSearchSites": "No sites that need to be re-searched", "doubanIdConverting": "Trying to convert the douban id, please wait...", "invalidDoubanId": "Invalid douban id", "torrentStatus": { "downloading": "Downloading", "sending": "Sending", "completed": "Completed", "inactive": "Inactive" } }, "settings": { "backup": { "title": "Parameter backup and recovery", "subTitle": "Note: Unless encryption is set, the backup file is in plain text and may contain personal information. Please take care of it.", "backup": "Backup", "restore": "Restore", "backupToGoogle": "Backup To Google", "restoreFromGoogle": "Restore From Google", "restoreConfirm": "Are you sure you want to restore the settings from the backup data? This will overwrite all current settings.", "restoreSuccess": "Parameter has been restored", "restoreError": "Parameter recovery failed!", "loadError": "Configuration information failed to load", "backupDone": "Backup completed", "backupError": "Backup parameters failed!", "errorMessage": { "QUOTA_BYTES_PER_ITEM": "The size of the content to be saved exceeds the Google limit (8K)" }, "clearFromGoogle": "Clear", "clearFromGoogleTip": "Clear backed up parameters from Google", "clearFromGoogleConfirm": "Do you want to clear the backed up parameters from Google?", "clearFromGoogleError": "Clear failed!", "clearFromGoogleSuccess": "Content cleared", "index": { "headers": { "name": "Service name", "type": "Type", "lastBackupTime": "Last backup time", "action": "Action" } }, "server": { "add": { "title": "Add Backup Server" }, "edit": { "title": "Edit Backup Server" }, "editor": { "type": "Server Type", "name": "Server name", "address": "Server address", "authCode": "Authorization Code", "applyAuthCode": "Apply", "loginName": "log-in name", "loginPwd": "login password", "digest": "Use Digest Authorization" }, "list": { "noData": "No backup data yet", "backupToServer": "Backup to server", "loadBackupList": "Load backup list" }, "getFileListError": "Failed to get the backup file, please confirm whether the network and backup server are available", "owss": { "addressTip": "The server address contains the port, such as: http:\/\/192.168.1.1:8088\/storage" } }, "restoreAll": "Restore All", "restoreCollection": "Restore Collection Only", "restoreCookies": "Restore Cookies Only", "restoreSearchResultSnapshot": "Restore Search Result Snapshot Only", "restoreKeepUploadTask": "Restore Reseed Task Only", "restoreDownloadHistory": "Restore Download History Only", "contentNotExist": { "cookies": "Cookies not exist in the backup file", "collection": "Collection not exists in the backup file", "searchResultSnapshot": "Search Result Snapshot not exists in the backup file", "keepUploadTask": "Reseed Task not exists in the backup file", "downloadHistory": "Download History not exists in the backup file" }, "backupItem": { "base": "General", "userDatas": "User Datas", "collection": "Collection", "cookies": "Cookies", "searchResultSnapshot": "Search Result Snapshot", "keepUploadTask": "Reseed Task", "downloadHistory": "Download History" }, "restoreErrorType": { "needSecretKey": "Need Secret Key", "errorSecretKey": "Error Secret Key" }, "enterSecretKey": "Please enter a Secret key:", "restoreCookiesConfirm": "Are you sure you want to restore cookies? This will overwrite current cookies." }, "base": { "title": "General", "defaultClient": "Default download server (required)", "autoUpdate": "Automatically update official data", "save": "Save", "allowSelectionTextSearch": "Enable page content selection search", "allowDropToSend": "Send a link to the download server when dragging and dropping links to the plugin icon", "clearCache": "Clear cache", "clearCacheConfirm": "Are you sure you want to clear the cache? After the cleaning is completed, the system configuration information will be re-downloaded from the official website next time.", "needConfirmWhenExceedSize": "Confirmation when the total volume of torrent downloaded in batches exceeds the following size", "exceedSize": "Size", "searchResultRows": "Number of results returned per site at the time of search", "saveDownloadHistory": "Enable download history to record torrent information sent with one click at a time", "connectClientTimeout": "Global timeout (milliseconds, 1000 milliseconds = 1 second), acting on the connection download server, downloading the torrent file, etc.", "noClient": "The download server has not been configured. Please configure the download service before selecting", "cacheIsCleared": "The cache has been cleared. If it needs to take effect immediately, please reopen the page.", "saved": "Parameter saved", "autoRefreshUserData": "Automatically refresh user data when the browser is open (Beta)", "autoRefreshUserDataTip1": "Automatically refreshes at", "autoRefreshUserDataTip2": "every day (if the browser opens after this time, it will be automatically refreshed when the browser is opened)", "autoRefreshUserDataTip3": "Retry", "autoRefreshUserDataTip4": "times after failure,", "autoRefreshUserDataTip5": "minute apart", "searchResultOrderBySitePriority": "When the search results are clicked on the site header, they are sorted by site priority (effective after refreshing the page after saving)", "saveSearchKey": "Save historical search keyword records", "showMoiveInfoCardOnSearch": "Show movie and rating information when searching by IMDb number", "getMovieInformationBeforeSearching": "When entering a search keyword, load relevant information from Douban for pre-selection", "maxMovieInformationCount": "Maximum display number of entries (1-20):", "searchModeForItem": "When clicking on a pre-selected item:", "showToolbarOnContentPage": "Enable site page plugin icons and toolbars (such as one-click downloads, etc.)", "lastUpdate": " (last updated on {time})", "lastUpdateUnknown": " (Update time is unknown)", "lastUpdateFailed": " (Failed to get update time)", "autoRefreshUserDataLastUpdate": " (last updated on {time})", "beforeSearchingItemSearchMode": { "id": "Search by IMDb ID to get more accurate content, but it need more time to get IMDb ID", "name": "Fuzzy search by name to get more content" }, "downloadFailedRetry": "Retry after download failed", "downloadFailedRetryTip1": "Retry", "downloadFailedRetryTip2": "times after failure,", "downloadFailedRetryTip3": "second apart. (0 means retry immediately after failure)", "tabs": { "base": "General", "search": "Search", "download": "Download", "advanced": "Advanced" }, "apiKey": { "omdb": "OMDb API Key", "douban": "Douban API Key" }, "apiKeyTip": "The OMDb API Key is used to obtain the rating information of the movie;\nThe Douban API Key is used to obtain the basic information of the movie, such as pictures, introductions, etc.", "verifyApiKey": "Verify Api Key", "batchDownloadInterval": "Per torrent interval (seconds) when downloading in bulk", "enableBackgroundDownload": "Enable background download task", "position": { "label": "Displayed on: ", "left": " Left side of page", "right": "Rigth side of page" }, "allowBackupCookies": "Backup cookies of the configured site. (Backup to Google is not supported)", "encryptBackupData": "Encrypt backup data. (Backup to Google is not supported)", "encryptMode": "Encryption: ", "encryptSecretKey": "Secret Key:", "encryptTip": "Note: The key is only saved in the current browser and will not be backed up. Please save it properly. If the key is lost after encryption, the data will not be recovered.", "createSecretKey": "Create", "allowSaveSnapshot": "Allow saving search results snapshots" }, "downloadClients": { "add": { "title": "Add download server", "titleStep1": "Select bittorrent client type", "titleStep2": "Detailed configuration", "validMsg": "Please select a bittorrent client type", "helpMsg": "Can't find the bittorrent client type you want? ", "nextStep": "Next", "prevStep": "Previous", "cancel": "Cancel" }, "edit": { "title": "Edit download server", "ok": "OK", "cancel": "Cancel" }, "editor": { "name": "Name of server", "type": "bittorrent client type", "address": "Server address", "addressTip": "The server address contains the port, such as: http:\/\/192.168.1.1:5000\/", "loginName": "log-in name", "loginPwd": "login password", "id": "ID", "autoStart": "Automatically start downloading when sending a torrent", "tagIMDb": "Add IMDb tag when sending a torrent(Beta)", "autoCreate": "", "test": "Test if the server can connect", "testSuccess": "Server can be connected", "testConnectionError": "Network connection error", "testError": "Server connection failed", "testUnknownError": "unknown errors", "testOtherError": "Other errors, the code returned by the server is: {code}", "testAddressError": "Server address error" }, "index": { "title": "Download server configuration", "subTitle": "You must to add at least one download server before you start using it.", "add": "New", "remove": "Remove", "clear": "Clear", "itemDuplicate": "The name already exists", "removeConfirm": "Are you sure you want to delete this download server?", "removeConfirmTitle": "Delete confirmation", "clearConfirm": "Are you sure you want to delete all download servers?", "removeSelectedConfirm": "Are you sure you want to delete the selected download server?", "ok": "OK", "cancel": "Cancel", "headers": { "name": "Name", "type": "Type", "address": "Address", "action": "Action" } } }, "downloadPaths": { "add": { "title": "Add new download Path", "path": "Paths list", "pathTip": "Press Enter multiple Paths separated first as the default Path", "ok": "OK", "cancel": "Cancel", "selectSite": "Select a site (not selected means all sites are available)" }, "edit": { "title": "Edit download Path definition", "site": "Site" }, "index": { "title": "Download Path settings", "selectedClient": "Servers need to be set", "add": "New", "remove": "Remove", "clear": "Clear", "itemDuplicate": "The name already exists", "removeConfirm": "Are you sure you want to delete this Download Path?", "removeConfirmTitle": "Delete confirmation", "removeSelectedConfirm": "Are you sure you want to delete the selected Download Path?", "ok": "OK", "cancel": "Cancel", "notSupport": "This server type is not supported at this time", "allSite": "", "headers": { "name": "Site", "path": "Download Path", "action": "Action" } }, "keyDescription": { "allowKeys": "The following keywords can be included in the path:", "siteName": "Will be replaced with the current site name;", "siteHost": "Will be replaced with the current site domain name;", "example": "Example: ", "dynamic": "An input box will pop up where the user enters the {key} part of the path;", "dynamicExample": "Example: \/volume1\/<...> ,input: test,result: \/volume1\/test" } }, "searchSolution": { "edit": { "title": "Search Solution Definition" }, "editor": { "name": "Solution Name", "range": "Search range", "headers": { "name": "Site" } }, "index": { "title": "Search Solution Definition", "itemDuplicate": "The name already exists", "removeConfirm": "Are you sure you want to delete this search solution?", "removeConfirmTitle": "Delete confirmation", "removeSelectedConfirm": "Are you sure you want to delete the selected search solution?", "help": "How to use?", "headers": { "name": "Name", "range": "Range", "action": "Action" } } }, "sitePlugins": { "add": { "title": "Add new plugin" }, "edit": { "title": "Edit plugin" }, "editor": { "defaultClient": "Default download server", "name": "Plugin name", "pages": "Applicable page", "pagesTip": "The page starts with '/' to indicate the root path of the website. After inputting, press Enter to add multiple, which can be a regular expression.", "scripts": "Script files", "scriptsTip": "\/ means to load the script from the resource directory root, you can add multiple", "script": "Javascript", "style": "Style", "styles": "Style files", "stylesTip": "\/ means to load the script from the resource directory root, you can add multiple" }, "index": { "title": "Site plugin configuration", "importAll": "Import all", "removeSelectedConfirm": "Are you sure you want to delete the selected plugin?", "removeConfirm": "Are you sure you want to delete this plugin?", "removeTitle": "Delete confirmation", "headers": { "name": "Name", "pages": "Applicable page", "enable": "Enable", "action": "Action" }, "importNameDuplicate": "The name [{name}] already exists. Please re-enter the new name:", "invalidPlugin": "Invalid plugin" } }, "sites": { "add": { "title": "Add Site", "next": "Next", "prev": "Previous", "help": "Can't find the site you want?", "validMsg": "Please select a site (support search)", "custom": "Custom", "step1": "Select site", "step2": "Confirm site configuration" }, "edit": { "title": "Edit Site" }, "editor": { "defaultClient": "Download server (if not selected, the default download server of the basic settings will prevail)", "name": "Site name", "tags": "Tags", "inputTags": "press Enter to add multiple", "schema": "Site schema", "description": "Site description", "host": "Host", "url": "Full url", "urlTip": "The full address of the website, such as: https:\/\/www.github.com\/", "passkey": "Passkey", "passkeyTip": "The key is only used to copy the download address operation. If you do not need this function, please leave it blank.", "allowSearch": "Allow Search", "allowGetUserInfo": "Allow access to user information (Beta)", "cdn": "Site CDN list", "cdnTip": "If you use a different URL than the system definition, you can fill in the currently used website address, fill in one address per line, the first one will be used as the address used for the search.", "priority": "Priority", "priorityTip": "Can be used for search sorting", "offline": "Site is offline (downtime\/shutdown)", "timezone": "Timezone", "upLoadLimit": "Upload limit", "upLoadLimitTip": "Upload limit (KB\/s), 0 or empty for no limit", "disableMessageCount": "Display message count" }, "userinfo": { "title": "User Info", "deleteConfirm": "confirm deletion? This operation cannot be restored, please make sure that you have made a corresponding backup." }, "index": { "importAll": "One-click import site", "importAllConfirm": "Are you sure you want to import site operations? This will import sites that have been logged in to the browser but have not been added.", "removeSelectedConfirm": "Are you sure you want to delete the selected site?", "removeConfirm": "Are you sure you want to delete this site?", "removeTitle": "Delete confirmation", "plugins": "Plugins", "showUserInfo": "UserInfo", "title": "Site settings", "subTitle": "Only the configured site will display the plugin icon and the corresponding function; the offline site will no longer participate in the search and information acquisition;", "searchEntry": "Search Entry", "importedText": "Imported successfully", "siteDuplicateText": "The site already exists", "headers": { "name": "Name", "tags": "Tags", "allowSearch": "Allow Search", "allowGetUserInfo": "User Info", "offline": "Offline", "activeURL": "URL", "action": "Action" }, "importConfig": "Import from config", "importConfirm": "Are you sure you want to import [{name}] ?", "importDuplicateConfirm": "The site {name} already exists. Do you need to import search entrys and plugins?", "resetFavicons": "Reset site favicons" } }, "siteSearchEntry": { "add": { "title": "New Search Entry" }, "edit": { "title": "Edit Search Entry" }, "editor": { "name": "Entry name", "entry": "Entry page (defaults to the system's defined entry page if not filled)", "parseScript": "Search result parsing script", "parseScriptFile": "Search result parsing script file", "resultSelector": "Torrent list selector", "category": "Category (not selected for all)", "queryString": "Append query parameters" }, "index": { "title": "Site search entry configuration", "removeSelectedConfirm": "Are you sure you want to delete the selected search entry?", "removeConfirm": "Are you sure you want to delete this search entry?", "removeTitle": "Delete confirmation", "help": "how to use?", "headers": { "name": "Name", "categories": "Categories", "enable": "Enable", "action": "Action" } } } }, "statistic": { "selectSite": "Select sites that need statistics", "goback": "Back", "share": "Generate a share image", "exportRawData": "Export original data", "allSite": "(All Sites)", "upload": "Upload", "download": "Download", "bonus": "Bonus", "baseDataTitle": "[{userName}@{site}] Basic Data", "baseDataSubTitle": "Uploaded: {uploaded}, Downloaded: {downloaded}, Bonus: {bonus}", "data": "Data", "seedingDataTitle": "[{userName}@{site}] Seeding", "seedingDataSubTitle": "Seeding size: {seedingSize}, count: {count}", "seedingSize": "Seeding size", "seedingCount": "Seeding count", "barDataTitle": "[{userName}@{site}] Upload Data", "size": "Size", "count": "Count", "total": "Total", "percent": "Percent", "note": "Note:
1. Chart history data comes from the overview page, manual or automatic update will be recorded;
2. Each tracker only saves one per day;", "dateRange": { "7day": "7 days", "30day": "30 days", "60day": "60 days", "90day": "90 days", "180day": "180 days", "all": "All days" } }, "service": { "testClientConnectivityFailed": "Test client connection failed[{address}]", "contextMenus": { "history": "View download history", "systemLog": "View plugin log", "issues": "Use question feedback", "searchSelectionText": "Search for \"%s\" related torrents", "searchSelectionTextOnThisSite": "Search only for this site \"%s\" related torrents", "searchByIMDb": "Search current IMDb related torrents", "searchByDouban": "Search current Douban related torrents", "searchByDefault": "Search by default", "searchInAllSite": "Search across all sites", "downloadClientPath": "{clientName} -> Specified path", "userCanceled": "User canceled", "pluginStatusIsUnknown": "The status of the plugin is unknown. The current operation may fail. Please refresh the page and try again.", "sendingLink": "Sending a link to download server", "downloadClientGetFailed": "Failed to get the download server.", "sendTorrentToClientDone": "The download link is sent.", "sendTorrentToClientError": "Download link failed to send!", "sendTorrentToDefaultClient": "Sent to the default server {client.name} -> {- client.address}", "sendTorrentToClient": "Send to other servers", "searchInSite": "Search in site", "searchInSolution": "Search in solution" }, "searcher": { "siteSearchConfigEntryIsEmpty": "The site [{site.name}] is not configured with a search page. Please configure it first.", "siteSearchEntryIsEmpty": "The site [{site.name}] does not specify a search entry. Please specify a search entry first.", "siteSearchResultParseFailed": "[{site.name}] data parsing failed!", "siteEvalScriptFailed": "[{site.name}] Script execution error!", "siteSearchResultError": "[{site.name}] No expected data was returned.", "siteAbortSearch": "Deactivating search request for [{site.host}]", "siteAbortSearchError": "[{site.host}] Failed to cancel search request!", "siteNetworkFailed": "[{site.name}]Network request failed! ({msg})" }, "controller": { "invalidAddress": "Invalid Address", "invalidDownloadServer": "Invalid Download Server", "downloadTimeout": "The download server connection timed out, please check the network settings or adjust the server timeout!", "downloadFinished": "Download server {name} handles [{action}] command completion", "downloadError": "Download server {name} processing [{action}] command failed!", "torrentAdded": "{title} The torrent has been added.", "torrentSavePath": "
Save to {path}", "transmissionSuccess": "{data.name} has been sent to Transmission, ID: {data.id}", "transmissionDuplicate": "The {name} torrent already exists! ID: {id}", "transmissionError": "The link failed to send. Please check if the download server is available.", "invalidTorrent": "Invalid Torrent", "getUserInfoSiteIsEmpty": "No site needs to get user information", "invalidImage": "Invalid Image", "noPermission": "No permission, please go to user authorization", "downloadTaskIsCreated": "Download task has been created ({count} total)", "downloadTaskIsCompleted": "Batch download task has been sent. Success: {success}, Failed: {failed}." }, "omnibox": { "search": "Search for the relevant seed for \"{text}\" in the ({solutionName}) solution" }, "user": { "notSupported": "Not Supported", "notLogged": "Not logged", "needLogin": "Not logged", "unknown": "Unknown", "getUserInfoFailed": "Failed to get username and id", "abortGetUserInfoFailed": "Cancel request to get user information failed" } }, "contentPage": { "backgroundServiceIsStoped": "Please refresh the page and try again", "actionExecutionFailed": "{action} Execution failed, maybe background service is unavailable", "pluginTitle": "PT Plugin Plus - Click to open the configuration page", "callbackFailed": "{label} An error has occurred. Please try again.", "notSupported": "This operation is not supported on the current page", "buttons": { "downloadAll": "Download All", "downloadAllTip": "Download all torrent from the current page to [{name}]", "downloadAllTo": "Download to", "downloadAllToTip": "Download all torrent from the current page to the specified server", "copyAllToClipboard": "Copy Link", "copyAllToClipboardTip": "Copy download link to clipboard", "needAuthorization": "Authorize", "needAuthorizationTip": "Download all torrent file features require permission, click to go to authorization", "saveAllTorrent": "All torrents", "saveAllTorrentTip": "Download all torrent files", "freeSpaceTip": "Default server free space\n{path}", "downloadTo": "Download to", "downloadToTip": "Download the current torrent to the specified server", "downloadToDefault": "Download", "downloadToDefaultTip": "Download the current torrent to [{name}]", "copyToClipboard": "Copy Link", "copyToClipboardTip": "Copy download link to clipboard", "menuDownloadTo": "Download to: {server}", "sayThanks": "Say Thanks", "sayThanksTip": "Say thanks to the current torrent", "cover": "Cover", "coverTip": "View by cover", "addToCollection": "Collection", "removeFromCollection": "Remove Collection" }, "needPasskey": "Please set the site key (Passkey) first.", "userCanceled": "User Canceled", "sendingTorrent": "Sending a torrent to the server, please wait...", "invalidDownloadServer": "Invalid Download Server", "invalidURL": "Invalid URL", "dropInvalidURL": "Invalid URL, please drag and drop download link", "getDownloadURLisUndefined": "'getDownloadURL' is undefined", "getDownloadURLsisUndefined": "'getDownloadURLs' is undefined", "getDownloadURLFailed": "Failed to get download link", "getDownloadURLsFailed": "Failed to get the download link, failed to correctly locate the link", "exceedSizeConfirm": "The current page seed size is {size} has exceeded {exceedSize} {exceedSizeUnit}, is it sent?", "exceedSizeCanceled": "Oversized has been cancelled", "downloadURLsFinished": "{count} links have been sent.", "downloadURLsTip": "Sending: {text}", "search": { "needLogin": "[{siteName}] needs to log in and search again", "noTorrents": "[{siteName}] did not find the relevant torrent", "torrentTableIsEmpty": "[{siteName}] is not targeting the torrent list, or there is no related torrent", "parseError": "[{siteName}] Error getting seed information: {error}" }, "dragTitle": "Press and hold to drag and drop; double click to reset" }, "downloadClient": { "timeout": "Access timed out", "unknownError": "Unknow Error", "notFound": "The specified address was not found and the server returned error: 404", "addURLSuccess": "URL has been added into {name}", "unsupportedMediaType": "Wrong torrent file", "serverIsUnavailable": "Server unavailable or network error", "permissionDenied": "Authentication failure", "serverConnectionFailed": "Server access failed", "destinationDenied": "The specified directory [{path}] is unavailable or no access permissions", "destinationDoesNotExist": "The specified directory [{path}] does not exist", "fileUploadFailed": "File upload failed", "maxNumberOfTasksReached": "Maximum number of tasks reached" }, "footer": { "replaceLanguageConfirm": "The language already exists. Do you need to replace it?", "invalidFile": "Invalid language file!" }, "collection": { "title": "Collection list", "add": "Add to collection", "remove": "Remove from collection", "addGroup": "Create Group", "addToGroup": "Add To Group", "noGroup": "", "changeGroupName": "Change the group name", "removeGroupConfirm": "Are you sure you want to delete this group?{count} collections will be removed from this group.", "setMovieId": "Enter IMDb ID (e.g. tt123567) or doubanId (e.g. 12345678):", "inputGroupName": "Enter the group name:", "headers": { "site": "Site", "title": "Title", "size": "Size", "time": "Add time", "action": "Action" } }, "searchResultSnapshot": { "title": "Search Result Snapshot", "show": "Show Snapshot", "removeConfirmTitle": "Delete confirmation", "removeConfirm": "Are you sure you want to delete this Snapshot?", "clearConfirm": "Are you sure you want to delete all Snapshot?", "create": "Create Snapshot", "createSuccess": "Create Snapshot Success", "createError": "Create Snapshot Error", "snapshotTime": "Snapshot Time: {time}", "headers": { "key": "Search Key", "time": "Time" } }, "keepUploadTask": { "title": "Reseed Task", "keepUpload": "Reseed", "removeConfirmTitle": "Delete confirmation", "removeConfirm": "Are you sure you want to delete this Task?", "clearConfirm": "Are you sure you want to delete all Task?", "create": "Create Reseed Task", "createSuccess": "Task creation completed", "createError": "Task creation failed", "noItem": "No matching seeds", "filterSearchResults": "Filter search results", "savePath": "Save Path: ", "defaultPath": "Default Path", "setSavePath": "Set Save Path", "torrentCount": "Reseed count", "sendBaseTorrent": "Send First Torrent", "sendOtherTorrents": "Send Other Torrents", "sendAllTorrents": "Send All Torrents", "sendSuccess": "Send Success", "sendError": "Send Error", "verification": "Verification", "baseTorrent": "Base Torrent", "otherTorrent": "Other Torrent", "size": "Size: ", "fileCount": "Files: ", "sendConfirm": "Are you sure you want to send these {count} torrents?", "addToKeepUpload": "Add to task list", "removeFromKeepUpload": "Remove from task list", "redownload" : "Re-download", "addToKeepUploadConfirm": "Are you sure want add this torrent to task list?", "status": { "label": "Status: ", "downloading": "Downloading", "failed": "Verification failed", "incorrectOrder": "Incorrect Order", "missingFiles": "Missing Files", "success": "Verify successful", "waiting": "Waiting for verification", "downloadFailed": "Download Failed" }, "headers": { "site": "Site", "title": "Title", "size": "Size", "time": "Time" } }, "movieInfoCard": { "alias": "Alias: ", "director": "Director: ", "writer": "Writer: ", "cast": "Cast: ", "type": "Type: ", "pubdate": "Pubdate: ", "duration": "Duration: ", "ratings": { "douban": "Douban {average} ({numRaters})", "imdb": "IMDb {average} ({numRaters})" } } } } ================================================ FILE: resource/i18n/zh-CN.json ================================================ { "name": "简体中文 Chinese (Simplified)", "code": "zh-CN", "authors": [ "栽培者", "MewX" ], "words": { "app": { "initError": "配置信息加载失败,没有获取到系统定义信息,请尝试刷新当前页面", "initializing": "程序正在准备一些数据,请稍候……", "author": "栽培者", "name": "PT 助手 Plus" }, "common": { "debugMode": "当前处于调试模式", "changeLanguage": "切换语言", "addLanguage": "临时添加新语言", "version": "版本", "systemLog": "系统日志", "darkMode": "反转颜色", "haveNewReleases": "有更新可用", "add": "新增", "edit": "编辑", "copy": "复制", "ok": "确认", "cancel": "取消", "remove": "删除", "clear": "清除", "removeConfirm": "确认要删除这条记录吗?", "removeSelectedConfirm": "确认要删除这些已选中的 {count} 条记录吗?", "removeConfirmTitle": "删除确认", "clearConfirm": "确认要删除所有记录吗?", "id": "ID", "readyToStart": "准备开始……", "help": "帮助", "export": "导出", "import": "导入", "share": "分享", "actionConfirm": "确认要进行此操作吗?", "importFailed": "导入失败", "importSuccess": "已成功导入", "all": "全部", "setDefault": "设为默认", "cancelDefault": "取消默认", "search": "搜索", "color": "颜色", "orderBy": "排序字段", "orderMode": { "asc": "升序", "desc": "降序" }, "close": "关闭", "copyed": "已复制", "hot": "热门", "loading": "正在加载……", "lastUpdate": "数据更新于 {time}", "refresh": "刷新" }, "topbar": { "title": "@:(app.name)", "navBarTip": "点击显示/隐藏导航栏", "help": "帮助", "github": "主页", "showNewTorrents": "获取各站第一页种子", "showNewTorrentsTip": "根据当前方案,搜索各站的第一页种子" }, "navigation": { "dashboard": { "title": "概览", "userData": "我的数据", "searchResults": "搜索结果", "history": "下载历史", "collection": "我的收藏", "searchResultSnapshot": "搜索结果快照", "keepUploadTask": "辅种任务" }, "settings": { "title": "参数设置", "base": "常规设置", "sites": "站点设置", "downloadClients": "下载服务器", "downloadPaths": "下载目录设置", "searchSolution": "搜索方案", "backup": "参数备份与恢复", "permissions": "权限设置" }, "thanks": { "title": "鸣谢", "reference": "项目参考与引用", "specialThanksTo": "特别感谢" }, "support": { "title": "支持本项目", "bugReport": "Bug反馈", "donate": "捐赠", "debugger": "调试信息" } }, "permissions": { "title": "感谢您选择 PT 助手", "subtitle": "为了不影响正常使用,请对需要的功能进行授权:", "authorize": "授权", "cancel": "我不用了", "cancelled": "世界如此之大,期待有缘再相会!", "details": { "allSites": "所有网站的访问权限,用于搜索和读取做种数据;", "tabs": "所有打开页面的读取权限,用于显示助手图标及各项操作;", "downloads": "下载权限,用于批量下载种子文件", "cookies": "Cookies操作权限,用于备份和恢复站点的登录状态等内容" }, "headers": { "title": "权限描述", "enabled": "已授权" }, "request": { "default": "是否授于该权限?", "cookies": "是否授于Cookies读写权限?" } }, "searchBox": { "searchTip": "输入种子关键字、IMDb编号,按回车进行搜索", "default": "<默认>", "defaultTip": "仅搜索已允许的站点", "all": "<所有站点>", "noSearchSolution": "暂无方案,请添加", "noAllowSearchSites": "暂未配置允许搜索的站点,请先配置", "searchThisKey": "搜索 “{key}”", "doubanTip": "以上数据来自©豆瓣电影 API v2 查询接口;如不想显示这些结果进行预选,可在“常规设置”中进行关闭", "toDouban": "在豆瓣进行查看" }, "donate": { "title": "感谢您的关注和支持" }, "history": { "title": "下载历史", "remove": "@:(common.remove)", "clear": "@:(common.clear)", "removeConfirm": "确认要删除这条记录吗?", "removeConfirmTitle": "删除确认", "clearConfirm": "确认要删除所有下载记录吗?", "ok": "@:(common.ok)", "cancel": "@:(common.cancel)", "download": "重新下载", "fail": "失败", "success": "成功", "unknown": "N/A", "defaultPath": "默认目录", "seedingTorrent": "正在发送种子到下载服务器……", "headers": { "site": "来源", "title": "标题", "status": "状态", "time": "下载时间", "action": "操作" } }, "home": { "title": "我的数据", "getInfos": "刷新我的数据", "cancelRequest": "取消请求", "requesting": "正在请求", "siteName": "网站名称", "userName": "用户名称", "userLevel": "用户等级", "levelRequirements": "等级要求", "seedingPoints": "保种积分", "showHnR": "H&R", "selectColumns": "过滤列", "week": "时间显示为周数", "timeline": "时间轴", "settings": "参数", "statistic": "数据图表", "newMessage": "新消息", "startGetting": "准备开始获取个人数据", "gettingForSite": "正在获取 {siteName} 个人数据", "requestCompleted": "请求完成,耗时:{second} 秒。", "getUserInfoError": "发生错误", "getUserInfoAbort": "{siteName} 获取用户资料请求已取消", "getUserInfoAbortError": "{siteName} 获取用户资料请求取消失败", "offline":"已离线", "headers": { "date": "日期", "site": "站点", "userName": "用户名", "levelName": "等级", "activitiyData": "数据量", "ratio": "分享率", "seeding": "做种数", "seedingSize": "做种体积", "bonus": "魔力值/积分", "seedingPoints": "做种积分", "bonusPerHour": "时魔", "joinTime": "入站时间", "lastUpdateTime": "数据更新于", "status": "状态", "uploads": "发布", "trueDownloaded": "真实下载", "classPoints": "等级积分", "unsatisfieds": "H&R考核中", "prewarn": "H&R预警" }, "levelRequirement": { "levelRequirements": "升级要求", "date": "日期", "site": "站点", "userName": "用户名", "levelName": "等级", "activitiyData": "数据量", "ratio": "分享率", "seeding": "做种数", "seedingSize": "做种体积", "seedingTime": "做种时间", "bonus": "魔力值", "seedingPoints": "做种积分", "bonusPerHour": "时魔", "joinTime": "入站时间", "lastUpdateTime": "数据更新于", "status": "状态", "uploaded": "上传量", "downloaded":"下载量", "uploads": "发布", "downloads": "完成", "trueDownloaded": "真实下载", "classPoints": "等级积分", "uniqueGroups": "独特分组", "perfectFLAC": "“完美”FLAC", "alternative": "择一" }, "tip": "N/A 表示暂不支持", "nodata": "您还没有添加站点,请前往『站点设置』添加站点。" }, "systemLog": { "title": "系统日志", "save": "保存日志", "headers": { "module": "模块", "event": "事件", "time": "时间", "msg": "描述", "action": "操作" } }, "reference": { "title": "本项目使用或参考了以下项目", "thanks": "PT 助手的诞生是建立在这些项目基础之上,在此感谢所有项目的参与人员,感谢他们的付出!", "headers": { "name": "名称", "ver": "版本", "url": "网址" } }, "team": { "title": "列表按字母排序", "contributors": "协作者们:", "issues": "反馈者们:" }, "timeline": { "share": "生成分享图片", "siteName": "网站名称", "blurSiteIcon": "模糊站点图标", "userName": "用户名称", "userId": "用户UID", "userLevel": "用户等级", "showSites": "展示站点", "close": "关闭", "shareMessage": "这些年走过的路", "time": { "year": "年", "month": "月", "day": "日", "hour": "时", "mins": "分", "week": "周", "ago": "前", "lessThanAWeek": "不满一周" }, "total": { "uploaded": "上传总量:", "downloaded": "下载总量:", "seedingSize": "做种总量:", "ratio": "总分享率:", "years": "..P龄:≈ {year} 年" }, "updateat": "数据更新于:", "user": { "uploaded": "上传量:", "downloaded": "下载量:", "seedingSize": "做种量:", "ratio": "分享率:", "bonus": "积分值:", "bonusPerHour": "时 魔:" }, "inputDisplayName": "请输入需要显示的名称:", "inputShareMessage": "请输入需要显示的信息:" }, "searchTorrent": { "title": "搜索结果", "download": "下载", "downloadFailed": "重下失败", "sendToClient": "推送", "sendToClientTip": "推送到下载服务器", "save": "保存", "saveTip": "下载种子文件到本地", "collection": "收藏", "searching": "正在搜索中,请稍候……", "cancelSearch": "取消搜索", "showCheckbox": "多选", "noTag": "无标签", "allSites": "全部站点", "multiDownloadConfirm": "当前下载的种子数量超过一个,浏览器可能会多次提示保存,是否继续?", "copyToClipboard": "复制", "copyToClipboardTip": "复制下载链接到剪切板", "reSearch": "重新再搜索", "showCategory": "分类", "filterSearchResults": "过滤搜索结果", "noResultsSites": "无结果站点:", "failedSites": "失败站点:", "reSearchFailedSites": "重试失败的站点", "failUrl": "链接无效", "headers": { "site": "站点", "title": "标题", "category": "分类/入口", "size": "大小", "seeders": "上传", "leechers": "下载", "completed": "完成", "comments": "评论", "time": "发布于(≈)", "action": "操作" }, "optionsIsMissing": "系统参数丢失", "sitesIsMissing": "请先设置站点", "optionsIsMissingErrorMsg": "系统参数丢失,多次尝试等待后无效,请重新打开配置页再试", "doubanIdConversionFailed": "豆瓣ID转换失败", "skipSites": "暂不支持搜索的站点:", "noAllowSearchSites": "您还没有配置允许搜索的站点,请先前往【站点设置】进行配置", "searchStartMsg": "准备开始搜索,共需搜索 {count} 个站点", "siteIsSearching": "正在搜索 [{siteName}]", "siteIsSearchDone": "{siteName} 搜索完成,共有 {count} 条结果", "siteSearchAbort": "{host} 搜索请求已取消", "siteSearchAbortError": "{host} 搜索请求取消失败", "siteSearchTimeout": "{host} 连接超时", "siteSearchError": "{host} 发生网络或其他错误", "notLogged": "未登录", "searchFinished": "搜索完成,共找到 {count} 条结果,耗时:{second} 秒。", "searchProgress": "已接收 {count} 条结果,搜索仍在进行……", "seedingTorrent": "正在发送种子到下载服务器……", "userCanceled": "用户已取消", "sendTorrentToClient": "发送种子到下载服务器", "sendTorrentToClientSuccess": "发送种子到下载服务器成功", "sendTorrentToClientError": "发送种子到下载服务器失败", "downloadSelectedError": "下载种子文件失败:{name}", "copyLinkToClipboardSuccess": "下载链接已复制到剪切板", "copyLinkToClipboardError": "复制下载链接失败!", "copySelectedToClipboardSuccess": "{count} 条下载链接已复制到剪切板", "downloadTo": "下载到:{path}", "noReSearchSites": "没有需要重新搜索的站点", "doubanIdConverting": "正在尝试转换豆瓣编号,请稍候……", "invalidDoubanId": "无效的豆瓣ID", "torrentStatus": { "downloading": "正在下载", "sending": "正在做种", "completed": "已完成,未做种", "inactive": "未活动" } }, "settings": { "backup": { "title": "参数备份与恢复", "subTitle": "注:除非设置加密,否则备份文件为明文,其中可能包含个人信息,请注意保管。", "backup": "备份", "restore": "恢复", "backupToGoogle": "备份到Google", "restoreFromGoogle": "从Google恢复", "restoreConfirm": "确认要从备份数据中恢复配置吗?这将覆盖当前设置信息。", "restoreSuccess": "参数已恢复", "restoreError": "参数恢复失败!", "loadError": "配置信息加载失败", "backupDone": "备份完成", "backupError": "备份参数失败!", "errorMessage": { "QUOTA_BYTES_PER_ITEM": "要保存的内容大小超出了Google限制(8K)" }, "clearFromGoogle": "清除", "clearFromGoogleTip": "从Google中清除已备份的参数", "clearFromGoogleConfirm": "是否要从Google中清除已备份的参数?", "clearFromGoogleError": "清除失败!", "clearFromGoogleSuccess": "内容已清除", "index": { "headers": { "name": "服务名称", "type": "类型", "lastBackupTime": "最近备份时间", "action": "操作" } }, "server": { "add": { "title": "添加备份服务器" }, "edit": { "title": "编辑备份服务器" }, "editor": { "type": "服务器类型", "name": "服务名称", "address": "服务器地址", "authCode": "授权码", "applyAuthCode": "申请授权码", "loginName": "登录名", "loginPwd": "登录密码", "digest": "启用 Digest 认证" }, "list": { "noData": "暂无备份数据", "backupToServer": "备份到服务器", "loadBackupList": "历史记录" }, "getFileListError": "获取备份文件失败,请确认网络和备份服务器是否可用", "owss": { "addressTip": "完整的服务器地址,如:http://192.168.1.1:8088/storage" } }, "restoreAll": "恢复所有设置", "restoreCollection": "仅恢复收藏数据", "restoreCookies": "仅恢复站点Cookies", "restoreSearchResultSnapshot": "仅恢复搜索结果快照", "restoreKeepUploadTask": "仅恢复辅种任务", "restoreDownloadHistory": "仅恢复下载历史", "contentNotExist": { "cookies": "备份文件中不存在Cookies", "collection": "备份文件中不存在收藏信息", "searchResultSnapshot": "备份文件中不存在搜索结果快照", "keepUploadTask": "备份文件中不存在辅种任务", "downloadHistory": "备份文件中不存在下载历史" }, "backupItem": { "base": "常规设置", "userDatas": "站点数据", "collection": "我的收藏", "cookies": "Cookies", "searchResultSnapshot": "搜索结果快照", "keepUploadTask": "辅种任务", "downloadHistory": "下载历史" }, "restoreErrorType": { "needSecretKey": "需要密钥", "errorSecretKey": "密钥错误" }, "enterSecretKey": "请输入密钥:", "restoreCookiesConfirm": "备份内容包含 Cookies,是否需要恢复Cookies?此操作可能造成一些意想不到的结果。" }, "base": { "title": "常规设置", "defaultClient": "默认下载服务器", "autoUpdate": "自动更新官方数据", "save": "保存", "allowSelectionTextSearch": "启用页面内容选择搜索", "allowDropToSend": "启用拖放链接到助手图标时,直接发送链接到下载服务器", "clearCache": "清除缓存", "clearCacheConfirm": "确认要清除缓存吗?清除完成后,下次将会从官网中重新下载系统配置信息。", "needConfirmWhenExceedSize": "当批量下载的种子总体积超过以下大小时需要确认", "exceedSize": "大小", "searchResultRows": "搜索时每站点返回结果数量", "saveDownloadHistory": "启用下载历史,以记录每次一键发送的种子信息", "connectClientTimeout": "全局超时时间(毫秒,1000毫秒=1秒),作用于连接下载服务器、下载种子文件等操作", "noClient": "尚未配置下载服务器,请配置下载服务后再选择", "cacheIsCleared": "缓存已清除,如需立即生效,请重新打开页面", "saved": "参数已保存", "autoRefreshUserData": "在浏览器打开的情况下自动刷新用户数据(Beta)", "autoRefreshUserDataTip1": "每天于", "autoRefreshUserDataTip2": "自动刷新(如果浏览器在这个时间之后打开,则在浏览器打开时自动刷新)", "autoRefreshUserDataTip3": "失败后重试", "autoRefreshUserDataTip4": "次,每次间隔", "autoRefreshUserDataTip5": "分钟", "searchResultOrderBySitePriority": "搜索结果点击站点表头时,按站点优先级别排序(保存后需刷新页面后生效)", "saveSearchKey": "保存搜索关键字", "showMoiveInfoCardOnSearch": "当以 IMDb 编号搜索时显示电影及评分信息", "getMovieInformationBeforeSearching": "当输入搜索关键字时,从豆瓣加载相关信息以供预选", "maxMovieInformationCount": "最多显示条目(1-20):", "searchModeForItem": "当点击预选条目时:", "showToolbarOnContentPage": "启用站点页面助手图标和工具栏(如一键下载等)", "lastUpdate": "(最后更新于 {time})", "lastUpdateUnknown": "(更新时间未知)", "lastUpdateFailed": "(更新时间获取失败)", "autoRefreshUserDataLastUpdate": "(最后更新于 {time})", "beforeSearchingItemSearchMode": { "id": "以ID进行搜索,以获得较精确的内容,但转换ID时需要时间", "name": "以名称进行模糊搜索,以获得较多的内容" }, "downloadFailedRetry": "当下载失败后进行重试", "downloadFailedRetryTip1": "重试", "downloadFailedRetryTip2": "次,每次间隔", "downloadFailedRetryTip3": "秒(0表示失败后立即重试)", "tabs": { "base": "常规", "search": "搜索", "download": "下载", "advanced": "高级" }, "apiKey": { "omdb": "OMDb API Key", "douban": "豆瓣 API Key" }, "apiKeyTip": "OMDb API Key 用于获取影片的评分信息;\n豆瓣 API Key 用于获取影片的基本资料,如图片、简介等;\n助手已内置了一些Key,如出现无法正常获取评分或影片资料,可进行设置;多个Key按回车进行区分。", "verifyApiKey": "验证 API Key", "batchDownloadInterval": "批量下载(发送)种子时,每个链接时间间隔(秒)", "enableBackgroundDownload": "当批量下载(发送)种子时,使用后台任务,完成后再通知我", "position": { "label": "工具栏位于:", "left": "页面左侧", "right": "页面右侧" }, "allowBackupCookies": "当进行备份操作时,同时备份已配置站点的 Cookies;(不支持备份到Google)", "encryptBackupData": "当进行备份时,对备份数据进行加密;(不支持备份到Google)", "encryptMode": "加密方式:", "encryptSecretKey": "密钥:", "encryptTip": "注意:密钥仅保存在当前浏览器,不会备份,请妥善保存;加密后如密钥丢失将无法恢复数据。", "createSecretKey": "随机生成", "allowSaveSnapshot": "允许保存搜索结果快照" }, "downloadClients": { "add": { "title": "新增下载服务器", "titleStep1": "选择服务器类型", "titleStep2": "详细配置", "validMsg": "请选择一个服务器类型", "helpMsg": "找不到想要的服务器类型?来这里添加吧!", "nextStep": "下一步", "prevStep": "上一步", "cancel": "取消" }, "edit": { "title": "编辑下载服务器", "ok": "确认", "cancel": "取消" }, "editor": { "name": "服务器名称", "type": "服务器类型", "address": "服务器地址", "addressTip": "完整的服务器地址(含端口),如:http://192.168.1.1:5000/", "loginName": "登录名", "loginPwd": "登录密码", "id": "ID", "autoStart": "发送种子时自动开始下载", "tagIMDb": "发送种子时自动添加IMDb标签(Beta)", "autoCreate": "<保存后自动生成>", "test": "测试服务器是否可连接", "testSuccess": "服务器可连接", "testConnectionError": "网络连接错误", "testError": "服务器连接失败", "testUnknownError": "未知错误", "testOtherError": "其他错误,服务器返回的代码为: {code}", "testAddressError": "服务器地址错误" }, "index": { "title": "下载服务器配置", "subTitle": "在开始使用之前,您至少需要添加一个下载服务器。", "add": "新增", "remove": "删除", "clear": "清除", "itemDuplicate": "该名称已存在", "removeConfirm": "确认要删除这个下载服务器吗?", "removeConfirmTitle": "删除确认", "clearConfirm": "确认要删除所有下载服务器吗?", "removeSelectedConfirm": "确认要删除已选中的下载服务器吗?", "ok": "确认", "cancel": "取消", "headers": { "name": "名称", "type": "类型", "address": "服务器地址", "action": "操作" } } }, "downloadPaths": { "add": { "title": "新增下载目录定义", "path": "目录列表", "pathTip": "多个目录按回车分隔,第一个为默认目录", "ok": "确认", "cancel": "取消", "selectSite": "选择一个站点(不选表示所有站点都可用)" }, "edit": { "title": "编辑下载目录定义", "site": "站点" }, "index": { "title": "下载目录设置", "selectedClient": "需要设置的服务器", "add": "新增", "remove": "删除", "clear": "清除", "itemDuplicate": "该名称已存在", "removeConfirm": "确认要删除这个保存目录吗?", "removeConfirmTitle": "删除确认", "removeSelectedConfirm": "确认要删除已选中的保存目录吗?", "ok": "确认", "cancel": "取消", "notSupport": "暂不支持该服务器类型", "allSite": "<所有站点>", "headers": { "name": "站点", "path": "保存目录", "action": "操作" } }, "keyDescription": { "allowKeys": "路径中可包含以下关键字:", "siteName": "将被替换为当前站点名称;", "siteHost": "将被替换为当前站点域名;", "example": "例:", "dynamic": "将会弹出输入框,由用户输入路径中 {key} 部分内容;", "dynamicExample": "例:/volume1/<...> ,输入:test,结果 /volume1/test" } }, "searchSolution": { "edit": { "title": "搜索方案定义" }, "editor": { "name": "方案名称", "range": "搜索范围", "headers": { "name": "站点" } }, "index": { "title": "搜索方案定义", "itemDuplicate": "该名称已存在", "removeConfirm": "确认要删除这个搜索方案吗?", "removeConfirmTitle": "删除确认", "removeSelectedConfirm": "确认要删除已选中的搜索方案吗?", "help": "如何使用?", "headers": { "name": "名称", "range": "范围", "action": "操作" } } }, "sitePlugins": { "add": { "title": "新增插件" }, "edit": { "title": "编辑插件" }, "editor": { "defaultClient": "默认下载服务器", "name": "插件名称", "pages": "适用页面", "pagesTip": "页面以'/'开始表示网站根目录,输入完成后按回车添加,可添加多个,可以是正则表达式", "scripts": "附加脚本文件", "scriptsTip": "/ 表示从资源目录根加载脚本,可添加多个", "script": "JS脚本", "style": "附加样式", "styles": "附加样式文件", "stylesTip": "/ 表示从资源目录根加载脚本,可添加多个" }, "index": { "title": "站点插件配置", "importAll": "导入所有", "removeSelectedConfirm": "确认要删除已选中的插件吗?", "removeConfirm": "确认要删除这个插件吗?", "removeTitle": "删除确认", "headers": { "name": "名称", "pages": "适用页面", "enable": "启用", "action": "操作" }, "importNameDuplicate": "该名称【{name}】已存在,请重新输入新的名称:", "invalidPlugin": "无效的插件" } }, "sites": { "add": { "title": "新增站点", "next": "下一步", "prev": "上一步", "help": "找不到想要的站点?来这里添加吧!", "validMsg": "请选择一个站点(支持搜索)", "custom": "自定义", "step1": "选择站点", "step2": "确认站点配置" }, "edit": { "title": "编辑站点" }, "editor": { "defaultClient": "指定下载服务器(如不选择则以基本设置的默认下载服务器为准)", "name": "站点名称", "tags": "站点标签", "inputTags": "标签输入完成后按回车添加,可添加多个", "schema": "网站架构", "description": "网站描述", "host": "域名", "url": "网站地址", "urlTip": "网站完整地址,如:https://www.github.com/", "passkey": "密钥", "passkeyTip": "密钥仅用于复制下载地址操作,如果不需要用到此功能,请留空", "allowSearch": "允许搜索", "allowGetUserInfo": "允许获取用户信息(Beta)", "cdn": "站点CDN列表", "cdnTip": "如您使用的网址和系统定义的不同,可在此填写当前使用的网站地址,每行填写一个地址,第一个将做为搜索时使用的地址", "priority": "优先级", "priorityTip": "可用于搜索排序", "offline": "站点已离线(停机/关闭)", "timezone": "时区", "upLoadLimit": "上传速度限制", "upLoadLimitTip": "上传速度限制 (KB/s), 0或不填 不限速", "disableMessageCount": "关闭消息提醒" }, "userinfo": { "title": "用户数据", "deleteConfirm": "确认删除?这个操作不可恢复,请确认你做好了相应备份。" }, "index": { "importAll": "一键导入站点", "importAllConfirm": "确认要进行导入站点操作吗?此操作会导入已在浏览器上登录过但未添加的站点。", "removeSelectedConfirm": "确认要删除已选中的站点吗?", "removeConfirm": "确认要删除这个站点吗?", "removeTitle": "删除确认", "plugins": "插件", "showUserInfo": "用户数据", "title": "站点设置", "subTitle": "只有配置过的站点才会显示插件图标及相应的功能;
已离线的站点不再参与搜索和信息获取;", "searchEntry": "搜索入口", "importedText": "已成功导入", "siteDuplicateText": "该站点已存在", "headers": { "name": "名称", "tags": "标签", "allowSearch": "允许搜索", "allowGetUserInfo": "个人信息", "offline": "已离线", "activeURL": "URL", "action": "操作" }, "importConfig": "从文件导入", "importConfirm": "确认要导入 【{name}】 吗?", "importDuplicateConfirm": "该站点 【{name}】 已存在,是否需要导入搜索入口和插件?", "resetFavicons": "重置站点图标缓存" } }, "siteSearchEntry": { "add": { "title": "新增搜索入口" }, "edit": { "title": "编辑搜索入口" }, "editor": { "name": "入口名称", "entry": "入口页面(如果不填写则默认为系统已定义的入口页面)", "parseScript": "搜索结果解析脚本", "parseScriptFile": "搜索结果解析脚本文件", "resultSelector": "种子列表定位选择器", "category": "资源分类(不选表示所有)", "queryString": "追加查询参数" }, "index": { "title": "站点搜索入口配置", "removeSelectedConfirm": "确认要删除已选中的搜索入口吗?", "removeConfirm": "确认要删除这个搜索入口吗?", "removeTitle": "删除确认", "help": "如何使用?", "headers": { "name": "名称", "categories": "已选择分类", "enable": "启用", "action": "操作" } } } }, "statistic": { "selectSite": "选择需要统计的站点", "goback": "返回", "share": "生成分享图片", "exportRawData": "导出原数据", "allSite": "<所有站点>", "upload": "上传", "download": "下载", "bonus": "积分", "baseDataTitle": "[{userName}@{site}] 基本数据", "baseDataSubTitle": "上传:{uploaded}, 下载:{downloaded},积分:{bonus}", "data": "数据", "seedingDataTitle": "[{userName}@{site}] 保种情况", "seedingDataSubTitle": "做种体积:{seedingSize}, 数量:{count} 个", "seedingSize": "做种体积", "seedingCount": "做种数", "barDataTitle": "[{userName}@{site}] 上传数据", "size": "体积", "count": "数量", "total": "总和", "percent": "占比", "note": "注:
1. 图表历史数据来自概览页,手动刷新或自动更新均会记录;
2. 助手从 v1.0.1 版开始正式记录每次刷新的数据,每个站每天仅保存一条;", "dateRange": { "7day": "近7天", "30day": "近30天", "60day": "近60天", "90day": "近90天", "180day": "近180天", "all": "所有" } }, "service": { "testClientConnectivityFailed": "测试客户连接失败[{address}]", "contextMenus": { "history": "查看下载历史", "systemLog": "查看助手日志", "issues": "使用问题反馈", "searchSelectionText": "搜索 \"%s\" 相关的种子", "searchSelectionTextOnThisSite": "仅搜索本站 \"%s\" 相关的种子", "searchByIMDb": "搜索当前IMDb相关种子", "searchByDouban": "搜索当前豆瓣链接相关种子", "searchByDefault": "按默认方式搜索", "searchInAllSite": "在所有站点中搜索", "downloadClientPath": "{clientName} -> 指定目录", "userCanceled": "用户已取消", "pluginStatusIsUnknown": "插件状态未知,当前操作可能失败,请刷新页面后再试", "sendingLink": "正在发送链接至下载服务器", "downloadClientGetFailed": "获取下载服务器失败。", "sendTorrentToClientDone": "下载链接发送完成。", "sendTorrentToClientError": "下载链接发送失败!", "sendTorrentToDefaultClient": "发送到默认服务器 {client.name} -> {- client.address}", "sendTorrentToClient": "发送到其他服务器", "searchInSite": "在指定的站点进行搜索", "searchInSolution": "以指定的方案中搜索" }, "searcher": { "siteSearchConfigEntryIsEmpty": "该站点[{site.name}]未配置搜索页面,请先配置", "siteSearchEntryIsEmpty": "该站点[{site.name}]未指定搜索入口,请先指定一个搜索入口", "siteSearchResultParseFailed": "[{site.name}]数据解析失败!", "siteEvalScriptFailed": "[{site.name}]脚本执行出错!", "siteSearchResultError": "[{site.name}]没有返回预期的数据。", "siteAbortSearch": "正在取消[{site.host}]的搜索请求", "siteAbortSearchError": "[{site.host}]取消搜索请求失败!", "siteNetworkFailed": "[{site.name}]网络请求失败!({msg})" }, "controller": { "invalidAddress": "无效的地址", "invalidDownloadServer": "无效的下载服务器", "downloadTimeout": "连接下载服务器超时,请检查网络设置或调整服务器超时时间!", "downloadFinished": "下载服务器{name}处理[{action}]命令完成", "downloadError": "下载服务器{name}处理[{action}]命令失败!", "torrentAdded": "{title} 种子已添加完成。", "torrentSavePath": "
保存至 {path}", "transmissionSuccess": "{data.name}已发送至 Transmission,编号:{data.id}", "transmissionDuplicate": "{name}种子已存在!编号:{id}", "transmissionError": "链接发送失败,请检查下载服务器是否可用。", "invalidTorrent": "无效的种子文件,请参考 『常见问题』", "getUserInfoSiteIsEmpty": "没有站点需要获取用户信息", "invalidImage": "无效的图片文件", "noPermission": "无权限,请前往用户授权", "downloadTaskIsCreated": "批量下载任务(共 {count} 条)已创建,任务完成后通知您!", "downloadTaskIsCompleted": "批量下载任务已发送完成,成功:{success},失败:{failed}" }, "omnibox": { "search": "在「{solutionName}」方案中搜索 “{text}” 的相关种子" }, "user": { "notSupported": "暂不支持", "notLogged": "未登录", "needLogin": "未登录", "unknown": "未知错误", "getUserInfoFailed": "获取用户名和编号失败", "abortGetUserInfoFailed": "取消获取用户信息请求失败" } }, "contentPage": { "backgroundServiceIsStoped": "插件已被禁用或重启过,请刷新页面后再重试", "actionExecutionFailed": "{action} 执行出错,可能后台服务不可用", "pluginTitle": "PT助手 - 点击打开配置页", "callbackFailed": "{label} 发生错误,请重试。", "notSupported": "当前页面不支持此操作", "buttons": { "downloadAll": "下载所有", "downloadAllTip": "将当前页面所有种子下载到[{name}]", "downloadAllTo": "下载到…", "downloadAllToTip": "将当前页面所有种子下载到指定服务器", "copyAllToClipboard": "复制链接", "copyAllToClipboardTip": "复制下载链接到剪切板", "needAuthorization": "需要授权", "needAuthorizationTip": "下载所有种子文件功能需要权限,点击前往授权", "saveAllTorrent": "所有种子", "saveAllTorrentTip": "下载所有种子文件", "freeSpaceTip": "默认服务器剩余空间\n{path}", "downloadTo": "下载到…", "downloadToTip": "将当前种子下载到指定的服务器", "downloadToDefault": "一键下载", "downloadToDefaultTip": "将当前种子下载到[{name}]", "copyToClipboard": "复制链接", "copyToClipboardTip": "复制下载链接到剪切板", "menuDownloadTo": "下载到:{server}", "sayThanks": "感谢发布者", "sayThanksTip": "对当前种子说谢谢", "cover": "封面模式", "coverTip": "以封面的方式进行查看", "addToCollection": "添加到收藏", "removeFromCollection": "从收藏删除" }, "needPasskey": "请先设置站点密钥(Passkey)。", "userCanceled": "用户取消操作", "sendingTorrent": "正在发送下载链接到服务器,请稍候……", "invalidDownloadServer": "无效的下载服务器", "invalidURL": "无效的链接", "dropInvalidURL": "无效的链接,请拖放下载链接", "getDownloadURLisUndefined": "getDownloadURL 方法未定义", "getDownloadURLsisUndefined": "getDownloadURLs 方法未定义", "getDownloadURLFailed": "获取下载链接失败", "getDownloadURLsFailed": "获取下载链接失败,未能正确定位到链接", "exceedSizeConfirm": "当前页面种子容量为 {size} 已超过 {exceedSize} {exceedSizeUnit},是否发送?", "exceedSizeCanceled": "容量超限,已取消", "downloadURLsFinished": "{count}条链接已发送完成。", "downloadURLsTip": "正在发送:{text}", "search": { "needLogin": "[{siteName}]需要登录后再搜索", "noTorrents": "[{siteName}]没有搜索到相关的种子", "torrentTableIsEmpty": "[{siteName}]没有定位到种子列表,或没有相关的种子", "parseError": "[{siteName}]获取种子信息出错: {error}" }, "dragTitle": "按住拖放;双击复位" }, "downloadClient": { "timeout": "连接超时", "unknownError": "未知错误", "notFound": "指定的地址未找到,服务器返回了 404", "addURLSuccess": "URL已添加至 {name} 。", "unsupportedMediaType": "种子文件有误", "serverIsUnavailable": "服务器不可用或网络错误", "permissionDenied": "身份验证失败", "serverConnectionFailed": "服务器连接失败", "destinationDenied": "指定的目录[{path}]不可用或无权限", "destinationDoesNotExist": "指定的目录[{path}]不存在", "fileUploadFailed": "文件上传失败", "maxNumberOfTasksReached": "达到的最大任务数" }, "footer": { "replaceLanguageConfirm": "该语言已存在,是否需要替换?", "invalidFile": "无效的语言文件!" }, "collection": { "title": "收藏列表", "add": "加入收藏", "remove": "取消收藏", "addGroup": "创建分组", "addToGroup": "添加到分组", "noGroup": "<未分组>", "changeGroupName": "修改分组名称", "removeGroupConfirm": "确认要删除这个分组吗?{count} 个收藏将从这个分组中删除。", "setMovieId": "设置电影 ID,可以是 IMDB ID(如:tt123456)或豆瓣ID(如:12345678)", "inputGroupName": "请输入分组名称:", "headers": { "site": "来源", "title": "标题", "size": "大小", "time": "添加时间", "action": "操作" } }, "searchResultSnapshot": { "title": "搜索结果快照", "show": "查看快照", "removeConfirmTitle": "删除快照确认", "removeConfirm": "确认要删除这个快照吗?", "clearConfirm": "确认要清除所有快照吗?", "create": "创建快照", "createSuccess": "快照创建完成", "createError": "快照创建失败", "snapshotTime": "快照时间: {time}", "headers": { "key": "搜索内容", "time": "时间" } }, "keepUploadTask": { "title": "辅种任务", "keepUpload": "辅种", "removeConfirmTitle": "删除辅种任务确认", "removeConfirm": "确认要删除这个辅种任务吗?", "clearConfirm": "确认要清除所有辅种任务吗?", "create": "生成辅种任务", "createSuccess": "辅种任务创建完成,请到辅种任务列表中进行后续操作", "createError": "辅种任务创建失败", "noItem": "没有符合条件的种子", "filterSearchResults": "过滤搜索结果", "savePath": "保存目录:", "defaultPath": "默认目录", "setSavePath": "设置保存目录", "torrentCount": "辅种数量:", "sendBaseTorrent": "发送基准种子到下载服务器", "sendOtherTorrents": "发送除基准外的其他种子到下载服务器", "sendAllTorrents": "发送所有辅种到下载服务器", "sendSuccess": "任务已发送", "sendError": "任务发送失败", "verification": "辅种验证", "baseTorrent": "基准文件", "otherTorrent": "辅种文件", "size": "大小:", "fileCount": "文件数:", "sendConfirm": "是否确认要发送这 {count} 个种子?", "addToKeepUpload": "添加至辅种列表", "removeFromKeepUpload": "移除种子", "redownload" : "重新下载", "addToKeepUploadConfirm": "该种子未能通过初步校验,确认要添加吗?", "status": { "label": "状态:", "downloading": "正在下载", "downloaded": "下载完成", "failed": "校验失败", "incorrectOrder": "文件顺序错误", "missingFiles": "缺少文件", "success": "校验成功", "waiting": "等待校验", "downloadFailed": "下载失败" }, "headers": { "site": "站点", "title": "标题", "size": "大小", "time": "时间" } }, "movieInfoCard": { "alias": "又名:", "director": "导演:", "writer": "编剧:", "cast": "主演:", "type": "类型:", "pubdate": "上映:", "duration": "片长:", "ratings": { "douban": "豆瓣 {average} 共 {numRaters} 人参与评价", "imdb": "IMDb {average} 共 {numRaters} 人参与评价" } } } } ================================================ FILE: resource/libs/album/album.js ================================================ /** * 对数字进行四舍五入操作 * @param {number} precision */ Number.prototype.toRound = function (precision) { if (isNaN(precision) || precision == null || precision < 0) { precision = 0; } if (window.accounting) { return parseFloat(accounting.toFixed(this, precision)); } else { return parseFloat(parseFloat(Math.round(this * Math.pow(10, precision)) / Math.pow(10, precision)).toFixed(precision)); } }; // 百分比 Number.prototype.toPercent = function (divisor, fix) { if (Math.abs(this) > 0) { if (fix == undefined) { fix = 2; } return ((100 - parseFloat((parseFloat(divisor) - parseFloat(this)) / parseFloat(divisor) * 100 + 0.005)).toRound(fix)) + '%'; } else { return "0"; } }; /** * 相册 * @author 栽培者 * @version 0.1.0 */ const _imageCache = []; window.album = function (options) { return ({ options: { // 初始图片列表,每个对象需要有以下属性 // url:图片原始地址 // thumb:缩略图地址 // title:显示标题 // key:关键字 images: [], url: "", fileIds: "", active: "", resizeRate: 5, listHeight: 160, // 保持缩略图栏 keepThumbBar: false, maxSize: 100, minSize: 5, allowContextmenu: false, allowDownload: true, // 是否显示属性栏 showPropColumn: false, // 是否显示顶部工具栏 showTopBar: true, // 是否显示标签信息栏 showLabelBar: true, clickImageToClose: true, // 用户自定义按钮 buttons: [], tags: [], defaultTag: "全部", activeTag: null, onButtonClick() {}, theme: "", onClose() {}, }, // 当前活动对象 activeItem: null, // 当前用于显示相册的“窗口” window: null, systemButtons: { left: null, right: null }, allowClose: true, shower: null, listBar: null, thumbImagesBar: null, activeImage: null, loadingBar: null, images: [], imageItems: {}, listBarOffset: 0, listBarWidth: 0, loadedImageWidth: 0, tags: {}, topBarTimer: null, overTopBar: false, theme: "", IE: ("v" == "\v"), init() { console.log('this\'s album.js'); this.options = $.extend(this.options, options); if (!this.options.parent) { this.options.parent = $(document.body); } else { this.allowClose = false; } if (this.options.theme) { this.theme = `-${this.options.theme}`; } this.parent = this.options.parent; // 主框架 this.window = $("
").appendTo(this.parent).focus(); // 背景 this.background = $(`
`).appendTo(this.window); // 显示大图片区域 this.shower = $("
").appendTo(this.window); // 控制图片功能区域 this.controlBar = $("
").appendTo(this.window); // 缩略图显示区域 this.listBar = $(`
`).css({ height: this.options.listHeight }).appendTo(this.controlBar); // 缩略图列表 this.thumbImagesBar = $("
").appendTo(this.listBar); // 标签列表 this.labelBar = $("
").appendTo(this.controlBar); // 标签显示容器 // this.label = $("").appendTo(this.labelBar); this.label = $("").appendTo(this.labelBar); // 数量显示容器 this.countBar = $("").appendTo(this.labelBar); // 当前缩放尺寸显示容器 this.zoomBar = $("").html("100%").appendTo(this.labelBar); this.systemButtons.zoomIn = $("").appendTo(this.labelBar); this.systemButtons.zoomOut = $("").appendTo(this.labelBar); if (!this.options.showLabelBar) { this.labelBar.hide(); this.listBar.css({ bottom: 0 }); } // 正在加载显示容器 this.loadingBar = $("
").hide().appendTo(this.window); $("").attr({ src: "loading.gif" }).appendTo(this.loadingBar); // 系统按钮 this.systemButtons.left = $("
").appendTo(this.window).hide(); this.systemButtons.right = $("
").appendTo(this.window).hide(); // 标签 this.tagsBar = $("
").hide().appendTo(this.window); // .css({ // bottom: this.options.listHeight+this.labelBar.height() // }) this.createTag(this.options.defaultTag, true); this.listBarOffset = this.parent.width() / 2; this.listBarWidth = this.listBar.width(); this.initSystemButtons(); this.initCustomButtons(); this.initEvents(); if (this.options.tags.length > 0) { for (let i = 0; i < this.options.tags.length; i++) { this.createTag(this.options.tags[i]); }; } if (this.options.images.length > 0) { this.initImageList(this.options.images, this.options.active); } else if (this.options.url) { this.load(this.options.url); } if (this.options.activeTag) { if (this.tags[this.options.activeTag]) { this.tags[this.options.activeTag].dom.click(); } } return this; }, /** * 初始化系统按钮 * @return {[type]} [description] */ initSystemButtons() { if (this.options.showTopBar == false) return; // 顶部工具栏 this.topBar = $("
").appendTo(this.window).hide(); // $("").appendTo(this.topBar); this.systemButtons.close = $("").appendTo(this.topBar); }, /** * [initCustomButtons 初始化自定按钮区] * @return {[type]} [description] */ initCustomButtons() { if (this.options.buttons.length == 0) return; // 用户自定义按钮区 this.customButtonBar = $("
").appendTo(this.window); const _self = this; engine.create("toolbar", { parent: this.customButtonBar, items: this.options.buttons, buttonType: 1, onitemclick(item) { _self.options.onButtonClick(item, _self.activeItem); } }); this.customButtonBar.removeClass("toolbar-container"); this.customButtonBar.find("table").css("display", "inline-block"); this.customButtonBar.css({ top: this.listBar.offset().top - 30 }); }, /** * 删除当前对象 * @return {null} */ remove() { if (!this.allowClose) return; clearTimeout(this.topBarTimer); this.window.empty().remove(); this.options.onClose && this.options.onClose(); }, /** * [behind 将当前框架置后] * @return {[type]} [description] */ behind(zIndex) { this.isBehind = true; if (!zIndex) { zIndex = 0; } this.zIndex = this.window.css("zIndex"); this.window.css("zIndex", zIndex); }, /** * 置前 * @return {[type]} [description] */ bringToFront() { if (!this.isBehind) return; this.isBehind = false; this.window.css("zIndex", this.zIndex); }, /** * 初始化事件 * @return {self} */ initEvents(self = this) { // 鼠标滚轮放大缩小 // DOMMouseScroll 为 FF this.shower.on("mousewheel DOMMouseScroll", event => { // $.log("mousewheel", event); const v = (event.type == "mousewheel" ? event.originalEvent.wheelDelta : event.originalEvent.detail); if (v < 0) { self.resize(-self.options.resizeRate); } else { self.resize(self.options.resizeRate); } event.preventDefault(); event.stopPropagation(); }); // 键盘事件捕捉 this.window.on("keydown", event => { // $.log("keydown", event); // event.preventDefault(); switch (event.keyCode) { // 向左 case 37: // Page up case 33: self.gotoImage("prev"); event.preventDefault(); event.stopPropagation(); break; // 向右 case 39: // Page down case 34: self.gotoImage("next"); event.preventDefault(); event.stopPropagation(); break; // 向上 case 38: self.showThumbsBar(); event.preventDefault(); event.stopPropagation(); break; // 向下 case 40: self.hideThumbsBar(); event.preventDefault(); event.stopPropagation(); break; // ESC case 27: self.remove(); event.preventDefault(); event.stopPropagation(); break; // Enter case 13: if (self.activeItem && self.activeItem.link) { self.label.click(); } // event.preventDefault(); event.stopPropagation(); break; } }).on("mousemove", () => { self.showTopBar(); }); if (!this.options.keepThumbBar) { // 鼠标离开和进入图片列表时 this.controlBar.on("mouseleave", () => { if(self.hideThumbsBarTimer) clearTimeout(self.hideThumbsBarTimer); // 隐藏图片列表 self.hideThumbsBarTimer = setTimeout(() => { self.hideThumbsBar(); }, 500); }).on("mouseenter", () => { if(self.hideThumbsBarTimer) clearTimeout(self.hideThumbsBarTimer); self.showThumbsBar(); // 鼠标滚轮上一张、下一张 }).on("mousewheel DOMMouseScroll", event => { // $.log("mousewheel", event); const v = (event.type == "mousewheel" ? event.originalEvent.wheelDelta : event.originalEvent.detail); if (v < 0) { self.gotoImage("next"); } else { self.gotoImage("prev"); } event.preventDefault(); event.stopPropagation(); }); // 隐藏图片列表 self.hideThumbsBarTimer = setTimeout(() => { self.hideThumbsBar(); }, 1000); } else { // 鼠标离开和进入图片列表时 this.controlBar.on("mouseenter", () => { self.showThumbsBar(); // 鼠标滚轮上一张、下一张 }).on("mousewheel DOMMouseScroll", event => { // $.log("mousewheel", event); const v = (event.type == "mousewheel" ? event.originalEvent.wheelDelta : event.originalEvent.detail); if (v < 0) { self.gotoImage("next"); } else { self.gotoImage("prev"); } event.preventDefault(); event.stopPropagation(); }); } // 上一张 this.systemButtons.left.click(() => { self.gotoImage("prev"); }).on("mouseleave", function () { $(this).animate({ opacity: .5 }); }).on("mouseenter", function () { $(this).animate({ opacity: 1 }); }); // 下一张 this.systemButtons.right.click(() => { self.gotoImage("next"); }).on("mouseleave", function () { $(this).animate({ opacity: .5 }); }).on("mouseenter", function () { $(this).animate({ opacity: 1 }); }); // 标签栏 this.tagsBar.on("mouseleave", function () { $(this).animate({ opacity: .5 }); }).on("mouseenter", function () { $(this).animate({ opacity: 1 }); }).animate({ opacity: .5 }); // 放大 this.systemButtons.zoomIn.click(() => { self.resize(self.options.resizeRate); }); // 缩小 this.systemButtons.zoomOut.click(() => { self.resize(-self.options.resizeRate); }); if (this.options.showTopBar) { // 关闭事件 this.systemButtons.close.click(() => { self.remove(); }); this.topBar.on("mouseenter mousemove", function () { self.overTopBar = true; $(this).addClass("over"); }).on("mouseleave", function () { self.overTopBar = false; $(this).removeClass("over"); }); } return this; }, /** * 显示顶部菜单栏 * @param {[type]} self [description] * @return {[type]} [description] */ showTopBar(self) { if (this.options.showTopBar == false) { return; } self = self || this; clearTimeout(this.topBarTimer); this.topBarTimer = null; this.topBar.fadeIn(); this.topBarTimer = setTimeout(() => { if (!self.overTopBar) self.topBar.fadeOut(); }, 2000); }, hideThumbsBar() { const _self = this; this.listBar.stop().animate({ height: 30, opacity: .2 }, () => { if (!_self.customButtonBar) return; _self.customButtonBar.css({ top: _self.listBar.offset().top - 30 }); }); }, showThumbsBar() { const _self = this; this.listBar.stop().animate({ height: this.options.listHeight, opacity: .8 }, () => { if (!_self.customButtonBar) return; _self.customButtonBar.css({ top: _self.listBar.offset().top - 30 }); }); }, /** * 加载指定的地址 * @param {string|object} 当指定为一个字符串时,表示链接地址 * @return {self} */ load() { const self = this; let options = { url: "", type: "POST", data: null, active: "" }; if (arguments.length == 2) { options.url = arguments[0]; options.data = arguments[1]; } else if (arguments.length == 1) { if (typeof (arguments[0]) == "string") { options.url = arguments[0]; } else { options = $.extend(options, arguments[0]); } } $.ajax({ url: options.url, type: options.type, data: options.data, success(data) { const result = data.result; if (!result) { alert("错误"); return; } if (result.length == 0) { // alert("暂无资料"); return false; } self.initImageList(result, options.active); } }); return this; }, /** * 添加明细列表 * @param {[type]} item [description] */ addItem(options, isTagClick) { const self = this; let item = { rate: 100, index: this.images.length, albumObject: this, tags: "", fileId: "" }; if (typeof (options) == "string") { item.url = options; } else { delete options.index; options.isloaded = false; item = $.extend(item, options); } if (!item.key) item.key = item.url; if (!item.thumb) { item.thumb = item.url; } if (!isTagClick) { this.tags[this.options.defaultTag].items.push(item); this.tags[this.options.defaultTag].dom.html(`${this.options.defaultTag} (${this.tags[this.options.defaultTag].items.length})`); // 是否有标签 if (item.tags) { this.tagsBar.show(); this.addTag(item); } } this.loadImage(item.thumb, img => { self.loadedImageWidth += img.width + 10; if (self.loadedImageWidth >= self.listBarWidth) { self.listBarWidth += img.width + 10; self.listBar.width(self.listBarWidth); } }); item.image = $("").attr({ src: item.thumb, key: item.key, title: item.title, tags: (Array.isArray(item.tags) ? item.tags.join(",") : item.tags) }).css({ "max-height": (this.options.listHeight - 10) }).click(function () { self.setActive(this.getAttribute("key")); }).appendTo(this.thumbImagesBar); this.images.push(item); this.imageItems[item.key] = item; }, /** * [addTag 添加标签] * @param {[type]} item [description] */ addTag(item) { let texts = []; if (typeof (item.tags) == "string") { texts.push(item.tags); } else if (Array.isArray(item.tags)) { texts = item.tags; } else { return; } for (const text of texts) { let tag = this.tags[text]; if (!tag) { tag = this.createTag(text); } tag.items.push(item); tag.dom.html(`${text} (${tag.items.length})`); } }, // createTag(tag, setActive) { const _self = this; const result = { name: tag, items: [], dom: $("").attr({ href: "javascript:void(0);", tag }).html(tag).click(function () { if (_self.lastActiveTag) { _self.lastActiveTag.removeClass("album-active"); } const tag = this.getAttribute("tag"); $(this).addClass("album-active"); _self.initImageList(_self.tags[tag].items, null, true); // if (tag=="全部") // { // _self.thumbImagesBar.find(".album-item").show(); // } // else // { // _self.thumbImagesBar.find(".album-item").hide(); // _self.thumbImagesBar.find(".album-item[tags*='"+this.getAttribute("tag")+"']").show(); // } _self.lastActiveTag = $(this); }).appendTo(this.tagsBar) }; if (setActive) { result.dom.addClass("album-active"); this.lastActiveTag = result.dom; } this.tags[tag] = result; return result; }, /** * 初始化小图片列表 * @param {string} active 当前活动的键值 * @return {self} */ initImageList(images, active, isTags) { this.thumbImagesBar.empty(); images = images || this.images; this.images = []; this.imageItems = {}; this.activeItem = null; if (images.length == 0) return; for (let i = 0; i < images.length; i++) { this.addItem(images[i], isTags); }; if (active) { this.setActive(active, true); } else { this.setActive(this.images[0].key, true); } if (images.length == 1) { this.listBar.hide(); } return this; }, /** * 清除图片列表 */ clear() { this.thumbImagesBar.empty(); this.images = []; this.imageItems = {}; this.activeItem = null; this.activeImage.hide(); }, /** * 设置当前活动对象 * @param {[type]} key [description] */ setActive(key, resize) { this.bringToFront(); const item = this.imageItems[key]; if (!item) return; const self = this; if (this.activeItem) { if (this.activeItem.key == key) return; this.activeItem.rate = 100; this.activeItem.image.removeClass("active"); } if (!this.activeImage) { this.activeImage = $("").css({ width: 100, position: "absolute" // }).draggable({ // cursor: "move" }).appendTo(this.shower); if (this.options.clickImageToClose) { this.activeImage.click(() => { self.remove(); }); } if (!this.options.allowContextmenu) { this.activeImage.on("contextmenu", event => { event.preventDefault(); event.stopPropagation(); }); } } item.image.addClass("active"); this.label.html(item.title); if (item.link) { this.label.attr("href", item.link); } else { this.label.removeAttr("href"); } this.countBar.html(`${item.index+1}/${this.images.length}`); this.activeItem = item; // 设置缩略图列表位置 const left = this.listBarOffset - item.image.position().left - item.image.width() / 2; // $.log(this.listBarOffset, item.image.position()); this.thumbImagesBar.css({ left }); if (!item.isloaded && item.url != item.thumb) { this.loadingBar.show(); this.activeImage.hide(); // 预加载图片 this.loadImage(item.url, img => { self.loadingBar.fadeOut(); self.activeItem.isloaded = true; self.activeItem.width = img.width; self.activeItem.height = img.height; self.setActiveImage(resize); }); // var img = new Image(); // // 预加载图片 // img.src = item.url; // img.onload = function() { // }; } else if (item.url == item.thumb) { const _img = new Image(); _img.onload = function () { self.loadingBar.fadeOut(); self.activeItem.isloaded = true; self.activeItem.width = this.width; self.activeItem.height = this.height; self.setActiveImage(resize); }; _img.src = item.url; // delete _img; } else { this.setActiveImage(resize); } }, /** * 设置当前活动的图像 * @param {[type]} resize [description] */ _setActiveImage(resize) { const item = this.activeItem; this.activeImage.hide().attr({ src: item.url, fileId: item.fileId }); // if (!resize) // { // this.activeImage.show(); // return; // } const parentSize = { width: this.window.width(), height: this.window.height() }; if (this.options.keepThumbBar) { parentSize.height -= this.options.listHeight; } const rateWidth = parseInt(parentSize.width.toPercent(item.width)); const rateHeight = parseInt(parentSize.height.toPercent(item.height)); let rate = Math.min(rateWidth, rateHeight); // $.log(rateWidth, rateHeight, item.width, item.height, parentSize.width, parentSize.height); if (rate > this.options.maxSize) { rate = this.options.maxSize; } this.activeItem.rate = rate; this.setSize(); this.systemButtons.left.show(); this.systemButtons.right.show(); if (this.images.length > 1) { if (item.index == 0) { this.systemButtons.left.hide(); } else if (item.index == this.images.length - 1) { this.systemButtons.right.hide(); } } else { this.systemButtons.left.hide(); this.systemButtons.right.hide(); } }, setActiveImage(resize, _self = this) { if (this.setActiveImageTimer) { clearTimeout(this.setActiveImageTimer); } this.setActiveImageTimer = setTimeout(() => { _self._setActiveImage(resize); }, 260); }, /** * 重设当前图片大小 * @param {integer} rate 倍数,正数为放大,负数为缩小 * @return {[type]} [description] */ resize(rate) { const min = this.options.minSize; const max = this.options.maxSize; if (!this.activeItem.rate) { this.activeItem.rate = 100; } this.activeItem.rate += rate; if (this.activeItem.rate < min) { this.activeItem.rate = min; } else if (this.activeItem.rate > max) { this.activeItem.rate = max; } if (this.timer) { clearTimeout(this.timer); } const self = this; this.timer = setTimeout(() => { self.setSize(); }, 50); }, setSize() { const width = this.activeItem.width * (this.activeItem.rate / 100); const height = this.activeItem.height * (this.activeItem.rate / 100); const left = this.listBarOffset - width / 2; // if (width= this.images.length) { index = 0; } var item = this.images[index]; if (item) { this.setActive(item.key); } }, /** * 加载图片 * @param {[type]} url [description] * @param {Function} callback [description] * @param {[type]} reload [description] * @return {[type]} [description] */ loadImage(url, callback, reload) { const cache = _imageCache[url]; if (cache && !reload && cache.width > 0) { if (callback) { callback(cache); } return cache; } else { var _img = new Image(); // _img.src = url + "?__t__" + Math.random(); _img.src = url; if (callback) { _img.onload = function () { callback(this); } } } _imageCache[url] = _img; return _img; } }).init(); }; ================================================ FILE: resource/libs/album/style.css ================================================ .album { width: 100%; height: 100%; z-index: 30000; position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; overflow: hidden; } .album .background { filter: alpha(opacity=50); opacity: 0.5; background-color: #ccc; width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; } .album .background-black { background-color: #333; width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; bottom: 0px; right: 0px; } .album .shower { position: absolute; top: 1px; bottom: 190px; width: 100%; text-align: center; } .album .topbar { position: absolute; top: 0px; right: 0px; opacity: 0.3; filter: alpha(opacity=30); background-color: #000; /*height: 40px;*/ padding: 8px 0 5px 5px; width: auto; overflow-y: hidden; } .album .topbar.over { opacity: 0.8; filter: alpha(opacity=80); } .album .topbar a.item { width: 32px; height: 32px; display: inline-block; margin: 3px 10px; cursor: pointer; filter: alpha(opacity=60); opacity: 0.6; background-image: url("chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons-32.png"); background-repeat: no-repeat; overflow: hidden; } .album .topbar a.item:hover { filter: alpha(opacity=100); opacity: 1; } .album .topbar .spliter { width: 2px; height: 28px; line-height: 32px; border-width: 2px; border-style: solid; } .album .topbar .rotate-left { background-position: 0px 0px; } .album .topbar .rotate-right { background-position: 0px -32px; } .album .topbar .close { background-position: 0px -64px; } .album .topbar .close:hover { background-color: #ff3300; } .album .topbar .download { background-position: 0px -96px; } .album .controlbar { position: absolute; bottom: 0px; width: 100%; } .album .listbar { position: absolute; bottom: 30px; height: 160px; width: 100%; text-align: center; background-color: #000; filter: alpha(opacity=30); opacity: 0.3; overflow: hidden; } .album .listbar-black { position: absolute; bottom: 30px; height: 160px; width: 100%; text-align: center; background-color: #fff; opacity: 0.9; overflow: hidden; } .album .thumbimages { height: 100%; position: absolute; display: inline-table; } .album .labelbar { position: absolute; top: -30px; bottom: 0px; padding: 8px; text-align: center; width: 100%; color: #fff; background-color: #000; /* background-color: rgba(0, 0, 0, 0.8); */ opacity: 0.8; filter: alpha(opacity=80); height: 30px; } .album .label { color: #fff; } .album .closebutton { position: absolute; top: 0px; right: 0px; width: 50px; height: 50px; cursor: pointer; text-align: center; border-radius: 0 0 0 50px; filter: alpha(opacity=70); opacity: 0.7; background: #ccc url("chrome-extension://__MSG_@@extension_id__/resource/libs/album/close.png") no-repeat 15px 3px; /* background-position: right; */ } .album .album-item { margin: 2px; border-style: solid; border-width: 1px; border-color: #aabbcc; cursor: pointer; } .album .active { border-width: 3px; border-color: #ff6600; } .album .loading { position: absolute; z-index: 30001; left: 50%; top: 50%; background-color: rgba(0, 0, 0, 0.4); filter: alpha(opacity=40); padding: 15px; border-radius: 8px; } .album .zoomBar { position: absolute; display: inline; width: 40px; text-align: center; right: 40px; } .album .status-count { position: absolute; left: 10px; } .album .button-left { position: absolute; width: 48px; height: 48px; top: 42%; cursor: pointer; filter: alpha(opacity=50); opacity: 0.5; background: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png') no-repeat scroll 0px 0px transparent; } .album .button-right { position: absolute; width: 48px; height: 48px; cursor: pointer; right: 0px; top: 42%; filter: alpha(opacity=50); opacity: 0.5; background: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png') no-repeat scroll 0px -48px transparent; } .album .button-zoom-in { position: absolute; width: 16px; height: 16px; cursor: pointer; right: 80px; margin-top: -1px; background: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png?v=2') no-repeat scroll 0px -112px transparent; } .album .button-zoom-out { position: absolute; width: 16px; height: 16px; cursor: pointer; right: 25px; margin-top: -1px; background: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png?v=2') no-repeat scroll -16px -112px transparent; } .album .custom-botton-bar { position: absolute; height: 26px; text-align: center; width: 100%; } .album button { background-color: #ffb94b; background-image: -webkit-gradient(linear, left top, left bottom, from(#fddb6f), to(#ffb94b)); background-image: -webkit-linear-gradient(top, #fddb6f, #ffb94b); background-image: -moz-linear-gradient(top, #fddb6f, #ffb94b); background-image: -ms-linear-gradient(top, #fddb6f, #ffb94b); background-image: -o-linear-gradient(top, #fddb6f, #ffb94b); background-image: linear-gradient(top, #fddb6f, #ffb94b); -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -moz-box-shadow: 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.3) inset; -webkit-box-shadow: 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.3) inset; box-shadow: 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.3) inset; border-width: 1px; border-style: solid; border-color: #d69e31 #e3a037 #d5982d #e3a037; float: left; height: 24px; padding: 3px; cursor: pointer; color: #8f5a0a; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fddb6f, endColorstr=#ffb94b, GradientType=0); } .album .album-tags { position: absolute; left: 0px; bottom: 30px; } .album .album-tags a.album-tag { display: table; padding: 5px; background-color: #777; color: #fff; margin: 1px; text-decoration: none; } .album .album-tags a.album-active { background-color: #369; border-right: 4px #09C solid; } ================================================ FILE: resource/publicSites/douban.com/common.js ================================================ (function ($, window) { class Common { init() { this.initButtons && this.initButtons(); // 设置当前页面 PTService.pageApp = this; } /** * 按指定的关键字进行搜索 * @param {*} key * @param {*} button */ search(key, button) { PTService.call(PTService.action.openOptions, `search-torrent/${key}`).catch((error) => { console.log(error); button && button.html(error.msg || "执行失败"); }); } }; window.DoubanCommon = Common; })(jQuery, window); ================================================ FILE: resource/publicSites/douban.com/config.json ================================================ { "name": "豆瓣", "ver": "0.0.1", "plugins": [{ "name": "电影详情页", "pages": ["\/subject\/\\d+\/"], "scripts": ["common.js", "subject.js"] }, { "name": "top250", "pages": ["/top250"], "scripts": ["common.js", "top250.js"] }, { "name": "explore", "pages": ["/explore", "/tv/"], "scripts": ["common.js", "explore.js"] }, { "name": "doulist", "pages": ["\/doulist\/\\d+\/"], "scripts": ["common.js", "doulist.js"] }], "schema": "publicSite", "host": "movie.douban.com", "path": "douban.com", "cdn": ["www.douban.com"] } ================================================ FILE: resource/publicSites/douban.com/doulist.js ================================================ (function ($, window) { class App extends window.DoubanCommon { /** * 初始化按钮列表 */ initButtons() { let items = $(".doulist-subject"); console.log(items.length); if (items.length > 0) { items.each((index, item) => { let $item = $(item); let $title = $item.find(".title a"); let link = $title.attr("href"); let match = link.match(/subject\/(\d+)/); if (match && match.length >= 2) { let title = $title.text().trim(); let key = ""; switch (true) { // 电影 case link.indexOf("movie.douban.com") > 0: key = `douban${match[1]}`; break; // 其他 default: key = title; break; } this.createButton($item.find(".post"), key, title); } }); } } createButton(parent, key, title) { if (!key) { return; } let label = "PT 助手搜索"; parent.css({ "max-height": "unset" }); let div = $("
") .attr("title", `搜索 ${title}`) .appendTo(parent); $("") .html(label) .on("click", event => { let button = $(event.target); this.search(key, button); }) .appendTo(div); } } new App().init(); })(jQuery, window); ================================================ FILE: resource/publicSites/douban.com/explore.js ================================================ (function ($, window) { class App extends window.DoubanCommon { constructor() { super(); this.lastId = ""; this.status = 0; } /** * 初始化按钮列表 */ initButtons() { // 检测DOM变化 $(".detail-pop").bind('DOMNodeInserted', (e) => { this.createButton($(e.target)) }) } createButton(parent) { if (this.status == 1) { return; } this.status = 1; let link = $("a[href*='movie.douban.com/subject']", parent); let key = ""; if (link.length > 0) { let match = link.attr("href").match(/subject\/(\d+)/); let title = link.text().split(" ")[0]; if (match && match.length >= 2) { let id = match[1]; key = `douban${id}`; if (id != this.lastId) { this.lastId = id; // 预转换 PTService.call(PTService.action.getIMDbIdFromDouban, id).catch((error) => { console.log(error); }); } key += "|" + title; } } if (!key) { this.status = 0; return; } let buttonId = "pt-plugin-search-button"; $("#" + buttonId, parent).remove(); $("").html("助手搜索").on("click", (event) => { this.search(key, $(event.target)); }).appendTo($(".collect-area", parent)); this.status = 0; } }; (new App()).init(); })(jQuery, window); ================================================ FILE: resource/publicSites/douban.com/subject.js ================================================ (function ($, window) { class App extends window.DoubanCommon { /** * 初始化按钮列表 */ initButtons() { let key = this.getIMDbId() || this.getTitle(); if (key) { // 搜索 PTService.addButton({ title: "搜索当前电影", icon: "search", label: "搜索", click: (success, error) => { this.search(key); success(); } }); let recommendButton = $("span.rec a[share-id]"); if (recommendButton.length > 0) { $("").html("用 PT 助手搜索").on("click", (event) => { this.search(key, $(event.target)); }).insertBefore(recommendButton); } } } /** * 获取 IMDb 编号 */ getIMDbId() { let link = $("a[href*='www.imdb.com/title/']:first"); if (link.length) { return link.text(); } // 尝试从文本中获取 link = $("#info").text().match(/IMDb: (tt\d+)/); if (link) { return link[1]; } return ""; } getTitle() { return document.title.replace(" (豆瓣)", ""); } }; (new App()).init(); })(jQuery, window); ================================================ FILE: resource/publicSites/douban.com/top250.js ================================================ (function ($, window) { class App extends window.DoubanCommon { /** * 初始化按钮列表 */ initButtons() { let items = $("a[href*='movie.douban.com/subject'] img"); if (items.length > 0) { items.each((index, item) => { let $item = $(item); this.createButton($item.parent(), $item.attr("alt")); }); } } createButton(parent, key) { if (!key) { return; } let title = "用 PT 助手搜索"; let div = $("
").attr("title", `搜索 ${key}`).appendTo(parent); $("").html(title).on("click", (event) => { let match = parent.attr("href").match(/subject\/(\d+)/); if (match && match.length >= 2) { key = `douban${match[1]}`; } let button = $(event.target); this.search(key, button); }).appendTo(div); } }; (new App()).init(); })(jQuery, window); ================================================ FILE: resource/publicSites/goodmovieslist.com/best-movies.js ================================================ (function($) { console.log("this is best-movies.js"); class App extends window.DoubanCommon { /** * 初始化按钮列表 */ initButtons() { let items = $("table.list_movies"); if (items.length > 0) { items.each((index, item) => { let $item = $(item); let imdbId = $item.attr("id"); if (imdbId) { this.createButton($item.find("tr > td:eq(0)"), imdbId); } }); } } createButton(parent, key) { if (!key) { return; } let div = $("
").appendTo( parent ); const className = "gsc-search-button gsc-search-button-v2"; const styles = "font-size: 12px;color: #fff;padding: 5px;margin-right: 5px;"; $(`