├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── build
└── icon.png
├── package.json
├── public
├── assets
│ ├── client
│ │ ├── deluge.ico
│ │ ├── qbittorrent.ico
│ │ ├── rutorrent.ico
│ │ └── transmission.ico
│ ├── donate
│ │ ├── Rhilip
│ │ │ ├── alipay.jpg
│ │ │ └── wechat.png
│ │ └── ledccn
│ │ │ └── wechat.png
│ ├── iyuu.png
│ └── iyuu_gui.png
└── index.html
├── resource
├── home.png
├── login.png
└── mission.png
├── src
├── App.vue
├── background.ts
├── components
│ ├── Aside.vue
│ ├── Gratitude
│ │ └── ShowPersons.vue
│ ├── Mission
│ │ └── Reseed.vue
│ ├── Setting
│ │ ├── BtClient
│ │ │ ├── ClientAdd.vue
│ │ │ └── ClientEdit.vue
│ │ ├── Other
│ │ │ ├── Normal.vue
│ │ │ └── weChat.vue
│ │ └── Site
│ │ │ ├── SiteAdd.vue
│ │ │ └── SiteEdit.vue
│ └── StateCard.vue
├── interfaces
│ ├── BtClient
│ │ ├── AbstractClient.ts
│ │ ├── deluge.ts
│ │ ├── qbittorrent.ts
│ │ └── transmission.ts
│ ├── IYUU
│ │ ├── Forms.ts
│ │ └── Site.ts
│ └── store.ts
├── main.ts
├── plugins
│ ├── backup.ts
│ ├── btclient
│ │ ├── deluge.ts
│ │ ├── factory.ts
│ │ ├── qbittorrent.ts
│ │ └── transmission.ts
│ ├── common.ts
│ ├── cookies.ts
│ ├── dayjs.ts
│ ├── element.ts
│ ├── iyuu.ts
│ ├── mission
│ │ └── reseed.ts
│ ├── sites
│ │ ├── default.ts
│ │ ├── factory.ts
│ │ ├── hdchina.ts
│ │ ├── hdcity.ts
│ │ └── hdsky.ts
│ └── uuid.ts
├── router
│ └── index.ts
├── shims-tsx.d.ts
├── shims-vue.d.ts
├── store
│ ├── index.ts
│ ├── modules
│ │ ├── IYUU.ts
│ │ ├── Mission.ts
│ │ └── Status.ts
│ └── store-accessor.ts
└── views
│ ├── Gratitude
│ ├── Declare.vue
│ └── Donate.vue
│ ├── Home.vue
│ ├── Layer.vue
│ ├── Login.vue
│ ├── Mission.vue
│ └── Setting
│ ├── Backup.vue
│ ├── BtClient.vue
│ ├── Other.vue
│ └── Site.vue
├── tsconfig.json
├── vue.config.js
└── yarn.lock
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | release:
10 | runs-on: ${{ matrix.os }}
11 |
12 | strategy:
13 | matrix:
14 | os: [macos-latest, ubuntu-latest, windows-latest]
15 | fail-fast: false
16 |
17 | env:
18 | ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron
19 | ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder
20 |
21 | steps:
22 | - name: Check out Git repository
23 | uses: actions/checkout@v1
24 |
25 | - name: Install Node.js, NPM and Yarn
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: 14
29 |
30 | - name: Get yarn cache
31 | id: yarn-cache
32 | run: echo "::set-output name=dir::$(yarn cache dir)"
33 |
34 | - name: Cache Node.js modules
35 | uses: actions/cache@v1
36 | with:
37 | path: ${{ steps.yarn-cache.outputs.dir }}
38 | key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
39 | restore-keys: |
40 | ${{ runner.os }}-yarn-
41 |
42 | - name: Cache Electron
43 | uses: actions/cache@v1
44 | with:
45 | path: ${{ github.workspace }}/.cache/electron
46 | key: ${{ runner.os }}-electron-cache-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
47 | restore-keys: |
48 | ${{ runner.os }}-electron-cache-
49 |
50 | - name: Cache Electron-Builder
51 | uses: actions/cache@v1
52 | with:
53 | path: ${{ github.workspace }}/.cache/electron-builder
54 | key: ${{ runner.os }}-electron-builder-cache-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
55 | restore-keys: |
56 | ${{ runner.os }}-electron-builder-cache-
57 |
58 | - name: Install dependencies
59 | run: yarn install
60 |
61 | - name: Build/release Electron app
62 | uses: samuelmeuli/action-electron-builder@v1
63 | with:
64 | # GitHub token, automatically provided to the action
65 | # (No need to define this secret in the repo settings)
66 | github_token: ${{ secrets.github_token }}
67 |
68 | # If the commit is tagged with a version (e.g. "v1.0.0"),
69 | # release the app after building
70 | release: ${{ startsWith(github.ref, 'refs/tags/v') }}
71 | use_vue_cli: 'true'
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 |
24 | #Electron-builder output
25 | /dist_electron
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # IYUU GUI
4 |
5 | 这是一个基于IYUU提供的API,产生的一个可视化操作项目。
6 | 目的是为了降低直接上手PHP版IYUUAutoReseed的难度。
7 |
8 | ## 各级页面预览
9 |
10 | - 登录页
11 |
12 | 
13 |
14 | - 首页
15 |
16 | 
17 |
18 | - 任务启动页
19 |
20 | 
21 |
22 | ## 任务计划
23 |
24 | - [ ] 完善 `IYUU GUI` 的文档
25 | - [ ] 支持转种任务的建立
26 | - [ ] 支持其他类型的下载客户端
27 |
28 |
29 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/build/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/build/icon.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iyuu",
3 | "version": "1.0.8",
4 | "author": "Rhilip",
5 | "license": "AGPL-3.0",
6 | "description": "Another Reseed Tools (GUI) powered by IYUU api",
7 | "private": true,
8 | "scripts": {
9 | "serve": "vue-cli-service serve",
10 | "build": "vue-cli-service build",
11 | "lint": "vue-cli-service lint",
12 | "electron:build": "vue-cli-service electron:build",
13 | "electron:serve": "vue-cli-service electron:serve",
14 | "postinstall": "electron-builder install-app-deps",
15 | "postuninstall": "electron-builder install-app-deps"
16 | },
17 | "main": "background.js",
18 | "dependencies": {
19 | "axios": "^0.21.1",
20 | "cookie": "^0.4.1",
21 | "core-js": "^3.6.5",
22 | "dayjs": "^1.8.29",
23 | "electron-updater": "^4.3.1",
24 | "element-ui": "^2.4.5",
25 | "file-saver": "^2.0.2",
26 | "jsonar": "^1.8.0",
27 | "lodash": "^4.17.15",
28 | "url-join": "^4.0.1",
29 | "vue": "^2.6.11",
30 | "vue-class-component": "^7.2.3",
31 | "vue-property-decorator": "^8.4.2",
32 | "vue-router": "^3.2.0",
33 | "vue-uuid": "^2.0.2",
34 | "vuex": "^3.4.0",
35 | "vuex-electron": "MaverickMartyn/vuex-electron"
36 | },
37 | "devDependencies": {
38 | "@types/cookie": "^0.4.0",
39 | "@types/electron-devtools-installer": "^2.2.0",
40 | "@types/file-saver": "^2.0.1",
41 | "@types/lodash": "^4.14.157",
42 | "@types/node": "^12.0.22",
43 | "@types/url-join": "^4.0.0",
44 | "@typescript-eslint/eslint-plugin": "^2.33.0",
45 | "@typescript-eslint/parser": "^2.33.0",
46 | "@vue/cli-plugin-babel": "~4.4.0",
47 | "@vue/cli-plugin-eslint": "~4.4.0",
48 | "@vue/cli-plugin-router": "~4.4.0",
49 | "@vue/cli-plugin-typescript": "~4.4.0",
50 | "@vue/cli-plugin-vuex": "~4.4.0",
51 | "@vue/cli-service": "~4.4.0",
52 | "@vue/eslint-config-typescript": "^5.0.2",
53 | "babel-eslint": "^10.1.0",
54 | "electron": "^8.4.0",
55 | "electron-devtools-installer": "^3.1.0",
56 | "eslint": "^6.7.2",
57 | "eslint-plugin-vue": "^6.2.2",
58 | "typescript": "~3.9.3",
59 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.4",
60 | "vue-cli-plugin-element": "~1.0.1",
61 | "vue-cli-plugin-lodash": "~0.1.3",
62 | "vue-template-compiler": "^2.6.11",
63 | "vuex-module-decorators": "^0.17.0"
64 | },
65 | "eslintConfig": {
66 | "root": true,
67 | "env": {
68 | "node": true
69 | },
70 | "extends": [
71 | "plugin:vue/recommended",
72 | "eslint:recommended",
73 | "@vue/typescript"
74 | ],
75 | "parserOptions": {
76 | "parser": "@typescript-eslint/parser"
77 | },
78 | "rules": {
79 | "arrow-parens": 0,
80 | "generator-star-spacing": 0,
81 | "vue/html-indent": "off",
82 | "vue/max-attributes-per-line": [
83 | "error",
84 | {
85 | "singleline": 5,
86 | "multiline": {
87 | "max": 5,
88 | "allowFirstLine": true
89 | }
90 | }
91 | ],
92 | "vue/html-closing-bracket-newline": [
93 | "error",
94 | {
95 | "singleline": "never",
96 | "multiline": "never"
97 | }
98 | ]
99 | }
100 | },
101 | "browserslist": [
102 | "electron >= 8.0.0"
103 | ]
104 | }
105 |
--------------------------------------------------------------------------------
/public/assets/client/deluge.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/client/deluge.ico
--------------------------------------------------------------------------------
/public/assets/client/qbittorrent.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/client/qbittorrent.ico
--------------------------------------------------------------------------------
/public/assets/client/rutorrent.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/client/rutorrent.ico
--------------------------------------------------------------------------------
/public/assets/client/transmission.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/client/transmission.ico
--------------------------------------------------------------------------------
/public/assets/donate/Rhilip/alipay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/donate/Rhilip/alipay.jpg
--------------------------------------------------------------------------------
/public/assets/donate/Rhilip/wechat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/donate/Rhilip/wechat.png
--------------------------------------------------------------------------------
/public/assets/donate/ledccn/wechat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/donate/ledccn/wechat.png
--------------------------------------------------------------------------------
/public/assets/iyuu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/iyuu.png
--------------------------------------------------------------------------------
/public/assets/iyuu_gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/public/assets/iyuu_gui.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/resource/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/resource/home.png
--------------------------------------------------------------------------------
/resource/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/resource/login.png
--------------------------------------------------------------------------------
/resource/mission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rhilip/IYUU-GUI/992931c23f5db7c38927f809ee2158dde36ae626/resource/mission.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
25 |
--------------------------------------------------------------------------------
/src/background.ts:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import { app, protocol, BrowserWindow, Tray, Menu, ipcMain } from 'electron'
4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
5 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
6 | import { autoUpdater } from "electron-updater"
7 | import * as path from "path";
8 | const isDevelopment = process.env.NODE_ENV !== 'production'
9 | const packageInfo = require('../package.json')
10 |
11 | // Keep a global reference of the window object, if you don't, the window will
12 | // be closed automatically when the JavaScript object is garbage collected.
13 | let win: BrowserWindow | null
14 | let tray: Tray | null
15 |
16 | const gotTheLock = app.requestSingleInstanceLock()
17 |
18 | // webSecurity is already disabled in BrowserWindow. However, it seems there is
19 | // a bug in Electron 9 https://github.com/electron/electron/issues/23664. There
20 | // is workaround suggested in the issue
21 | app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors');
22 |
23 | // Scheme must be registered before the app is ready
24 | protocol.registerSchemesAsPrivileged([
25 | { scheme: 'app', privileges: { secure: true, standard: true } }
26 | ])
27 |
28 | declare var __static: string;
29 |
30 | function createWindow() {
31 | // Create the browser window.
32 | win = new BrowserWindow({
33 | width: 1000,
34 | height: 700,
35 | icon: path.join(__static, 'assets/iyuu.png'),
36 |
37 | title: `IYUU GUI v${packageInfo.version}`,
38 |
39 | useContentSize: true,
40 | webPreferences: {
41 | // Use pluginOptions.nodeIntegration, leave this alone
42 | // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
43 | nodeIntegration: (process.env
44 | .ELECTRON_NODE_INTEGRATION as unknown) as boolean,
45 |
46 | // 直接使用这个方式,关闭chrome的CORS保护
47 | // refs: https://github.com/SimulatedGREG/electron-vue/issues/387
48 | webSecurity: false as boolean,
49 | }
50 | })
51 |
52 | // 关闭顶端菜单栏
53 | win.setMenu(null)
54 |
55 | // 禁止更改窗口大小
56 | win.setResizable(false)
57 |
58 | if (process.env.WEBPACK_DEV_SERVER_URL) {
59 | // Load the url of the dev server if in development mode
60 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
61 | if (!process.env.IS_TEST) win.webContents.openDevTools()
62 | } else {
63 | createProtocol('app')
64 | // Load the index.html when not in development
65 | win.loadURL('app://./index.html')
66 | autoUpdater.checkForUpdatesAndNotify()
67 | }
68 |
69 | initTray()
70 |
71 | win.on('close', (e: Event) => {
72 | e.preventDefault()
73 | win && win.hide();
74 | })
75 |
76 | win.on('closed', () => {
77 | win = null
78 | tray = null
79 | })
80 | }
81 |
82 | function initTray() {
83 | tray = new Tray(path.join(__static, 'assets/iyuu.png'))
84 | const contextMenu = Menu.buildFromTemplate([
85 | {
86 | label: '退出', click: () => {
87 | app.quit()
88 | }
89 | }
90 | ])
91 |
92 | tray.on('click', () => {
93 | win && win.show();
94 | })
95 |
96 | // Call this again for Linux because we modified the context menu
97 | tray.setContextMenu(contextMenu)
98 | }
99 |
100 | if (!gotTheLock) {
101 | app.quit()
102 | } else {
103 | app.on('second-instance', (event, commandLine, workingDirectory) => {
104 | // Someone tried to run a second instance, we should focus our window.
105 | if (win) {
106 | if (win.isMinimized()) win.restore()
107 | if (!win.isVisible()) win.show()
108 | if (!win.isFocused()) win.focus()
109 | }
110 | })
111 |
112 | // Quit when all windows are closed.
113 | app.on('window-all-closed', () => {
114 | // On macOS it is common for applications and their menu bar
115 | // to stay active until the user quits explicitly with Cmd + Q
116 | if (process.platform !== 'darwin') {
117 | app.quit()
118 | }
119 | })
120 |
121 | app.on('before-quit', () => {
122 | win && win.removeAllListeners('close');
123 | win && win.close();
124 | });
125 |
126 | app.on('activate', () => {
127 | // On macOS it's common to re-create a window in the app when the
128 | // dock icon is clicked and there are no other windows open.
129 | if (win === null) {
130 | createWindow()
131 | }
132 | })
133 |
134 | app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
135 | // Verification logic.
136 | event.preventDefault()
137 | callback(true)
138 | })
139 |
140 | // This method will be called when Electron has finished
141 | // initialization and is ready to create browser windows.
142 | // Some APIs can only be used after this event occurs.
143 | app.on('ready', async () => {
144 | if (isDevelopment && !process.env.IS_TEST) {
145 | // Install Vue Devtools
146 | try {
147 | await installExtension(VUEJS_DEVTOOLS)
148 | } catch (e) {
149 | console.error('Vue Devtools failed to install:', e.toString())
150 | }
151 | }
152 | createWindow()
153 | })
154 |
155 | ipcMain.on('close-me', (evt, arg) => {
156 | app.quit()
157 | })
158 | }
159 |
160 | // Exit cleanly on request from parent process in development mode.
161 | if (isDevelopment) {
162 | if (process.platform === 'win32') {
163 | process.on('message', (data) => {
164 | if (data === 'graceful-exit') {
165 | app.quit()
166 | }
167 | })
168 | } else {
169 | process.on('SIGTERM', () => {
170 | app.quit()
171 | })
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/components/Aside.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
88 |
89 |
--------------------------------------------------------------------------------
/src/components/Gratitude/ShowPersons.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ personTypeNote }}
6 |
7 |
8 |
13 |
14 | {{ item }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
44 |
45 |
--------------------------------------------------------------------------------
/src/components/Mission/Reseed.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
16 | 全选
17 |
18 |
19 |
20 | {{ client.name }} ({{ client.type }})
21 |
22 |
23 |
24 |
25 |
29 | 全选
30 |
31 |
32 |
33 | {{ site.site }}
34 |
35 |
36 |
37 |
38 |
43 |
44 |
49 |
50 |
54 |
55 |
56 |
57 |
61 |
62 |
63 |
64 |
194 |
195 |
--------------------------------------------------------------------------------
/src/components/Setting/BtClient/ClientAdd.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
16 |
21 | {{ name }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 取一个好听的名字,方便你以后认出它来
32 |
33 |
34 |
35 |
36 |
37 | 完整的服务器地址(含端口),如:http://192.168.1.1:5000/
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
65 |
66 |
67 |
68 |
169 |
170 |
--------------------------------------------------------------------------------
/src/components/Setting/BtClient/ClientEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
142 |
143 |
--------------------------------------------------------------------------------
/src/components/Setting/Other/Normal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 控制请求IYUU服务器时,单次提交的最多infoHash数量。推荐设置在2000左右。
7 | 该值不易过大或过小,过大会导致IYUU服务器响应超时,过小则导致软件多次请求。
8 |
9 |
10 |
11 |
12 |
13 | 由于不可控的网络问题,例如:下载器压力过大时长时间未响应,或下载种子时不能及时响应。
14 | 此时,软件将重试多次,以尽可能的满足推送要求。
15 |
16 |
17 |
18 |
19 | 更新设置
20 |
21 |
22 |
23 |
24 |
25 |
60 |
61 |
--------------------------------------------------------------------------------
/src/components/Setting/Other/weChat.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 内容模板中的 {full_log} 等字符为占位符,如不需要可以自行删去。
12 |
13 |
14 |
15 |
16 | 更新设置
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/Setting/Site/SiteAdd.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 | 站点名称
15 |
16 |
17 | 本处列出IYUU目前所有支持且尚未添加站点。
18 |
19 |
20 |
21 |
22 |
26 |
31 |
32 |
33 | 请特别注意:部分站点在使用前要求备案或验证。
34 |
35 |
36 |
37 |
38 |
39 | 除部分站点外,本软件通过构造可以直接下载的种子链接发送给下载器。(不检测链接是否真实)
40 | 请在此处直接写好下载链接构造式,例如:
41 | https://pt.example.com/download.php?id={}&passkey=abcdefgh2321234323
42 | 其中 {} 表示种子ID信息,请勿修改,已有模板中的 {passkey} 等信息请替换成自己信息。
43 |
44 |
45 |
46 |
47 |
48 | 除部分无法构造种子下载链接的站点外,只使用自动辅种功能,可以不配置站点Cookies信息。
49 | 软件同时支持 {key}={value};格式
以及 EditCookies插件的导出格式。
50 | 但请特别注意:软件仅验证是否符合格式但同样不检测Cookies是否真实可用。
51 |
52 |
53 |
54 |
55 |
56 | 关于站点的一些高级设置,请在添加站点后使用“编辑”功能进行控制。
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
191 |
192 |
195 |
--------------------------------------------------------------------------------
/src/components/Setting/Site/SiteEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 | 下载器推送方式:
26 |
27 | 下载频率限制(为0时不做限制):
28 |
29 |
30 | 每次运行
31 |
32 |
33 | 请求数
34 |
36 |
37 |
38 | 请求间隔(秒)
39 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 |
166 |
167 |
170 |
--------------------------------------------------------------------------------
/src/components/StateCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | {{ data }}
8 |
9 | {{ prepend }}
10 |
11 |
12 |
13 |
14 |
15 |
32 |
33 |
--------------------------------------------------------------------------------
/src/interfaces/BtClient/AbstractClient.ts:
--------------------------------------------------------------------------------
1 | // 此处为构建统一的下载器模型提供可能
2 |
3 | export interface TorrentClient {
4 | config: TorrentClientConfig;
5 |
6 | ping: () => Promise;
7 |
8 | getAllTorrents: () => Promise
9 | getTorrentsBy: (filter: TorrentFilterRules) => Promise
10 | getTorrent: (id: any) => Promise;
11 |
12 | addTorrent: (url: string, options?: Partial) => Promise;
13 | pauseTorrent: (id: any) => Promise;
14 | resumeTorrent: (id: any) => Promise;
15 | removeTorrent: (id: any, removeData?: boolean) => Promise;
16 | }
17 |
18 | export type clientType = 'qbittorrent' | 'transmission' | 'deluge' | 'rtorrent'
19 |
20 | export interface TorrentClientConfig {
21 | /**
22 | * The uuid of client, it's automatically generate by uuid4 when add client
23 | */
24 | uuid: string;
25 |
26 | type: clientType;
27 |
28 | /**
29 | * The name of client which can help users recognise it quickly
30 | */
31 | name: string;
32 |
33 | /**
34 | * The full url of torrent client webapi, like:
35 | * - transmission: http://ip:port/transmission/rpc
36 | * - qbittorrent: http://ip:port/
37 | */
38 | address: string;
39 |
40 | username?: string;
41 | password?: string;
42 |
43 | /**
44 | * request timeout
45 | */
46 | timeout?: number;
47 | }
48 |
49 | export interface Torrent {
50 | id: string | number;
51 | infoHash: string;
52 |
53 | name: string;
54 |
55 | /**
56 | * progress percent out of 100
57 | */
58 | progress: number;
59 | isCompleted: boolean;
60 |
61 | /**
62 | * 1:1 is 1, half seeded is 0.5
63 | */
64 | ratio: number;
65 |
66 | /**
67 | * date as iso string
68 | */
69 | dateAdded: string;
70 |
71 | savePath: string;
72 | label?: string;
73 | state: TorrentState;
74 |
75 | /**
76 | * total size of the torrent, in bytes
77 | */
78 | totalSize: number;
79 | }
80 |
81 | export interface TorrentFilterRules {
82 | ids?: any;
83 | complete?: boolean;
84 | }
85 |
86 | export interface AddTorrentOptions {
87 | /**
88 | * 是否本地下载
89 | */
90 | localDownload: boolean;
91 |
92 | /**
93 | * 是否将种子置于暂停状态
94 | */
95 | addAtPaused: boolean;
96 |
97 | /**
98 | * 种子下载地址
99 | */
100 | savePath: string;
101 |
102 | /**
103 | * called a label in some clients and a category in others
104 | * Notice: Some clients didn't support it
105 | */
106 | label?: string;
107 | }
108 |
109 | export enum TorrentState {
110 | downloading = 'downloading',
111 | seeding = 'seeding',
112 | paused = 'paused',
113 | queued = 'queued',
114 | checking = 'checking',
115 | error = 'error',
116 | unknown = 'unknown',
117 | }
118 |
119 |
--------------------------------------------------------------------------------
/src/interfaces/BtClient/deluge.ts:
--------------------------------------------------------------------------------
1 | import {TorrentFilterRules} from "@/interfaces/BtClient/AbstractClient";
2 |
3 |
4 | export type DelugeMethod =
5 | 'auth.login' | 'web.update_ui' | 'core.get_torrents_status' |
6 | 'core.add_torrent_url' | 'core.add_torrent_file' |
7 | 'core.remove_torrent' | 'core.pause_torrent' | 'core.resume_torrent'
8 |
9 | export interface DelugeDefaultResponse {
10 | /**
11 | * mostly usless id that increments with every request
12 | */
13 | id: number;
14 | error: null | string;
15 | result: any;
16 | }
17 |
18 | export type DelugeTorrentField =
19 | "comment"
20 | | "active_time"
21 | | "is_seed"
22 | | "hash"
23 | | "upload_payload_rate"
24 | | "move_completed_path"
25 | | "private"
26 | | "total_payload_upload"
27 | | "paused"
28 | | "seed_rank"
29 | | "seeding_time"
30 | | "max_upload_slots"
31 | | "prioritize_first_last"
32 | | "distributed_copies"
33 | | "download_payload_rate"
34 | | "message"
35 | | "num_peers"
36 | | "max_download_speed"
37 | | "max_connections"
38 | | "compact"
39 | | "ratio"
40 | | "total_peers"
41 | | "total_size"
42 | | "total_wanted"
43 | | "state"
44 | | "file_priorities"
45 | | "max_upload_speed"
46 | | "remove_at_ratio"
47 | | "tracker"
48 | | "save_path"
49 | | "progress"
50 | | "time_added"
51 | | "tracker_host"
52 | | "total_uploaded"
53 | | "files"
54 | | "total_done"
55 | | "num_pieces"
56 | | "tracker_status"
57 | | "total_seeds"
58 | | "move_on_completed"
59 | | "next_announce"
60 | | "stop_at_ratio"
61 | | "file_progress"
62 | | "move_completed"
63 | | "piece_length"
64 | | "all_time_download"
65 | | "move_on_completed_path"
66 | | "num_seeds"
67 | | "peers"
68 | | "name"
69 | | "trackers"
70 | | "total_payload_download"
71 | | "is_auto_managed"
72 | | "seeds_peers_ratio"
73 | | "queue"
74 | | "num_files"
75 | | "eta"
76 | | "stop_ratio"
77 | | "is_finished"
78 | | "label" // if they don't have the label plugin it shouldn't fail
79 |
80 | export interface DelugeRawTorrent {
81 | hash: string,
82 | name: string,
83 | progress: number,
84 | ratio: number,
85 | time_added: number,
86 | save_path: string,
87 | 'label'?: string,
88 | state: 'Downloading' | 'Seeding' | 'Active' | 'Paused' | 'Queued' | 'Checking' | 'Error',
89 | 'total_size': number
90 | }
91 |
92 | export interface DelugeTorrentFilterRules extends TorrentFilterRules {
93 | hash?: string,
94 | state?: string
95 | }
96 |
97 |
98 | export interface DelugeBooleanStatus extends DelugeDefaultResponse {
99 | result: boolean;
100 | }
--------------------------------------------------------------------------------
/src/interfaces/BtClient/qbittorrent.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddTorrentOptions,
3 | Torrent,
4 | TorrentClientConfig,
5 | TorrentFilterRules
6 | } from "@/interfaces/BtClient/AbstractClient";
7 |
8 | type TrueFalseStr = 'true' | 'false';
9 |
10 | export interface QbittorrentTorrent extends Torrent {
11 | id: string;
12 | }
13 |
14 |
15 | export interface QbittorrentTorrentClientConfig extends TorrentClientConfig {
16 | username: string;
17 | password: string;
18 | }
19 |
20 | export interface QbittorrentTorrentFilterRules extends TorrentFilterRules {
21 | hashes?: string|string[];
22 | filter?: QbittorrentTorrentFilters;
23 | category?: string;
24 | sort?: string;
25 | offset?: number;
26 | reverse?: boolean|TrueFalseStr;
27 | }
28 |
29 | export interface QbittorrentAddTorrentOptions extends AddTorrentOptions {
30 | /**
31 | * Download folder
32 | */
33 | savepath: string;
34 | /**
35 | * Cookie sent to download the .torrent file
36 | */
37 | cookie: string;
38 | /**
39 | * Category for the torrent
40 | */
41 | category: string;
42 | /**
43 | * Skip hash checking. Possible values are true, false (default)
44 | */
45 | skip_checking: TrueFalseStr;
46 | /**
47 | * Add torrents in the paused state. Possible values are true, false (default)
48 | */
49 | paused: TrueFalseStr;
50 | /**
51 | * Create the root folder. Possible values are true, false, unset (default)
52 | */
53 | root_folder: TrueFalseStr | null;
54 | /**
55 | * Rename torrent
56 | */
57 | rename: string;
58 | /**
59 | * Set torrent upload speed limit. Unit in bytes/second
60 | */
61 | upLimit: number;
62 | /**
63 | * Set torrent download speed limit. Unit in bytes/second
64 | */
65 | dlLimit: number;
66 | /**
67 | * Whether Automatic Torrent Management should be used, disables use of savepath
68 | */
69 | useAutoTMM: TrueFalseStr;
70 | /**
71 | * Enable sequential download. Possible values are true, false (default)
72 | */
73 | sequentialDownload: TrueFalseStr;
74 | /**
75 | * Prioritize download first last piece. Possible values are true, false (default)
76 | */
77 | firstLastPiecePrio: TrueFalseStr;
78 | }
79 |
80 | export type QbittorrentTorrentFilters =
81 | | 'all'
82 | | 'downloading'
83 | | 'completed'
84 | | 'paused'
85 | | 'active'
86 | | 'inactive'
87 | | 'resumed'
88 | | 'stalled'
89 | | 'stalled_uploading'
90 | | 'stalled_downloading';
91 |
92 | export enum QbittorrentTorrentState {
93 | /**
94 | * Some error occurred, applies to paused torrents
95 | */
96 | Error = 'error',
97 | /**
98 | * Torrent is paused and has finished downloading
99 | */
100 | PausedUP = 'pausedUP',
101 | /**
102 | * Torrent is paused and has NOT finished downloading
103 | */
104 | PausedDL = 'pausedDL',
105 | /**
106 | * Queuing is enabled and torrent is queued for upload
107 | */
108 | QueuedUP = 'queuedUP',
109 | /**
110 | * Queuing is enabled and torrent is queued for download
111 | */
112 | QueuedDL = 'queuedDL',
113 | /**
114 | * Torrent is being seeded and data is being transferred
115 | */
116 | Uploading = 'uploading',
117 | /**
118 | * Torrent is being seeded, but no connection were made
119 | */
120 | StalledUP = 'stalledUP',
121 | /**
122 | * Torrent has finished downloading and is being checked; this status also applies to preallocation (if enabled) and checking resume data on qBt startup
123 | */
124 | CheckingUP = 'checkingUP',
125 | /**
126 | * Same as checkingUP, but torrent has NOT finished downloading
127 | */
128 | CheckingDL = 'checkingDL',
129 | /**
130 | * Torrent is being downloaded and data is being transferred
131 | */
132 | Downloading = 'downloading',
133 | /**
134 | * Torrent is being downloaded, but no connection were made
135 | */
136 | StalledDL = 'stalledDL',
137 | /**
138 | * Torrent is forced to downloading to ignore queue limit
139 | */
140 | ForcedDL = 'forcedDL',
141 | /**
142 | * Torrent is forced to uploading and ignore queue limit
143 | */
144 | ForcedUP = 'forcedUP',
145 | /**
146 | * Torrent has just started downloading and is fetching metadata
147 | */
148 | MetaDL = 'metaDL',
149 | /**
150 | * Torrent is allocating disk space for download
151 | */
152 | Allocating = 'allocating',
153 | QueuedForChecking = 'queuedForChecking',
154 | /**
155 | * Checking resume data on qBt startup
156 | */
157 | CheckingResumeData = 'checkingResumeData',
158 | /**
159 | * Torrent is moving to another location
160 | */
161 | Moving = 'moving',
162 | /**
163 | * Unknown status
164 | */
165 | Unknown = 'unknown',
166 | /**
167 | * Torrent data files is missing
168 | */
169 | MissingFiles = 'missingFiles',
170 | }
171 |
172 | export interface rawTorrent {
173 | /**
174 | * Torrent name
175 | */
176 | name: string;
177 | hash: string;
178 | magnet_uri: string;
179 | /**
180 | * datetime in seconds
181 | */
182 | added_on: number;
183 | /**
184 | * Torrent size
185 | */
186 | size: number;
187 | /**
188 | * Torrent progress
189 | */
190 | progress: number;
191 | /**
192 | * Torrent download speed (bytes/s)
193 | */
194 | dlspeed: number;
195 | /**
196 | * Torrent upload speed (bytes/s)
197 | */
198 | upspeed: number;
199 | /**
200 | * Torrent priority (-1 if queuing is disabled)
201 | */
202 | priority: number;
203 | /**
204 | * Torrent seeds connected to
205 | */
206 | num_seeds: number;
207 | /**
208 | * Torrent seeds in the swarm
209 | */
210 | num_complete: number;
211 | /**
212 | * Torrent leechers connected to
213 | */
214 | num_leechs: number;
215 | /**
216 | * Torrent leechers in the swarm
217 | */
218 | num_incomplete: number;
219 | /**
220 | * Torrent share ratio
221 | */
222 | ratio: number;
223 | /**
224 | * Torrent ETA
225 | */
226 | eta: number;
227 | /**
228 | * Torrent state
229 | */
230 | state: QbittorrentTorrentState;
231 | /**
232 | * Torrent sequential download state
233 | */
234 | seq_dl: boolean;
235 | /**
236 | * Torrent first last piece priority state
237 | */
238 | f_l_piece_prio: boolean;
239 | /**
240 | * Torrent copletion datetime in seconds
241 | */
242 | completion_on: number;
243 | /**
244 | * Torrent tracker
245 | */
246 | tracker: string;
247 | /**
248 | * Torrent download limit
249 | */
250 | dl_limit: number;
251 | /**
252 | * Torrent upload limit
253 | */
254 | up_limit: number;
255 | /**
256 | * Amount of data downloaded
257 | */
258 | downloaded: number;
259 | /**
260 | * Amount of data uploaded
261 | */
262 | uploaded: number;
263 | /**
264 | * Amount of data downloaded since program open
265 | */
266 | downloaded_session: number;
267 | /**
268 | * Amount of data uploaded since program open
269 | */
270 | uploaded_session: number;
271 | /**
272 | * Amount of data left to download
273 | */
274 | amount_left: number;
275 | /**
276 | * Torrent save path
277 | */
278 | save_path: string;
279 | /**
280 | * Amount of data completed
281 | */
282 | completed: number;
283 | /**
284 | * Upload max share ratio
285 | */
286 | max_ratio: number;
287 | /**
288 | * Upload max seeding time
289 | */
290 | max_seeding_time: number;
291 | /**
292 | * Upload share ratio limit
293 | */
294 | ratio_limit: number;
295 | /**
296 | * Upload seeding time limit
297 | */
298 | seeding_time_limit: number;
299 | /**
300 | * Indicates the time when the torrent was last seen complete/whole
301 | */
302 | seen_complete: number;
303 | /**
304 | * Last time when a chunk was downloaded/uploaded
305 | */
306 | last_activity: number;
307 | /**
308 | * Size including unwanted data
309 | */
310 | total_size: number;
311 | time_active: number;
312 | /**
313 | * Category name
314 | */
315 | category: string;
316 | }
--------------------------------------------------------------------------------
/src/interfaces/BtClient/transmission.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddTorrentOptions,
3 | Torrent,
4 | TorrentClientConfig,
5 | TorrentFilterRules
6 | } from "@/interfaces/BtClient/AbstractClient";
7 |
8 | export interface TransmissionBaseResponse {
9 | arguments: any;
10 | result: 'success' | string;
11 | tag?: number
12 | }
13 |
14 | export interface TransmissionTorrentGetResponse extends TransmissionBaseResponse {
15 | arguments: {
16 | torrents: rawTorrent[]
17 | }
18 | }
19 |
20 | export interface AddTorrentResponse extends TransmissionBaseResponse {
21 | arguments: {
22 | 'torrent-added': {
23 | id: number;
24 | hashString: string;
25 | name: string;
26 | };
27 | };
28 | }
29 |
30 | export type TransmissionTorrentIds = number | Array | 'recently-active'
31 |
32 | export type TransmissionRequestMethod =
33 | 'session-get' | 'session-stats' |
34 | 'torrent-get' | 'torrent-add' | 'torrent-start' | 'torrent-stop' | 'torrent-remove'
35 |
36 | export interface TransmissionAddTorrentOptions extends AddTorrentOptions {
37 | "download-dir": string,
38 | filename: string,
39 | metainfo: string,
40 | paused: boolean,
41 |
42 | }
43 |
44 | export interface TransmissionTorrent extends Torrent {
45 | id: number | string;
46 | }
47 |
48 | export interface TransmissionTorrentFilterRules extends TorrentFilterRules {
49 | ids?: TransmissionTorrentIds;
50 | }
51 |
52 | export interface TransmissionArguments {
53 |
54 | }
55 |
56 | export interface TransmissionTorrentBaseArguments extends TransmissionArguments {
57 | ids?: TransmissionTorrentIds
58 | }
59 |
60 | export interface TransmissionTorrentGetArguments extends TransmissionTorrentBaseArguments {
61 | fields: TransmissionTorrentsField[]
62 | }
63 |
64 | export interface TransmissionTorrentRemoveArguments extends TransmissionTorrentBaseArguments {
65 | 'delete-local-data'?: boolean
66 | }
67 |
68 | export interface TransmissionTorrentClientConfig extends TorrentClientConfig {
69 |
70 | }
71 |
72 | // 这里只写出了部分我们需要的
73 | export interface rawTorrent {
74 | addedDate: number,
75 | id: number,
76 | hashString: string,
77 | isFinished: boolean,
78 | name: string,
79 | percentDone: number,
80 | uploadRatio: number,
81 | downloadDir: string,
82 | status: number,
83 | totalSize: number,
84 | leftUntilDone: number,
85 | labels: string[]
86 | }
87 |
88 | export type TransmissionTorrentsField =
89 | 'activityDate'
90 | | 'addedDate'
91 | | 'bandwidthPriority'
92 | | 'comment'
93 | | 'corruptEver'
94 | | 'creator'
95 | | 'dateCreated'
96 | | 'desiredAvailable'
97 | | 'doneDate'
98 | | 'downloadDir'
99 | | 'downloadedEver'
100 | | 'downloadLimit'
101 | | 'downloadLimited'
102 | | 'editDate'
103 | | 'error'
104 | | 'errorString'
105 | | 'eta'
106 | | 'etaIdle'
107 | | 'files'
108 | | 'fileStats'
109 | | 'hashString'
110 | | 'haveUnchecked'
111 | | 'haveValid'
112 | | 'honorsSessionLimits'
113 | | 'id'
114 | | 'isFinished'
115 | | 'isPrivate'
116 | | 'isStalled'
117 | | 'labels'
118 | | 'leftUntilDone'
119 | | 'magnetLink'
120 | | 'manualAnnounceTime'
121 | | 'maxConnectedPeers'
122 | | 'metadataPercentComplete'
123 | | 'name'
124 | | 'peer-limit'
125 | | 'peers'
126 | | 'peersConnected'
127 | | 'peersFrom'
128 | | 'peersGettingFromUs'
129 | | 'peersSendingToUs'
130 | | 'percentDone'
131 | | 'pieces'
132 | | 'pieceCount'
133 | | 'pieceSize'
134 | | 'priorities'
135 | | 'queuePosition'
136 | | 'rateDownload (B/s)'
137 | | 'rateUpload (B/s)'
138 | | 'recheckProgress'
139 | | 'secondsDownloading'
140 | | 'secondsSeeding'
141 | | 'seedIdleLimit'
142 | | 'seedIdleMode'
143 | | 'seedRatioLimit'
144 | | 'seedRatioMode'
145 | | 'sizeWhenDone'
146 | | 'startDate'
147 | | 'status'
148 | | 'trackers'
149 | | 'trackerStats'
150 | | 'totalSize'
151 | | 'torrentFile'
152 | | 'uploadedEver'
153 | | 'uploadLimit'
154 | | 'uploadLimited'
155 | | 'uploadRatio'
156 | | 'wanted'
157 | | 'webseeds'
158 | | 'webseedsSendingToUs'
--------------------------------------------------------------------------------
/src/interfaces/IYUU/Forms.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 此文件对IYUU的相关API请求参数和相应参数进行描述
3 | *
4 | * 多数接口有简写地址:
5 | * https://api.iyuu.cn/index.php?s=App.Api.Sites
6 | * https://api.iyuu.cn/api/sites
7 | *
8 | * 命名规则示例:
9 | * 接口地址:http://api.iyuu.cn/index.php?s=App.Api.Notify
10 | * 其接口名为 App.Api.Notify
11 | * 则其接口请求参数命名为 apiNotifyRequest
12 | * 接口响应参数命名为 apiNotifyResponse 如果该接口范式,则必须继承 BaseResponse
13 | *
14 | */
15 | import {Site} from "@/interfaces/IYUU/Site";
16 |
17 | export interface BaseResponse {
18 | /**
19 | * 状态码,200表示成功,4xx表示客户端非法请求,5xx表示服务器错误
20 | * - 400 ret=400,客户端参数错误或非法请求
21 | * - 404 表示接口服务不存在
22 | * - 406 ret=406,access_token令牌校验不通过
23 | * - 407 ret=407,app_key权限不足,或未知应用
24 | * - 408 ret=408,当前用户禁止使用,或用户未登录
25 | * - 500 表示服务端内部错误
26 | */
27 | ret: number,
28 |
29 | /**
30 | * 业务数据,由各自接口指定,通常为对象
31 | */
32 | data: any,
33 |
34 | /**
35 | * 提示信息,失败时的错误提示
36 | */
37 | msg: string
38 | }
39 |
40 | export interface TorrentInfo {
41 | sid: number,
42 | torrent_id: number,
43 | info_hash: string
44 | }
45 |
46 | /**
47 | * @description 用户登录
48 | * 根据合作站点标识、ID、passkey、爱语飞飞Token进行登录操作
49 | * @url https://api.iyuu.cn/index.php?s=App.User.Login
50 | * @method GET
51 | * @docs https://api.iyuu.cn/docs.php?service=App.User.Login&detail=1&type=fold
52 | */
53 | export interface userLoginRequest {
54 | token: string,
55 | site: string,
56 | id: string,
57 | passkey: string,
58 | }
59 |
60 | export interface userLoginResponse extends BaseResponse {
61 | /**
62 | * 注意一下,登录成功,data项才是这样,不然是个空字典
63 | * 但是由于Typescript的类型规定,导致使用 Partial<{ ... }> 还是 { ... } | {} 都会报编译错误,
64 | * 所以须先判断ret项
65 | */
66 | data: {
67 | /**
68 | * 是否登录成功true、失败false
69 | */
70 | success: boolean,
71 |
72 | /**
73 | * 用户ID
74 | */
75 | user_id: number,
76 |
77 | /**
78 | * 这个目前文档没列出来,但是实际上有使用,返回格式
79 | * IYUU自动辅种工具:站点******,用户ID:***** 登录成功!
80 | */
81 | errmsg: string
82 | }
83 | }
84 |
85 |
86 | /**
87 | * @description 获取所有站点信息
88 | * 返回支持的站点列表
89 | * @url https://api.iyuu.cn/index.php?s=App.Api.Sites
90 | * @method GET
91 | * @docs https://api.iyuu.cn/docs.php?service=App.Api.Sites&detail=1&type=fold
92 | */
93 | export interface apiSitesRequest {
94 | /**
95 | * 爱语飞飞Token
96 | */
97 | sign: string,
98 |
99 | /**
100 | * 客户端版本号
101 | *
102 | * 版本为空或低于v1.9.1会返回旧数据 { "download_page": "download.php?id={}" }
103 | * 其他情况返回新数据 { "download_page": "download.php?id={}&passkey={passkey}" }
104 | */
105 | version?: string
106 | }
107 |
108 | export interface apiSitesResponse extends BaseResponse {
109 | data: {
110 | sites: Site[]
111 | }
112 | }
113 |
114 | /**
115 | * @description 查询辅种
116 | * 返回所有辅种数据 infohash索引
117 | * @url http://api.iyuu.cn/index.php?s=App.Api.Infohash
118 | * @method POST
119 | * @docs http://api.iyuu.cn/docs.php?service=App.Api.Infohash&detail=1&type=fold
120 | */
121 | export interface apiInfohashRequest {
122 | sign: string,
123 | timestamp: number,
124 | version: string,
125 | hash: string[],
126 | sha1: string // sha1(hash)
127 | }
128 |
129 | export interface apiInfohashResponse extends BaseResponse {
130 | data: {
131 | [propName: string]: {
132 | torrent: TorrentInfo[]
133 | }
134 | }
135 | }
136 |
137 | /**
138 | * @description 查询辅种
139 | * 返回所有辅种数据 infohash索引
140 | * @url http://api.iyuu.cn/index.php?s=App.Api.Hash
141 | * @method POST
142 | * @docs http://api.iyuu.cn/docs.php?service=App.Api.Hash&detail=1&type=fold
143 | */
144 | export interface apiHashRequest extends apiInfohashRequest {}
145 |
146 | export interface apiHashResponse extends BaseResponse {
147 | data: {
148 | hash: string,
149 | torrent: TorrentInfo[]
150 | }[]
151 | }
152 |
153 | /**
154 | * @description 通知接口
155 | * 上报错误种子、异常状态等
156 | * @url http://api.iyuu.cn/index.php?s=App.Api.Notify
157 | * @method GET
158 | * @docs http://api.iyuu.cn/docs.php?service=App.Api.Notify&detail=1&type=fold
159 | */
160 | export interface apiNotifyRequest {
161 | sigh: string,
162 | site: string,
163 | sid: number,
164 | torrent_id: number,
165 | error: string
166 | }
167 |
168 | export interface apiNotifyResponse extends BaseResponse {
169 | data: {
170 | success: boolean
171 | }
172 | }
--------------------------------------------------------------------------------
/src/interfaces/IYUU/Site.ts:
--------------------------------------------------------------------------------
1 | export interface Site {
2 | id: number,
3 | site: string,
4 | base_url: string,
5 | download_page: string,
6 | is_https: number
7 | }
8 |
9 | export interface EnableSite extends Site {
10 | // 这两个应该在添加站点时启用
11 | link: string,
12 | cookies: string,
13 |
14 | // 剩下的应该在编辑站点时使用
15 | /**
16 | * 是否在本地客户端下载种子,然后发送种子内容到下载器
17 | */
18 | download_torrent: boolean,
19 |
20 | /**
21 | * 下载频率限制,只允许一组
22 | */
23 | rate_limit: {
24 | // 按总量
25 | maxRequests: number // 单次运行推送总量
26 | requestsDelay: number // 两次下载间隔
27 | }
28 | }
--------------------------------------------------------------------------------
/src/interfaces/store.ts:
--------------------------------------------------------------------------------
1 | // 一些不是很适合放在子项里面的 Vuex 中类型定义
2 |
3 | export interface LogInfo {
4 | timestamp: number,
5 | message: string
6 | }
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 |
4 | import store from './store'
5 | import router from './router'
6 |
7 | import './plugins/element.ts'
8 | import './plugins/uuid.ts'
9 |
10 | Vue.config.productionTip = false
11 |
12 | new Vue({
13 | store,
14 | router,
15 | render: h => h(App)
16 | }).$mount('#app')
17 |
--------------------------------------------------------------------------------
/src/plugins/backup.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import UUID from 'uuid'
3 | // @ts-ignore 这个库暂时还没有Typescript支持
4 | import jsonar from 'jsonar'
5 | import FileSaver from 'file-saver'
6 |
7 | import {EnableSite} from "@/interfaces/IYUU/Site";
8 | import {TorrentClientConfig} from "@/interfaces/BtClient/AbstractClient";
9 | import {IYUUStore, MissionStore, StatusStore} from "@/store";
10 | import {isForceDownloadSite} from "@/plugins/sites/factory";
11 |
12 | const packageInfo = require('../../package.json')
13 |
14 | // 从JSON字段中解析,解析失败返回空对象
15 | function decodeFromJsonString(text: string) {
16 | try {
17 | return JSON.parse(text);
18 | } catch (e) {
19 | return {}
20 | }
21 | }
22 |
23 | export class IYUUAutoReseedBridge {
24 | // 入口方法,内部根据字符串信息判断是使用jsonar库解析PHP Array还是JSON解析
25 | public static decodeFromFile(file: File) {
26 | return new Promise((resolve, reject) => {
27 | const r = new FileReader();
28 | r.onload = (e) => {
29 | // @ts-ignore
30 | const content: string = e.target.result
31 |
32 | let parsedConfig;
33 | if (content.indexOf(' -1) {
34 | parsedConfig = this.decodeFromPHPArrayString(content)
35 | } else {
36 | parsedConfig = decodeFromJsonString(content)
37 | }
38 |
39 | if (parsedConfig['iyuu.cn'] === IYUUStore.token) {
40 | resolve(parsedConfig)
41 | } else {
42 | reject('配置项未通过检验,或你传入文件中的爱语飞飞令牌与当前登录的不符。')
43 | }
44 | }
45 | r.readAsText(file);
46 | })
47 | }
48 |
49 | // 从PHPArray中解析,解析失败返回空对象
50 | private static decodeFromPHPArrayString(phpConfigRaw: string) {
51 | const returnFlag = phpConfigRaw.indexOf('return')
52 | const toParsePhpConfig = phpConfigRaw.slice(returnFlag + 6); // return及之前全部忽略
53 | try {
54 | return jsonar.parse(toParsePhpConfig);
55 | } catch (e) {
56 | return {}
57 | }
58 | }
59 |
60 | public static importFromJSON(config: any) {
61 | let clientCount = 0;
62 | let siteCount = 0;
63 |
64 | const clients: any[] = config.default.clients;
65 | for (let i = 0; i < clients.length; i++) {
66 | const client = clients[i]
67 | // 构造config
68 | let mergedClientConfig: TorrentClientConfig = {
69 | type: client.type.toLowerCase(),
70 | name: client.type,
71 | uuid: UUID.v4(),
72 | address: client.host,
73 | username: client.username,
74 | password: client.password,
75 | timeout: (client.type === 'transmission' ? 60 * 2 : 60) * 1e3
76 | }
77 |
78 | // 通过地址判断是不是有重复
79 | if (IYUUStore.enable_clients.findIndex(c => c.address === mergedClientConfig.address) === -1) {
80 | IYUUStore.addEnableClient(mergedClientConfig)
81 | clientCount++
82 | }
83 | }
84 |
85 | // 遍历我们的未添加sites列表
86 | for (let j = 0; j < IYUUStore.unsignedSites.length; j++) {
87 | let site = IYUUStore.unsignedSites[j]
88 | let mergedSiteConfig: EnableSite = _.merge(site,{
89 | cookies: "",
90 | download_torrent: false,
91 | link: "",
92 | rate_limit: {maxRequests: 0, requestsDelay: 0},
93 | })
94 | if (site.site in config) {
95 | const siteConfigFromPHP = config[site.site]
96 |
97 | // 如果没有cookies和passkey,直接跳过
98 | if (!siteConfigFromPHP.cookies && !siteConfigFromPHP.passkey) {
99 | continue
100 | }
101 |
102 | mergedSiteConfig.cookies = siteConfigFromPHP.cookies
103 | mergedSiteConfig.download_torrent = isForceDownloadSite(site.site)
104 | let link = IYUUStore.siteDownloadLinkTpl(site.site)
105 | if (siteConfigFromPHP.passkey) {
106 | link = link.replace(/{passkey}/ig, siteConfigFromPHP.passkey)
107 | }
108 | if (siteConfigFromPHP.url_replace) {
109 | for (const [key, value] of Object.entries(siteConfigFromPHP.url_replace)) {
110 | if (typeof value === "string") {
111 | link = link.replaceAll(key, value)
112 | }
113 | }
114 | }
115 |
116 | if (siteConfigFromPHP.url_join) {
117 | link += (link.lastIndexOf('?') > -1 ? '&' : '?' ) + siteConfigFromPHP.url_join.join('&')
118 | }
119 |
120 | if (siteConfigFromPHP.limitRule) {
121 | mergedSiteConfig.rate_limit.maxRequests = siteConfigFromPHP.limitRule.count || 0
122 | mergedSiteConfig.rate_limit.requestsDelay = siteConfigFromPHP.limitRule.sleep || 0
123 | }
124 |
125 | if (!link.match(/({[^}]+?})/ig) && link.search('{}') > -1) {
126 | mergedSiteConfig.link = link
127 | IYUUStore.addEnableSite(mergedSiteConfig)
128 | siteCount++
129 | }
130 | }
131 | }
132 |
133 | return {client: clientCount, site: siteCount}
134 | }
135 | }
136 |
137 | export class ConfigFileBridge {
138 | public static decodeFromFile(file:File) {
139 | return new Promise((resolve, reject) => {
140 | const r = new FileReader();
141 | r.onload = (e) => {
142 | // @ts-ignore
143 | const content: string = e.target.result
144 |
145 | let parsedConfig = decodeFromJsonString(content);
146 |
147 | if (parsedConfig['token'] === IYUUStore.token) {
148 | resolve(parsedConfig)
149 | } else {
150 | reject('配置项未通过检验,或你传入文件中的爱语飞飞令牌与当前登录的不符。')
151 | }
152 | }
153 | r.readAsText(file);
154 | })
155 | }
156 |
157 | public static importFromJSON(config: any) {
158 | IYUUStore.restoreFromConfig({
159 | token: config.token,
160 | sites: config.sites,
161 | clients: config.clients,
162 | apiPreInfoHash: config.config.apiPreInfoHash,
163 | maxRetry: config.config.maxRetry,
164 | weChatNotify: config.config.weChatNotify
165 | })
166 | StatusStore.restoreFromConfig({
167 | reseedTorrentCount: config.state.reseedTorrentCount,
168 | startAppCount: config.state.startAppCount,
169 | startMissionCount: config.state.startMissionCount,
170 | })
171 | MissionStore.restoreFromConfig({
172 | reseeded: config.hashes.reseeded
173 | })
174 | }
175 |
176 | public static exportToJSON() {
177 | const exportJSON = {
178 | version: packageInfo.version,
179 | token: IYUUStore.token,
180 | sites: IYUUStore.enable_sites,
181 | clients: IYUUStore.enable_clients,
182 | state: {
183 | startAppCount: StatusStore.startAppCount,
184 | startMissionCount: StatusStore.startMissionCount,
185 | reseedTorrentCount: StatusStore.reseedTorrentCount
186 | },
187 | hashes: {
188 | reseeded: MissionStore.reseeded
189 | },
190 | config: {
191 | apiPreInfoHash: IYUUStore.apiPreInfoHash,
192 | maxRetry: IYUUStore.maxRetry,
193 | weChatNotify: IYUUStore.weChatNotify
194 | }
195 | }
196 |
197 | const blob = new Blob([JSON.stringify(exportJSON)], {
198 | type: "text/plain;charset=utf-8"
199 | })
200 | FileSaver.saveAs(blob, "iyuu_config.json");
201 | }
202 | }
--------------------------------------------------------------------------------
/src/plugins/btclient/deluge.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddTorrentOptions,
3 | Torrent,
4 | TorrentClient,
5 | TorrentClientConfig,
6 | TorrentState
7 | } from "@/interfaces/BtClient/AbstractClient";
8 | import {
9 | DelugeBooleanStatus, DelugeDefaultResponse,
10 | DelugeMethod, DelugeRawTorrent,
11 | DelugeTorrentField, DelugeTorrentFilterRules
12 | } from "@/interfaces/BtClient/deluge";
13 | import urljoin from "url-join";
14 | import axios, {AxiosResponse} from "axios";
15 |
16 |
17 | // @ts-ignore
18 | export const defaultDelugeConfig: TorrentClientConfig = {
19 | type: 'deluge',
20 | name: 'deluge',
21 | uuid: '8951af53-c6de-49a2-b2c4-3fb63809f453',
22 | address: 'http://localhost:8112/',
23 | password: '',
24 | timeout: 60 * 1e3
25 | };
26 |
27 | export default class Deluge implements TorrentClient {
28 | readonly config: TorrentClientConfig;
29 | private readonly address: string;
30 |
31 | private _msgId: number;
32 | private isLogin: boolean = false
33 |
34 | private torrentRequestField: DelugeTorrentField[]= [
35 | 'hash',
36 | 'name',
37 | 'progress',
38 | 'ratio',
39 | 'time_added',
40 | 'save_path',
41 | 'label',
42 | 'state',
43 | 'total_size'
44 | ]
45 |
46 | constructor(options: Partial) {
47 | this.config = {...defaultDelugeConfig, ...options}
48 | this._msgId = 0
49 |
50 | // 修正服务器地址
51 | let address = this.config.address;
52 | if (address.indexOf('json') === -1) {
53 | address = urljoin(address, '/json')
54 | }
55 | this.address = address
56 | }
57 |
58 | async ping(): Promise {
59 | return await this.login();
60 | }
61 |
62 | async addTorrent(url: string, options: Partial = {}): Promise {
63 | let delugeOptions = {
64 | add_paused: false
65 | }
66 |
67 | if (options.addAtPaused) {
68 | delugeOptions.add_paused = options.addAtPaused
69 | }
70 | if (options.savePath) {
71 | // @ts-ignore
72 | delugeOptions.download_location = options.savePath
73 | }
74 |
75 | // 由于Deluge添加链接和种子的方法名不一样,分开处理
76 | let method: 'core.add_torrent_file' | 'core.add_torrent_url';
77 | let params: any;
78 | if (options.localDownload) { // 文件 add_torrent_file
79 | method = 'core.add_torrent_file'
80 | const req = await axios.get(url, {
81 | responseType: 'arraybuffer'
82 | })
83 | let metainfo = Buffer.from(req.data, 'binary').toString('base64')
84 | params = ['', metainfo, delugeOptions]
85 | } else { // 连接 add_torrent_url
86 | method = 'core.add_torrent_url'
87 | params = [url, delugeOptions]
88 | }
89 |
90 | try {
91 | const res = await this.request(method,params)
92 | const data: DelugeDefaultResponse = res.data
93 | return data.result !== null
94 | } catch (e) {
95 | return false
96 | }
97 | }
98 |
99 | async getAllTorrents(): Promise {
100 | return await this.getTorrentsBy({})
101 | }
102 |
103 | async getTorrent(id: string): Promise {
104 | // @ts-ignore
105 | return await this.getTorrentsBy({ids: id})
106 | }
107 |
108 | async getTorrentsBy(filter: DelugeTorrentFilterRules): Promise {
109 | if (filter.ids) {
110 | filter.hash = filter.ids
111 | delete filter.hash
112 | }
113 |
114 | if (filter.complete) {
115 | filter.state = 'Seeding'
116 | delete filter.complete
117 | }
118 |
119 | const req = await this.request('core.get_torrents_status', [
120 | filter,
121 | this.torrentRequestField,
122 | ]);
123 |
124 | const data: DelugeDefaultResponse = req.data
125 |
126 | // @ts-ignore
127 | return Object.values(data.result).map(t => Deluge._normalizeTorrent(t));
128 | }
129 |
130 | async pauseTorrent(id: any): Promise {
131 | try {
132 | const req = await this.request('core.pause_torrent', [id]);
133 | const data: DelugeBooleanStatus = req.data
134 | return data.result
135 | } catch (e) {
136 | return false
137 | }
138 | }
139 |
140 | async removeTorrent(id: string, removeData: boolean = false): Promise {
141 | try {
142 | const req = await this.request('core.remove_torrent', [id, removeData]);
143 | const data: DelugeBooleanStatus = req.data
144 | return data.result
145 | } catch (e) {
146 | return false
147 | }
148 | }
149 |
150 | async resumeTorrent(id: any): Promise {
151 | try {
152 | const req = await this.request('core.resume_torrent', [id]);
153 | const data: DelugeBooleanStatus = req.data
154 | return data.result
155 | } catch (e) {
156 | return false
157 | }
158 | }
159 |
160 | private async login(): Promise {
161 | try {
162 | const res = await this.request('auth.login', [this.config.password])
163 | const data: DelugeBooleanStatus = res.data
164 | this.isLogin = data.result
165 | return data.result
166 | } catch (e) {
167 | return false
168 | }
169 | }
170 |
171 | private async request(method: DelugeMethod, params: any[]): Promise {
172 | if (!this.isLogin && method !=='auth.login') {
173 | await this.login()
174 | }
175 |
176 | return await axios.post(this.address, {
177 | id: this._msgId++,
178 | method: method,
179 | params: params
180 | }, {
181 | headers: {
182 | 'content-type': 'application/json'
183 | }
184 | })
185 | }
186 |
187 | private static _normalizeTorrent(torrent: DelugeRawTorrent): Torrent {
188 | const dateAdded = new Date(torrent.time_added * 1000).toISOString();
189 | // normalize state to enum
190 | let state = TorrentState.unknown;
191 | if (Object.keys(TorrentState).includes(torrent.state.toLowerCase())) {
192 | state = TorrentState[torrent.state.toLowerCase() as keyof typeof TorrentState];
193 | }
194 |
195 | return {
196 | dateAdded: dateAdded,
197 | id: torrent.hash,
198 | infoHash: torrent.hash,
199 | isCompleted: torrent.progress >= 100,
200 | name: torrent.name,
201 | progress: torrent.progress,
202 | ratio: torrent.ratio,
203 | savePath: torrent.save_path,
204 | state: state,
205 | totalSize: torrent.total_size
206 | }
207 | }
208 | }
--------------------------------------------------------------------------------
/src/plugins/btclient/factory.ts:
--------------------------------------------------------------------------------
1 | import {TorrentClient, TorrentClientConfig} from "@/interfaces/BtClient/AbstractClient";
2 | import Qbittorrent, {defaultQbittorrentConfig} from "@/plugins/btclient/qbittorrent";
3 | import Transmission, {defaultTransmissionConfig} from "@/plugins/btclient/transmission";
4 | import Deluge, {defaultDelugeConfig} from "@/plugins/btclient/deluge";
5 |
6 | export const supportClientType = {
7 | 'qbittorrent': defaultQbittorrentConfig,
8 | 'transmission': defaultTransmissionConfig,
9 | 'deluge': defaultDelugeConfig
10 | }
11 |
12 | export default function (config: TorrentClientConfig): TorrentClient {
13 | switch (config.type) {
14 | case "qbittorrent":
15 | return new Qbittorrent(config)
16 | case "transmission":
17 | return new Transmission(config)
18 | case "deluge":
19 | return new Deluge(config)
20 | case "rtorrent":
21 | return new Qbittorrent(config) // FIXME
22 | }
23 | }
--------------------------------------------------------------------------------
/src/plugins/btclient/qbittorrent.ts:
--------------------------------------------------------------------------------
1 | import {TorrentClient, TorrentState} from "@/interfaces/BtClient/AbstractClient";
2 | import {
3 | QbittorrentAddTorrentOptions,
4 | QbittorrentTorrent,
5 | QbittorrentTorrentClientConfig,
6 | QbittorrentTorrentFilterRules,
7 | QbittorrentTorrentState,
8 | rawTorrent,
9 | } from "@/interfaces/BtClient/qbittorrent";
10 |
11 | import axios, {AxiosResponse, Method} from 'axios'
12 | import urljoin from 'url-join'
13 | import {getRandomInt} from "@/plugins/common";
14 |
15 | export const defaultQbittorrentConfig: QbittorrentTorrentClientConfig = {
16 | type: 'qbittorrent',
17 | name: 'qbittorrent',
18 | uuid: '0da0e93a-3f5f-4bdd-8f73-aaa006d14771',
19 | address: 'http://localhost:9091/',
20 | username: '',
21 | password: '',
22 | timeout: 60 * 1e3
23 | };
24 |
25 | export default class Qbittorrent implements TorrentClient {
26 | isLogin: boolean | null = null;
27 | lastCheckLoginAt: number = 0;
28 | config: QbittorrentTorrentClientConfig;
29 |
30 | constructor(options: Partial = {}) {
31 | this.config = {...defaultQbittorrentConfig, ...options}
32 | }
33 |
34 | async ping(): Promise {
35 | try {
36 | const pong = await this.login();
37 | this.isLogin = pong.data === 'Ok.'
38 | this.lastCheckLoginAt = Date.now()
39 | return this.isLogin
40 | } catch (e) {
41 | return false
42 | }
43 | }
44 |
45 | async login(): Promise {
46 | const form = new FormData();
47 | form.append('username', this.config.username)
48 | form.append('password', this.config.password)
49 |
50 | return await axios.post(urljoin(this.config.address,'/api/v2', '/auth/login'), form, {
51 | timeout: this.config.timeout,
52 | withCredentials: true
53 | })
54 | }
55 |
56 | async request(method: Method, path: string,
57 | params?: any, data?: any,
58 | headers?: any): Promise {
59 | // qbt 默认Session时长 3600s,这里取 600s
60 | // FIXME 是否每次操作都login一下好些?就像PTPP一样
61 | if (this.isLogin === null || Date.now() - 600 * 1e3 > this.lastCheckLoginAt) {
62 | await this.ping()
63 | }
64 |
65 | return await axios.request({
66 | method: method,
67 | url: urljoin(this.config.address, '/api/v2', path),
68 | params: params,
69 | data: data,
70 | headers: headers,
71 | timeout: this.config.timeout,
72 | withCredentials: true
73 | })
74 | }
75 |
76 | async addTorrent(urls: string, options: Partial = {}): Promise {
77 | const formData = new FormData()
78 |
79 | // 处理链接
80 | if (urls.startsWith('magnet:') || !options.localDownload) {
81 | formData.append('urls', urls)
82 | } else if (options.localDownload) {
83 | const req = await axios.get(urls,{
84 | responseType: 'blob'
85 | })
86 | formData.append('torrents', req.data, String(getRandomInt(0, 4096)) + '.torrent')
87 | }
88 | delete options.localDownload
89 |
90 | // 将通用字段转成qbt字段
91 | if (options.savePath) {
92 | options.savepath = options.savePath
93 | delete options.savePath
94 | }
95 |
96 | if (options.label) {
97 | options.category = options.label
98 | delete options.label
99 | }
100 |
101 | if ('addAtPaused' in options) {
102 | options.paused = options.addAtPaused ? 'true' : 'false';
103 | delete options.addAtPaused
104 | }
105 |
106 | options.useAutoTMM = 'false'; // 关闭自动管理
107 |
108 | for (const [key, value] of Object.entries(options)) {
109 | // @ts-ignore
110 | formData.append(key, value);
111 | }
112 |
113 | const res = await this.request('POST', '/torrents/add', undefined, formData)
114 | return res.data == 'Ok.'
115 | }
116 |
117 | async getTorrentsBy(filter: QbittorrentTorrentFilterRules): Promise {
118 | if (filter.hashes) {
119 | filter.hashes = this._normalizeHashes(filter.hashes)
120 | }
121 |
122 | // 将通用项处理成qbt对应的项目
123 | if (filter.complete) {
124 | filter.filter = 'completed'
125 | delete filter.complete
126 | }
127 |
128 | const res = await this.request('GET', '/torrents/info', filter)
129 | return res.data.map((torrent: rawTorrent) => this._normalizeTorrent(torrent))
130 | }
131 |
132 | async getAllTorrents(): Promise {
133 | return await this.getTorrentsBy({})
134 | }
135 |
136 | async getTorrent(id: any): Promise {
137 | const resp = await this.getTorrentsBy({
138 | hashes: id
139 | })
140 |
141 | return resp[0]
142 | }
143 |
144 | async pauseTorrent(hashes: string | string[] | 'all'): Promise {
145 | const params = {
146 | hashes: this._normalizeHashes(hashes),
147 | };
148 |
149 | await this.request('GET', '/torrents/pause', params);
150 | return true;
151 | }
152 |
153 | async removeTorrent(hashes: any, removeData: boolean = false): Promise {
154 | const params = {
155 | hashes: this._normalizeHashes(hashes),
156 | removeData,
157 | };
158 | await this.request('GET', '/torrents/delete', params);
159 | return true;
160 | }
161 |
162 | async resumeTorrent(hashes: string | string[] | 'all'): Promise {
163 | const params = {
164 | hashes: this._normalizeHashes(hashes),
165 | };
166 | await this.request('GET', '/torrents/resume', params);
167 | return true;
168 | }
169 |
170 | _normalizeHashes(hashs: string | string[]): string {
171 | if (Array.isArray(hashs)) {
172 | return hashs.join('|')
173 | }
174 | return hashs
175 | }
176 |
177 | _normalizeTorrent(torrent: rawTorrent): QbittorrentTorrent {
178 | let state = TorrentState.unknown;
179 |
180 | switch (torrent.state) {
181 | case QbittorrentTorrentState.ForcedDL:
182 | case QbittorrentTorrentState.MetaDL:
183 | state = TorrentState.downloading;
184 | break;
185 | case QbittorrentTorrentState.Allocating:
186 | // state = 'stalledDL';
187 | state = TorrentState.queued;
188 | break;
189 | case QbittorrentTorrentState.ForcedUP:
190 | state = TorrentState.seeding;
191 | break;
192 | case QbittorrentTorrentState.PausedDL:
193 | state = TorrentState.paused;
194 | break;
195 | case QbittorrentTorrentState.PausedUP:
196 | // state = 'completed';
197 | state = TorrentState.paused;
198 | break;
199 | case QbittorrentTorrentState.QueuedDL:
200 | case QbittorrentTorrentState.QueuedUP:
201 | state = TorrentState.queued;
202 | break;
203 | case QbittorrentTorrentState.CheckingDL:
204 | case QbittorrentTorrentState.CheckingUP:
205 | case QbittorrentTorrentState.QueuedForChecking:
206 | case QbittorrentTorrentState.CheckingResumeData:
207 | case QbittorrentTorrentState.Moving:
208 | state = TorrentState.checking;
209 | break;
210 | case QbittorrentTorrentState.Unknown:
211 | case QbittorrentTorrentState.MissingFiles:
212 | state = TorrentState.error;
213 | break;
214 | default:
215 | break;
216 | }
217 |
218 | const isCompleted = torrent.progress === 1;
219 |
220 | return {
221 | id: torrent.hash,
222 | infoHash: torrent.hash,
223 | name: torrent.name,
224 | state,
225 | dateAdded: new Date(torrent.added_on * 1000).toISOString(),
226 | isCompleted,
227 | progress: torrent.progress,
228 | label: torrent.category,
229 | savePath: torrent.save_path,
230 | totalSize: torrent.total_size,
231 | ratio: torrent.ratio
232 | };
233 | }
234 | }
--------------------------------------------------------------------------------
/src/plugins/btclient/transmission.ts:
--------------------------------------------------------------------------------
1 | import {
2 | TorrentClient,
3 | TorrentClientConfig,
4 | TorrentState
5 | } from "@/interfaces/BtClient/AbstractClient";
6 |
7 | import axios, {AxiosResponse} from 'axios'
8 | import urljoin from "url-join";
9 | import {
10 | AddTorrentResponse,
11 | rawTorrent, TransmissionAddTorrentOptions,
12 | TransmissionBaseResponse,
13 | TransmissionRequestMethod,
14 | TransmissionTorrent, TransmissionTorrentBaseArguments,
15 | TransmissionTorrentClientConfig, TransmissionTorrentFilterRules,
16 | TransmissionTorrentGetArguments, TransmissionTorrentGetResponse, TransmissionTorrentRemoveArguments
17 | } from "@/interfaces/BtClient/transmission";
18 |
19 |
20 | export const defaultTransmissionConfig: TransmissionTorrentClientConfig = {
21 | type: 'transmission',
22 | name: 'transmission',
23 | uuid: '69e8ecd2-9a96-436c-ac6f-c199c1abb846',
24 | address: 'http://localhost:9091/transmission/rpc',
25 | username: '',
26 | password: '',
27 | timeout: 3 * 60 * 1e3
28 | };
29 |
30 | export default class Transmission implements TorrentClient {
31 | config: TorrentClientConfig;
32 | private readonly address: string;
33 | private readonly authHeader: string;
34 |
35 | private sessionId : string = '';
36 |
37 | constructor(options: Partial = {}) {
38 | this.config = {...defaultTransmissionConfig, ...options}
39 |
40 | // 修正服务器地址
41 | let address = this.config.address;
42 | if (address.indexOf('rpc') === -1) {
43 | address = urljoin(address, '/transmission/rpc')
44 | }
45 | this.address = address
46 |
47 | this.authHeader = 'Basic ' + new Buffer(
48 | this.config.username + (this.config.password ? ':' + this.config.password : '')
49 | ).toString('base64'); // 直接在constructor的时候生成,防止后续使用时多次生成
50 | }
51 |
52 | async addTorrent(url: string, options: Partial = {}): Promise {
53 | if (options.localDownload) {
54 | const req = await axios.get(url, {
55 | responseType: 'arraybuffer'
56 | })
57 | options.metainfo = Buffer.from(req.data, 'binary').toString('base64')
58 | } else {
59 | options.filename = url
60 | }
61 | delete options.localDownload
62 |
63 | if (options.savePath) {
64 | options['download-dir'] = options.savePath
65 | delete options.savePath
66 | }
67 |
68 | // Transmission 3.0 以上才支持label
69 | if (options.label) {
70 | delete options.label
71 | }
72 |
73 | options.paused = options.addAtPaused;
74 | delete options.addAtPaused
75 | try {
76 | const resp = await this.request('torrent-add', options)
77 | const data: AddTorrentResponse = resp.data
78 | return data.result === 'success'
79 | } catch (e) {
80 | return false
81 | }
82 | }
83 |
84 | async getAllTorrents(): Promise {
85 | return await this.getTorrentsBy({})
86 | }
87 |
88 | async getTorrent(id: number | string): Promise {
89 | const torrents = await this.getTorrentsBy({
90 | ids: [id]
91 | })
92 | return torrents[0]
93 | }
94 |
95 | async getTorrentsBy(filter: TransmissionTorrentFilterRules): Promise {
96 | let args: TransmissionTorrentGetArguments = {
97 | fields: [
98 | 'addedDate',
99 | 'id',
100 | 'hashString',
101 | 'isFinished',
102 | 'name',
103 | 'percentDone',
104 | 'uploadRatio',
105 | 'downloadDir',
106 | 'status',
107 | 'totalSize',
108 | 'leftUntilDone',
109 | 'labels'
110 | ],
111 | }
112 |
113 | if (filter.ids) {
114 | args.ids = filter.ids
115 | }
116 |
117 | const resp = await this.request('torrent-get', args)
118 | const data:TransmissionTorrentGetResponse = resp.data
119 | let returnTorrents = data.arguments.torrents.map(s => this._normalizeTorrent(s))
120 |
121 | if (filter.complete) {
122 | returnTorrents = returnTorrents.filter(s => s.isCompleted)
123 | }
124 |
125 | return returnTorrents
126 | }
127 |
128 | async pauseTorrent(id: any): Promise {
129 | const args: TransmissionTorrentBaseArguments = {
130 | ids: id
131 | }
132 | await this.request('torrent-stop', args)
133 | return true
134 | }
135 |
136 | async ping(): Promise {
137 | try {
138 | const resp = await this.request('session-get')
139 | const data: TransmissionBaseResponse = resp.data
140 | return data.result === 'success';
141 | } catch (e) {
142 | return false
143 | }
144 | }
145 |
146 | async removeTorrent(id: number, removeData: boolean | undefined): Promise {
147 | const args:TransmissionTorrentRemoveArguments = {
148 | ids: id,
149 | 'delete-local-data': removeData
150 | }
151 | await this.request('torrent-remove', args)
152 | return true
153 | }
154 |
155 | async resumeTorrent(id: any): Promise {
156 | const args: TransmissionTorrentBaseArguments = {
157 | ids: id
158 | }
159 | await this.request('torrent-start', args)
160 | return true
161 | }
162 |
163 | async request(method:TransmissionRequestMethod, args: any = {}): Promise {
164 | try {
165 | return await axios.post(this.address, {
166 | method: method,
167 | arguments: args,
168 | }, {
169 | headers: {
170 | Authorization: this.authHeader,
171 | 'X-Transmission-Session-Id': this.sessionId
172 | },
173 | timeout: this.config.timeout
174 | })
175 | } catch (error) {
176 | if (error.response && error.response.status === 409) {
177 | this.sessionId = error.response.headers['x-transmission-session-id'] // lower cased header in axios
178 | return await this.request(method, args)
179 | } else {
180 | throw error
181 | }
182 | }
183 | }
184 |
185 | _normalizeTorrent(torrent: rawTorrent): TransmissionTorrent {
186 | const dateAdded = new Date(torrent.addedDate * 1000).toISOString();
187 |
188 | let state = TorrentState.unknown;
189 | if (torrent.status === 6) {
190 | state = TorrentState.seeding;
191 | } else if (torrent.status === 4) {
192 | state = TorrentState.downloading;
193 | } else if (torrent.status === 0) {
194 | state = TorrentState.paused;
195 | } else if (torrent.status === 2) {
196 | state = TorrentState.checking;
197 | } else if (torrent.status === 3 || torrent.status === 5) {
198 | state = TorrentState.queued;
199 | }
200 |
201 | return {
202 | id: torrent.id,
203 | infoHash: torrent.hashString,
204 | name: torrent.name,
205 | progress: torrent.percentDone,
206 | isCompleted: torrent.leftUntilDone < 1,
207 | ratio: torrent.uploadRatio,
208 | dateAdded: dateAdded,
209 | savePath: torrent.downloadDir,
210 | label: torrent.labels && torrent.labels.length ? torrent.labels[0] : undefined,
211 | state: state,
212 | totalSize: torrent.totalSize
213 | };
214 | }
215 | }
--------------------------------------------------------------------------------
/src/plugins/common.ts:
--------------------------------------------------------------------------------
1 | // 存放一些公共方法
2 |
3 | import {shell} from 'electron'
4 | import {LogInfo} from "@/interfaces/store";
5 | import dayjs from "dayjs";
6 |
7 | export async function shellOpen(href: string) {
8 | await shell.openExternal(href)
9 | }
10 |
11 | export function getRandomInt(min: number, max: number) {
12 | min = Math.ceil(min);
13 | max = Math.floor(max);
14 | return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
15 | }
16 |
17 | export function formatLogs(logs: LogInfo[]) {
18 | return logs
19 | .map(log => `${dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss')} ${log.message}`) // 对象整理成字符串
20 | .join('\n') // 用 \n 分割
21 | }
22 |
23 | export function sleep(ms: number) {
24 | return new Promise(resolve => setTimeout(resolve, ms));
25 | }
--------------------------------------------------------------------------------
/src/plugins/cookies.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import cookieParser from 'cookie'
3 | import {remote} from 'electron'
4 | import {EnableSite} from "@/interfaces/IYUU/Site";
5 |
6 | const session = remote.session
7 |
8 | export default class Cookies {
9 | private static editCookiesKeys: string[] = [
10 | "domain", "expirationDate", "httpOnly", "name", "path",
11 | "sameSite", "secure", "session", "storeId", "value", "id"
12 | ]
13 |
14 | static parseCookies(raw: string): { [key: string]: string } {
15 | if (raw.match(/^\[/)) {
16 | // 如果开头是 [ ,我们认为是 editCookies 导出的结构
17 | let cookies = JSON.parse(raw)
18 | if (cookies.length === 0) {
19 | throw new TypeError('Zero Cookies Length')
20 | } else {
21 | let out: { [key: string]: string } = {}
22 | for (let i = 0; i < cookies.length; i++) {
23 | const cookie = cookies[i]
24 | if (!_.every(this.editCookiesKeys, _.partial(_.has, cookies[i]))) {
25 | throw new TypeError('Some key miss in EditCookies export cookies')
26 | }
27 | out[cookie.name] = cookie.value
28 | }
29 | return out
30 | }
31 | } else {
32 | // 不然,我们认为是 {key}={value}; 形式,直接使用 cookieParser
33 | return cookieParser.parse(raw)
34 | }
35 | }
36 |
37 | static validCookies(raw: string): Boolean {
38 | try {
39 | this.parseCookies(raw)
40 | } catch (e) {
41 | return false
42 | }
43 | return true
44 | }
45 |
46 | // 对 Electron 的 Cookies 对象 wrapper
47 | static async getCookies(filter: Electron.CookiesGetFilter): Promise {
48 | return await session.defaultSession.cookies.get(filter)
49 | }
50 |
51 | static async setCookie(cookie: Electron.CookiesSetDetails): Promise {
52 | return await session.defaultSession.cookies.set(cookie)
53 | }
54 |
55 | static async removeCookie(url: string, name: string): Promise {
56 | return await session.defaultSession.cookies.remove(url, name)
57 | }
58 |
59 | // 在wrapper基础上扩展
60 | static async setCookiesByUrlAndCookiejar(url: string, cookiejar: { [key: string]: string }): Promise {
61 | for (const [key, value] of Object.entries(cookiejar)) {
62 | await this.setCookie({
63 | url: url,
64 | name: key,
65 | value: value
66 | })
67 | }
68 | }
69 |
70 | static async setCookiesBySite(site: EnableSite) {
71 | await this.setCookiesByUrlAndCookiejar(
72 | (site.is_https > 0 ? 'https://' : 'http://') + site.base_url + '/',
73 | this.parseCookies(site.cookies)
74 | )
75 | }
76 | }
--------------------------------------------------------------------------------
/src/plugins/dayjs.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import duration from 'dayjs/plugin/duration'
3 |
4 | dayjs.extend(duration)
5 |
6 | export default dayjs
--------------------------------------------------------------------------------
/src/plugins/element.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Element from 'element-ui'
3 | import 'element-ui/lib/theme-chalk/index.css'
4 |
5 | Vue.use(Element)
6 |
--------------------------------------------------------------------------------
/src/plugins/iyuu.ts:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto'
2 | import axios, {AxiosInstance, AxiosResponse} from 'axios'
3 | import {Notification} from 'element-ui'
4 |
5 | import {IYUUStore} from '@/store'
6 | import * as Forms from '@/interfaces/IYUU/Forms'
7 |
8 |
9 | function sha1(data: string | string[]) {
10 | if (Array.isArray(data)) {
11 | data = JSON.stringify(data)
12 | }
13 | return crypto.createHash('sha1').update(data).digest('hex')
14 | }
15 |
16 | function timestamp() {
17 | // @ts-ignore
18 | return parseInt(Date.now() / 1000)
19 | }
20 |
21 | class IyuuEndpoint {
22 | private instance: AxiosInstance;
23 | private apiPoint = 'https://api.iyuu.cn/';
24 | private version = '1.10.6'; // FAKE origin version
25 |
26 | constructor() {
27 | this.instance = axios.create({
28 | baseURL: this.apiPoint,
29 | transformResponse: [function (resp) {
30 | // 统一处理IYUU服务器返回信息
31 | // 不知道为什么,这里获得的data项不是JSON字典,而是JSON字符串,将其转换一下
32 | // 如果状态码不对,也统一报错
33 | const data = JSON.parse(resp)
34 | if (data.ret !== 200) {
35 | Notification.error({
36 | title: '服务器返回错误: ' + data.ret,
37 | message: data.data.errmsg ? data.data.errmsg : data.msg
38 | })
39 | }
40 |
41 | return data
42 | }]
43 | })
44 | }
45 |
46 | async sendWeChatMsg(text: string, desp: string|null = null): Promise {
47 | const params = new FormData();
48 | params.append('text', text);
49 | if (desp) {
50 | params.append('desp', desp);
51 | }
52 |
53 | return await axios.post(`https://iyuu.cn/${this.getSign()}.send`,params)
54 | }
55 |
56 | getSign():string {
57 | // @ts-ignore
58 | return IYUUStore.token
59 | }
60 |
61 | // 用户登录绑定操作
62 | userLogin(userLoginForm: Forms.userLoginRequest): Promise {
63 | // 要求对合作站点用户密钥进行sha1操作 sha1(passkey)
64 | userLoginForm.passkey = sha1(userLoginForm.passkey)
65 |
66 | return new Promise((resolve, reject) => {
67 | this.instance.get('/user/login', {
68 | params: userLoginForm
69 | }).then(resp => {
70 | const data: Forms.userLoginResponse = resp.data
71 | if (data.ret === 200) {
72 | Notification.success({
73 | title: '登录验证成功',
74 | message: data.data.errmsg
75 | })
76 | IYUUStore.setToken(userLoginForm.token).then(() => {
77 | resolve(data)
78 | })
79 | } else {
80 | reject(data)
81 | }
82 | })
83 | })
84 | }
85 |
86 | // 获取所有站点支持列表
87 | apiSites(userLoginForm: Forms.userLoginRequest | null = null): Promise {
88 | let sign: string;
89 | if (userLoginForm === null) {
90 | sign = this.getSign()
91 | } else {
92 | sign = userLoginForm.token
93 | }
94 |
95 | return new Promise((resolve, reject) => {
96 | this.instance.get('/api/sites', {
97 | params: {
98 | sign: sign,
99 | version: this.version
100 | } as Forms.apiSitesRequest
101 | }).then(resp => {
102 | const data: Forms.apiSitesResponse = resp.data
103 |
104 | if (data.data.sites) {
105 | Notification.success('从IYUU服务器获取站点数据成功')
106 | IYUUStore.updateSites(data.data.sites).then(() => {
107 | resolve(data)
108 | })
109 | } else {
110 | // TODO cleanToken
111 | reject(data)
112 | }
113 | })
114 | })
115 | }
116 |
117 | private _buildHashForm(infohash:string|string[]): Forms.apiInfohashRequest|Forms.apiHashRequest {
118 | // 强行转成列表
119 | if (typeof infohash === 'string') {
120 | infohash = [infohash]
121 | }
122 |
123 | return {
124 | sign: this.getSign(),
125 | timestamp: timestamp(),
126 | version: this.version,
127 | // @ts-ignore
128 | hash: infohash,
129 | sha1: sha1(infohash),
130 | }
131 | }
132 |
133 | apiInfohash(infohash: string | string[]): Promise {
134 | return new Promise(resolve => {
135 | this.instance.post('/api/infohash', this._buildHashForm(infohash)).then(resp => {
136 | const data: Forms.apiInfohashResponse = resp.data
137 | resolve(data)
138 | })
139 | })
140 | }
141 |
142 | apiHash(infohash: string|string[]): Promise {
143 | return new Promise(resolve => {
144 | this.instance.post('/api/hash', this._buildHashForm(infohash)).then(resp => {
145 | const data: Forms.apiHashResponse = resp.data
146 | resolve(data)
147 | })
148 | })
149 | }
150 |
151 | apiNotify(info: {
152 | site: string,
153 | sid: number,
154 | torrent_id: number,
155 | error: string
156 | }): Promise
157 | {
158 | const form: Forms.apiNotifyRequest = { sigh: this.getSign(), ...info }
159 | return new Promise(resolve => {
160 | this.instance.get('/api/notify', {
161 | params: form
162 | }).then(resp => {
163 | const data: Forms.apiNotifyResponse = resp.data
164 | resolve(data)
165 | })
166 | })
167 | }
168 | }
169 |
170 | const iyuuEndpoint = new IyuuEndpoint()
171 | Object.freeze(iyuuEndpoint)
172 | export default iyuuEndpoint
--------------------------------------------------------------------------------
/src/plugins/mission/reseed.ts:
--------------------------------------------------------------------------------
1 | // 转发使用的核心方法
2 | import UUID from 'uuid'
3 | import _ from 'lodash'
4 | import dayjs from "dayjs";
5 | import {ipcRenderer} from 'electron'
6 |
7 | import {EnableSite} from "@/interfaces/IYUU/Site";
8 | import {TorrentInfo} from "@/interfaces/IYUU/Forms";
9 | import {AddTorrentOptions, TorrentClientConfig} from "@/interfaces/BtClient/AbstractClient";
10 |
11 | import {formatLogs, sleep} from "@/plugins/common";
12 | import iyuuEndpoint from "@/plugins/iyuu";
13 | import downloadFactory from '@/plugins/sites/factory';
14 | import btClientFactory from "@/plugins/btclient/factory";
15 | import {MissionStore, IYUUStore, StatusStore} from '@/store/store-accessor' // circular import; OK though
16 | import {CookiesExpiredError, TorrentNotExistError} from "@/plugins/sites/default";
17 |
18 | const packageInfo = require('../../../package.json')
19 |
20 | export interface ReseedStartOption {
21 | dryRun: boolean
22 | label: string,
23 | weChatNotify: boolean, // 是否将信息推送给微信
24 | closeAppAfterRun: boolean
25 | }
26 |
27 | class RateLimitError extends Error {
28 | }
29 |
30 | export default class Reseed {
31 | // 传入参数
32 | private readonly sites: EnableSite[];
33 | private readonly clients: TorrentClientConfig[];
34 | private readonly options: Partial;
35 |
36 | // constructor时生成
37 | readonly logId: string;
38 |
39 | private siteIds: number[]
40 | private rateLimitSites: {
41 | [prop_name: string]: {
42 | last_hit_timestamp: number,
43 | total_count: number
44 | }
45 | };
46 |
47 | private state: {
48 | hashCount: number,
49 | reseedCount: number,
50 | reseedSuccess: number,
51 | reseedFail: number,
52 | reseedPass: number
53 | }
54 |
55 | constructor(sites: EnableSite[], clientsConfig: TorrentClientConfig[], options: Partial) {
56 | this.sites = sites
57 | this.clients = clientsConfig
58 | this.options = options
59 |
60 | this.logId = UUID.v4()
61 |
62 | this.siteIds = sites.map(s => s.id)
63 | this.rateLimitSites = {}
64 | this.state = {
65 | hashCount: 0,
66 | reseedCount: 0,
67 | reseedSuccess: 0,
68 | reseedFail: 0,
69 | reseedPass: 0
70 | }
71 | }
72 |
73 | private missionStart() {
74 | StatusStore.missionStart()
75 |
76 | MissionStore.updateCurrentMissionState({
77 | processing: true,
78 | logId: this.logId
79 | })
80 | }
81 |
82 | private async missionEnd() {
83 | this.logger(`辅种任务已完成,任务Id: ${this.logId}`)
84 | MissionStore.updateCurrentMissionState({
85 | processing: false,
86 | logId: this.logId
87 | })
88 |
89 | // 发送微信通知
90 | if (this.options.weChatNotify) {
91 | await this.sendWeChatNotify()
92 | }
93 |
94 | // 自动退出
95 | if (this.options.closeAppAfterRun) {
96 | ipcRenderer.send('close-me')
97 | }
98 | }
99 |
100 | private formatTpl(tpl: string): string {
101 | return tpl
102 | .replace(new RegExp('{version}','g'), packageInfo.version)
103 | .replace(new RegExp('{mission_id}','g'), this.logId)
104 | .replace(new RegExp('{clients_count}','g'), String(this.clients.length))
105 | .replace(new RegExp('{sites_count}','g'), String(this.sites.length))
106 | .replace(new RegExp('{hashs_count}','g'), String(this.state.hashCount))
107 | .replace(new RegExp('{reseed_count}','g'), String(this.state.reseedCount))
108 | .replace(new RegExp('{reseed_success}','g'), String(this.state.reseedSuccess))
109 | .replace(new RegExp('{reseed_fail}','g'), String(this.state.reseedFail))
110 | .replace(new RegExp('{reseed_pass}','g'), String(this.state.reseedPass))
111 | .replace(new RegExp('{full_log}','g'), formatLogs(MissionStore.logByLogId(this.logId)))
112 | }
113 |
114 | private async sendWeChatNotify() {
115 | let title = this.formatTpl(IYUUStore.weChatNotify.reseed.title).slice(0, 100)
116 | let descr = this.formatTpl(IYUUStore.weChatNotify.reseed.descr)
117 |
118 | await iyuuEndpoint.sendWeChatMsg(title, descr)
119 | }
120 |
121 | async start(): Promise {
122 | this.missionStart()
123 |
124 | this.logger(`开始辅种任务,任务Id: ${this.logId}。启用下载服务器数 ${this.clients.length}, 启用站点数 ${this.sites.length}。`)
125 | if (this.options.dryRun) {
126 | this.logger(`这是一次空运行,软件不会把种子链接或者种子文件发送给做种服务器,如果该站点属于特殊站点,软件同样不会解析页面来获取下载链接。`)
127 |
128 | const rateLimitSites = this.sites.filter(s => s.rate_limit && (s.rate_limit.maxRequests > 0 || s.rate_limit.requestsDelay > 0))
129 | if (rateLimitSites.length > 0) {
130 | this.logger(`这是一次空运行,站点 ${rateLimitSites.map(s => s.site).join(',')} 的下载频率限制同样不会生效。`)
131 | }
132 | }
133 |
134 | // 开始遍历下载器
135 | for (let i = 0; i < this.clients.length; i++) {
136 | try {
137 | await this.loopClient(this.clients[i])
138 | } catch (e) {
139 | this.logger(`下载器 ${this.clients[i].name}(${this.clients[i].type}) 处理错误: ${e}`)
140 | }
141 |
142 | }
143 |
144 | await this.missionEnd()
145 | }
146 |
147 | private logger(msg: string) {
148 | MissionStore.appendLog({logId: this.logId, logMessage: msg})
149 | }
150 |
151 | private async loopClient(config: TorrentClientConfig) {
152 | const client = btClientFactory(config)
153 |
154 | this.logger(`正在检查下载服务器 ${client.config.name}(${client.config.type}) 的连接性。`)
155 | const connect = await client.ping()
156 | if (connect) {
157 | this.logger(`已成功连接到下载服务器 ${client.config.name}(${client.config.type}),开始请求已完成种子清单。`)
158 |
159 | // 获得已下载完成的种子
160 | const torrents = await client.getTorrentsBy({
161 | complete: true
162 | })
163 | this.state.hashCount += torrents.length
164 |
165 | // 从缓存中获取该下载器已经转发过的infoHash
166 | const reseedTorrentInfoHashs = MissionStore.reseededByClientId(client.config.uuid)
167 |
168 | // 筛选出未被转发过的种子infohash
169 | const unReseedTorrents = torrents.filter(t => !reseedTorrentInfoHashs.includes(t.infoHash))
170 |
171 | this.logger(`从下载器 ${client.config.name}(${client.config.type}) 中获取到 ${torrents.length} 个已完成种子,其中 未被转发过的有 ${unReseedTorrents.length} 个。`)
172 | this.logger(`当前下载器总共缓存了 ${reseedTorrentInfoHashs.length} 个种子 infoHash 历史。`)
173 |
174 | const chunkUnReseedTorrents = _.chunk(unReseedTorrents, IYUUStore.apiPreInfoHash)
175 | if (chunkUnReseedTorrents.length > 1) {
176 | this.logger(`由于当前infoHash总数超过最大单次请求限制(设置值 ${IYUUStore.apiPreInfoHash}),程序将分成多次进行请求。`)
177 | }
178 |
179 | for (let i = 0; i < chunkUnReseedTorrents.length; i++) {
180 | const chunkUnReseedTorrent = chunkUnReseedTorrents[i]
181 | // 将分片信息请求IYUU服务器
182 | const resp = await iyuuEndpoint.apiHash(chunkUnReseedTorrent.map(t => t.infoHash))
183 | this.logger(`在提交的 ${chunkUnReseedTorrent.length} 个infoHash值里, IYUU服务器共返回 ${resp.data.length || 0} 个可辅种结果。`)
184 | this.state.reseedCount += resp.data.length || 0
185 | for (let j = 0; j < resp.data.length; j++) {
186 | const reseedTorrentsDataFromIYUU = resp.data[j]
187 | const reseedTorrentDataFromClient = torrents.find(t => t.infoHash === reseedTorrentsDataFromIYUU.hash)
188 |
189 | // 筛选需要转发的种子
190 | const canReseedTorrents = reseedTorrentsDataFromIYUU.torrent.filter(t => {
191 | return this.siteIds.includes(t.sid) // 对应种子在转发站点中
192 | && !reseedTorrentInfoHashs.includes(t.info_hash); // 这个种子未命中该下载器的转发缓存
193 | })
194 |
195 | this.logger(`种子 ${reseedTorrentsDataFromIYUU.hash}: IYUU 返回 ${reseedTorrentsDataFromIYUU.torrent.length} 个待选种子,其中可添加 ${canReseedTorrents.length} 个`)
196 | this.state.reseedPass += reseedTorrentsDataFromIYUU.torrent.length - canReseedTorrents.length
197 |
198 | let partialFail = 0; // 部分辅种 infohash 出现错误,此时不应添加原种子缓存
199 | for (let k = 0; k < canReseedTorrents.length; k++) {
200 | const reseedTorrent: TorrentInfo = canReseedTorrents[k]
201 |
202 | const siteInfoForThisTorrent = this.sites.find(s => s.id === reseedTorrent.sid)
203 | if (siteInfoForThisTorrent && reseedTorrentDataFromClient) { // 因为ts限制,这里要加一层判断(但实际并没有必要)
204 | if (this.options.dryRun) {
205 | this.logger(`将会推送 站点 ${siteInfoForThisTorrent.site},id为 ${reseedTorrent.torrent_id} 的 种子链接/种子文件 到下载器 ${client.config.name}(${client.config.type})。`)
206 | continue;
207 | }
208 |
209 | // 检查是否触及到站点流控规则
210 | try {
211 | await this.siteRateLimitCheck(siteInfoForThisTorrent)
212 | } catch (e) {
213 | continue
214 | }
215 |
216 | let downloadOptionsForThisTorrent: AddTorrentOptions = {
217 | savePath: reseedTorrentDataFromClient.savePath,
218 | addAtPaused: true, // 置于暂停状态
219 | localDownload: siteInfoForThisTorrent.download_torrent
220 | }
221 |
222 | /* TODO 设置标签
223 | * if (this.options.label) {
224 | * downloadOptionsForThisTorrent.label = this.options.label
225 | * }
226 | */
227 |
228 | try {
229 | // 使用工厂函数方法,构造种子真实下载链接
230 | let torrentLink = await downloadFactory(reseedTorrent, siteInfoForThisTorrent)
231 |
232 | // 加重试版 推送种子链接(由本地btclient代码根据传入参数决定推送的是链接还是文件)
233 | let retryCount = 0;
234 | while (retryCount++ < IYUUStore.maxRetry) {
235 | const addTorrentStatue = await client.addTorrent(torrentLink, downloadOptionsForThisTorrent)
236 | if (addTorrentStatue) {
237 | this.logger(`添加站点 ${siteInfoForThisTorrent.site} 种子 ${reseedTorrent.info_hash} 成功。`)
238 | StatusStore.torrentReseed() // 增加辅种成功计数
239 | MissionStore.appendReseeded({ // 将这个infoHash加入缓存中
240 | clientId: client.config.uuid, infoHash: reseedTorrent.info_hash
241 | })
242 | this.state.reseedSuccess++
243 | break; // 退出重试循环
244 | } else {
245 | partialFail++;
246 | this.state.reseedFail++
247 | this.logger(`添加站点 ${siteInfoForThisTorrent.site} 种子 ${reseedTorrent.info_hash} 失败。`)
248 | if (retryCount === IYUUStore.maxRetry) {
249 | this.logger(`请考虑为下载器 ${client.config.name}(${client.config.type}) 手动下载添加,链接 ${torrentLink} 。`)
250 | } else {
251 | const sleepSecond = Math.min(30, Math.pow(2, retryCount))
252 | this.logger(`等待 ${sleepSecond} 秒进行第 ${retryCount} 次推送重试`)
253 | await sleep(sleepSecond * 1e3)
254 | }
255 | }
256 | }
257 | } catch (e) {
258 | this.logger(`种子下载链接构造失败, 站点 ${siteInfoForThisTorrent.site} 种子id: ${reseedTorrent.sid}。原因: ${e}`)
259 |
260 | // Cookies过期
261 | if (e instanceof CookiesExpiredError) {
262 | this.tempRemoveSite(siteInfoForThisTorrent, `${e}`)
263 | }
264 |
265 | if (!(e instanceof TorrentNotExistError)) {
266 | this.state.reseedFail++
267 | partialFail++;
268 | }
269 | }
270 | }
271 | }
272 |
273 | if (!this.options.dryRun // 非空运行
274 | && canReseedTorrents.length > 0 // 可用辅种数量大于0
275 | && partialFail === 0 // 未出现失败辅种
276 | ) {
277 | MissionStore.appendReseeded({ // 将原种的infohash加入缓存
278 | clientId: client.config.uuid, infoHash: reseedTorrentsDataFromIYUU.hash
279 | })
280 | }
281 | }
282 | }
283 |
284 | // TODO
285 | } else {
286 | this.logger(`下载器 ${client.config.name}(${client.config.type}) 连接失败`)
287 | }
288 | }
289 |
290 | private tempRemoveSite(site: EnableSite, reason: string = '') {
291 | this.logger(`本次运行不再推送站点 ${site.site}, 原因 : "${reason}"。`)
292 | const siteOrderId = this.siteIds.findIndex(i => i === site.id)
293 | this.siteIds.splice(siteOrderId, 1)
294 | }
295 |
296 |
297 | private async siteRateLimitCheck(site: EnableSite) {
298 | const siteRateLimitRule = site.rate_limit
299 | if (siteRateLimitRule) {
300 | // 建立字典
301 | if (!(site.site in this.rateLimitSites)) {
302 | this.rateLimitSites[site.site] = {
303 | last_hit_timestamp: 0,
304 | total_count: 0
305 | }
306 | }
307 |
308 | // 检查该站点是否达到最大下载
309 | if (siteRateLimitRule.maxRequests > 0) {
310 | if (this.rateLimitSites[site.site].total_count > siteRateLimitRule.maxRequests) {
311 | this.tempRemoveSite(site, `触及到推送限制规则: 单次运行最多推送 ${siteRateLimitRule.maxRequests} 个种子`)
312 | throw new RateLimitError(`站点 ${site.site} 触及到推送限制规则`)
313 | } else {
314 | this.rateLimitSites[site.site].total_count++
315 | }
316 | }
317 |
318 | if (siteRateLimitRule.requestsDelay > 0) {
319 | const waitTime = siteRateLimitRule.requestsDelay * 1e3
320 | const dateNow = Date.now()
321 | // 计算剩余等待时间,并等待
322 | const elapseTime = dateNow - (waitTime + this.rateLimitSites[site.site].last_hit_timestamp)
323 | if (elapseTime > 0) {
324 | this.logger(`站点 ${site.site} 通过推送间隔检查,上次推送时间 ${dayjs(this.rateLimitSites[site.site].last_hit_timestamp).format('YYYY-MM-DD HH:mm:ss')},间隔 ${elapseTime / 1e3} 秒。`)
325 | } else {
326 | this.logger(`站点 ${site.site} 触及到推送限制规则: 连续两次推送间隔 ${siteRateLimitRule.requestsDelay} 秒。等待 ${-elapseTime / 1e3}秒后重试。`)
327 | await sleep(-elapseTime)
328 | }
329 |
330 | this.rateLimitSites[site.site].last_hit_timestamp = Date.now()
331 | }
332 | }
333 | }
334 | }
--------------------------------------------------------------------------------
/src/plugins/sites/default.ts:
--------------------------------------------------------------------------------
1 | // 普通站点链接构造逻辑
2 |
3 | import {EnableSite} from "@/interfaces/IYUU/Site";
4 | import Cookies from "@/plugins/cookies";
5 | import {TorrentInfo} from "@/interfaces/IYUU/Forms";
6 |
7 | export class CookiesExpiredError extends Error {}
8 | export class TorrentNotExistError extends Error {}
9 | export class TorrentNotValidError extends Error {}
10 |
11 | export default async function (reseedInfo: TorrentInfo, site: EnableSite) {
12 | // 将种子连接模板中剩下的{} 替换成 IYUU给出的种子id
13 | let url = site.link.replace(/{}/ig, String(reseedInfo.torrent_id))
14 |
15 | // 如果站点需要本地下载,则设置Cookies
16 | if (site.download_torrent) {
17 | await Cookies.setCookiesBySite(site)
18 | }
19 |
20 | // 返回原链接
21 | return url
22 | }
--------------------------------------------------------------------------------
/src/plugins/sites/factory.ts:
--------------------------------------------------------------------------------
1 | // 种子下载链接构造 工厂函数
2 |
3 | import {EnableSite} from "@/interfaces/IYUU/Site";
4 | import {TorrentInfo} from "@/interfaces/IYUU/Forms";
5 |
6 | import defaultSiteDownload from '@/plugins/sites/default'
7 | import HdChinaDownload from '@/plugins/sites/hdchina'
8 | import HDCityDownload from '@/plugins/sites/hdcity'
9 | import HDSkyDownload from '@/plugins/sites/hdsky'
10 |
11 | // 合作站点
12 | export const coSite = [
13 | 'ourbits', 'hddolby', 'hdhome', 'pthome', 'chdbits', 'hdai'
14 | ]
15 |
16 | export const forceDownloadSite = [
17 | 'hdchina', 'hdcity', 'hdsky',
18 | // 使用 '&uid={uid}&hash={hash}' 的站点
19 | 'pthome', 'hdhome', 'hddolby', 'hdai'
20 | ]
21 |
22 | export function isForceDownloadSite(name: string) {
23 | return forceDownloadSite.includes(name)
24 | }
25 |
26 | export function defaultSiteRateLimit(name: string) {
27 | switch (name) {
28 | case 'ourbits':
29 | case 'moecat':
30 | case 'ssd':
31 | return {maxRequests: 20, requestsDelay: 15}
32 | case 'hddolby':
33 | case 'hdhome':
34 | case 'pthome':
35 | return {maxRequests: 20, requestsDelay: 5}
36 | case 'hdsky':
37 | return {maxRequests: 20, requestsDelay: 20}
38 | case 'hdchina':
39 | return {maxRequests: 10, requestsDelay: 5}
40 | case 'pt':
41 | return {maxRequests: 20, requestsDelay: 20}
42 | default:
43 | return {maxRequests: 0, requestsDelay: 0}
44 | }
45 | }
46 |
47 | export default async function (reseedInfo: TorrentInfo, site: EnableSite) {
48 | switch (site.site) {
49 | case 'hdchina':
50 | return await HdChinaDownload(reseedInfo, site)
51 | case 'hdcity':
52 | return await HDCityDownload(reseedInfo, site)
53 | case 'hdsky':
54 | return await HDSkyDownload(reseedInfo, site)
55 | default:
56 | /**
57 | * 由于我暂时无精力实现以下站点的传入uid和hash功能,
58 | * 且这些站点在有cookies的情况下,不需要 '&uid={uid}&hash={hash}' 字符串
59 | * 所以将这些站点移入 forceDownloadSite 且在传入链接中删去以上字段,
60 | * 强行使用 /download.php?id={} + Cookies 的形式下载种子
61 | */
62 | site.download_page = site.download_page.replace('&uid={uid}&hash={hash}', '')
63 | return await defaultSiteDownload(reseedInfo, site)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/plugins/sites/hdchina.ts:
--------------------------------------------------------------------------------
1 | // hdchina,HDSKY站点通用下载链接构造逻辑
2 |
3 | import axios from 'axios'
4 | import urljoin from "url-join";
5 |
6 | import {EnableSite} from "@/interfaces/IYUU/Site";
7 | import {TorrentInfo} from "@/interfaces/IYUU/Forms";
8 |
9 | import Cookies from "@/plugins/cookies";
10 | import iyuuEndpoint from '@/plugins/iyuu'
11 | import {CookiesExpiredError, TorrentNotExistError} from "@/plugins/sites/default";
12 |
13 | export default async function (reseedInfo: TorrentInfo, site: EnableSite) {
14 | // 设置站点Cookies
15 | await Cookies.setCookiesBySite(site)
16 | const baseUrl = (site.is_https > 0 ? 'https://' : 'http://') + site.base_url
17 |
18 | // 构造对应种子详情页链接
19 | const detailsUrl = urljoin(baseUrl, `/details.php?id=${reseedInfo.torrent_id}`)
20 | const detailsPageRep = await axios.get(detailsUrl)
21 | const detailsPage = detailsPageRep.data
22 | if (detailsPage.search('该页面必须在登录后才能访问') > -1) {
23 | throw new CookiesExpiredError('站点 Cookies 已过期,请更新后重新辅种!')
24 | }
25 |
26 | // 直接使用正则提取
27 | let path = (detailsPage.match(/href="(download\.php\?hash=[^"]+?)">/) || ['', ''])[1]
28 | if (path != '') {
29 | return urljoin(baseUrl, path)
30 | }
31 |
32 | // 未提取到,则当作该种子不存在,提交IYUU并抛出异常
33 | await iyuuEndpoint.apiNotify({
34 | site: site.site,
35 | sid: site.id,
36 | torrent_id: reseedInfo.sid,
37 | error: '404'
38 | })
39 | throw new TorrentNotExistError(`没有该ID的种子 (站点 ${site.site} ID ${reseedInfo.sid})`)
40 | }
--------------------------------------------------------------------------------
/src/plugins/sites/hdcity.ts:
--------------------------------------------------------------------------------
1 | // hdcity站点下载链接构造逻辑
2 |
3 | import urljoin from "url-join";
4 | import axios from "axios";
5 |
6 | import {EnableSite} from "@/interfaces/IYUU/Site";
7 | import {TorrentInfo} from "@/interfaces/IYUU/Forms";
8 |
9 | import Cookies from "@/plugins/cookies";
10 | import iyuuEndpoint from "@/plugins/iyuu";
11 | import {CookiesExpiredError, TorrentNotExistError} from "@/plugins/sites/default";
12 |
13 | let cuhash: string | null = null;
14 | let lastCheck: number = 0;
15 |
16 | export default async function (reseedInfo: TorrentInfo, site: EnableSite) {
17 | const baseUrl = (site.is_https > 0 ? 'https://' : 'http://') + site.base_url
18 |
19 | const dateNow = Date.now()
20 | const elapseTime = dateNow - (600 * 1e3 + lastCheck)
21 | if (typeof cuhash !== 'string' || elapseTime < 0) {
22 | // 设置站点Cookies
23 | await Cookies.setCookiesBySite(site)
24 | const detailsUrl = urljoin(baseUrl, `t-${reseedInfo.torrent_id}`)
25 | const detailsPageRep = await axios.get(detailsUrl)
26 | const detailsPage = detailsPageRep.data
27 | if (detailsPage.search('HDCITY PORTAL') > -1) {
28 | throw new CookiesExpiredError('站点 Cookies 已过期,请更新后重新辅种!')
29 | }
30 | if (detailsPage.search('木有该ID的种子,可能输入错误或已被删除。') > -1) {
31 | await iyuuEndpoint.apiNotify({
32 | site: site.site,
33 | sid: site.id,
34 | torrent_id: reseedInfo.sid,
35 | error: '404'
36 | })
37 | throw new TorrentNotExistError(`没有该ID的种子 (站点 ${site.site} ID ${reseedInfo.sid})`)
38 | }
39 |
40 | // 使用正则提取cuhash
41 | let tmpCuhash = (detailsPage.match('cuhash=([a-z0-9]+)') || ['', ''])[1]
42 | if (tmpCuhash) {
43 | cuhash = tmpCuhash
44 | }
45 |
46 | lastCheck = Date.now()
47 | }
48 |
49 | if (cuhash) {
50 | return urljoin(baseUrl, `download?id=${reseedInfo.torrent_id}&cuhash=${cuhash}`)
51 | }
52 |
53 | throw new Error(`未提取到该ID的种子链接 (站点 ${site.site} ID ${reseedInfo.sid})`)
54 | }
--------------------------------------------------------------------------------
/src/plugins/sites/hdsky.ts:
--------------------------------------------------------------------------------
1 | // HDSKY站点下载链接构造逻辑
2 |
3 | import axios from 'axios'
4 | import urljoin from "url-join";
5 |
6 | import {EnableSite} from "@/interfaces/IYUU/Site";
7 | import {TorrentInfo} from "@/interfaces/IYUU/Forms";
8 |
9 | import Cookies from "@/plugins/cookies";
10 | import iyuuEndpoint from '@/plugins/iyuu'
11 | import {CookiesExpiredError, TorrentNotExistError} from "@/plugins/sites/default";
12 |
13 | export default async function (reseedInfo: TorrentInfo, site: EnableSite) {
14 | // 设置站点Cookies
15 | await Cookies.setCookiesBySite(site)
16 | const baseUrl = (site.is_https > 0 ? 'https://' : 'http://') + site.base_url
17 |
18 | // 构造对应种子详情页链接
19 | const detailsUrl = urljoin(baseUrl, `/details.php?id=${reseedInfo.torrent_id}`)
20 | const detailsPageRep = await axios.get(detailsUrl)
21 | const detailsPage = detailsPageRep.data
22 | if (detailsPage.search('该页面必须在登录后才能访问') > -1) {
23 | throw new CookiesExpiredError('站点 Cookies 已过期,请更新后重新辅种!')
24 | }
25 |
26 | // 直接使用正则提取
27 | let path = (detailsPage.match(/action=['"]([^"]+?download\.php\?[^"]+?)['"]/) || ['', ''])[1]
28 | if (path != '') {
29 | return path.replace(/&/ig,'&') // 由于下载链接中直接带有域名,跳过
30 | }
31 |
32 | // 未提取到,则当作该种子不存在,提交IYUU并抛出异常
33 | await iyuuEndpoint.apiNotify({
34 | site: site.site,
35 | sid: site.id,
36 | torrent_id: reseedInfo.sid,
37 | error: '404'
38 | })
39 | throw new TorrentNotExistError(`没有该ID的种子 (站点 ${site.site} ID ${reseedInfo.sid})`)
40 | }
--------------------------------------------------------------------------------
/src/plugins/uuid.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import UUID from "vue-uuid";
3 |
4 | // @ts-ignore
5 | Vue.use(UUID);
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter, { RouteConfig } from 'vue-router'
3 | import { Notification } from 'element-ui'
4 |
5 | import {IYUUStore} from '@/store'
6 |
7 | Vue.use(VueRouter)
8 |
9 | const routes: Array = [
10 | // 用户登录部分
11 | {
12 | path: '/login',
13 | name: 'Login',
14 | component: () => import('@/views/Login.vue')
15 | },
16 |
17 | // 概览部分
18 | {
19 | path: '/',
20 | component: () => import('@/views/Layer.vue'),
21 | meta: {
22 | requiresLogin: true
23 | },
24 | children: [
25 | // 概览部分
26 | {
27 | path: '',
28 | name: 'Home',
29 | component: () => import('@/views/Home.vue'),
30 | meta: { content: '概览' }
31 | },
32 | {
33 | path: 'mission',
34 | name: 'Mission',
35 | component: () => import('@/views/Mission.vue'),
36 | meta: { content: '启动任务' }
37 | },
38 |
39 | // 软件设置部分
40 | {
41 | path: 'setting/site',
42 | name: 'Setting/Site',
43 | component: () => import('@/views/Setting/Site.vue'),
44 | meta: { content: '辅种站点设置' }
45 | },
46 | {
47 | path: 'setting/btclient',
48 | name: 'Setting/BtClient',
49 | component: () => import('@/views/Setting/BtClient.vue'),
50 | meta: { content: '下载器设置' }
51 | },
52 | {
53 | path: 'setting/other',
54 | name: 'Setting/Other',
55 | component: () => import('@/views/Setting/Other.vue'),
56 | meta: { content: '其他设置' }
57 | },
58 | {
59 | path: 'setting/backup',
60 | name: 'Setting/Backup',
61 | component: () => import('@/views/Setting/Backup.vue'),
62 | meta: { content: '参数备份与恢复' }
63 | },
64 |
65 | // 鸣谢部分
66 | {
67 | path: 'gratitude/declare',
68 | name: 'Declare',
69 | component: () => import('@/views/Gratitude/Declare.vue'),
70 | meta: { content: '项目说明' }
71 | },
72 | {
73 | path: 'gratitude/donate',
74 | name: 'Donate',
75 | component: () => import('@/views/Gratitude/Donate.vue'),
76 | meta: { content: '捐赠支持' }
77 | }
78 | ]
79 | },
80 |
81 | // Miss路由时
82 | {
83 | path: '*',
84 | redirect: '/'
85 | }
86 | ]
87 |
88 | const router = new VueRouter({
89 | routes
90 | })
91 |
92 | router.beforeEach((to, from, next) => {
93 | // @ts-ignore
94 | if (to.matched.some(record => record.meta.requiresLogin) && !IYUUStore.token) {
95 | Notification.error('未登录,返回登录窗口')
96 | next('/login')
97 | } else {
98 | next()
99 | }
100 | })
101 |
102 | export default router
103 |
--------------------------------------------------------------------------------
/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue'
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex, {Store} from 'vuex'
3 | // Note: you shouldn't need to import store modules here.
4 | import {initializeStores, modules} from '@/store/store-accessor'
5 |
6 | // @ts-ignore
7 | import {createPersistedState} from 'vuex-electron'
8 |
9 | Vue.use(Vuex)
10 |
11 |
12 | // Initialize the modules using a Vuex plugin that runs when the root store is
13 | // first initialized. This is vital to using static modules because the
14 | // modules don't know the root store when they are loaded. Initializing them
15 | // when the root store is created allows them to be hooked up properly.
16 | const initializer = (store: Store) => initializeStores(store)
17 |
18 | export const plugins = [
19 | initializer,
20 | createPersistedState({
21 | ignoredPaths: [
22 | "Mission.logs", "Mission.currentMission"
23 | ],
24 | ignoredCommits: [
25 | 'Mission/appendLog',
26 | 'Mission/updateCurrentMissionState'
27 | ]
28 | })
29 | ]
30 |
31 | export * from '@/store/store-accessor' // re-export the modules
32 |
33 | // Export the root store. You can add mutations & actions here as well.
34 | // Note that this is a standard Vuex store, not a vuex-module-decorator one.
35 | // (Perhaps could be, but I put everything in modules)
36 | export default new Store({
37 | plugins, // important!
38 | modules, // important!
39 | state: {},
40 | mutations: {},
41 | actions: {},
42 | strict: process.env.NODE_ENV !== 'production'
43 | })
--------------------------------------------------------------------------------
/src/store/modules/IYUU.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import _ from 'lodash'
3 | import {Notification} from 'element-ui'
4 | import {Module, VuexModule, Mutation, MutationAction} from 'vuex-module-decorators'
5 |
6 | import {Site, EnableSite} from "@/interfaces/IYUU/Site";
7 | import {TorrentClientConfig} from "@/interfaces/BtClient/AbstractClient";
8 | import {MissionStore} from '@/store/store-accessor' // circular import; OK though
9 |
10 | @Module({namespaced: true, name: 'IYUU'})
11 | export default class IYUU extends VuexModule {
12 | // 相当于原来的state
13 | token: string | null = null // 用户Token
14 | sites: Site[] = [] // 此处缓存可以使用sites列表(来自服务器)
15 | enable_sites: EnableSite[] = [] // 此处缓存用户已经添加了的站点信息
16 | enable_clients: TorrentClientConfig[] = [] // 此处缓存用户已经添加了的客户端信息
17 |
18 | apiPreInfoHash: number = 2000 // 单次请求最大infohash数
19 | maxRetry: number = 2 // 下载推送尝试次数
20 |
21 | weChatNotify = {
22 | reseed: {
23 | title: 'IYUU GUI自动辅种-统计报表',
24 | descr: `### GUI 版本号: {version}
25 | ** 任务id: {mission_id}**
26 | ** 下载服务器: {clients_count} 个** [本次任务配置启用的下载器数量]
27 | ** 辅种站点: {sites_count} 个** [本次任务配置启用的辅种站点数量]
28 | ** 总做种: {hashs_count} 个** [客户端做种的hash总数]
29 | ** 返回数据: {reseed_count} 个** [服务器返回的可辅种数据]
30 | ** 成功: {reseed_success} 个** [会把hash加入辅种缓存]
31 | ** 失败: {reseed_fail} 个** [种子下载失败或网络超时等原因引起]
32 | ** 跳过: {reseed_pass} 个** [因为下载器辅种缓存等原因跳过]
33 |
34 | ------
35 |
36 | ### 详细日志
37 |
38 | {full_log}
39 | `
40 | },
41 | transfer: {
42 | title: '',
43 | descr: ''
44 | }
45 | }
46 |
47 | get signedSites() {
48 | return this.enable_sites
49 | }
50 |
51 | get signedBtClient() {
52 | return this.enable_clients
53 | }
54 |
55 | get unsignedSites() { // 获取用户未添加站点列表
56 | return _.filter(this.sites, (site: Site) => {
57 | return _.findIndex(this.enable_sites, {site: site.site}) === -1
58 | })
59 | }
60 |
61 | get siteInfo() { // 通过站点名获取来自服务器的站点信息
62 | return (siteName: string) => {
63 | return this.sites.find((s: Site) => s.site === siteName)
64 | }
65 | }
66 |
67 | get enableSiteInfo() { // 通过站点名来获取用户添加的站点信息
68 | return (siteName: string) => {
69 | return this.enable_sites.find((s: EnableSite) => s.site === siteName)
70 | }
71 | }
72 |
73 | get siteDownloadLinkTpl() {
74 | return (siteName: string) => {
75 | const siteInfo = this.siteInfo(siteName) as Site
76 |
77 | let linkTpl = ''
78 | if (siteInfo) {
79 | linkTpl += siteInfo.is_https > 0 ? 'https://' : 'http://'
80 | linkTpl += siteInfo.base_url + '/'
81 | linkTpl += siteInfo.download_page
82 | }
83 |
84 | return linkTpl
85 | }
86 | }
87 |
88 | @MutationAction({mutate: ['token']})
89 | async setToken(token: string) {
90 | return {token: token}
91 | }
92 |
93 | @MutationAction({mutate: ['token', 'enable_sites', 'enable_clients']})
94 | async cleanToken() {
95 | return {
96 | token: '',
97 | enable_sites: [],
98 | enable_clients: []
99 | }
100 | }
101 |
102 | @MutationAction({mutate: ['sites']})
103 | async updateSites(sites: Site[]) {
104 | return {sites: sites}
105 | }
106 |
107 | @MutationAction({mutate: ['sites']})
108 | async cleanSites() {
109 | return {sites: []}
110 | }
111 |
112 | @Mutation
113 | addEnableSite(site: EnableSite) {
114 | this.enable_sites.push(site)
115 | Notification.success(`新增站点 ${site.site} 信息成功`)
116 | }
117 |
118 | @Mutation
119 | editEnableSite(site: EnableSite) {
120 | const siteIndex = this.enable_sites.findIndex((s: { site: string }) => s.site === site.site)
121 | this.enable_sites[siteIndex] = site
122 | Notification.success(`更新站点 ${site.site} 信息成功`)
123 | }
124 |
125 | @Mutation
126 | removeEnableSite(siteId: number) {
127 | const siteInfo: EnableSite = this.enable_sites[siteId]
128 | this.enable_sites.splice(siteId, 1)
129 | Notification.success('成功删除站点 ' + siteInfo.site)
130 | }
131 |
132 | @MutationAction({mutate: ['enable_sites']})
133 | async cleanEnableSites() {
134 | return {enable_sites: []}
135 | }
136 |
137 | @Mutation
138 | addEnableClient(client: TorrentClientConfig) {
139 | this.enable_clients.push(client)
140 | Notification.success(`新增下载服务器 ${client.name}(${client.type}) 信息成功`)
141 | }
142 |
143 | @Mutation
144 | editEnableClient(client: TorrentClientConfig) {
145 | const clientIndex = this.enable_clients.findIndex((c) => c.uuid === client.uuid)
146 | this.enable_clients[clientIndex] = client
147 | Notification.success(`更新下载服务器 ${client.name}(${client.type}) 信息成功`)
148 | }
149 |
150 | @Mutation
151 | removeEnableClient(clientId: number) {
152 | const clientInfo: TorrentClientConfig = this.enable_clients[clientId]
153 | this.enable_clients.splice(clientId, 1)
154 | MissionStore.cleanReseededByClientId(clientInfo.uuid)
155 | Notification.success(`成功删除下载服务器 ${clientInfo.name}(${clientInfo.type})`)
156 | }
157 |
158 | @MutationAction({mutate: ['enable_clients']})
159 | async cleanEnableClients() {
160 | await MissionStore.cleanReseeded()
161 | return {enable_clients: []}
162 | }
163 |
164 | @Mutation
165 | updateNormalConfig(config: {
166 | apiPreInfoHash: number,
167 | maxRetry: number
168 | }) {
169 | this.apiPreInfoHash = config.apiPreInfoHash
170 | this.maxRetry = config.maxRetry
171 | }
172 |
173 | @Mutation
174 | restoreFromConfig(config: {
175 | token: string
176 | sites: EnableSite[]
177 | clients: TorrentClientConfig[]
178 |
179 | apiPreInfoHash: number
180 | maxRetry: number
181 |
182 | weChatNotify: any
183 | }) {
184 | this.token = config.token
185 | this.enable_sites = config.sites
186 | this.enable_clients = config.clients
187 | this.apiPreInfoHash = config.apiPreInfoHash
188 | this.maxRetry = config.maxRetry
189 | this.weChatNotify = config.weChatNotify
190 | }
191 |
192 | @Mutation
193 | updateWechatNotify(config: {
194 | part: 'reseed' | 'transfer',
195 | title: string,
196 | descr: string
197 | }) {
198 | this.weChatNotify[config.part] = {
199 | title: config.title,
200 | descr: config.descr
201 | }
202 | }
203 | }
--------------------------------------------------------------------------------
/src/store/modules/Mission.ts:
--------------------------------------------------------------------------------
1 | import {Module, Mutation, MutationAction, VuexModule} from "vuex-module-decorators";
2 | import {LogInfo} from "@/interfaces/store";
3 |
4 | @Module({namespaced: true, name: 'Mission'})
5 | export default class Mission extends VuexModule {
6 | logs: { [logId: string]: LogInfo[] } = {} // 不需要持久化
7 | reseeded: { [clientId: string]: string[] } = {}
8 |
9 | currentMission = { // 不需要持久化
10 | logId: '',
11 | processing: false
12 | }
13 |
14 | get logByLogId(): (logId: string) => LogInfo[] {
15 | return (logId: string) => {
16 | return logId in this.logs ? this.logs[logId] : []
17 | }
18 | }
19 |
20 | get reseededByClientId(): (clientId: string) => string[] {
21 | return (clientId: string) => {
22 | return clientId in this.reseeded ? this.reseeded[clientId] : []
23 | }
24 | }
25 |
26 | @Mutation
27 | updateCurrentMissionState(mission: { processing: boolean; logId: string }) {
28 | this.currentMission = mission
29 | }
30 |
31 | @Mutation
32 | appendLog(logInfo: {
33 | logId: string,
34 | logMessage: string
35 | }) {
36 | if (!(logInfo.logId in this.logs)) {
37 | this.logs[logInfo.logId] = []
38 | }
39 |
40 | this.currentMission.logId = logInfo.logId
41 | this.logs[logInfo.logId].push({
42 | timestamp: Date.now(),
43 | message: logInfo.logMessage
44 | })
45 | }
46 |
47 | @Mutation
48 | appendReseeded(hashInfo: {
49 | clientId: string,
50 | infoHash: string
51 | }) {
52 | if (!(hashInfo.clientId in this.reseeded)) {
53 | this.reseeded[hashInfo.clientId] = []
54 | }
55 |
56 | this.reseeded[hashInfo.clientId].push(hashInfo.infoHash)
57 | }
58 |
59 | @Mutation
60 | cleanReseededByClientId(clientId: string) {
61 | if (clientId in this.reseeded) {
62 | this.reseeded[clientId] = []
63 | }
64 | }
65 |
66 | @MutationAction({mutate: ['reseeded']})
67 | async cleanReseeded() {
68 | return {reseeded: []}
69 | }
70 |
71 | @Mutation
72 | restoreFromConfig(config: {
73 | reseeded: any
74 | }) {
75 | this.reseeded = config.reseeded
76 | }
77 | }
--------------------------------------------------------------------------------
/src/store/modules/Status.ts:
--------------------------------------------------------------------------------
1 | import {Module, Mutation, MutationAction, VuexModule} from "vuex-module-decorators";
2 |
3 | @Module({namespaced: true, name: 'Status'})
4 | export default class Status extends VuexModule {
5 | startAppCount: number = 0
6 | startMissionCount: number = 0
7 | reseedTorrentCount: number = 0
8 |
9 | @Mutation
10 | appStart() {
11 | this.startAppCount++
12 | }
13 |
14 | @MutationAction({mutate:['startAppCount']})
15 | async cleanAppStart() {
16 | return {startAppCount: 0}
17 | }
18 |
19 | @Mutation
20 | missionStart() {
21 | this.startMissionCount++
22 | }
23 |
24 | @MutationAction({mutate:['startMissionCount']})
25 | async cleanMissionStart() {
26 | return {startMissionCount: 0}
27 | }
28 |
29 | @Mutation
30 | torrentReseed() {
31 | this.reseedTorrentCount++
32 | }
33 |
34 | @MutationAction({mutate:['reseedTorrentCount']})
35 | async cleanTorrentReseed() {
36 | return {reseedTorrentCount: 0}
37 | }
38 |
39 | @Mutation
40 | restoreFromConfig(config: {
41 | startAppCount: number
42 | startMissionCount: number
43 | reseedTorrentCount: number
44 | }) {
45 | this.startAppCount = config.startAppCount
46 | this.startMissionCount = config.startMissionCount
47 | this.reseedTorrentCount = config.reseedTorrentCount
48 | }
49 | }
--------------------------------------------------------------------------------
/src/store/store-accessor.ts:
--------------------------------------------------------------------------------
1 | // This is the "store accessor":
2 | // It initializes all the modules using a Vuex plugin (see store/index.ts)
3 | // In here you import all your modules, call getModule on them to turn them
4 | // into the actual stores, and then re-export them.
5 |
6 | import {Store} from 'vuex'
7 | import {getModule} from 'vuex-module-decorators'
8 |
9 | import IYUUModule from "@/store/modules/IYUU";
10 | import MissionModule from "@/store/modules/Mission";
11 | import StatusModule from "@/store/modules/Status";
12 |
13 | // Each store is the singleton instance of its module class
14 | // Use these -- they have methods for state/getters/mutations/actions
15 | // (result from getModule(...))
16 | export let IYUUStore: IYUUModule
17 | export let MissionStore: MissionModule
18 | export let StatusStore: StatusModule
19 |
20 | // initializer plugin: sets up state/getters/mutations/actions for each store
21 | export function initializeStores(store: Store): void {
22 | IYUUStore = getModule(IYUUModule, store)
23 | MissionStore = getModule(MissionModule, store)
24 | StatusStore = getModule(StatusModule, store)
25 | }
26 |
27 | // for use in 'modules' store init (see store/index.ts), so each module
28 | // appears as an element of the root store's state.
29 | // (This is required!)
30 | export const modules = {
31 | 'IYUU': IYUUModule,
32 | 'Mission': MissionModule,
33 | 'Status': StatusModule
34 | }
35 |
--------------------------------------------------------------------------------
/src/views/Gratitude/Declare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 问题反馈
6 |
9 |
10 |
11 | 受限于开发者个人水平,在实际开发过程中可能存在各种问题未被发现。
12 | 如果你在使用过程发现问题,请考虑通过以下途径进行反馈。
13 |
14 |
15 | -
16 | IYUU-GUI 官方仓库(含源码、Issue、Wiki):
17 |
18 | https://github.com/Rhilip/IYUU-GUI
19 |
20 |
21 | -
22 | IYUUAutoReseed 源码仓库:
23 |
24 | gitee源码仓库
25 |
26 |
27 |
28 | GitHub源码仓库
29 |
30 |
31 | -
32 | IYUUAutoReseed 官方教程:
33 |
34 | https://gitee.com/ledc/IYUUAutoReseed/tree/master/wiki
35 |
36 |
37 | -
38 | IYUUAutoReseed 官方问答社区:
39 |
40 | http://wenda.iyuu.cn
41 |
42 |
43 | -
44 | 【IYUU自动辅种交流】QQ群:
45 | 859882209 (一群,已满)
46 |
47 | 931954050 (二群)
48 |
49 |
50 |
51 |
52 |
53 |
54 | 特别鸣谢
55 |
58 |
59 |
60 |
61 | 在项目的开发和测试中,他们给予了很多帮助和支持,在此表示感谢。
62 | 列表中未能一一列出所有给予帮助的同学,也对他们表示感谢,如有遗漏敬请谅解。
63 |
64 |
65 |
69 |
73 |
77 |
78 |
79 |
80 |
81 | 项目参考和引用
82 |
85 |
86 |
87 |
88 | 首先感谢 @ledccn 开发的辅种软件 IYUUAutoReseed ,并提供相应 API 使得二次开发更为方便。
89 | 在可视化开发过程中, @ledccn 也给予本人(@Rhilip)很多帮助。
90 |
91 | 此外,IYUU GUI 的诞生也是建立在这些项目基础之上,在此一并感谢所有项目的参与人员,感谢他们的付出!
92 |
93 |
94 |
95 |
98 |
99 |
100 | {{ scope.row.name }}
101 |
102 |
103 |
104 |
107 |
109 |
110 |
111 | {{ scope.row.link }}
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
240 |
241 |
251 |
--------------------------------------------------------------------------------
/src/views/Gratitude/Donate.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 感谢你的关注和支持
6 |
7 |
8 | 本项目由相关作者在业余时间完成,如果你喜欢本项目,可以通过捐助来支持作者继续开发。
9 |
10 |
11 |
12 |
13 | 支持GUI开发作者 @Rhilip, 以进行本项目的后续开发维护
14 |
15 |
16 |
17 |
18 |
19 | 支付宝
20 |
21 |
22 |
23 |
24 |
25 | 微信支付
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 支持IYUU开发作者 @ledccn, 用于服务器维护及续期,增加服务的延续性
35 |
36 |
37 |
38 |
39 |
40 | 微信赞赏码
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
59 |
60 |
65 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 公告栏
24 |
25 |
26 |
27 |
32 |
35 |
36 | 以上仅为 IYUU-GUI 的更新公告,如需查询 IYUUAutoreseed 官方更新,请点击
37 |
38 | IYUUAutoReseed / wiki / 公告栏.md
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
65 |
66 |
69 |
--------------------------------------------------------------------------------
/src/views/Layer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 进入
37 |
38 |
39 | 重置
40 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | API service By IYUU
53 |
54 |
55 |
56 | GUI designed By Rhilip
57 |
58 |
59 |
60 |
61 |
62 |
63 |
193 |
194 |
203 |
204 |
--------------------------------------------------------------------------------
/src/views/Mission.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 启动任务
6 |
7 |
8 |
9 |
10 |
13 | 辅种任务
14 |
15 |
16 |
17 | 为添加的下载器和站点进行批量辅种任务,你可以在点击后的设任务置对话框里进一步设置启用站点和下载服务器。
18 |
19 |
20 |
34 |
35 |
36 |
37 |
38 |
任务日志
39 |
40 |
41 |
42 |
43 |
44 | {{ missionState.processing ? '正在运行' : '运行完成' }},运行id: {{ logId }}
45 |
46 |
47 |
48 |
49 |
50 |
{{ formatLogs(logs) }}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
186 |
187 |
194 |
--------------------------------------------------------------------------------
/src/views/Setting/Backup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 缓存清空
6 |
7 |
8 | 在某些情况下,你可能需要清空软件的部分配置项缓存。
9 | 例如:当IYUU添加了新站点支持时,你可以通过清除 站点缓存,来重新拉取最新的站点信息。
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
24 | 点击清理
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 备份与还原
33 |
34 |
35 |
36 |
37 |
38 |
39 |
41 |
42 | 备份参数
43 |
44 |
45 |
46 | 备份 IYUU GUI 所存储的各类参数信息,方便恢复或转移至其他机器使用。
47 | 注意:IYUU GUI参数备份文件与PHP版的IYUUAutoReseed互不通用。
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 | 还原参数
61 |
62 |
63 |
64 |
65 | 从已有 IYUU GUI 参数备份文件从还原各类参数信息(站点、下载器等信息)
66 |
67 |
68 |
69 |
70 |
71 |
72 |
78 |
79 |
80 | 导入配置
81 |
82 |
83 |
84 |
85 | 如果你使用过PHP版的IYUUAutoReseed,可以传入使用的 config.json 或 config.php 文件。
86 | 特别注意:程序会自动覆盖你已配置当前的站点信息和下载器信息,请谨慎操作。
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
183 |
184 |
187 |
--------------------------------------------------------------------------------
/src/views/Setting/BtClient.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
11 |
12 |
13 |
14 |
15 |
19 |
22 |
27 |
31 |
32 | {{ $store.getters['Mission/reseededByClientId'](scope.row.uuid).length }}
33 |
35 |
36 |
37 |
38 |
39 |
43 | 添加新下载器
44 |
45 |
46 |
47 |
50 | 编辑
51 |
52 |
56 | 删除
57 |
58 |
59 |
60 |
61 |
62 | 你总共添加了 {{ $store.getters['IYUU/signedBtClient'].length }} 个下载器
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
114 |
115 |
123 |
--------------------------------------------------------------------------------
/src/views/Setting/Other.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 常规设置
8 |
9 |
10 |
11 |
12 |
13 | 微信推送模板
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 | 调试工具
23 |
24 |
25 |
26 | 打开调试工具(如果你确定是高手,或者想要观察软件是怎么工作的)
27 | 那么你可以点击此处打开Developer Tools面板
28 |
29 |
30 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
73 |
74 |
77 |
--------------------------------------------------------------------------------
/src/views/Setting/Site.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
14 |
15 |
16 | 站点下载链接构造模板
17 |
18 |
19 |
20 | {{ linkTpl(scope.row.link) }}
21 |
22 |
23 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
39 | 添加新站点
40 |
41 |
42 |
43 |
46 | 编辑
47 |
48 |
52 | 删除
53 |
54 |
55 |
56 |
57 |
58 | 目前 IYUU 共支持 {{ $store.state.IYUU.sites.length }} 个站点,
59 | 你已经启用了 {{ $store.state.IYUU.enable_sites.length }} 个站点
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
69 |
70 |
71 |
72 |
146 |
147 |
150 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "allowJs": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "sourceMap": true,
14 | "baseUrl": ".",
15 | "types": [
16 | "webpack-env"
17 | ],
18 | "paths": {
19 | "@/*": [
20 | "src/*"
21 | ]
22 | },
23 | "lib": [
24 | "esnext",
25 | "dom",
26 | "dom.iterable",
27 | "scripthost"
28 | ]
29 | },
30 | "include": [
31 | "src/**/*.ts",
32 | "src/**/*.tsx",
33 | "src/**/*.vue",
34 | "tests/**/*.ts",
35 | "tests/**/*.tsx"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pluginOptions: {
3 | electronBuilder: {
4 |
5 | // refs: https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#configuring-electron-builder
6 | // refs: https://www.electron.build/configuration/configuration
7 | builderOptions: {
8 | appId: "info.rhilip.iyuu",
9 | productName: 'IYUU GUI',
10 | copyright: 'Copyright © 2020-2030 Rhilip',
11 |
12 | "nsis": {
13 | "deleteAppDataOnUninstall": true
14 | },
15 | publish: ['github']
16 | },
17 |
18 | // refs: https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/683
19 | nodeIntegration: true,
20 |
21 | // refs: https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#typescript-options
22 | disableMainProcessTypescript: false, // Manually disable typescript plugin for main process. Enable if you want to use regular js for the main process (src/background.js by default).
23 | mainProcessTypeChecking: false, // Manually enable type checking during webpck bundling for background file.
24 |
25 | // If you are using Yarn Workspaces, you may have multiple node_modules folders
26 | // List them all here so that VCP Electron Builder can find them
27 | nodeModulesPath: ['../../node_modules', './node_modules']
28 | }
29 | },
30 |
31 | // refs: https://github.com/championswimmer/vuex-module-decorators#using-with-target-es5
32 | transpileDependencies: ['vuex-module-decorators']
33 | }
--------------------------------------------------------------------------------