├── .babelrc.js ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── renovate.json └── workflows │ ├── ci.yaml │ └── deploy.yaml ├── .gitignore ├── .prettierrc.js ├── AV-Info-Saver.code-workspace ├── License ├── config ├── empty.cjs ├── metadata.cjs ├── webpack.config.base.cjs ├── webpack.config.dev.cjs └── webpack.config.production.cjs ├── dist └── index.prod.user.js ├── package-lock.json ├── package.json ├── readme.md ├── readme ├── assets │ ├── finder-series.png │ ├── finder-single.png │ ├── hero.png │ ├── icon.png │ ├── screen recording.mp4 │ └── screen-recording-trimmed.mp4 ├── readme.cn.md └── readme.md ├── src ├── create-btn.ts ├── index.ts ├── makers │ ├── 01-uncensored │ │ ├── 1pondo.ts │ │ ├── caribbean.ts │ │ └── tokyo-hot.ts │ ├── 02-censored │ │ ├── ca-group.ts │ │ ├── mousouzoku.ts │ │ ├── prestige.ts │ │ └── sod.ts │ ├── 03-western │ │ ├── brazzers.ts │ │ └── naughty-america.ts │ └── 04-amateur │ │ └── mgstage.ts ├── style │ └── main.less ├── typings.d.ts ├── utils.ts └── utils │ ├── check-digit.ts │ ├── check-indicator.ts │ ├── codify.ts │ ├── datify.ts │ ├── final.ts │ ├── first-letter-uppercase.ts │ ├── get-info.ts │ ├── refine-actress.ts │ └── refine-title.ts └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | 4 | const presets = ['@babel/preset-env', '@babel/preset-typescript'] 5 | 6 | return { 7 | presets, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | not IE 11 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | trim_trailing_whitespace=true 5 | insert_final_newline=true 6 | indent_style=space 7 | indent_size=4 8 | 9 | [{.babelrc,.stylelintrc,.eslintrc,jest.config,*.bowerrc,*.jsb3,*.jsb2,*.json,*.yaml,*.yml}] 10 | indent_style=space 11 | indent_size=2 12 | 13 | [{*.js,*.vue,*.ts,*.cjs}] 14 | indent_style=space 15 | indent_size=2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/index.prod.user.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = { 4 | env: { 5 | browser: true, 6 | es2021: true, 7 | }, 8 | extends: ['eslint:recommended'], 9 | overrides: [], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 'latest', 13 | sourceType: 'module', 14 | }, 15 | plugins: ['@typescript-eslint'], 16 | globals: { 17 | require: 'readonly', 18 | module: 'readonly', 19 | __dirname: 'readonly', 20 | }, 21 | rules: { 22 | 'no-empty': 'off', 23 | 'no-unused-vars': 'warn', 24 | 'no-console': 'warn', 25 | 'no-debugger': 'warn', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "schedule:monthly", 4 | ":masterIssue", 5 | "config:base" 6 | ], 7 | "prHourlyLimit": 0, 8 | "lockFileMaintenance": { 9 | "extends": [ 10 | "schedule:weekly" 11 | ], 12 | "automerge": true, 13 | "enabled": true 14 | }, 15 | "postUpdateOptions": [ 16 | "npmDedupe" 17 | ], 18 | "separateMajorMinor": false, 19 | "updateNotScheduled": false, 20 | "rangeStrategy": "pin" 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - gh-pages 7 | - "renovate/**" 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup node 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '16' 19 | cache: 'npm' 20 | cache-dependency-path: '**/package-lock.json' 21 | 22 | - run: npm ci 23 | - run: npm run build 24 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '16' 18 | cache: 'npm' 19 | cache-dependency-path: '**/package-lock.json' 20 | 21 | - run: npm ci 22 | - run: npm run build 23 | 24 | - run: | 25 | sudo apt-get install tree -y 26 | tree -H '.' -L 1 --noreport --charset utf-8 ./dist | tee dist/index.html 27 | 28 | - name: Deploy 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./dist 33 | commit_message: deploy ${{ github.ref }} 34 | force_orphan: true 35 | user_name: github-actions[bot] 36 | user_email: github-actions[bot]@users.noreply.github.com 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .vscode 4 | index.debug.user.js 5 | index.dev.user.js 6 | package-lock.json 7 | .DS_Store 8 | .code-workspace 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | printWidth: 120, // 一行最多 100 字符 4 | semi: false, // 尾部分号 5 | singleQuote: true, // 单引号 6 | } 7 | -------------------------------------------------------------------------------- /AV-Info-Saver.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "cSpell.words": [ 9 | "Brazzers", 10 | "caribbeancom", 11 | "datify", 12 | "honnaka", 13 | "httpbin", 14 | "ideapocket", 15 | "mgstage", 16 | "moodyz", 17 | "mousouzoku", 18 | "Pondo" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2020-2022 Trim21 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /config/empty.cjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/config/empty.cjs -------------------------------------------------------------------------------- /config/metadata.cjs: -------------------------------------------------------------------------------- 1 | const { author, dependencies, repository, version } = require('../package.json') 2 | 3 | module.exports = { 4 | name: 'AV Info Saver - AV 作品信息一键保存工具', 5 | namespace: '', 6 | version: version, 7 | author: author, 8 | source: repository.url, 9 | license: 'MIT', 10 | match: [ 11 | /* ========== 无码 ========== */ 12 | 'https://*.1pondo.tv/movies/*', 13 | 'https://*.caribbeancom.com/moviepages/*', 14 | 'https://*.tokyo-hot.com/product/*', 15 | 16 | /* ========== 有码 ========== */ 17 | // CA 集团厂商 18 | 'https://*.attackers.net/works/detail/*', 19 | 'https://*.ideapocket.com/works/detail/*', 20 | 'https://*.madonna-av.com/works/detail/*', 21 | 'https://*.mousouzoku-av.com/works/detail/*', 22 | 'https://s1s1s1.com/*', 23 | 'https://moodyz.com/*', 24 | 'https://honnaka.jp/*', 25 | 'https://premium-beauty.com/*', 26 | 'https://mvg.jp/*', 27 | // 其他 28 | 'https://*.prestige-av.com/goods/*', 29 | 'https://*.sod.co.jp/prime/videos/*', 30 | 31 | /* ========== 欧美 ========== */ 32 | 'https://*.brazzers.com/video/*', 33 | 'https://*.naughtyamerica.com/scene/*', 34 | 35 | /* ========== 素人 ========== */ 36 | 'https://*.mgstage.com/product/product_detail/*', 37 | ], 38 | require: [ 39 | `https://cdn.jsdelivr.net/npm/jquery@${dependencies.jquery}/dist/jquery.min.js`, 40 | `https://cdn.jsdelivr.net/npm/axios@${dependencies.axios}/dist/axios.min.js`, 41 | `https://cdn.jsdelivr.net/npm/axios-userscript-adapter@${dependencies['axios-userscript-adapter']}/dist/axiosGmxhrAdapter.min.js`, 42 | ], 43 | grant: ['GM.xmlHttpRequest', 'GM.download'], 44 | connect: ['httpbin.org'], 45 | 'run-at': 'document-end', 46 | } 47 | -------------------------------------------------------------------------------- /config/webpack.config.base.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 4 | 5 | const webpackConfig = { 6 | resolve: { 7 | extensions: ['.js', '.ts'], 8 | alias: { 9 | '@': path.resolve(__dirname, '../src'), 10 | }, 11 | }, 12 | optimization: { 13 | minimize: false, 14 | moduleIds: 'named', 15 | }, 16 | entry: './src/index.ts', 17 | output: { 18 | path: path.resolve(__dirname, '../dist') 19 | }, 20 | target: 'web', 21 | externals: { 22 | jquery: '$', 23 | axios: 'axios', 24 | 'axios-userscript-adapter': 'axiosGmxhrAdapter' 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | use: { 30 | loader: 'babel-loader', 31 | }, 32 | test: /\.js$/, 33 | }, 34 | { 35 | test: /\.ts$/, 36 | loader: 'babel-loader', // use ts-loader if you like 37 | }, 38 | { 39 | test: /\.less$/, 40 | use: [ 41 | 'style-loader', 42 | 'css-loader', 43 | 'less-loader', 44 | ] 45 | }, 46 | { 47 | test: /\.css$/, 48 | use: [ 49 | 'style-loader', 50 | 'css-loader', 51 | ] 52 | } 53 | ] 54 | }, 55 | plugins: process.env.npm_config_report ? [new BundleAnalyzerPlugin()] : [], 56 | } 57 | 58 | module.exports = webpackConfig 59 | -------------------------------------------------------------------------------- /config/webpack.config.dev.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { merge } = require('webpack-merge') 3 | const LiveReloadPlugin = require('webpack-livereload-plugin') 4 | const UserScriptMetaDataPlugin = require('userscript-metadata-webpack-plugin') 5 | 6 | const metadata = require('./metadata.cjs') 7 | const webpackConfig = require('./webpack.config.base.cjs') 8 | 9 | metadata.require.push( 10 | 'file://' + path.resolve(__dirname, '../dist/index.debug.user.js') 11 | ) 12 | 13 | const cfg = merge(webpackConfig, { 14 | entry: { 15 | debug: webpackConfig.entry, 16 | dev: path.resolve(__dirname, './empty.cjs'), 17 | }, 18 | output: { 19 | filename: 'index.[name].user.js', 20 | path: path.resolve(__dirname, '../dist'), 21 | }, 22 | devtool: 'eval-source-map', 23 | watch: true, 24 | watchOptions: { 25 | ignored: /node_modules/, 26 | }, 27 | plugins: [ 28 | new LiveReloadPlugin({ 29 | delay: 500, 30 | }), 31 | new UserScriptMetaDataPlugin({ 32 | metadata, 33 | }), 34 | ], 35 | }) 36 | 37 | module.exports = cfg 38 | -------------------------------------------------------------------------------- /config/webpack.config.production.cjs: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const UserScriptMetaDataPlugin = require('userscript-metadata-webpack-plugin') 3 | 4 | const metadata = require('./metadata.cjs') 5 | const webpackConfig = require('./webpack.config.base.cjs') 6 | 7 | const cfg = merge(webpackConfig, { 8 | mode: 'production', 9 | output: { 10 | filename: 'index.prod.user.js', 11 | }, 12 | plugins: [ 13 | new UserScriptMetaDataPlugin({ 14 | metadata, 15 | }), 16 | ], 17 | }) 18 | 19 | module.exports = cfg 20 | -------------------------------------------------------------------------------- /dist/index.prod.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AV Info Saver - AV 作品信息一键保存工具 3 | // @namespace 4 | // @version 1.0.0 5 | // @author TROJAN 6 | // @source https://github.com/trojanyao/AV-Info-Saver 7 | // @license MIT 8 | // @match https://*.1pondo.tv/movies/* 9 | // @match https://*.caribbeancom.com/moviepages/* 10 | // @match https://*.tokyo-hot.com/product/* 11 | // @match https://*.attackers.net/works/detail/* 12 | // @match https://*.ideapocket.com/works/detail/* 13 | // @match https://*.madonna-av.com/works/detail/* 14 | // @match https://*.mousouzoku-av.com/works/detail/* 15 | // @match https://s1s1s1.com/* 16 | // @match https://moodyz.com/* 17 | // @match https://honnaka.jp/* 18 | // @match https://premium-beauty.com/* 19 | // @match https://mvg.jp/* 20 | // @match https://*.prestige-av.com/goods/* 21 | // @match https://*.sod.co.jp/prime/videos/* 22 | // @match https://*.brazzers.com/video/* 23 | // @match https://*.naughtyamerica.com/scene/* 24 | // @match https://*.mgstage.com/product/product_detail/* 25 | // @require https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js 26 | // @require https://cdn.jsdelivr.net/npm/axios@0.26.0/dist/axios.min.js 27 | // @require https://cdn.jsdelivr.net/npm/axios-userscript-adapter@0.1.11/dist/axiosGmxhrAdapter.min.js 28 | // @grant GM.xmlHttpRequest 29 | // @grant GM.download 30 | // @connect httpbin.org 31 | // @run-at document-end 32 | // ==/UserScript== 33 | 34 | 35 | /******/ (() => { // webpackBootstrap 36 | /******/ "use strict"; 37 | var __webpack_exports__ = {}; 38 | 39 | ;// CONCATENATED MODULE: ./src/create-btn.ts 40 | function createBtn() { 41 | /* ========== 界面 ========== */ 42 | // === 容器 === 43 | const wrapper = document.createElement('div'); 44 | wrapper.style.background = 'rgba(255, 255, 255, 0.60)'; 45 | wrapper.style.borderRadius = '16px'; 46 | wrapper.style.border = '1px solid #F1F1F1'; 47 | wrapper.style.padding = '16px'; 48 | wrapper.style.backdropFilter = 'blur(18px)'; 49 | wrapper.style.boxShadow = '0px 2px 24px 0px rgba(0, 0, 0, 0.03)'; 50 | wrapper.style.position = 'fixed'; 51 | wrapper.style.top = '120px'; 52 | wrapper.style.right = '16px'; 53 | wrapper.style.display = 'flex'; 54 | wrapper.style.alignItems = 'center'; 55 | wrapper.style.gap = '8px'; 56 | wrapper.style.zIndex = '99999'; 57 | document.querySelector('body').appendChild(wrapper); // === 下载按钮 === 58 | 59 | const a = document.createElement('a'); 60 | a.target = '_blank'; 61 | a.style.width = a.style.height = '48px'; 62 | a.style.background = 'linear-gradient(180deg, #F7F7F7 0%, #F0F0F0 100%)'; 63 | a.style.borderRadius = '50%'; 64 | a.style.boxShadow = '-1px -1px 2px 0px rgba(207, 207, 207, 0.25) inset, 1px 1px 2px 0px rgba(255, 255, 255, 0.30) inset'; 65 | a.style.display = 'flex'; 66 | a.style.justifyContent = a.style.alignItems = 'center'; // 按钮按下 67 | 68 | a.onmousedown = () => { 69 | a.style.background = 'linear-gradient(0, #F7F7F7 0%, #F0F0F0 100%)'; 70 | a.style.boxShadow = '-1px -1px 2px 0px rgba(219, 219, 219, 0.50) inset, 1px 1px 2px 0px rgba(229, 229, 229, 0.30) inset'; 71 | }; // 按钮释放 72 | 73 | 74 | a.onmouseup = () => { 75 | a.style.background = 'linear-gradient(180deg, #F7F7F7 0%, #F0F0F0 100%)'; 76 | a.style.boxShadow = '-1px -1px 2px 0px rgba(207, 207, 207, 0.25) inset, 1px 1px 2px 0px rgba(255, 255, 255, 0.30) inset'; 77 | }; 78 | 79 | wrapper.appendChild(a); // 图标 80 | 81 | const SVG_NS = 'http://www.w3.org/2000/svg'; 82 | const svgIcon = document.createElementNS(SVG_NS, 'svg'); 83 | svgIcon.setAttribute('width', '24px'); 84 | svgIcon.setAttribute('height', '24px'); 85 | svgIcon.setAttribute('viewBox', '0 0 24 24'); 86 | svgIcon.setAttribute('fill', 'none'); 87 | const path = document.createElementNS(SVG_NS, 'path'); 88 | path.setAttribute('d', 'M12 13.5L18 7.5H13.5V1.5H10.5V7.5H6L12 13.5ZM17.454 11.046L15.7725 12.7275L21.8685 15L12 18.6795L2.1315 15L8.2275 12.7275L6.546 11.046L0 13.5V19.5L12 24L24 19.5V13.5L17.454 11.046Z'); 89 | path.setAttribute('fill', '#58CB5C'); 90 | svgIcon.appendChild(path); 91 | a.appendChild(svgIcon); // === 标题 & 菜单 === 92 | 93 | const menuDiv = document.createElement('div'); 94 | menuDiv.style.display = 'flex'; 95 | menuDiv.style.flexDirection = 'column'; 96 | menuDiv.style.gap = '8px'; 97 | wrapper.appendChild(menuDiv); // 标题 98 | 99 | const titleDiv = document.createElement('div'); 100 | titleDiv.innerText = 'AV 作品信息一键保存工具'; 101 | titleDiv.style.color = '#282828'; 102 | titleDiv.style.fontSize = '16px'; 103 | titleDiv.style.fontFamily = '"PingFang SC", sans-serif'; 104 | titleDiv.style.fontWeight = '600'; 105 | titleDiv.style.lineHeight = '1'; 106 | menuDiv.appendChild(titleDiv); // 菜单容器 107 | 108 | const toggleDiv = document.createElement('div'); 109 | toggleDiv.style.display = 'flex'; 110 | toggleDiv.style.justifyContent = 'space-between'; 111 | toggleDiv.style.alignItems = 'center'; 112 | menuDiv.appendChild(toggleDiv); // 文本 113 | 114 | const toggleText = document.createElement('div'); 115 | toggleText.innerText = '打开作品页后自动保存'; 116 | toggleText.style.color = '#515151'; 117 | toggleText.style.fontSize = '14px'; 118 | toggleText.style.fontFamily = '"PingFang SC", sans-serif'; 119 | toggleText.style.fontWeight = '450'; 120 | toggleText.style.lineHeight = '1'; 121 | toggleDiv.appendChild(toggleText); // === 开关 === 122 | 123 | const autoSave = localStorage.getItem('av-info-saver-auto-save'); // 自动保存开关状态 124 | 125 | const toggleBtnBgNormal = 'linear-gradient(180deg, #FFF 0%, #F1F1F1 100%)'; // 开关默认的背景 126 | 127 | const toggleBtnBgActived = 'linear-gradient(90deg, #4CAF50 0%, #2DE035 100%)'; // 开关打开时的背景 128 | 129 | const toggleBtnShadowNormal = '5px 5px 13px 0px rgba(219, 219, 219, 0.60) inset, -5px -5px 10px 0px rgba(255, 255, 255, 0.90) inset, 5px -5px 10px 0px rgba(219, 219, 219, 0.20) inset, -5px 5px 10px 0px rgba(219, 219, 219, 0.20) inset'; // 开关默认的内阴影 130 | 131 | const toggleBtnShadowActived = '-5px -5px 10px 0px rgba(33, 201, 39, 0.90) inset, 5px -5px 10px 0px rgba(88, 203, 92, 0.20) inset, -5px 5px 10px 0px rgba(88, 203, 92, 0.20) inset'; // 开关打开时的内阴影 132 | 133 | const toggleBtn = document.createElement('div'); 134 | toggleBtn.style.width = '32px'; 135 | toggleBtn.style.height = '16px'; 136 | toggleBtn.style.background = autoSave ? toggleBtnBgActived : toggleBtnBgNormal; 137 | toggleBtn.style.borderRadius = '100px'; 138 | toggleBtn.style.border = '1px solid #58CB5C'; 139 | toggleBtn.style.boxSizing = 'border-box'; 140 | toggleBtn.style.boxShadow = autoSave ? toggleBtnShadowActived : toggleBtnShadowNormal; 141 | toggleBtn.style.position = 'relative'; 142 | toggleBtn.style.transition = 'all 200ms ease-out'; 143 | toggleBtn.style.cursor = 'pointer'; 144 | toggleDiv.appendChild(toggleBtn); // 开关按钮 145 | 146 | const btn = document.createElement('div'); 147 | const btnTransformDefault = 'translateX(0)'; // 开关按钮默认偏移量 148 | 149 | const btnTransformActived = 'translateX(16px)'; // 开关按钮打开时的偏移量 150 | 151 | btn.style.position = 'absolute'; 152 | btn.style.width = btn.style.height = '14px'; 153 | btn.style.background = '#FFF'; 154 | btn.style.borderRadius = '50%'; 155 | btn.style.boxShadow = '1px 1px 2px 0px rgba(255, 255, 255, 0.30) inset, -1px -1px 2px 0px rgba(219, 219, 219, 0.50) inset'; 156 | btn.style.display = 'flex'; 157 | btn.style.justifyContent = btn.style.alignItems = 'center'; 158 | btn.style.transform = autoSave ? btnTransformActived : btnTransformDefault; 159 | btn.style.transition = 'all 200ms ease-out'; 160 | toggleBtn.appendChild(btn); // 小圆点 161 | 162 | const dot = document.createElement('div'); 163 | dot.style.width = dot.style.height = '4px'; 164 | dot.style.background = 'radial-gradient(50% 50% at 50% 50%, #54CB59 45.31%, #20C927 100%)'; 165 | dot.style.borderRadius = '50%'; 166 | btn.appendChild(dot); // 开关切换 167 | 168 | toggleBtn.onclick = () => { 169 | const autoSave = localStorage.getItem('av-info-saver-auto-save'); 170 | 171 | if (!autoSave) { 172 | // === 打开开关 === 173 | toggleBtn.style.background = toggleBtnBgActived; 174 | toggleBtn.style.boxShadow = toggleBtnShadowActived; 175 | btn.style.transform = btnTransformActived; 176 | localStorage.setItem('av-info-saver-auto-save', 'yes'); 177 | } else { 178 | // === 关闭开关 === 179 | toggleBtn.style.background = toggleBtnBgNormal; 180 | toggleBtn.style.boxShadow = toggleBtnShadowNormal; 181 | btn.style.transform = btnTransformDefault; 182 | localStorage.removeItem('av-info-saver-auto-save'); 183 | } 184 | }; 185 | 186 | return a; 187 | } 188 | ;// CONCATENATED MODULE: external "axios" 189 | const external_axios_namespaceObject = axios; 190 | ;// CONCATENATED MODULE: external "axiosGmxhrAdapter" 191 | const external_axiosGmxhrAdapter_namespaceObject = axiosGmxhrAdapter; 192 | ;// CONCATENATED MODULE: ./src/utils.ts 193 | 194 | 195 | function get(url, config) { 196 | return axios.get(url, { 197 | adapter, 198 | ...config 199 | }); 200 | } 201 | function post(url, data, config) { 202 | return axios.post(url, data, { 203 | adapter, 204 | ...config 205 | }); 206 | } // 代码暂停执行 207 | 208 | function sleep(ms) { 209 | return new Promise(resolve => setTimeout(resolve, ms)); 210 | } 211 | ;// CONCATENATED MODULE: ./src/utils/get-info.ts 212 | const DEFAULT_KEY_LIST = { 213 | seriesKey: 'シリーズ', 214 | dateKey: '発売日', 215 | actressKey: '女優', 216 | codeKey: '品番' 217 | }; 218 | /** 219 | * 从详情列表中查找返回对应信息 220 | * @param {Array} infoList 作品信息列表 221 | * @param {string} selector 列表项下信息值对应的 CSS 选择器 222 | * @param {InfoKeyList} keyList 查询的信息键列表 223 | */ 224 | 225 | function getInfo(infoList, selector) { 226 | var _getValue, _getValue2, _getValue3, _getValue3$, _getValue4; 227 | 228 | let keyList = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : DEFAULT_KEY_LIST; 229 | let actressSelector = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : ''; 230 | keyList = { ...DEFAULT_KEY_LIST, 231 | ...keyList 232 | }; 233 | 234 | function getValue(key) { 235 | if (!key) { 236 | return; 237 | } 238 | 239 | const infoItem = infoList === null || infoList === void 0 ? void 0 : infoList.find(info => { 240 | var _innerText; 241 | 242 | return info === null || info === void 0 ? void 0 : (_innerText = info.innerText) === null || _innerText === void 0 ? void 0 : _innerText.includes(key); 243 | }); 244 | let tempSelector = key === keyList.actressKey ? actressSelector || selector : selector; 245 | return Array.from(infoItem === null || infoItem === void 0 ? void 0 : infoItem.querySelectorAll(tempSelector), item => (item === null || item === void 0 ? void 0 : item.innerText) || ''); 246 | } 247 | 248 | return { 249 | seriesName: (_getValue = getValue(keyList.seriesKey)) === null || _getValue === void 0 ? void 0 : _getValue[0], 250 | date: (_getValue2 = getValue(keyList.dateKey)) === null || _getValue2 === void 0 ? void 0 : _getValue2[0], 251 | actress: getValue(keyList.actressKey), 252 | code: (_getValue3 = getValue(keyList.codeKey)) === null || _getValue3 === void 0 ? void 0 : (_getValue3$ = _getValue3[0]) === null || _getValue3$ === void 0 ? void 0 : _getValue3$.replace('DVD', ''), 253 | duration: (_getValue4 = getValue(keyList.durationKey)) === null || _getValue4 === void 0 ? void 0 : _getValue4[0] 254 | }; 255 | } 256 | ;// CONCATENATED MODULE: ./src/utils/datify.ts 257 | /** 258 | * 将不同格式的発売日标准化为统一格式 259 | * YYYY.MM.DD 260 | * 261 | * @param {string} date 未处理的日期字符串 262 | * @returns {string} 处理后的标准日期格式 263 | */ 264 | function datify(date) { 265 | let newDate; // 符合标准格式:直接返回 266 | 267 | if (/\d{4}\.\d{2}\.\d{2}/.test(date)) { 268 | newDate = date; 269 | return newDate; 270 | } // 使用 “/” 作为分隔符的格式 271 | 272 | 273 | if (/\d+\/\d+\/\d+/.test(date)) { 274 | newDate = new Date(date).toLocaleDateString('zh-CN', { 275 | year: 'numeric', 276 | month: '2-digit', 277 | day: '2-digit' 278 | }).replaceAll('/', '.'); 279 | return newDate; 280 | } // 含有汉字的格式 281 | 282 | 283 | let year = date.match(/(\d{4})年/)[1]; 284 | let month = date.match(/(\d{1,2})月/)[1]; 285 | let day = date.match(/(\d{1,2})日/)[1]; 286 | newDate = `${year}.${month.length === 1 ? '0' + month : month}.${day.length === 1 ? '0' + day : day}`; 287 | return newDate; 288 | } 289 | ;// CONCATENATED MODULE: ./src/utils/codify.ts 290 | /** 291 | * 将不同格式的番号标准化为统一格式 292 | * 293 | * @param {string} code 传入的未处理的番号 294 | * @returns {string} 标准化处理后的番号 295 | */ 296 | function codify(code) { 297 | /** 298 | * 无需处理 299 | * - 東京熱番号 300 | * - 包含连字符的番号 301 | */ 302 | if (/n\d+/.test(code) || /-/.test(code)) { 303 | return code; 304 | } 305 | /* 有字母的番号 */ 306 | 307 | 308 | if (/[a-z,A-Z]/.test(code)) { 309 | const codeNum = code.match(/\d+/); // 数字 310 | 311 | const codeCap = code.match(/[A-Z,a-z]+/); // 字母 312 | 313 | let newCodeNum; // 处理番号数字位大于3,有多个0的情况 314 | 315 | if (codeNum[0] && codeNum[0].length > 3) { 316 | newCodeNum = codeNum[0].slice(-3); 317 | code = code.replace(codeNum[0], newCodeNum); 318 | } 319 | 320 | return `${codeCap}-${codeNum}`.toUpperCase(); 321 | } 322 | /* 其他情况 */ 323 | 324 | 325 | return code; 326 | } 327 | ;// CONCATENATED MODULE: ./src/utils/refine-title.ts 328 | /** 329 | * 删除作品标题头尾包含的演员名 330 | * 331 | * @param {AVWork} av 未处理的 AV 对象 332 | * @returns {AVWork} 处理后的 AV 对象 333 | */ 334 | function refineTitle(av) { 335 | // 去头尾演员名 336 | let startAct = new RegExp('^' + av.actress, 'g'); 337 | let endAct = new RegExp(av.actress + '$', 'g'); 338 | let titleHasActress = startAct.test(av.workName) || endAct.test(av.workName); // 替换斜线 339 | 340 | av.workName = av.workName.replaceAll('/', ' '); 341 | 342 | if (titleHasActress) { 343 | var _av$actress; 344 | 345 | av.workName = av.workName.replace((_av$actress = av.actress) === null || _av$actress === void 0 ? void 0 : _av$actress.join(' '), ''); 346 | } // 头尾去空格 347 | 348 | 349 | av.workName = av.workName.trim(); 350 | return av; 351 | } 352 | ;// CONCATENATED MODULE: ./src/utils/refine-actress.ts 353 | /** 354 | * 美化演员列表 355 | * - 剔除头尾空格 356 | * - 剔除名字内空格(日本演员) 357 | * 358 | * @param {AVWork} av 未处理的 AV 对象 359 | * @returns {AVWork} 处理后的 AV 对象 360 | */ 361 | function refineActress(av) { 362 | av.actress = av.actress.map(a => { 363 | // 先剔除头尾空格 364 | let newA = a.trim(); // 剔除内部空格(仅作用于非英文名演员、非素人演员) 365 | 366 | if (!newA.match(/[a-zA-Z]+/g) && !av.actressRealName) { 367 | newA = newA.replaceAll(/\s/g, ''); 368 | } 369 | 370 | return newA; 371 | }); 372 | return av; 373 | } 374 | ;// CONCATENATED MODULE: ./src/utils/check-indicator.ts 375 | // 编号标识列表 376 | const INDICATORS = ['vol.', 'Vol.', 'VOL.', 'Case.', 'File.', 'FILE.', 'Part', 'Talk.', 'パート']; 377 | /** 378 | * 检测系列作品的标题中是否包含有标识的编号 379 | * 380 | * @param {string} workName 作品名 381 | * @returns {string | undefined} 检测出的标识和编号或 undefined 382 | */ 383 | 384 | function checkIndicator(workName) { 385 | // 是否包含编号标识 386 | const indicator = INDICATORS.find(d => workName.includes(d)); 387 | 388 | if (indicator) { 389 | var _workName$match; 390 | 391 | // 包含编号标识:返回标识和编号部分 392 | // 标识符和编号部分 393 | const regExp = new RegExp(indicator + '\\s{0,3}\\d+'); 394 | const num = (_workName$match = workName.match(regExp)) === null || _workName$match === void 0 ? void 0 : _workName$match[0]; 395 | return num; 396 | } // 不包含编号标识:返回 undefined 397 | 398 | 399 | return undefined; 400 | } 401 | ;// CONCATENATED MODULE: ./src/utils/check-digit.ts 402 | /** 403 | * 检测系列作品的标题中是否包含纯数字编号 404 | * 405 | * @param {string} workName 作品名 406 | * @param {string} seriesName 系列名 407 | * @returns { string | undefined } 检测出的纯数字编号或 undefined(未检测出) 408 | */ 409 | function checkDigit(workName, seriesName) { 410 | const seriesNameNoSpace = seriesName.replaceAll(' ', ''); 411 | /* 有纯数字编号也肯定是在标题中的系列名后跟着纯数字 */ 412 | 413 | const reg = new RegExp(`\\s*${seriesNameNoSpace}\\s*(\\d+)`, 'g'); 414 | const matches = [...workName.replaceAll(' ', '').matchAll(reg)]; 415 | 416 | for (const match of matches) { 417 | if (match.length >= 2) { 418 | // 说明匹配到了捕获组,将其返回(系列编号) 419 | return match[1]; 420 | } 421 | } // 不包含纯数字 422 | 423 | 424 | return undefined; 425 | } 426 | ;// CONCATENATED MODULE: ./src/makers/02-censored/ca-group.ts 427 | 428 | 429 | /** 430 | * 根据域名判断厂商名 431 | * @returns 432 | */ 433 | function getMakerName() { 434 | const host = window.location.host; 435 | let makerName = ''; 436 | 437 | switch (host) { 438 | case 'attackers.net': 439 | makerName = 'Attackers'; 440 | break; 441 | 442 | case 'ideapocket.com': 443 | makerName = 'Idea Pocket'; 444 | break; 445 | 446 | case 'madonna-av.com': 447 | makerName = 'Madonna'; 448 | break; 449 | 450 | case 'moodyz.com': 451 | makerName = 'MOODYZ'; 452 | break; 453 | 454 | case 'premium-beauty.com': 455 | makerName = 'Premium'; 456 | break; 457 | 458 | case 's1s1s1.com': 459 | makerName = 'S1'; 460 | break; 461 | 462 | case 'honnaka.jp': 463 | makerName = '本中'; 464 | break; 465 | 466 | case 'mvg.jp': 467 | makerName = 'MVG'; 468 | break; 469 | } 470 | 471 | return makerName; 472 | } 473 | 474 | async function CA() { 475 | var _img$dataset, _document$querySelect; 476 | 477 | // 厂商名 478 | const makerName = getMakerName(); // 封面地址 479 | 480 | const swiperSlide = document.querySelectorAll('.p-slider img'); 481 | const img = (swiperSlide === null || swiperSlide === void 0 ? void 0 : swiperSlide[1]) || (swiperSlide === null || swiperSlide === void 0 ? void 0 : swiperSlide[0]); 482 | let imgUrl = (img === null || img === void 0 ? void 0 : img.src) || (img === null || img === void 0 ? void 0 : (_img$dataset = img.dataset) === null || _img$dataset === void 0 ? void 0 : _img$dataset.src); // 跨域获取 483 | 484 | const res = await fetch(imgUrl); 485 | 486 | try { 487 | const blob = await res.blob(); 488 | imgUrl = window.URL.createObjectURL(blob); 489 | } catch (e) { 490 | throw new Error(`[下载图片失败] ${e}`); 491 | } // 作品名 492 | 493 | 494 | const workName = (_document$querySelect = document.querySelector('h2.p-workPage__title')) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.innerText; // 页面数据列表 495 | 496 | let infoList = [...document.querySelectorAll('.p-workPage__table > .item')]; 497 | const tempAV = getInfo(infoList, '.item'); 498 | const av = { 499 | makerName, 500 | workName, 501 | imgUrl, 502 | ...tempAV 503 | }; 504 | return { ...av, 505 | finalName: ca_group_final(av) 506 | }; 507 | } 508 | 509 | 510 | 511 | 512 | 513 | // 序号优先的系列 514 | 515 | const DIGIT_FIRST_SERIES = []; 516 | /** 517 | * 拼接最终文件名 518 | */ 519 | 520 | function ca_group_final(av) { 521 | let finalName; 522 | /* === 1. 处理演员名 === */ 523 | 524 | av = refineActress(av); 525 | const actressString = av.actress.join(' '); 526 | /* === 2. 处理标题 === */ 527 | 528 | av = refineTitle(av); 529 | /* === 3. 处理番号 === */ 530 | 531 | av.code = codify(av.code); 532 | /* === 3. 处理系列名 === */ 533 | 534 | if (av && av.seriesName) { 535 | /* 系列作品 */ 536 | av.seriesName = av.seriesName.trim(); 537 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 538 | 539 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 540 | 541 | if (numType1 || !numType1 && numType2) { 542 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 543 | const digitFirstSeries = DIGIT_FIRST_SERIES.find(x => av.seriesName === x); 544 | 545 | if (digitFirstSeries) { 546 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 547 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code}`; 548 | } else { 549 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 550 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}`; 551 | } 552 | } else { 553 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 554 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})`; 555 | } 556 | } else { 557 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 558 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}`; 559 | } 560 | 561 | return `【${av.makerName}】${finalName}.jpg`; 562 | } 563 | ;// CONCATENATED MODULE: ./src/utils/first-letter-uppercase.ts 564 | /** 565 | * 将句子转换为每个单词首字母大写、其他字母小写的格式 566 | */ 567 | function firstLetterUppercase(original) { 568 | var _original$split$map; 569 | 570 | return original === null || original === void 0 ? void 0 : (_original$split$map = original.split(/\s/).map(word => word.charAt(0) + word.slice(1).toLowerCase())) === null || _original$split$map === void 0 ? void 0 : _original$split$map.join(' '); 571 | } 572 | ;// CONCATENATED MODULE: ./src/makers/03-western/naughty-america.ts 573 | 574 | async function NA() { 575 | var _document$querySelect, _document$querySelect2, _sceneInfo$querySelec, _sceneInfo$querySelec2, _sceneInfo$querySelec3, _sceneInfo$querySelec4, _sceneInfo$querySelec5, _sceneInfo$querySelec6; 576 | 577 | // 封面地址 578 | let imgUrl = `https:${(_document$querySelect = document.querySelector('.play-trailer > picture > source')) === null || _document$querySelect === void 0 ? void 0 : (_document$querySelect2 = _document$querySelect.dataset) === null || _document$querySelect2 === void 0 ? void 0 : _document$querySelect2.srcset}`.replace('webp', 'jpg'); 579 | const res = await fetch(imgUrl); 580 | 581 | try { 582 | const blob = await res.blob(); 583 | imgUrl = window.URL.createObjectURL(blob); 584 | } catch (e) { 585 | throw new Error(`[下载图片失败] ${e}`); 586 | } // 页面数据容器 587 | 588 | 589 | const sceneInfo = document.querySelector('.scene-info'); // 作品名 590 | 591 | const workName = firstLetterUppercase((_sceneInfo$querySelec = sceneInfo.querySelector('.scene-title')) === null || _sceneInfo$querySelec === void 0 ? void 0 : _sceneInfo$querySelec.innerText); // 系列名 592 | 593 | const seriesName = firstLetterUppercase((_sceneInfo$querySelec2 = sceneInfo.querySelector('.site-title')) === null || _sceneInfo$querySelec2 === void 0 ? void 0 : _sceneInfo$querySelec2.innerText); // 日期 594 | 595 | const date = new Date((_sceneInfo$querySelec3 = sceneInfo.querySelector('.entry-date')) === null || _sceneInfo$querySelec3 === void 0 ? void 0 : _sceneInfo$querySelec3.innerText).toLocaleDateString('zh-CN'); // 演员列表 596 | 597 | const actress = Array.from(sceneInfo.querySelectorAll('.performer-list > a'), a => firstLetterUppercase(a === null || a === void 0 ? void 0 : a.innerText)); // 时长 598 | 599 | const duration = (_sceneInfo$querySelec4 = sceneInfo.querySelector('.duration')) === null || _sceneInfo$querySelec4 === void 0 ? void 0 : (_sceneInfo$querySelec5 = _sceneInfo$querySelec4.innerText) === null || _sceneInfo$querySelec5 === void 0 ? void 0 : (_sceneInfo$querySelec6 = _sceneInfo$querySelec5.match(/\d+\smin/)) === null || _sceneInfo$querySelec6 === void 0 ? void 0 : _sceneInfo$querySelec6[0].replace(' ', ''); 600 | const labels = sceneInfo.querySelectorAll('.flag-bg'); 601 | const resolutions = Array.from(labels, label => (label === null || label === void 0 ? void 0 : label.innerText) === 'HD' ? '1080p' : label === null || label === void 0 ? void 0 : label.innerText); 602 | const av = { 603 | makerName: 'Naughty America', 604 | workName, 605 | seriesName, 606 | date, 607 | actress, 608 | duration, 609 | resolutions, 610 | imgUrl 611 | }; 612 | return { ...av, 613 | finalName: naughty_america_final(av) 614 | }; 615 | } 616 | 617 | /** 618 | * 拼接最终文件名 619 | */ 620 | 621 | function naughty_america_final(av) { 622 | var _av$resolutions; 623 | 624 | //【厂商】(日期)演员 - 作品名 625 | av.seriesName = av.seriesName.trim(); 626 | const finalName = `${av.seriesName}(${datify(av.date)})${av.actress.join(', ')} - ${av.workName} [${av.duration}; ${(_av$resolutions = av.resolutions) === null || _av$resolutions === void 0 ? void 0 : _av$resolutions.join(', ')}]`; 627 | return `【${av.makerName}】${finalName}.jpg`; 628 | } 629 | ;// CONCATENATED MODULE: ./src/makers/01-uncensored/1pondo.ts 630 | async function OnePondo() { 631 | var _getInfo, _getInfo$trim, _document$URL, _document$URL$match, _getInfo2; 632 | 633 | // 作品名 634 | const workName = document.querySelector('.movie-overview h1').innerText; // 展开信息列表 635 | 636 | const showInfo = document.querySelector('button.see-more'); 637 | await showInfo.click(); // 作品详情列表 638 | 639 | const infoList = [...(document.querySelectorAll('.movie-detail > ul > li') || [])]; 640 | /** 641 | * 从详情列表中查找返回对应信息 642 | */ 643 | 644 | function getInfo(key) { 645 | var _infoItem$querySelect; 646 | 647 | const infoItem = infoList === null || infoList === void 0 ? void 0 : infoList.find(info => { 648 | var _innerText; 649 | 650 | return info === null || info === void 0 ? void 0 : (_innerText = info.innerText) === null || _innerText === void 0 ? void 0 : _innerText.includes(key); 651 | }); 652 | return (infoItem === null || infoItem === void 0 ? void 0 : (_infoItem$querySelect = infoItem.querySelector('span:last-child')) === null || _infoItem$querySelect === void 0 ? void 0 : _infoItem$querySelect.innerText) || ''; 653 | } // 系列名 654 | 655 | 656 | const seriesName = getInfo('シリーズ') || ''; // 日期 657 | 658 | const date = getInfo('配信日') || ''; // 演员列表 659 | 660 | const actress = ((_getInfo = getInfo('出演')) === null || _getInfo === void 0 ? void 0 : (_getInfo$trim = _getInfo.trim()) === null || _getInfo$trim === void 0 ? void 0 : _getInfo$trim.split(/\s/)) || []; // 番号 661 | 662 | const code = ((_document$URL = document.URL) === null || _document$URL === void 0 ? void 0 : (_document$URL$match = _document$URL.match(/\d+_\d+/)) === null || _document$URL$match === void 0 ? void 0 : _document$URL$match[0]) || ''; // 封面地址 663 | 664 | const imgUrl = `https://www.1pondo.tv/assets/sample/${code}/str.jpg`; // 时长 665 | 666 | const duration = ((_getInfo2 = getInfo('再生時間')) === null || _getInfo2 === void 0 ? void 0 : _getInfo2.replaceAll(':', '.')) || ''; 667 | const av = { 668 | makerName: '1 Pondo', 669 | workName, 670 | seriesName, 671 | date, 672 | actress, 673 | code, 674 | imgUrl, 675 | duration 676 | }; 677 | return { ...av, 678 | finalName: _1pondo_final(av) 679 | }; 680 | } 681 | 682 | 683 | 684 | 685 | 686 | // 序号优先的系列 687 | 688 | const _1pondo_DIGIT_FIRST_SERIES = []; 689 | /** 690 | * 拼接最终文件名 691 | */ 692 | 693 | function _1pondo_final(av) { 694 | let finalName; 695 | /* === 1. 处理演员名 === */ 696 | 697 | av = refineActress(av); 698 | /* === 2. 处理标题 === */ 699 | 700 | av = refineTitle(av); 701 | /* === 3. 处理系列名 === */ 702 | 703 | if (av && av.seriesName) { 704 | /* 系列作品 */ 705 | av.seriesName = av.seriesName.trim(); 706 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 707 | 708 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 709 | 710 | if (numType1 || !numType1 && numType2) { 711 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 712 | const digitFirstSeries = _1pondo_DIGIT_FIRST_SERIES.find(x => av.seriesName === x); 713 | 714 | if (digitFirstSeries) { 715 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 716 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${av.actress}(${av.code})[${av.duration}]`; 717 | } else { 718 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 719 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${av.actress} [${av.duration}]`; 720 | } 721 | } else { 722 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 723 | finalName = `${av.seriesName}(${datify(av.date)})${av.actress}(${av.code})[${av.duration}]`; 724 | } 725 | } else { 726 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 727 | finalName = `(${datify(av.date)})${av.actress}(${codify(av.code)})${av.workName} [${av.duration}]`; 728 | } 729 | 730 | return `【${av.makerName}】${finalName}.jpg`; 731 | } 732 | ;// CONCATENATED MODULE: ./src/utils/final.ts 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | /** 741 | * 是否需要移除系列名后的空格 742 | * @param list 需要移除的系列列表 743 | * @param seriesName 当前作品系列名 744 | */ 745 | function seriesEndSpace(list, seriesName) { 746 | return list !== null && list !== void 0 && list.includes(seriesName) ? '' : ' '; 747 | } 748 | /** 749 | * 拼接最终文件名(无码) 750 | */ 751 | 752 | 753 | function finalUncensored(av) { 754 | var _av, _av2, _av2$resolutions; 755 | 756 | let DIGIT_FIRST_SERIES = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 757 | let finalName; 758 | /* === 1. 处理演员名 === */ 759 | 760 | av = refineActress(av); 761 | const actressString = av.actress.join(' '); 762 | /* === 2. 处理标题 === */ 763 | 764 | av = refineTitle(av); 765 | /* === 3. 处理系列名 === */ 766 | 767 | if (av && av.seriesName) { 768 | /* 系列作品 */ 769 | av.seriesName = av.seriesName.trim(); 770 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 771 | 772 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 773 | 774 | if (numType1 || !numType1 && numType2) { 775 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 776 | const digitFirstSeries = DIGIT_FIRST_SERIES.find(x => av.seriesName === x); 777 | 778 | if (digitFirstSeries) { 779 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 780 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})`; 781 | } else { 782 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 783 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString} `; 784 | } 785 | } else { 786 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 787 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})`; 788 | } 789 | } else { 790 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 791 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName} `; 792 | } 793 | 794 | return `【${av.makerName}】${finalName}[${av.duration}${(_av = av) !== null && _av !== void 0 && _av.resolutions ? `; ${(_av2 = av) === null || _av2 === void 0 ? void 0 : (_av2$resolutions = _av2.resolutions) === null || _av2$resolutions === void 0 ? void 0 : _av2$resolutions.join(', ')}` : ''}].jpg`; 795 | } 796 | /** 797 | * 拼接最终文件名(有码) 798 | */ 799 | 800 | function finalCensored(av) { 801 | let digitFirstSeriesList = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 802 | let noSeriesEndSpaceList = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; 803 | let finalName; 804 | /* === 1. 处理演员名 === */ 805 | 806 | av = refineActress(av); 807 | const actressString = av.actress.join(' '); 808 | /* === 2. 处理标题 === */ 809 | 810 | av = refineTitle(av); 811 | /* === 3. 处理番号 === */ 812 | 813 | av.code = codify(av.code); 814 | /* === 3. 处理系列名 === */ 815 | 816 | if (av && av.seriesName) { 817 | /* 系列作品 */ 818 | av.seriesName = av.seriesName.trim(); 819 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 820 | 821 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 822 | 823 | if (numType1 || !numType1 && numType2) { 824 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 825 | const digitFirstSeries = digitFirstSeriesList.find(x => av.seriesName === x); 826 | 827 | if (digitFirstSeries) { 828 | var _av3; 829 | 830 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 831 | finalName = `${av.seriesName}${seriesEndSpace(noSeriesEndSpaceList, (_av3 = av) === null || _av3 === void 0 ? void 0 : _av3.seriesName)}${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})`; 832 | } else { 833 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 834 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}`; 835 | } 836 | } else { 837 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 838 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})`; 839 | } 840 | } else { 841 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 842 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}`; 843 | } 844 | 845 | return `【${av.makerName}】${finalName}.jpg`; 846 | } 847 | ;// CONCATENATED MODULE: ./src/makers/02-censored/prestige.ts 848 | // 厂商名转换列表 849 | 850 | const MAKER_TRANS = { 851 | プレステージ: 'Prestige' 852 | }; 853 | async function Prestige() { 854 | var _document$querySelect, _document$querySelect2, _document$querySelect3, _ref, _ref$src; 855 | 856 | // 作品名 857 | const spanText = (_document$querySelect = document.querySelector('.min-h-main h1 span')) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.innerText; 858 | const workName = (_document$querySelect2 = document.querySelector('.min-h-main h1')) === null || _document$querySelect2 === void 0 ? void 0 : (_document$querySelect3 = _document$querySelect2.innerText) === null || _document$querySelect3 === void 0 ? void 0 : _document$querySelect3.replace(spanText, '').trim(); // 封面地址 859 | 860 | const imgList = document.querySelectorAll('img.swiper-picture'); 861 | const imgUrl = (_ref = (imgList === null || imgList === void 0 ? void 0 : imgList[1]) || document.querySelector('img.swiper-picture')) === null || _ref === void 0 ? void 0 : (_ref$src = _ref.src) === null || _ref$src === void 0 ? void 0 : _ref$src.replace(/\?\S+/, ''); // 作品详情列表 862 | 863 | const infoList = [...document.querySelectorAll('.text-xl > .hidden > div.flex')]; // 查找对应的详情 864 | 865 | function getInfo(key) { 866 | var _infoItem$querySelect; 867 | 868 | const infoItem = infoList === null || infoList === void 0 ? void 0 : infoList.find(info => { 869 | var _innerText; 870 | 871 | return info === null || info === void 0 ? void 0 : (_innerText = info.innerText) === null || _innerText === void 0 ? void 0 : _innerText.includes(key); 872 | }); 873 | if (!infoItem) return; 874 | const list = infoItem !== null && infoItem !== void 0 && (_infoItem$querySelect = infoItem.querySelectorAll('div.flex-1 a')) !== null && _infoItem$querySelect !== void 0 && _infoItem$querySelect.length ? [...infoItem.querySelectorAll('div.flex-1 a')] : [...infoItem.querySelectorAll('div.flex-1 p')]; 875 | const valueList = Array.from(list, item => item === null || item === void 0 ? void 0 : item.innerText); 876 | return key === '出演者' ? valueList : valueList === null || valueList === void 0 ? void 0 : valueList[0]; 877 | } // 厂商名 878 | 879 | 880 | const makerValue = getInfo('メーカー'); 881 | const makerName = MAKER_TRANS[makerValue] || makerValue; // 系列名 882 | 883 | const seriesName = getInfo('シリーズ'); // 日期 884 | 885 | const date = getInfo('発売日'); // 番号 886 | 887 | const code = getInfo('品番'); // 演员 888 | 889 | const actress = getInfo('出演者'); 890 | let av = { 891 | makerName, 892 | workName, 893 | seriesName, 894 | date, 895 | actress, 896 | code, 897 | imgUrl 898 | }; 899 | return { ...av, 900 | finalName: finalCensored(av, prestige_DIGIT_FIRST_SERIES, NO_End_SPACE_SERIES) 901 | }; 902 | } // 编号在前的系列 903 | 904 | const prestige_DIGIT_FIRST_SERIES = ['人妻さんいらっしゃい!', '職女。']; // 无需加尾空格的系列 905 | 906 | const NO_End_SPACE_SERIES = ['人妻さんいらっしゃい!', '職女。']; 907 | ;// CONCATENATED MODULE: ./src/makers/01-uncensored/tokyo-hot.ts 908 | /** 909 | * 手动维护系列列表,从作品名中识别 910 | */ 911 | // 系列列表 912 | 913 | const seriesList = ['鬼逝', 'Wカン', '東熱激情 淫乱女教師中出授業 特集', '東熱激情 密着パンスト24時!特集']; 914 | 915 | function getSeriesName(workName) { 916 | return seriesList === null || seriesList === void 0 ? void 0 : seriesList.find(item => workName === null || workName === void 0 ? void 0 : workName.includes(item)); 917 | } 918 | 919 | async function TokyoHot() { 920 | var _document$querySelect, _document$querySelect2, _getInfo, _getInfo2, _getInfo3, _getInfo3$, _document$querySelect3; 921 | 922 | // 作品名 923 | const workName = (_document$querySelect = document.querySelector('.contents > h2')) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.innerText; // 封面地址 924 | 925 | let imgUrl = (_document$querySelect2 = document.querySelector('video')) === null || _document$querySelect2 === void 0 ? void 0 : _document$querySelect2.poster; 926 | 927 | try { 928 | const res = await fetch(imgUrl); 929 | const blob = await res.blob(); 930 | imgUrl = window.URL.createObjectURL(blob); 931 | } catch (e) { 932 | throw new Error(`[下载图片失败] ${e}`); 933 | } // 系列名 934 | 935 | 936 | const seriesName = getSeriesName(workName); // 作品信息列表 937 | 938 | const keyList = [...document.querySelectorAll('dl.info > dt')]; 939 | 940 | function getInfo(key) { 941 | const keyItem = keyList === null || keyList === void 0 ? void 0 : keyList.find(item => { 942 | var _innerText; 943 | 944 | return item === null || item === void 0 ? void 0 : (_innerText = item.innerText) === null || _innerText === void 0 ? void 0 : _innerText.includes(key); 945 | }); 946 | if (!keyItem) return; 947 | const valueItem = keyItem === null || keyItem === void 0 ? void 0 : keyItem.nextElementSibling; 948 | if (!valueItem) return; 949 | return key === '出演者' ? Array.from(valueItem === null || valueItem === void 0 ? void 0 : valueItem.querySelectorAll('a'), x => x === null || x === void 0 ? void 0 : x.innerText) : [valueItem === null || valueItem === void 0 ? void 0 : valueItem.innerText]; 950 | } // 日期 951 | 952 | 953 | const date = (_getInfo = getInfo('配信開始日')) === null || _getInfo === void 0 ? void 0 : _getInfo[0]; // 演员 954 | 955 | const actress = getInfo('出演者'); // 番号 956 | 957 | const code = (_getInfo2 = getInfo('作品番号')) === null || _getInfo2 === void 0 ? void 0 : _getInfo2[0]; // 时长 958 | 959 | const duration = (_getInfo3 = getInfo('収録時間')) === null || _getInfo3 === void 0 ? void 0 : (_getInfo3$ = _getInfo3[0]) === null || _getInfo3$ === void 0 ? void 0 : _getInfo3$.replaceAll(':', '.'); // 清晰度和大小 960 | 961 | const sizeList = (_document$querySelect3 = document.querySelectorAll('.download')) === null || _document$querySelect3 === void 0 ? void 0 : _document$querySelect3[1].querySelectorAll('.dbox'); 962 | let resolutions = []; 963 | sizeList.forEach(sizeItem => { 964 | var _sizeItem$querySelect, _sizeItem$querySelect2, _sizeItem$querySelect3; 965 | 966 | const format = (_sizeItem$querySelect = sizeItem.querySelector('h4').innerText.match(/MP4|WMV/g)) === null || _sizeItem$querySelect === void 0 ? void 0 : _sizeItem$querySelect[0]; 967 | const size = (_sizeItem$querySelect2 = sizeItem.querySelector('h4').innerText.match(/\d+.\d+/g)) === null || _sizeItem$querySelect2 === void 0 ? void 0 : _sizeItem$querySelect2[0]; 968 | const resolution = (_sizeItem$querySelect3 = sizeItem.querySelector('p').innerText.match(/x(\d+)/)) === null || _sizeItem$querySelect3 === void 0 ? void 0 : _sizeItem$querySelect3[1]; 969 | 970 | if (parseInt(resolution) >= 720) { 971 | resolutions.push(`${parseFloat(size).toFixed(1)}GB-${format}-${resolution}p`); 972 | } 973 | }); 974 | const av = { 975 | makerName: '東京熱', 976 | workName, 977 | seriesName, 978 | date, 979 | actress, 980 | code, 981 | imgUrl, 982 | duration, 983 | resolutions 984 | }; 985 | return { ...av, 986 | finalName: finalUncensored(av) 987 | }; 988 | } 989 | ;// CONCATENATED MODULE: ./src/makers/03-western/brazzers.ts 990 | async function Brazzers() { 991 | var _infoDiv$querySelecto, _infoDiv$querySelecto2, _infoDiv$querySelecto3, _infoDiv$querySelecto4, _infoDiv$querySelecto5, _imgUrl$match; 992 | 993 | // 信息容器 994 | const infoDiv = document.querySelector('#root > div > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > section > div > div'); // 作品名 995 | 996 | const workName = infoDiv === null || infoDiv === void 0 ? void 0 : (_infoDiv$querySelecto = infoDiv.querySelector('h2.font-secondary')) === null || _infoDiv$querySelecto === void 0 ? void 0 : _infoDiv$querySelecto.innerText; // 日期 997 | 998 | const date = new Date(infoDiv === null || infoDiv === void 0 ? void 0 : (_infoDiv$querySelecto2 = infoDiv.querySelector('div:nth-child(2)')) === null || _infoDiv$querySelecto2 === void 0 ? void 0 : _infoDiv$querySelecto2.innerText).toLocaleDateString('zh-CN'); // 演员列表 999 | 1000 | const actress = (infoDiv === null || infoDiv === void 0 ? void 0 : (_infoDiv$querySelecto3 = infoDiv.querySelectorAll('div')) === null || _infoDiv$querySelecto3 === void 0 ? void 0 : (_infoDiv$querySelecto4 = _infoDiv$querySelecto3[3]) === null || _infoDiv$querySelecto4 === void 0 ? void 0 : (_infoDiv$querySelecto5 = _infoDiv$querySelecto4.innerText) === null || _infoDiv$querySelecto5 === void 0 ? void 0 : _infoDiv$querySelecto5.split(', ')) || []; // 封面地址 1001 | 1002 | let imgUrl = document.querySelector('video + div').style['background-image']; 1003 | imgUrl = (_imgUrl$match = imgUrl.match(/"(\S+)"/)) === null || _imgUrl$match === void 0 ? void 0 : _imgUrl$match[1]; // 跨域获取 1004 | 1005 | const res = await fetch(imgUrl); 1006 | const blob = await res.blob(); 1007 | imgUrl = window.URL.createObjectURL(blob); 1008 | const av = { 1009 | makerName: 'Brazzers', 1010 | workName, 1011 | seriesName: '系列名待补充', 1012 | date, 1013 | actress, 1014 | imgUrl 1015 | }; 1016 | return { ...av, 1017 | finalName: brazzers_final(av) 1018 | }; 1019 | } 1020 | 1021 | /** 1022 | * 拼接最终文件名 1023 | */ 1024 | 1025 | function brazzers_final(av) { 1026 | //【厂商】(日期)演员 - 作品名 1027 | av.seriesName = av.seriesName.trim(); 1028 | const finalName = `${av.seriesName}(${datify(av.date)})${av.actress.join(', ')} - ${av.workName}`; 1029 | return `【${av.makerName}】${finalName}.jpg`; 1030 | } 1031 | ;// CONCATENATED MODULE: ./src/makers/01-uncensored/caribbean.ts 1032 | async function Caribbean() { 1033 | var _document$querySelect, _document$querySelect2, _bgImg$match, _bgImg$match$, _document$querySelect3, _getInfo, _getInfo$trim, _document$URL, _document$URL$match, _getInfo2; 1034 | 1035 | // 封面地址 1036 | const bgImg = (_document$querySelect = document.querySelector('.vjs-poster')) === null || _document$querySelect === void 0 ? void 0 : (_document$querySelect2 = _document$querySelect.style) === null || _document$querySelect2 === void 0 ? void 0 : _document$querySelect2.backgroundImage; 1037 | const imgUrl = `https://www.caribbeancom.com${(_bgImg$match = bgImg.match(/"\S+"/)) === null || _bgImg$match === void 0 ? void 0 : (_bgImg$match$ = _bgImg$match[0]) === null || _bgImg$match$ === void 0 ? void 0 : _bgImg$match$.replaceAll('"', '')}`; // 作品名 1038 | 1039 | const workName = (_document$querySelect3 = document.querySelector('.heading > h1')) === null || _document$querySelect3 === void 0 ? void 0 : _document$querySelect3.innerText; // 页面数据列表 1040 | 1041 | let infoList = Array.from(document.querySelectorAll('.movie-info > ul > li')); 1042 | /** 1043 | * 从详情列表中查找返回对应信息 1044 | */ 1045 | 1046 | function getInfo(key) { 1047 | var _infoItem$querySelect; 1048 | 1049 | const infoItem = infoList === null || infoList === void 0 ? void 0 : infoList.find(info => { 1050 | var _innerText; 1051 | 1052 | return info === null || info === void 0 ? void 0 : (_innerText = info.innerText) === null || _innerText === void 0 ? void 0 : _innerText.includes(key); 1053 | }); 1054 | return (infoItem === null || infoItem === void 0 ? void 0 : (_infoItem$querySelect = infoItem.querySelector('span:last-child')) === null || _infoItem$querySelect === void 0 ? void 0 : _infoItem$querySelect.innerText) || ''; 1055 | } // 系列名 1056 | 1057 | 1058 | const seriesName = getInfo('シリーズ') || ''; // 日期 1059 | 1060 | const date = getInfo('配信日') || ''; // 演员列表 1061 | 1062 | const actress = ((_getInfo = getInfo('出演')) === null || _getInfo === void 0 ? void 0 : (_getInfo$trim = _getInfo.trim()) === null || _getInfo$trim === void 0 ? void 0 : _getInfo$trim.split(/\s/)) || []; // 番号 1063 | 1064 | const code = ((_document$URL = document.URL) === null || _document$URL === void 0 ? void 0 : (_document$URL$match = _document$URL.match(/\d{6}-\d{3}/g)) === null || _document$URL$match === void 0 ? void 0 : _document$URL$match[0]) || ''; // 时长 1065 | 1066 | const duration = ((_getInfo2 = getInfo('再生時間')) === null || _getInfo2 === void 0 ? void 0 : _getInfo2.replaceAll(':', '.')) || ''; 1067 | const av = { 1068 | makerName: '加勒比', 1069 | workName, 1070 | seriesName, 1071 | date, 1072 | actress, 1073 | code, 1074 | imgUrl, 1075 | duration 1076 | }; 1077 | return { ...av, 1078 | finalName: caribbean_final(av) 1079 | }; 1080 | } 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | // 序号优先的系列 1087 | 1088 | const caribbean_DIGIT_FIRST_SERIES = ['新入社員のお仕事']; 1089 | /** 1090 | * 拼接最终文件名 1091 | */ 1092 | 1093 | function caribbean_final(av) { 1094 | let finalName; 1095 | /* === 1. 处理演员名 === */ 1096 | 1097 | av = refineActress(av); 1098 | const actressString = av.actress.join(' '); 1099 | /* === 2. 处理标题 === */ 1100 | 1101 | av = refineTitle(av); 1102 | /* === 3. 处理系列名 === */ 1103 | 1104 | if (av && av.seriesName) { 1105 | /* 系列作品 */ 1106 | av.seriesName = av.seriesName.trim(); 1107 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 1108 | 1109 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 1110 | 1111 | if (numType1 || !numType1 && numType2) { 1112 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 1113 | const digitFirstSeries = caribbean_DIGIT_FIRST_SERIES.find(x => av.seriesName === x); 1114 | 1115 | if (digitFirstSeries) { 1116 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 1117 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})[${av.duration}]`; 1118 | } else { 1119 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 1120 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString} [${av.duration}]`; 1121 | } 1122 | } else { 1123 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 1124 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})[${av.duration}]`; 1125 | } 1126 | } else { 1127 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 1128 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName} [${av.duration}]`; 1129 | } 1130 | 1131 | return `【${av.makerName}】${finalName}.jpg`; 1132 | } 1133 | ;// CONCATENATED MODULE: ./src/makers/02-censored/sod.ts 1134 | 1135 | 1136 | async function SOD() { 1137 | var _document$querySelect, _document$querySelect2; 1138 | 1139 | // 作品名 1140 | const workName = document.querySelector('#videos_head > h1 + h1').innerText; // 封面地址 1141 | 1142 | let imgUrl = (_document$querySelect = document.querySelector('.videos_samimg > a')) === null || _document$querySelect === void 0 ? void 0 : (_document$querySelect2 = _document$querySelect.href) === null || _document$querySelect2 === void 0 ? void 0 : _document$querySelect2.replace('http:', 'https:'); // 跨域获取 1143 | 1144 | try { 1145 | const res = await fetch(imgUrl); 1146 | const blob = await res.blob(); 1147 | imgUrl = window.URL.createObjectURL(blob); 1148 | } catch (e) { 1149 | throw new Error(`[下载图片失败] ${e}`); 1150 | } // 页面数据列表 1151 | 1152 | 1153 | const infoList = [...document.querySelectorAll('#v_introduction tr')]; 1154 | const tempAV = getInfo(infoList, '.v_intr_tx', { 1155 | dateKey: '発売年月日', 1156 | actressKey: '出演者' 1157 | }, '.v_intr_tx a'); 1158 | const av = { 1159 | makerName: 'SODクリエイト', 1160 | workName, 1161 | imgUrl, 1162 | ...tempAV 1163 | }; 1164 | return { ...av, 1165 | finalName: sod_final(av) 1166 | }; 1167 | } // 系列字段中不是系列的名称 1168 | 1169 | const NotRealSeries = ['おねだりプリン']; // 处理系列名 1170 | 1171 | function refineSeries(seriesName) { 1172 | const subList = NotRealSeries.filter(x => (x === null || x === void 0 ? void 0 : x.includes(seriesName)) || seriesName.includes(x)); 1173 | let newSeriesName = seriesName; 1174 | subList.forEach(item => { 1175 | var _newSeriesName; 1176 | 1177 | newSeriesName = (_newSeriesName = newSeriesName) === null || _newSeriesName === void 0 ? void 0 : _newSeriesName.replace(item, ''); 1178 | }); 1179 | return newSeriesName; 1180 | } 1181 | 1182 | function sod_final(av) { 1183 | av.seriesName = refineSeries(av.seriesName); 1184 | return finalCensored(av); 1185 | } 1186 | ;// CONCATENATED MODULE: ./src/makers/04-amateur/mgstage.ts 1187 | async function MGS() { 1188 | var _document$querySelect, _document$querySelect2, _document$querySelect3, _getInfo; 1189 | 1190 | // 作品名 1191 | const workName = (_document$querySelect = document.querySelector('.common_detail_cover h1')) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.innerText; // 封面地址 1192 | 1193 | let imgUrl = (_document$querySelect2 = document.querySelector('.detail_data h2 img')) === null || _document$querySelect2 === void 0 ? void 0 : (_document$querySelect3 = _document$querySelect2.src) === null || _document$querySelect3 === void 0 ? void 0 : _document$querySelect3.replace(/\w{2}_\w{1,2}_/, 'pb_e_'); 1194 | const res = await fetch(imgUrl); 1195 | 1196 | try { 1197 | const blob = await res.blob(); 1198 | imgUrl = window.URL.createObjectURL(blob); 1199 | } catch (e) { 1200 | throw new Error(`[下载图片失败] ${e}`); 1201 | } // 作品详情列表 1202 | 1203 | 1204 | const infoList = [...document.querySelectorAll('.detail_data > table:last-child > tbody > tr')]; 1205 | /** 1206 | * 从详情列表中查找返回对应信息 1207 | */ 1208 | 1209 | function getInfo(key) { 1210 | const infoItem = infoList.find(info => { 1211 | var _info$querySelector, _info$querySelector$i; 1212 | 1213 | return info === null || info === void 0 ? void 0 : (_info$querySelector = info.querySelector('th')) === null || _info$querySelector === void 0 ? void 0 : (_info$querySelector$i = _info$querySelector.innerText) === null || _info$querySelector$i === void 0 ? void 0 : _info$querySelector$i.includes(key); 1214 | }); 1215 | let info = ''; 1216 | 1217 | if (infoItem) { 1218 | if (key === '出演') { 1219 | var _infoItem$querySelect, _Array$from; 1220 | 1221 | /** 1222 | * 演员名有链接:取 标签 1223 | * 演员名无链接:取 标签 1224 | */ 1225 | const hasLink = infoItem === null || infoItem === void 0 ? void 0 : (_infoItem$querySelect = infoItem.querySelectorAll('td > a')) === null || _infoItem$querySelect === void 0 ? void 0 : _infoItem$querySelect.length; 1226 | info = (_Array$from = Array.from(hasLink ? infoItem === null || infoItem === void 0 ? void 0 : infoItem.querySelectorAll('td > a') : infoItem === null || infoItem === void 0 ? void 0 : infoItem.querySelectorAll('td'), a => { 1227 | var _innerText; 1228 | 1229 | return a === null || a === void 0 ? void 0 : (_innerText = a.innerText) === null || _innerText === void 0 ? void 0 : _innerText.trim(); 1230 | })) === null || _Array$from === void 0 ? void 0 : _Array$from.join(', '); 1231 | } else { 1232 | var _infoItem$querySelect2; 1233 | 1234 | info = infoItem === null || infoItem === void 0 ? void 0 : (_infoItem$querySelect2 = infoItem.querySelector('td')) === null || _infoItem$querySelect2 === void 0 ? void 0 : _infoItem$querySelect2.innerText; 1235 | } 1236 | } 1237 | 1238 | return info; 1239 | } // 厂商名 1240 | 1241 | 1242 | const makerName = getInfo('メーカー'); // 系列名 1243 | 1244 | const seriesName = getInfo('シリーズ'); // 日期 1245 | 1246 | const date = getInfo('配信開始日'); // 演员列表 1247 | 1248 | const actress = (_getInfo = getInfo('出演')) === null || _getInfo === void 0 ? void 0 : _getInfo.split(', '); // 番号 1249 | 1250 | const code = getInfo('品番'); // 时长 1251 | 1252 | const duration = getInfo('収録時間'); 1253 | const av = { 1254 | workName, 1255 | imgUrl, 1256 | makerName, 1257 | seriesName, 1258 | date, 1259 | actress, 1260 | actressRealName: 'xxx', 1261 | code, 1262 | duration 1263 | }; 1264 | return { ...av, 1265 | finalName: mgstage_final(av) 1266 | }; 1267 | } 1268 | 1269 | 1270 | // import { refineActress } from '@/utils/refine-actress' 1271 | 1272 | 1273 | // 序号优先的系列 1274 | 1275 | const mgstage_DIGIT_FIRST_SERIES = ['ラグジュTV', 'マジ軟派、初撮。', '働くドMさん.']; // 厂商名转换列表 1276 | 1277 | const mgstage_MAKER_TRANS = { 1278 | 'プレステージプレミアム(PRESTIGE PREMIUM)': 'Prestige Premium' 1279 | }; 1280 | /** 1281 | * 是否需要移除系列名后的空格 1282 | */ 1283 | 1284 | function mgstage_seriesEndSpace(name) { 1285 | const noEndSpaceSeries = ['マジ軟派、初撮。']; 1286 | return noEndSpaceSeries !== null && noEndSpaceSeries !== void 0 && noEndSpaceSeries.includes(name) ? '' : ' '; 1287 | } 1288 | /** 1289 | * 拼接最终文件名 1290 | */ 1291 | 1292 | 1293 | function mgstage_final(av) { 1294 | let finalName; 1295 | /* === 1. 处理演员名 === */ 1296 | // av = refineActress(av) 1297 | 1298 | const actressString = av.actress.join(', '); 1299 | /* === 2. 处理标题 === */ 1300 | 1301 | av = refineTitle(av); 1302 | /* === 3. 处理系列名 === */ 1303 | 1304 | if (av && av.seriesName) { 1305 | /* 系列作品 */ 1306 | av.seriesName = av.seriesName.trim(); 1307 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 1308 | 1309 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 1310 | 1311 | if (numType1 || !numType1 && numType2) { 1312 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 1313 | const digitFirstSeries = mgstage_DIGIT_FIRST_SERIES.find(x => av.seriesName === x); 1314 | 1315 | if (digitFirstSeries) { 1316 | var _av; 1317 | 1318 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)素人女优真实姓名 [时长] 1319 | finalName = `${av.seriesName}${mgstage_seriesEndSpace((_av = av) === null || _av === void 0 ? void 0 : _av.seriesName)}${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})${av.actressRealName} [${av.duration}]`; 1320 | } else { 1321 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名(素人女优真实姓名)[时长] 1322 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}(${av.actressRealName})[${av.duration}]`; 1323 | } 1324 | } else { 1325 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)素人女优真实姓名 [时长] */ 1326 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})${av.actressRealName} [${av.duration}]`; 1327 | } 1328 | } else { 1329 | /* 非系列作品:【厂商】(日期)演员(番号)作品名(素人女优真实姓名)[时长] */ 1330 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}(${av.actressRealName})[${av.duration}]`; 1331 | } 1332 | 1333 | return `【${(mgstage_MAKER_TRANS === null || mgstage_MAKER_TRANS === void 0 ? void 0 : mgstage_MAKER_TRANS[av.makerName]) || av.makerName}】${finalName}.jpg`.replaceAll('/', '-'); 1334 | } 1335 | ;// CONCATENATED MODULE: ./src/makers/02-censored/mousouzoku.ts 1336 | // 官网没有系列字段,从作品名中识别 1337 | const series = ['OLスーツ倶楽部']; 1338 | async function Mousouzoku() { 1339 | var _document$querySelect, _document$querySelect2; 1340 | 1341 | // 作品名 1342 | const workName = (_document$querySelect = document.querySelector('h1.ttl-works')) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.innerText; // 封面地址 1343 | 1344 | const imgUrl = (_document$querySelect2 = document.querySelector('.tmb-img')) === null || _document$querySelect2 === void 0 ? void 0 : _document$querySelect2.href; // 页面数据列表 1345 | 1346 | let infoWrap = document.querySelector('dl.bx-info'); 1347 | /** 1348 | * 根据属性名查找对应的属性值 1349 | */ 1350 | 1351 | function getInfo(key) { 1352 | var _ref, _infoKey$nextElementS; 1353 | 1354 | const infoKey = (_ref = [...infoWrap.querySelectorAll('dt')]) === null || _ref === void 0 ? void 0 : _ref.find(item => { 1355 | var _innerText; 1356 | 1357 | return item === null || item === void 0 ? void 0 : (_innerText = item.innerText) === null || _innerText === void 0 ? void 0 : _innerText.includes(key); 1358 | }); 1359 | if (!infoKey) return; 1360 | const valueList = Array.from(infoKey === null || infoKey === void 0 ? void 0 : (_infoKey$nextElementS = infoKey.nextElementSibling) === null || _infoKey$nextElementS === void 0 ? void 0 : _infoKey$nextElementS.querySelectorAll('p'), x => x === null || x === void 0 ? void 0 : x.innerText); 1361 | return (valueList === null || valueList === void 0 ? void 0 : valueList.length) > 1 || key === '出演者' ? valueList : valueList === null || valueList === void 0 ? void 0 : valueList[0]; 1362 | } // 厂商名 1363 | 1364 | 1365 | const makerName = getInfo('メーカー'); // 系列名 1366 | 1367 | const seriesName = series === null || series === void 0 ? void 0 : series.find(x => workName === null || workName === void 0 ? void 0 : workName.includes(x)); // 日期 1368 | 1369 | const date = getInfo('発売日'); // 演员列表 1370 | 1371 | const actress = getInfo('出演者'); // 番号 1372 | 1373 | const code = getInfo('品番'); 1374 | const av = { 1375 | makerName, 1376 | workName, 1377 | seriesName, 1378 | date, 1379 | actress, 1380 | code, 1381 | imgUrl 1382 | }; 1383 | return { ...av, 1384 | finalName: mousouzoku_final(av) 1385 | }; 1386 | } 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | // 序号优先的系列 1393 | 1394 | const mousouzoku_DIGIT_FIRST_SERIES = []; 1395 | /** 1396 | * 拼接最终文件名 1397 | */ 1398 | 1399 | function mousouzoku_final(av) { 1400 | let finalName; 1401 | /* === 处理厂商名 === */ 1402 | 1403 | av.makerName = av.makerName.replace('/', '·'); // 替换子厂商后的左斜杠 1404 | 1405 | /* === 1. 处理演员名 === */ 1406 | 1407 | av = refineActress(av); 1408 | const actressString = av.actress.join(' '); 1409 | /* === 2. 处理标题 === */ 1410 | 1411 | av = refineTitle(av); 1412 | /* === 3. 处理番号 === */ 1413 | 1414 | av.code = codify(av.code); 1415 | /* === 3. 处理系列名 === */ 1416 | 1417 | if (av && av.seriesName) { 1418 | /* 系列作品 */ 1419 | av.seriesName = av.seriesName.trim(); 1420 | const numType1 = checkIndicator(av.workName); // 标识 + 编号 1421 | 1422 | const numType2 = checkDigit(av.workName, av.seriesName); // 纯数字编号 1423 | 1424 | if (numType1 || !numType1 && numType2) { 1425 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 1426 | const digitFirstSeries = mousouzoku_DIGIT_FIRST_SERIES.find(x => av.seriesName === x); 1427 | 1428 | if (digitFirstSeries) { 1429 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 1430 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})`; 1431 | } else { 1432 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 1433 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}`; 1434 | } 1435 | } else { 1436 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 1437 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})`; 1438 | } 1439 | } else { 1440 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 1441 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}`; 1442 | } 1443 | 1444 | return `【${av.makerName}】${finalName}.jpg`; 1445 | } 1446 | ;// CONCATENATED MODULE: ./src/index.ts 1447 | 1448 | 1449 | /* ===== Makers ===== */ 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | window.onload = async () => { 1463 | await main(); 1464 | }; 1465 | 1466 | async function main() { 1467 | const a = createBtn(); // 创建按钮 1468 | 1469 | const domain = window.location.host; 1470 | const av = await trySwitch(domain); 1471 | 1472 | if (av) { 1473 | a.download = av.finalName; 1474 | a.href = av.imgUrl; // eslint-disable-next-line 1475 | 1476 | console.log(a.download); // 自动保存开启 1477 | 1478 | if (localStorage.getItem('autoSave') === 'yes') { 1479 | a.click(); 1480 | } 1481 | } 1482 | } 1483 | /** 1484 | * 根据不同厂商,使用不同的处理脚本,设置不同的按钮内容 1485 | * @param domain 网站域名 1486 | */ 1487 | 1488 | 1489 | async function trySwitch(domain) { 1490 | let av = {}; 1491 | 1492 | switch (domain) { 1493 | /* ========== 无码 ========== */ 1494 | case 'www.1pondo.tv': 1495 | av = await OnePondo(); 1496 | break; 1497 | 1498 | case 'www.caribbeancom.com': 1499 | av = await Caribbean(); 1500 | break; 1501 | 1502 | case 'my.tokyo-hot.com': 1503 | av = await TokyoHot(); 1504 | break; 1505 | 1506 | /* ========== 有码 ========== */ 1507 | // CA 集团 1508 | 1509 | case 'attackers.net': 1510 | case 'honnaka.jp': 1511 | case 'ideapocket.com': 1512 | case 'madonna-av.com': 1513 | case 'moodyz.com': 1514 | case 'mvg.jp': 1515 | case 'premium-beauty.com': 1516 | case 's1s1s1.com': 1517 | av = await CA(); 1518 | break; 1519 | 1520 | case 'www.mousouzoku-av.com': 1521 | av = await Mousouzoku(); 1522 | break; 1523 | // Prestige 集团 1524 | 1525 | case 'www.prestige-av.com': 1526 | av = await Prestige(); 1527 | break; 1528 | // SOD 集团 1529 | 1530 | case 'ec.sod.co.jp': 1531 | av = await SOD(); 1532 | break; 1533 | 1534 | /* ========== 欧美 ========== */ 1535 | 1536 | case 'www.brazzers.com': 1537 | await sleep(5000); 1538 | av = await Brazzers(); 1539 | break; 1540 | 1541 | case 'www.naughtyamerica.com': 1542 | av = await NA(); 1543 | break; 1544 | 1545 | /* ========== 素人 ========== */ 1546 | 1547 | case 'www.mgstage.com': 1548 | av = await MGS(); 1549 | break; 1550 | 1551 | default: 1552 | break; 1553 | } 1554 | 1555 | return av; 1556 | } 1557 | /******/ })() 1558 | ; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "av-info-saver", 3 | "description": "将厂商、系列、发布日期、演员名、番号等 AV 作品信息作为封面图片文件名,一键保存到本地的油猴脚本。", 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "TROJAN", 7 | "email": "ytj1996@gmail.com" 8 | }, 9 | "scripts": { 10 | "postversion": "git push --follow-tags", 11 | "analize": "npm_config_report=true npm run build", 12 | "build": "webpack --mode production --config config/webpack.config.production.cjs", 13 | "dev": "webpack --mode development --config config/webpack.config.dev.cjs", 14 | "lint": "" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/trojanyao/AV-Info-Saver" 19 | }, 20 | "private": true, 21 | "dependencies": { 22 | "axios": "0.26.0", 23 | "axios-userscript-adapter": "0.1.11", 24 | "jquery": "3.6.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "7.17.5", 28 | "@babel/preset-env": "7.16.11", 29 | "@babel/preset-typescript": "7.16.7", 30 | "@types/greasemonkey": "4.0.2", 31 | "@typescript-eslint/eslint-plugin": "5.12.1", 32 | "@typescript-eslint/parser": "5.12.1", 33 | "babel-loader": "8.2.3", 34 | "browserslist": "4.19.3", 35 | "css-loader": "6.6.0", 36 | "eslint": "^8.55.0", 37 | "eslint-config-prettier": "^8.5.0", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "less": "4.1.2", 40 | "less-loader": "10.2.0", 41 | "style-loader": "3.3.1", 42 | "typescript": "4.5.5", 43 | "userscript-metadata-webpack-plugin": "0.1.1", 44 | "webpack": "5.69.1", 45 | "webpack-bundle-analyzer": "4.5.0", 46 | "webpack-cli": "4.9.2", 47 | "webpack-livereload-plugin": "3.0.2", 48 | "webpack-merge": "5.8.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 |

AV 作品信息一键保存工具

8 | 9 |

10 | 11 | ![hero](https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/master/readme/assets/hero.png) 12 | 13 |

14 | Buy Me A Coffee 15 |

16 | 17 | ## 背景 18 | 19 | 多年以来,自己保存 AV 作品时习惯将厂商、系列、发布日期、演员、番号这些信息直接作为文件名记录在作品封面图文件中,另外再将对应的种子、影片、字幕重命名为同名文件。这样一来,就形成了统一规范;通过按文件名排序,管理起来清晰、方便、一目了然。 20 | 21 | ![单品](https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/master/readme/assets/finder-single.png) 22 | 23 | ![系列作品](https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/master/readme/assets/finder-series.png) 24 | 25 | 之前,每次保存时都要手动将每条信息复制粘贴到文件名中——虽然 AV 作品可以带给人愉悦,但不应把生命浪费在这个机械化重复劳动的过程上。因此编写了这个油猴脚本,实现从 AV 厂商官网一键下载作品封面图,并将作品信息按固定格式保存在文件名中。 26 | 27 | 效率从原来手动保存的 20\~30s 压缩到了 1\~2s 。 28 | 29 | https://github.com/trojanyao/AV-Info-Saver/assets/22811809/cf0b8908-6e07-41e3-9ed5-26bd874572ff 30 | 31 | ## 安装 & 使用 32 | 33 | 1. **安装油猴脚本浏览器插件** 34 | 35 | 此处不赘述,前往官网自行研究安装:https://www.tampermonkey.net/ 。 36 | 37 | 2. **安装 AV Info Saver 脚本** 38 | 39 | - Greasy Fork【🥇推荐,可以获得更新提醒】 40 | 41 | [AV Info Saver - AV 作品信息一键保存工具](https://greasyfork.org/zh-CN/scripts/482729-av-info-saver-av-作品信息一键保存工具) 42 | 43 | > 由于我主动将该脚本标记为了成人相关,可能需要登录账号才可查看。 44 | 45 | - GitHub 原始脚本地址 46 | 47 | ``` 48 | https://github.com/trojanyao/AV-Info-Saver/raw/master/dist/index.prod.user.js 49 | ``` 50 | 51 | 3. **打开相应的 AV 厂商官网作品页,点击按钮,一键保存!** 52 | 53 | > 注意:为了不影响各官网原有功能,该脚本 **仅在作品详情页生效**。如果安装后在首页没看到下载面板,别着急,随便点进去一个作品页,适配了的话会出现在右上角。 54 | 55 | 适配厂商 & 后续更新 56 | 57 | v1.0.0 适配了 12 个常见的主流厂商,包括无码、有码、欧美、素人。详见 [v1.0.0 更新说明](https://github.com/trojanyao/AV-Info-Saver/releases/tag/v1.0.0),之后适配的也都会在对应的更新说明中列出。 58 | 59 | 由于 COVID-19 之后各厂商的作品质量出现断崖式下跌,小厂商更是存活艰难,有的甚至关站停更。我现在已经不怎么看新作品和小厂商的作品了,第一批适配的 12 个主流厂商基本能覆盖日常使用。因此短期内可能不会再适配新厂商;除非研究历史作品的过程中,出现某个使用特别频繁的厂商。 60 | 61 | 当然,如果你特别喜欢某个厂商,可以 fork 本项目自己适配,也欢迎提交 PR 。 62 | 63 | ## 开发 & 贡献 64 | 本项目基于 https://github.com/trim21/webpack-userscript-template 模板,以实现用 Webpack 模块化开发油猴脚本。相关知识请参考 [说明](https://github.com/trojanyao/AV-Info-Saver/tree/master/readme/readme.cn.md) 。 65 | 66 | 自行适配的请参考以下步骤: 67 | 68 | 1. 在 [`config/metadata.cjs`](https://github.com/trojanyao/AV-Info-Saver/tree/master/config/metadata.cjs) 中新增 URL 匹配规则,建议仅在作品详情页生效; 69 | 70 | > 注意:每次修改该文件都要 `npm run dev` 重新生成,安装。 71 | 72 | 2. 在 [`src/makers`](https://github.com/trojanyao/AV-Info-Saver/tree/master/src/makers) 中对应的子目录下新增对应厂商的脚本; 73 | 74 | > 具体业务逻辑可参考已适配的其他厂商。 75 | 76 | 3. 在 [`index.ts`](https://github.com/trojanyao/AV-Info-Saver/tree/master/src/index.ts) 中导入上一步创建的脚本,并在 `trySwitch()` 方法中根据域名匹配; 77 | 78 | 4. 测试打包。 79 | 80 | > 建议针对不同情况测试,已适配的厂商格式如下。 81 | > 82 | > - 单品 83 | > 84 | > - 单人:`【厂商】(发布日期)演员(番号)作品名 [时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 85 | > 86 | > - 多人:`【厂商】(发布日期)演员1 演员2(番号)作品名 [时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 87 | > 88 | > - 系列作品 89 | > 90 | > - 无编号 91 | > 92 | > - 单人:`【厂商】系列名(发布日期)演员(番号)[时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 93 | > - 多人:`【厂商】系列名(发布日期)演员1 演员2(番号)[时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 94 | > 95 | > - 有编号 96 | > 97 | > - 日期优先 98 | > 99 | > - 单人:`【厂商】系列名(发布日期)编号(番号)演员 [时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 100 | > 101 | > - 多人:`【厂商】系列名(发布日期)编号(番号)演员1 演员2 [时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 102 | > 103 | > - 编号优先 104 | > 105 | > - 单人:`【厂商】系列名 编号(发布日期)演员(番号)[时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 106 | > 107 | > - 多人:`【厂商】系列名 编号(发布日期)演员1 演员2(番号)[时长; 大小1-格式1-分辨率1, 大小2-格式2-分辨率2].jpg` 108 | > 109 | > 说明: 110 | > 111 | > 1. 时长:因为在下载作品时有的不完整,记录时长信息是为了便于确认作品完整性。由于欧美和素人作品的封面图往往是一张单纯的截图,因此也保存下时长信息。 112 | > 2. 大小、格式、分辨率:有些厂商(如:東京熱)针对同一个作品会发行不同格式、不同清晰度的版本,记录下这些信息方便在下载时作参考,选择质量最高的版本。 113 | > 3. 编号优先:有些厂商或系列在发行时并非严格按照日期顺序编号,如果按日期排序就可能出现某个作品编号在其他作品之后,但实际发行日期靠前的情况,看起来有些混乱。因此编号优先让我们完全以编号排序为准,保证正确的顺序。 114 | -------------------------------------------------------------------------------- /readme/assets/finder-series.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/readme/assets/finder-series.png -------------------------------------------------------------------------------- /readme/assets/finder-single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/readme/assets/finder-single.png -------------------------------------------------------------------------------- /readme/assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/readme/assets/hero.png -------------------------------------------------------------------------------- /readme/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/readme/assets/icon.png -------------------------------------------------------------------------------- /readme/assets/screen recording.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/readme/assets/screen recording.mp4 -------------------------------------------------------------------------------- /readme/assets/screen-recording-trimmed.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trojanyao/AV-Info-Saver/a1c69559057e169cb97f1df9306b70dbc0305820/readme/assets/screen-recording-trimmed.mp4 -------------------------------------------------------------------------------- /readme/readme.cn.md: -------------------------------------------------------------------------------- 1 | # 使用 WebPack 来构件 UserScript 2 | 3 | [使用这个 repo 作为模板](https://github.com/Trim21/webpack-userscript-template/generate). 4 | 5 | ## 开发 6 | 7 | 1. 允许 Tampermonkey 访问文件网址 `右键插件图标`-`插件管理页面`-`访问文件网址` 或者参照[官方 faq](https://tampermonkey.net/faq.php?ext=dhdg#Q204) 8 | 2. 使用 `npm ci` or `npm i` 安装依赖。 9 | 3. `npm run dev` 来进行自动编译。 10 | 4. 在浏览器中打开 `webpack-userscript-template/dist/index.dev.user.js` ,使用 Userscript manager 安装脚本。 11 | 12 | 被安装的用户脚本包含`// @require file://path/to/dist/index.prod.user.js`, 13 | 所以在每次加载的时候会运行 `index.prod.user.js`。 14 | `index.prod.user.js`是 webpack 以[src/js/index.ts](./src/js/index.ts)作为入口打包出来的完整的 userscript。 15 | 16 | 每次你修改了你的[metadata](./config/metadata.js),你需要重新安装`index.dev.user.js`。 17 | 18 | 6. 修改 [src/js/index.ts](./src/js/index.ts) 。如果你需要的话你可以引入 css 或者 less 文件。你也可以通过设置 webpack 来引入 scss。 19 | 7. 在 并且打开控制台,你可以看到用户脚本被运行。 20 | 21 | livereload 默认启用。在浏览器中进行自动刷新需要 [这个 chrome 插件](https://chrome.google.com/webstore/detail/jnihajbhpnppcggbcgedagnkighmdlei) 22 | 23 | ## TypeScript 24 | 25 | 已经设置好了`ts-loader`,可以直接 typescript。[example](src/index.ts) 26 | 27 | ## 使用依赖 28 | 29 | 有两个办法引入依赖。 30 | 31 | ### 像以往的 UserScript 一样 32 | 33 | 在 [metadata 的 require 部分](./config/metadata.cjs#L13-L17) 中修改你引入的依赖。然后在 [config/webpack.config.base.cjs](./config/webpack.config.base.js#L21-L25) 的`exclude`配置中里面把他们排除。 34 | 35 | ### 跟以往的 WebPack 一样 36 | 37 | 直接使用 npm 安装,然后什么都不用管。 38 | 39 | 这会造成最终打包出来的文件体积增大,可读性下降。 40 | 41 | ## build 42 | 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | `dist/index.prod.user.js` 就是最终打包出来的 UserScript。 48 | 49 | ## deploy 50 | 51 | [github actions](./.github/workflows/nodejs.yml#L68) 会自动在每个 tag 把`dist/index.prod.user.js`部属到`gh-pages`分支的根目录去。 52 | 53 | [example](https://github.com/Trim21/webpack-userscript-template/tree/gh-pages) 54 | 55 | [deployed](https://trim21.github.io/webpack-userscript-template/) 56 | 57 | 也可以使用 greasyfork 的自动同步功能来自动同步此链接。(greasyfork 的代码规则禁止代码混淆或最小化) 58 | -------------------------------------------------------------------------------- /readme/readme.md: -------------------------------------------------------------------------------- 1 | # This is a project help you build userscript with webpack 2 | 3 | Just [use this git repo as a template](https://github.com/Trim21/webpack-userscript-template/generate). 4 | 5 | [中文说明](./readme.cn.md) 6 | 7 | ## dev 8 | 9 | 1. Allow Tampermonkey's access to local file URIs [tampermonkey/faq](https://tampermonkey.net/faq.php?ext=dhdg#Q204) 10 | 2. install deps with `npm i` or `npm ci`. 11 | 3. `npm run dev` to start your development. 12 | 4. open `webpack-userscript-template/dist/index.dev.user.js` in your Chrome and install it with your userscript manager. 13 | 14 | this userscript's meta contains `// @require file://path/to/dist/index.debug.user.js`, 15 | which take [src/index.ts](./src/index.ts) as entry point. 16 | 17 | every times you edit your metadata, you'll have to restart webpack watch server and install new UserScript in your browser again, 18 | because Tampermonkey don't read it from dist every times. 19 | 20 | 5. edit [src/index.ts](./src/index.ts), you can even import css or less files. You can use scss if you like. 21 | 6. go wo and open console, you'll see it's working. 22 | 23 | livereload is default enabled, use [this chrome extension](https://chrome.google.com/webstore/detail/jnihajbhpnppcggbcgedagnkighmdlei) 24 | 25 | ## TypeScript 26 | 27 | use typescript as normal, see [example](src/index.ts) 28 | 29 | ## dependencies 30 | 31 | There are two ways to using a package on npm. 32 | 33 | ### UserScript way 34 | 35 | like original UserScript way, you will need to add them to your [user script metadata's require section](./config/metadata.cjs#L13-L17) , and exclude them in [config/webpack.config.base.cjs](./config/webpack.config.base.cjs#L18-L20) 36 | 37 | ### Webpack way 38 | 39 | just install a package and import it in your js file. webpack will pack them with in your final production js file. 40 | 41 | ## build 42 | 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | `dist/index.prod.user.js` is the finally script. you can manually copy it to greaskfork for deploy. 48 | 49 | ### minify 50 | 51 | There is a [limit in greasyfork](https://greasyfork.org/en/help/code-rules), your code must not be obfuscated or minified. 52 | 53 | ## auto deploy 54 | 55 | [github actions](./.github/workflows/deploy.yaml#L36) will deploy production userscript to gh-pages branch. 56 | 57 | [example](https://github.com/Trim21/webpack-userscript-template/tree/gh-pages) 58 | 59 | [deployed](https://trim21.github.io/webpack-userscript-template/) 60 | 61 | You can auto use greasyfork's auto update function. 62 | -------------------------------------------------------------------------------- /src/create-btn.ts: -------------------------------------------------------------------------------- 1 | export function createBtn() { 2 | /* ========== 界面 ========== */ 3 | // === 容器 === 4 | const wrapper = document.createElement('div') 5 | wrapper.style.background = 'rgba(255, 255, 255, 0.60)' 6 | wrapper.style.borderRadius = '16px' 7 | wrapper.style.border = '1px solid #F1F1F1' 8 | wrapper.style.padding = '16px' 9 | wrapper.style.backdropFilter = 'blur(18px)' 10 | wrapper.style.boxShadow = '0px 2px 24px 0px rgba(0, 0, 0, 0.03)' 11 | 12 | wrapper.style.position = 'fixed' 13 | wrapper.style.top = '120px' 14 | wrapper.style.right = '16px' 15 | 16 | wrapper.style.display = 'flex' 17 | wrapper.style.alignItems = 'center' 18 | wrapper.style.gap = '8px' 19 | wrapper.style.zIndex = '99999' 20 | 21 | document.querySelector('body').appendChild(wrapper) 22 | 23 | // === 下载按钮 === 24 | const a = document.createElement('a') 25 | a.target = '_blank' 26 | 27 | a.style.width = a.style.height = '48px' 28 | a.style.background = 'linear-gradient(180deg, #F7F7F7 0%, #F0F0F0 100%)' 29 | a.style.borderRadius = '50%' 30 | a.style.boxShadow = 31 | '-1px -1px 2px 0px rgba(207, 207, 207, 0.25) inset, 1px 1px 2px 0px rgba(255, 255, 255, 0.30) inset' 32 | 33 | a.style.display = 'flex' 34 | a.style.justifyContent = a.style.alignItems = 'center' 35 | 36 | // 按钮按下 37 | a.onmousedown = () => { 38 | a.style.background = 'linear-gradient(0, #F7F7F7 0%, #F0F0F0 100%)' 39 | a.style.boxShadow = 40 | '-1px -1px 2px 0px rgba(219, 219, 219, 0.50) inset, 1px 1px 2px 0px rgba(229, 229, 229, 0.30) inset' 41 | } 42 | // 按钮释放 43 | a.onmouseup = () => { 44 | a.style.background = 'linear-gradient(180deg, #F7F7F7 0%, #F0F0F0 100%)' 45 | a.style.boxShadow = 46 | '-1px -1px 2px 0px rgba(207, 207, 207, 0.25) inset, 1px 1px 2px 0px rgba(255, 255, 255, 0.30) inset' 47 | } 48 | 49 | wrapper.appendChild(a) 50 | 51 | // 图标 52 | const SVG_NS = 'http://www.w3.org/2000/svg' 53 | const svgIcon = document.createElementNS(SVG_NS, 'svg') 54 | svgIcon.setAttribute('width', '24px') 55 | svgIcon.setAttribute('height', '24px') 56 | svgIcon.setAttribute('viewBox', '0 0 24 24') 57 | svgIcon.setAttribute('fill', 'none') 58 | 59 | const path = document.createElementNS(SVG_NS, 'path') 60 | path.setAttribute( 61 | 'd', 62 | 'M12 13.5L18 7.5H13.5V1.5H10.5V7.5H6L12 13.5ZM17.454 11.046L15.7725 12.7275L21.8685 15L12 18.6795L2.1315 15L8.2275 12.7275L6.546 11.046L0 13.5V19.5L12 24L24 19.5V13.5L17.454 11.046Z' 63 | ) 64 | path.setAttribute('fill', '#58CB5C') 65 | svgIcon.appendChild(path) 66 | 67 | a.appendChild(svgIcon) 68 | 69 | // === 标题 & 菜单 === 70 | const menuDiv = document.createElement('div') 71 | menuDiv.style.display = 'flex' 72 | menuDiv.style.flexDirection = 'column' 73 | menuDiv.style.gap = '8px' 74 | wrapper.appendChild(menuDiv) 75 | 76 | // 标题 77 | const titleDiv = document.createElement('div') 78 | titleDiv.innerText = 'AV 作品信息一键保存工具' 79 | titleDiv.style.color = '#282828' 80 | titleDiv.style.fontSize = '16px' 81 | titleDiv.style.fontFamily = '"PingFang SC", sans-serif' 82 | titleDiv.style.fontWeight = '600' 83 | titleDiv.style.lineHeight = '1' 84 | menuDiv.appendChild(titleDiv) 85 | 86 | // 菜单容器 87 | const toggleDiv = document.createElement('div') 88 | toggleDiv.style.display = 'flex' 89 | toggleDiv.style.justifyContent = 'space-between' 90 | toggleDiv.style.alignItems = 'center' 91 | menuDiv.appendChild(toggleDiv) 92 | 93 | // 文本 94 | const toggleText = document.createElement('div') 95 | toggleText.innerText = '打开作品页后自动保存' 96 | toggleText.style.color = '#515151' 97 | toggleText.style.fontSize = '14px' 98 | toggleText.style.fontFamily = '"PingFang SC", sans-serif' 99 | toggleText.style.fontWeight = '450' 100 | toggleText.style.lineHeight = '1' 101 | toggleDiv.appendChild(toggleText) 102 | 103 | // === 开关 === 104 | const autoSave = localStorage.getItem('av-info-saver-auto-save') // 自动保存开关状态 105 | const toggleBtnBgNormal = 'linear-gradient(180deg, #FFF 0%, #F1F1F1 100%)' // 开关默认的背景 106 | const toggleBtnBgActived = 'linear-gradient(90deg, #4CAF50 0%, #2DE035 100%)' // 开关打开时的背景 107 | const toggleBtnShadowNormal = 108 | '5px 5px 13px 0px rgba(219, 219, 219, 0.60) inset, -5px -5px 10px 0px rgba(255, 255, 255, 0.90) inset, 5px -5px 10px 0px rgba(219, 219, 219, 0.20) inset, -5px 5px 10px 0px rgba(219, 219, 219, 0.20) inset' // 开关默认的内阴影 109 | const toggleBtnShadowActived = 110 | '-5px -5px 10px 0px rgba(33, 201, 39, 0.90) inset, 5px -5px 10px 0px rgba(88, 203, 92, 0.20) inset, -5px 5px 10px 0px rgba(88, 203, 92, 0.20) inset' // 开关打开时的内阴影 111 | 112 | const toggleBtn = document.createElement('div') 113 | toggleBtn.style.width = '32px' 114 | toggleBtn.style.height = '16px' 115 | toggleBtn.style.background = autoSave ? toggleBtnBgActived : toggleBtnBgNormal 116 | toggleBtn.style.borderRadius = '100px' 117 | toggleBtn.style.border = '1px solid #58CB5C' 118 | toggleBtn.style.boxSizing = 'border-box' 119 | toggleBtn.style.boxShadow = autoSave ? toggleBtnShadowActived : toggleBtnShadowNormal 120 | toggleBtn.style.position = 'relative' 121 | toggleBtn.style.transition = 'all 200ms ease-out' 122 | toggleBtn.style.cursor = 'pointer' 123 | toggleDiv.appendChild(toggleBtn) 124 | 125 | // 开关按钮 126 | const btn = document.createElement('div') 127 | const btnTransformDefault = 'translateX(0)' // 开关按钮默认偏移量 128 | const btnTransformActived = 'translateX(16px)' // 开关按钮打开时的偏移量 129 | btn.style.position = 'absolute' 130 | btn.style.width = btn.style.height = '14px' 131 | btn.style.background = '#FFF' 132 | btn.style.borderRadius = '50%' 133 | btn.style.boxShadow = 134 | '1px 1px 2px 0px rgba(255, 255, 255, 0.30) inset, -1px -1px 2px 0px rgba(219, 219, 219, 0.50) inset' 135 | 136 | btn.style.display = 'flex' 137 | btn.style.justifyContent = btn.style.alignItems = 'center' 138 | 139 | btn.style.transform = autoSave ? btnTransformActived : btnTransformDefault 140 | btn.style.transition = 'all 200ms ease-out' 141 | toggleBtn.appendChild(btn) 142 | 143 | // 小圆点 144 | const dot = document.createElement('div') 145 | dot.style.width = dot.style.height = '4px' 146 | dot.style.background = 'radial-gradient(50% 50% at 50% 50%, #54CB59 45.31%, #20C927 100%)' 147 | dot.style.borderRadius = '50%' 148 | btn.appendChild(dot) 149 | 150 | // 开关切换 151 | toggleBtn.onclick = () => { 152 | const autoSave = localStorage.getItem('av-info-saver-auto-save') 153 | 154 | if (!autoSave) { 155 | // === 打开开关 === 156 | toggleBtn.style.background = toggleBtnBgActived 157 | toggleBtn.style.boxShadow = toggleBtnShadowActived 158 | btn.style.transform = btnTransformActived 159 | 160 | localStorage.setItem('av-info-saver-auto-save', 'yes') 161 | } else { 162 | // === 关闭开关 === 163 | toggleBtn.style.background = toggleBtnBgNormal 164 | toggleBtn.style.boxShadow = toggleBtnShadowNormal 165 | btn.style.transform = btnTransformDefault 166 | 167 | localStorage.removeItem('av-info-saver-auto-save') 168 | } 169 | } 170 | 171 | return a 172 | } 173 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createBtn } from './create-btn' 2 | import { sleep } from './utils' 3 | 4 | /* ===== Makers ===== */ 5 | import CA from '@/makers/02-censored/ca-group' 6 | import { NA } from '@/makers/03-western/naughty-america' 7 | import { OnePondo } from '@/makers/01-uncensored/1pondo' 8 | import Prestige from '@/makers/02-censored/prestige' 9 | import TokyoHot from '@/makers/01-uncensored/tokyo-hot' 10 | import Brazzers from '@/makers/03-western/brazzers' 11 | import Caribbean from '@/makers/01-uncensored/caribbean' 12 | import SOD from '@/makers/02-censored/sod' 13 | import MGS from '@/makers/04-amateur/mgstage' 14 | import Mousouzoku from '@/makers/02-censored/mousouzoku' 15 | 16 | window.onload = async () => { 17 | await main() 18 | } 19 | 20 | async function main() { 21 | const a = createBtn() // 创建按钮 22 | 23 | const domain = window.location.host 24 | 25 | const av: any = await trySwitch(domain) 26 | 27 | if (av) { 28 | a.download = av.finalName 29 | a.href = av.imgUrl 30 | 31 | // eslint-disable-next-line 32 | console.log(a.download) 33 | 34 | // 自动保存开启 35 | if (localStorage.getItem('autoSave') === 'yes') { 36 | a.click() 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * 根据不同厂商,使用不同的处理脚本,设置不同的按钮内容 43 | * @param domain 网站域名 44 | */ 45 | async function trySwitch(domain: string) { 46 | let av: any = {} 47 | 48 | switch (domain) { 49 | /* ========== 无码 ========== */ 50 | case 'www.1pondo.tv': 51 | av = await OnePondo() 52 | break 53 | case 'www.caribbeancom.com': 54 | av = await Caribbean() 55 | break 56 | case 'my.tokyo-hot.com': 57 | av = await TokyoHot() 58 | break 59 | 60 | /* ========== 有码 ========== */ 61 | // CA 集团 62 | case 'attackers.net': 63 | case 'honnaka.jp': 64 | case 'ideapocket.com': 65 | case 'madonna-av.com': 66 | case 'moodyz.com': 67 | case 'mvg.jp': 68 | case 'premium-beauty.com': 69 | case 's1s1s1.com': 70 | av = await CA() 71 | break 72 | case 'www.mousouzoku-av.com': 73 | av = await Mousouzoku() 74 | break 75 | 76 | // Prestige 集团 77 | case 'www.prestige-av.com': 78 | av = await Prestige() 79 | break 80 | 81 | // SOD 集团 82 | case 'ec.sod.co.jp': 83 | av = await SOD() 84 | break 85 | 86 | /* ========== 欧美 ========== */ 87 | case 'www.brazzers.com': 88 | await sleep(5000) 89 | av = await Brazzers() 90 | break 91 | case 'www.naughtyamerica.com': 92 | av = await NA() 93 | break 94 | 95 | /* ========== 素人 ========== */ 96 | case 'www.mgstage.com': 97 | av = await MGS() 98 | break 99 | 100 | default: 101 | break 102 | } 103 | 104 | return av 105 | } 106 | -------------------------------------------------------------------------------- /src/makers/01-uncensored/1pondo.ts: -------------------------------------------------------------------------------- 1 | import { type AVWork } from '../../typings' 2 | 3 | export async function OnePondo() { 4 | // 作品名 5 | const workName: string = (document.querySelector('.movie-overview h1') as HTMLElement).innerText 6 | 7 | // 展开信息列表 8 | const showInfo = document.querySelector('button.see-more') as HTMLElement 9 | await showInfo.click() 10 | 11 | // 作品详情列表 12 | const infoList = [...(document.querySelectorAll('.movie-detail > ul > li') || [])] 13 | /** 14 | * 从详情列表中查找返回对应信息 15 | */ 16 | function getInfo(key: string) { 17 | const infoItem = infoList?.find((info) => (info as HTMLElement)?.innerText?.includes(key)) 18 | return (infoItem?.querySelector('span:last-child') as HTMLElement)?.innerText || '' 19 | } 20 | 21 | // 系列名 22 | const seriesName: string = getInfo('シリーズ') || '' 23 | 24 | // 日期 25 | const date: string = getInfo('配信日') || '' 26 | 27 | // 演员列表 28 | const actress: string[] = getInfo('出演')?.trim()?.split(/\s/) || [] 29 | 30 | // 番号 31 | const code: string = document.URL?.match(/\d+_\d+/)?.[0] || '' 32 | 33 | // 封面地址 34 | const imgUrl: string = `https://www.1pondo.tv/assets/sample/${code}/str.jpg` 35 | 36 | // 时长 37 | const duration: string = getInfo('再生時間')?.replaceAll(':', '.') || '' 38 | 39 | const av: AVWork = { 40 | makerName: '1 Pondo', 41 | workName, 42 | seriesName, 43 | date, 44 | actress, 45 | code, 46 | imgUrl, 47 | duration, 48 | } 49 | return { ...av, finalName: final(av) } 50 | } 51 | 52 | import { datify } from '@/utils/datify' 53 | import { codify } from '@/utils/codify' 54 | import { refineTitle } from '@/utils/refine-title' 55 | import { refineActress } from '@/utils/refine-actress' 56 | import { checkIndicator } from '@/utils/check-indicator' 57 | import { checkDigit } from '@/utils/check-digit' 58 | 59 | // 序号优先的系列 60 | const DIGIT_FIRST_SERIES: string[] = [] 61 | 62 | /** 63 | * 拼接最终文件名 64 | */ 65 | function final(av: AVWork) { 66 | let finalName: string 67 | 68 | /* === 1. 处理演员名 === */ 69 | av = refineActress(av) 70 | 71 | /* === 2. 处理标题 === */ 72 | av = refineTitle(av) 73 | 74 | /* === 3. 处理系列名 === */ 75 | if (av && av.seriesName) { 76 | /* 系列作品 */ 77 | av.seriesName = av.seriesName.trim() 78 | 79 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 80 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 81 | 82 | if (numType1 || (!numType1 && numType2)) { 83 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 84 | const digitFirstSeries = DIGIT_FIRST_SERIES.find((x) => av.seriesName === x) 85 | 86 | if (digitFirstSeries) { 87 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 88 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${av.actress}(${av.code})[${ 89 | av.duration 90 | }]` 91 | } else { 92 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 93 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${av.actress} [${ 94 | av.duration 95 | }]` 96 | } 97 | } else { 98 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 99 | finalName = `${av.seriesName}(${datify(av.date)})${av.actress}(${av.code})[${av.duration}]` 100 | } 101 | } else { 102 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 103 | finalName = `(${datify(av.date)})${av.actress}(${codify(av.code)})${av.workName} [${av.duration}]` 104 | } 105 | 106 | return `【${av.makerName}】${finalName}.jpg` 107 | } 108 | -------------------------------------------------------------------------------- /src/makers/01-uncensored/caribbean.ts: -------------------------------------------------------------------------------- 1 | import { type AVWork } from '../../typings' 2 | 3 | export default async function Caribbean() { 4 | // 封面地址 5 | const bgImg: string = (document.querySelector('.vjs-poster') as HTMLElement)?.style?.backgroundImage 6 | const imgUrl: string = `https://www.caribbeancom.com${bgImg.match(/"\S+"/)?.[0]?.replaceAll('"', '')}` 7 | 8 | // 作品名 9 | const workName = (document.querySelector('.heading > h1') as HTMLElement)?.innerText 10 | 11 | // 页面数据列表 12 | let infoList = Array.from(document.querySelectorAll('.movie-info > ul > li')) 13 | /** 14 | * 从详情列表中查找返回对应信息 15 | */ 16 | function getInfo(key: string) { 17 | const infoItem = infoList?.find((info) => (info as HTMLElement)?.innerText?.includes(key)) 18 | return (infoItem?.querySelector('span:last-child') as HTMLElement)?.innerText || '' 19 | } 20 | 21 | // 系列名 22 | const seriesName: string = getInfo('シリーズ') || '' 23 | 24 | // 日期 25 | const date: string = getInfo('配信日') || '' 26 | 27 | // 演员列表 28 | const actress: string[] = getInfo('出演')?.trim()?.split(/\s/) || [] 29 | 30 | // 番号 31 | const code: string = document.URL?.match(/\d{6}-\d{3}/g)?.[0] || '' 32 | 33 | // 时长 34 | const duration: string = getInfo('再生時間')?.replaceAll(':', '.') || '' 35 | 36 | const av: AVWork = { 37 | makerName: '加勒比', 38 | workName, 39 | seriesName, 40 | date, 41 | actress, 42 | code, 43 | imgUrl, 44 | duration, 45 | } 46 | return { ...av, finalName: final(av) } 47 | } 48 | 49 | import { datify } from '@/utils/datify' 50 | import { codify } from '@/utils/codify' 51 | import { refineTitle } from '@/utils/refine-title' 52 | import { refineActress } from '@/utils/refine-actress' 53 | import { checkIndicator } from '@/utils/check-indicator' 54 | import { checkDigit } from '@/utils/check-digit' 55 | 56 | // 序号优先的系列 57 | const DIGIT_FIRST_SERIES: string[] = ['新入社員のお仕事'] 58 | 59 | /** 60 | * 拼接最终文件名 61 | */ 62 | function final(av: AVWork) { 63 | let finalName: string 64 | 65 | /* === 1. 处理演员名 === */ 66 | av = refineActress(av) 67 | const actressString = av.actress.join(' ') 68 | 69 | /* === 2. 处理标题 === */ 70 | av = refineTitle(av) 71 | 72 | /* === 3. 处理系列名 === */ 73 | if (av && av.seriesName) { 74 | /* 系列作品 */ 75 | av.seriesName = av.seriesName.trim() 76 | 77 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 78 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 79 | 80 | if (numType1 || (!numType1 && numType2)) { 81 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 82 | const digitFirstSeries = DIGIT_FIRST_SERIES.find((x) => av.seriesName === x) 83 | 84 | if (digitFirstSeries) { 85 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 86 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})[${ 87 | av.duration 88 | }]` 89 | } else { 90 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 91 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString} [${ 92 | av.duration 93 | }]` 94 | } 95 | } else { 96 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 97 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})[${av.duration}]` 98 | } 99 | } else { 100 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 101 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName} [${av.duration}]` 102 | } 103 | 104 | return `【${av.makerName}】${finalName}.jpg` 105 | } 106 | -------------------------------------------------------------------------------- /src/makers/01-uncensored/tokyo-hot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 手动维护系列列表,从作品名中识别 3 | */ 4 | 5 | import { AVWork } from '@/typings' 6 | import { finalUncensored } from '@/utils/final' 7 | 8 | // 系列列表 9 | const seriesList = ['鬼逝', 'Wカン', '東熱激情 淫乱女教師中出授業 特集', '東熱激情 密着パンスト24時!特集'] 10 | function getSeriesName(workName: string) { 11 | return seriesList?.find((item) => workName?.includes(item)) 12 | } 13 | 14 | export default async function TokyoHot() { 15 | // 作品名 16 | const workName: string = (document.querySelector('.contents > h2') as HTMLElement)?.innerText 17 | 18 | // 封面地址 19 | let imgUrl: string = document.querySelector('video')?.poster 20 | try { 21 | const res = await fetch(imgUrl) 22 | const blob = await res.blob() 23 | imgUrl = window.URL.createObjectURL(blob) 24 | } catch (e) { 25 | throw new Error(`[下载图片失败] ${e}`) 26 | } 27 | 28 | // 系列名 29 | const seriesName: string = getSeriesName(workName) 30 | 31 | // 作品信息列表 32 | const keyList = [...document.querySelectorAll('dl.info > dt')] 33 | function getInfo(key: string) { 34 | const keyItem = keyList?.find((item) => (item as HTMLElement)?.innerText?.includes(key)) 35 | if (!keyItem) return 36 | 37 | const valueItem = keyItem?.nextElementSibling 38 | if (!valueItem) return 39 | 40 | return key === '出演者' 41 | ? Array.from(valueItem?.querySelectorAll('a'), (x) => x?.innerText) 42 | : [(valueItem as HTMLElement)?.innerText] 43 | } 44 | 45 | // 日期 46 | const date: string = getInfo('配信開始日')?.[0] 47 | 48 | // 演员 49 | const actress: string[] = getInfo('出演者') 50 | 51 | // 番号 52 | const code: string = getInfo('作品番号')?.[0] 53 | 54 | // 时长 55 | const duration: string = getInfo('収録時間')?.[0]?.replaceAll(':', '.') 56 | 57 | // 清晰度和大小 58 | const sizeList = document.querySelectorAll('.download')?.[1].querySelectorAll('.dbox') 59 | let resolutions: string[] = [] 60 | sizeList.forEach((sizeItem) => { 61 | const format = sizeItem.querySelector('h4').innerText.match(/MP4|WMV/g)?.[0] 62 | const size = sizeItem.querySelector('h4').innerText.match(/\d+.\d+/g)?.[0] 63 | const resolution = sizeItem.querySelector('p').innerText.match(/x(\d+)/)?.[1] 64 | 65 | if (parseInt(resolution) >= 720) { 66 | resolutions.push(`${parseFloat(size).toFixed(1)}GB-${format}-${resolution}p`) 67 | } 68 | }) 69 | 70 | const av: AVWork = { 71 | makerName: '東京熱', 72 | workName, 73 | seriesName, 74 | date, 75 | actress, 76 | code, 77 | imgUrl, 78 | duration, 79 | resolutions, 80 | } 81 | return { ...av, finalName: finalUncensored(av) } 82 | } 83 | -------------------------------------------------------------------------------- /src/makers/02-censored/ca-group.ts: -------------------------------------------------------------------------------- 1 | import { getInfo } from '@/utils/get-info' 2 | import { type AVWork } from '@/typings' 3 | 4 | /** 5 | * 根据域名判断厂商名 6 | * @returns 7 | */ 8 | function getMakerName() { 9 | const host: string = window.location.host 10 | let makerName: string = '' 11 | 12 | switch (host) { 13 | case 'attackers.net': 14 | makerName = 'Attackers' 15 | break 16 | case 'ideapocket.com': 17 | makerName = 'Idea Pocket' 18 | break 19 | case 'madonna-av.com': 20 | makerName = 'Madonna' 21 | break 22 | case 'moodyz.com': 23 | makerName = 'MOODYZ' 24 | break 25 | case 'premium-beauty.com': 26 | makerName = 'Premium' 27 | break 28 | case 's1s1s1.com': 29 | makerName = 'S1' 30 | break 31 | case 'honnaka.jp': 32 | makerName = '本中' 33 | break 34 | case 'mvg.jp': 35 | makerName = 'MVG' 36 | break 37 | } 38 | 39 | return makerName 40 | } 41 | 42 | export default async function CA() { 43 | // 厂商名 44 | const makerName = getMakerName() 45 | 46 | // 封面地址 47 | const swiperSlide = document.querySelectorAll('.p-slider img') 48 | const img = (swiperSlide?.[1] || swiperSlide?.[0]) as HTMLImageElement 49 | let imgUrl: string = img?.src || img?.dataset?.src 50 | // 跨域获取 51 | const res = await fetch(imgUrl) 52 | try { 53 | const blob = await res.blob() 54 | imgUrl = window.URL.createObjectURL(blob) 55 | } catch (e) { 56 | throw new Error(`[下载图片失败] ${e}`) 57 | } 58 | 59 | // 作品名 60 | const workName: string = (document.querySelector('h2.p-workPage__title') as HTMLElement)?.innerText 61 | 62 | // 页面数据列表 63 | let infoList = [...document.querySelectorAll('.p-workPage__table > .item')] 64 | 65 | const tempAV = getInfo(infoList, '.item') 66 | 67 | const av: AVWork = { 68 | makerName, 69 | workName, 70 | imgUrl, 71 | ...tempAV, 72 | } 73 | return { ...av, finalName: final(av) } 74 | } 75 | 76 | import { datify } from '@/utils/datify' 77 | import { codify } from '@/utils/codify' 78 | import { refineTitle } from '@/utils/refine-title' 79 | import { refineActress } from '@/utils/refine-actress' 80 | import { checkIndicator } from '@/utils/check-indicator' 81 | import { checkDigit } from '@/utils/check-digit' 82 | 83 | // 序号优先的系列 84 | const DIGIT_FIRST_SERIES: string[] = [] 85 | 86 | /** 87 | * 拼接最终文件名 88 | */ 89 | function final(av: AVWork) { 90 | let finalName: string 91 | 92 | /* === 1. 处理演员名 === */ 93 | av = refineActress(av) 94 | const actressString = av.actress.join(' ') 95 | 96 | /* === 2. 处理标题 === */ 97 | av = refineTitle(av) 98 | 99 | /* === 3. 处理番号 === */ 100 | av.code = codify(av.code) 101 | 102 | /* === 3. 处理系列名 === */ 103 | if (av && av.seriesName) { 104 | /* 系列作品 */ 105 | av.seriesName = av.seriesName.trim() 106 | 107 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 108 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 109 | 110 | if (numType1 || (!numType1 && numType2)) { 111 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 112 | const digitFirstSeries = DIGIT_FIRST_SERIES.find((x) => av.seriesName === x) 113 | 114 | if (digitFirstSeries) { 115 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 116 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code}` 117 | } else { 118 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 119 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}` 120 | } 121 | } else { 122 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 123 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})` 124 | } 125 | } else { 126 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 127 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}` 128 | } 129 | 130 | return `【${av.makerName}】${finalName}.jpg` 131 | } 132 | -------------------------------------------------------------------------------- /src/makers/02-censored/mousouzoku.ts: -------------------------------------------------------------------------------- 1 | import { type AVWork } from '@/typings' 2 | 3 | // 官网没有系列字段,从作品名中识别 4 | const series = ['OLスーツ倶楽部'] 5 | 6 | export default async function Mousouzoku() { 7 | // 作品名 8 | const workName: string = (document.querySelector('h1.ttl-works') as HTMLElement)?.innerText 9 | 10 | // 封面地址 11 | const imgUrl: string = (document.querySelector('.tmb-img') as HTMLBaseElement)?.href 12 | 13 | // 页面数据列表 14 | let infoWrap = document.querySelector('dl.bx-info') 15 | 16 | /** 17 | * 根据属性名查找对应的属性值 18 | */ 19 | function getInfo(key: string) { 20 | const infoKey = [...infoWrap.querySelectorAll('dt')]?.find((item) => 21 | (item as HTMLElement)?.innerText?.includes(key) 22 | ) 23 | if (!infoKey) return 24 | 25 | const valueList = Array.from( 26 | infoKey?.nextElementSibling?.querySelectorAll('p'), 27 | (x) => (x as HTMLElement)?.innerText 28 | ) 29 | 30 | return valueList?.length > 1 || key === '出演者' ? valueList : valueList?.[0] 31 | } 32 | 33 | // 厂商名 34 | const makerName: string = getInfo('メーカー') as string 35 | 36 | // 系列名 37 | const seriesName: string = series?.find(x => workName?.includes(x)) 38 | 39 | // 日期 40 | const date: string = getInfo('発売日') as string 41 | 42 | // 演员列表 43 | const actress = getInfo('出演者') as string[] 44 | 45 | // 番号 46 | const code: string = getInfo('品番') as string 47 | 48 | const av: AVWork = { 49 | makerName, 50 | workName, 51 | seriesName, 52 | date, 53 | actress, 54 | code, 55 | imgUrl, 56 | } 57 | return { ...av, finalName: final(av) } 58 | } 59 | 60 | import { datify } from '@/utils/datify' 61 | import { codify } from '@/utils/codify' 62 | import { refineTitle } from '@/utils/refine-title' 63 | import { refineActress } from '@/utils/refine-actress' 64 | import { checkIndicator } from '@/utils/check-indicator' 65 | import { checkDigit } from '@/utils/check-digit' 66 | 67 | // 序号优先的系列 68 | const DIGIT_FIRST_SERIES: string[] = [] 69 | 70 | /** 71 | * 拼接最终文件名 72 | */ 73 | function final(av: AVWork) { 74 | let finalName: string 75 | 76 | /* === 处理厂商名 === */ 77 | av.makerName = av.makerName.replace('/', '·') // 替换子厂商后的左斜杠 78 | 79 | /* === 1. 处理演员名 === */ 80 | av = refineActress(av) 81 | const actressString = av.actress.join(' ') 82 | 83 | /* === 2. 处理标题 === */ 84 | av = refineTitle(av) 85 | 86 | /* === 3. 处理番号 === */ 87 | av.code = codify(av.code) 88 | 89 | /* === 3. 处理系列名 === */ 90 | if (av && av.seriesName) { 91 | /* 系列作品 */ 92 | av.seriesName = av.seriesName.trim() 93 | 94 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 95 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 96 | 97 | if (numType1 || (!numType1 && numType2)) { 98 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 99 | const digitFirstSeries = DIGIT_FIRST_SERIES.find((x) => av.seriesName === x) 100 | 101 | if (digitFirstSeries) { 102 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 103 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})` 104 | } else { 105 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 106 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}` 107 | } 108 | } else { 109 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 110 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})` 111 | } 112 | } else { 113 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 114 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}` 115 | } 116 | 117 | return `【${av.makerName}】${finalName}.jpg` 118 | } 119 | -------------------------------------------------------------------------------- /src/makers/02-censored/prestige.ts: -------------------------------------------------------------------------------- 1 | import { AVWork } from '@/typings' 2 | import { finalCensored } from '@/utils/final' 3 | 4 | // 厂商名转换列表 5 | const MAKER_TRANS: { [name: string]: string } = { 6 | プレステージ: 'Prestige', 7 | } 8 | 9 | export default async function Prestige() { 10 | // 作品名 11 | const spanText = (document.querySelector('.min-h-main h1 span') as HTMLElement)?.innerText 12 | const workName: string = (document.querySelector('.min-h-main h1') as HTMLElement)?.innerText 13 | ?.replace(spanText, '') 14 | .trim() 15 | 16 | // 封面地址 17 | const imgList = document.querySelectorAll('img.swiper-picture') 18 | const imgUrl = ((imgList?.[1] || document.querySelector('img.swiper-picture')) as HTMLImageElement)?.src?.replace( 19 | /\?\S+/, 20 | '' 21 | ) 22 | 23 | // 作品详情列表 24 | const infoList = [...document.querySelectorAll('.text-xl > .hidden > div.flex')] 25 | // 查找对应的详情 26 | function getInfo(key: string) { 27 | const infoItem = infoList?.find((info) => (info as HTMLElement)?.innerText?.includes(key)) 28 | if (!infoItem) return 29 | 30 | const list = infoItem?.querySelectorAll('div.flex-1 a')?.length 31 | ? [...infoItem.querySelectorAll('div.flex-1 a')] 32 | : [...infoItem.querySelectorAll('div.flex-1 p')] 33 | const valueList = Array.from(list, (item) => (item as HTMLElement)?.innerText) 34 | return key === '出演者' ? valueList : valueList?.[0] 35 | } 36 | 37 | // 厂商名 38 | const makerValue: string = getInfo('メーカー') as string 39 | const makerName: string = MAKER_TRANS[makerValue as string] || makerValue 40 | 41 | // 系列名 42 | const seriesName: string = getInfo('シリーズ') as string 43 | 44 | // 日期 45 | const date: string = getInfo('発売日') as string 46 | 47 | // 番号 48 | const code: string = getInfo('品番') as string 49 | 50 | // 演员 51 | const actress: string[] = getInfo('出演者') as string[] 52 | 53 | let av: AVWork = { 54 | makerName, 55 | workName, 56 | seriesName, 57 | date, 58 | actress, 59 | code, 60 | imgUrl, 61 | } 62 | return { ...av, finalName: finalCensored(av, DIGIT_FIRST_SERIES, NO_End_SPACE_SERIES) } 63 | } 64 | 65 | // 编号在前的系列 66 | const DIGIT_FIRST_SERIES: string[] = ['人妻さんいらっしゃい!', '職女。'] 67 | 68 | // 无需加尾空格的系列 69 | const NO_End_SPACE_SERIES: string[] = ['人妻さんいらっしゃい!', '職女。'] 70 | -------------------------------------------------------------------------------- /src/makers/02-censored/sod.ts: -------------------------------------------------------------------------------- 1 | import { getInfo } from '@/utils/get-info' 2 | import { finalCensored } from '@/utils/final' 3 | import { AVWork } from '@/typings' 4 | 5 | export default async function SOD() { 6 | // 作品名 7 | const workName: string = (document.querySelector('#videos_head > h1 + h1') as HTMLInputElement).innerText 8 | 9 | // 封面地址 10 | let imgUrl: string = (document.querySelector('.videos_samimg > a') as HTMLBaseElement)?.href?.replace( 11 | 'http:', 12 | 'https:' 13 | ) 14 | // 跨域获取 15 | try { 16 | const res = await fetch(imgUrl) 17 | const blob = await res.blob() 18 | imgUrl = window.URL.createObjectURL(blob) 19 | } catch (e) { 20 | throw new Error(`[下载图片失败] ${e}`) 21 | } 22 | 23 | // 页面数据列表 24 | const infoList = [...document.querySelectorAll('#v_introduction tr')] 25 | 26 | const tempAV = getInfo(infoList, '.v_intr_tx', { dateKey: '発売年月日', actressKey: '出演者' }, '.v_intr_tx a') 27 | 28 | const av = { 29 | makerName: 'SODクリエイト', 30 | workName, 31 | imgUrl, 32 | ...tempAV, 33 | } 34 | return { ...av, finalName: final(av) } 35 | } 36 | 37 | // 系列字段中不是系列的名称 38 | const NotRealSeries = ['おねだりプリン'] 39 | // 处理系列名 40 | function refineSeries(seriesName: string) { 41 | const subList = NotRealSeries.filter((x) => x?.includes(seriesName) || seriesName.includes(x)) 42 | 43 | let newSeriesName = seriesName 44 | subList.forEach((item: string) => { 45 | newSeriesName = newSeriesName?.replace(item, '') 46 | }) 47 | return newSeriesName 48 | } 49 | 50 | function final(av: AVWork) { 51 | av.seriesName = refineSeries(av.seriesName) 52 | return finalCensored(av) 53 | } 54 | -------------------------------------------------------------------------------- /src/makers/03-western/brazzers.ts: -------------------------------------------------------------------------------- 1 | import { AVWork } from '@/typings' 2 | 3 | export default async function Brazzers() { 4 | // 信息容器 5 | const infoDiv = document.querySelector( 6 | '#root > div > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div > div > section > div > div' 7 | ) 8 | 9 | // 作品名 10 | const workName: string = (infoDiv?.querySelector('h2.font-secondary') as HTMLElement)?.innerText 11 | 12 | // 日期 13 | const date: string = new Date((infoDiv?.querySelector('div:nth-child(2)') as HTMLElement)?.innerText).toLocaleDateString('zh-CN') 14 | 15 | // 演员列表 16 | const actress: string[] = infoDiv?.querySelectorAll('div')?.[3]?.innerText?.split(', ') || [] 17 | 18 | // 封面地址 19 | let imgUrl = ((document.querySelector('video + div') as HTMLElement).style as any)['background-image'] 20 | imgUrl = imgUrl.match(/"(\S+)"/)?.[1] 21 | // 跨域获取 22 | const res = await fetch(imgUrl) 23 | const blob = await res.blob() 24 | imgUrl = window.URL.createObjectURL(blob) 25 | 26 | const av: AVWork = { 27 | makerName: 'Brazzers', 28 | workName, 29 | seriesName: '系列名待补充', 30 | date, 31 | actress, 32 | imgUrl, 33 | } 34 | return { ...av, finalName: final(av) } 35 | } 36 | 37 | import { datify } from '@/utils/datify' 38 | 39 | /** 40 | * 拼接最终文件名 41 | */ 42 | function final(av: AVWork) { 43 | //【厂商】(日期)演员 - 作品名 44 | av.seriesName = av.seriesName.trim() 45 | const finalName: string = `${av.seriesName}(${datify(av.date)})${av.actress.join(', ')} - ${av.workName}` 46 | 47 | return `【${av.makerName}】${finalName}.jpg` 48 | } 49 | -------------------------------------------------------------------------------- /src/makers/03-western/naughty-america.ts: -------------------------------------------------------------------------------- 1 | import { AVWork } from '@/typings' 2 | import { firstLetterUppercase } from '@/utils/first-letter-uppercase' 3 | 4 | export async function NA() { 5 | // 封面地址 6 | let imgUrl: string = `https:${ 7 | (document.querySelector('.play-trailer > picture > source') as HTMLElement)?.dataset?.srcset 8 | }`.replace('webp', 'jpg') 9 | const res = await fetch(imgUrl) 10 | try { 11 | const blob = await res.blob() 12 | imgUrl = window.URL.createObjectURL(blob) 13 | } catch (e) { 14 | throw new Error(`[下载图片失败] ${e}`) 15 | } 16 | 17 | // 页面数据容器 18 | const sceneInfo = document.querySelector('.scene-info') 19 | 20 | // 作品名 21 | const workName: string = firstLetterUppercase((sceneInfo.querySelector('.scene-title') as HTMLElement)?.innerText) 22 | 23 | // 系列名 24 | const seriesName: string = firstLetterUppercase((sceneInfo.querySelector('.site-title') as HTMLElement)?.innerText) 25 | 26 | // 日期 27 | const date: string = new Date((sceneInfo.querySelector('.entry-date') as HTMLElement)?.innerText).toLocaleDateString( 28 | 'zh-CN' 29 | ) 30 | 31 | // 演员列表 32 | const actress: string[] = Array.from(sceneInfo.querySelectorAll('.performer-list > a'), (a) => 33 | firstLetterUppercase((a as HTMLElement)?.innerText) 34 | ) 35 | 36 | // 时长 37 | const duration = (sceneInfo.querySelector('.duration') as HTMLElement)?.innerText 38 | ?.match(/\d+\smin/)?.[0] 39 | .replace(' ', '') 40 | 41 | const labels = sceneInfo.querySelectorAll('.flag-bg') 42 | const resolutions: string[] = Array.from(labels, (label) => 43 | (label as HTMLElement)?.innerText === 'HD' ? '1080p' : (label as HTMLElement)?.innerText 44 | ) 45 | 46 | const av: AVWork = { 47 | makerName: 'Naughty America', 48 | workName, 49 | seriesName, 50 | date, 51 | actress, 52 | duration, 53 | resolutions, 54 | imgUrl, 55 | } 56 | return { ...av, finalName: final(av) } 57 | } 58 | 59 | import { datify } from '@/utils/datify' 60 | 61 | /** 62 | * 拼接最终文件名 63 | */ 64 | function final(av: AVWork) { 65 | //【厂商】(日期)演员 - 作品名 66 | av.seriesName = av.seriesName.trim() 67 | const finalName: string = `${av.seriesName}(${datify(av.date)})${av.actress.join(', ')} - ${av.workName} [${ 68 | av.duration 69 | }; ${av.resolutions?.join(', ')}]` 70 | 71 | return `【${av.makerName}】${finalName}.jpg` 72 | } 73 | -------------------------------------------------------------------------------- /src/makers/04-amateur/mgstage.ts: -------------------------------------------------------------------------------- 1 | import { type AVWork } from '@/typings' 2 | 3 | export default async function MGS() { 4 | // 作品名 5 | const workName: string = (document.querySelector('.common_detail_cover h1') as HTMLElement)?.innerText 6 | 7 | // 封面地址 8 | let imgUrl: string = (document.querySelector('.detail_data h2 img') as HTMLImageElement)?.src?.replace( 9 | /\w{2}_\w{1,2}_/, 10 | 'pb_e_' 11 | ) 12 | 13 | const res = await fetch(imgUrl) 14 | try { 15 | const blob = await res.blob() 16 | imgUrl = window.URL.createObjectURL(blob) 17 | } catch (e) { 18 | throw new Error(`[下载图片失败] ${e}`) 19 | } 20 | 21 | // 作品详情列表 22 | const infoList = [...document.querySelectorAll('.detail_data > table:last-child > tbody > tr')] 23 | /** 24 | * 从详情列表中查找返回对应信息 25 | */ 26 | function getInfo(key: string) { 27 | const infoItem = infoList.find((info) => (info?.querySelector('th') as HTMLElement)?.innerText?.includes(key)) 28 | 29 | let info: string = '' 30 | if (infoItem) { 31 | if (key === '出演') { 32 | /** 33 | * 演员名有链接:取 标签 34 | * 演员名无链接:取 标签 35 | */ 36 | const hasLink = infoItem?.querySelectorAll('td > a')?.length 37 | info = Array.from(hasLink ? infoItem?.querySelectorAll('td > a') : infoItem?.querySelectorAll('td'), (a) => 38 | (a as HTMLElement)?.innerText?.trim() 39 | )?.join(', ') 40 | } else { 41 | info = infoItem?.querySelector('td')?.innerText 42 | } 43 | } 44 | return info 45 | } 46 | 47 | // 厂商名 48 | const makerName: string = getInfo('メーカー') 49 | 50 | // 系列名 51 | const seriesName: string = getInfo('シリーズ') 52 | 53 | // 日期 54 | const date: string = getInfo('配信開始日') 55 | 56 | // 演员列表 57 | const actress: string[] = getInfo('出演')?.split(', ') 58 | 59 | // 番号 60 | const code: string = getInfo('品番') 61 | 62 | // 时长 63 | const duration: string = getInfo('収録時間') 64 | 65 | const av = { 66 | workName, 67 | imgUrl, 68 | makerName, 69 | seriesName, 70 | date, 71 | actress, 72 | actressRealName: 'xxx', 73 | code, 74 | duration, 75 | } 76 | return { ...av, finalName: final(av) } 77 | } 78 | 79 | import { datify } from '@/utils/datify' 80 | import { codify } from '@/utils/codify' 81 | import { refineTitle } from '@/utils/refine-title' 82 | // import { refineActress } from '@/utils/refine-actress' 83 | import { checkIndicator } from '@/utils/check-indicator' 84 | import { checkDigit } from '@/utils/check-digit' 85 | 86 | // 序号优先的系列 87 | const DIGIT_FIRST_SERIES: string[] = ['ラグジュTV', 'マジ軟派、初撮。', '働くドMさん.'] 88 | 89 | // 厂商名转换列表 90 | const MAKER_TRANS: { [name: string]: string } = { 91 | 'プレステージプレミアム(PRESTIGE PREMIUM)': 'Prestige Premium', 92 | } 93 | 94 | /** 95 | * 是否需要移除系列名后的空格 96 | */ 97 | function seriesEndSpace(name: string) { 98 | const noEndSpaceSeries = ['マジ軟派、初撮。'] 99 | return noEndSpaceSeries?.includes(name) ? '' : ' ' 100 | } 101 | 102 | /** 103 | * 拼接最终文件名 104 | */ 105 | function final(av: AVWork) { 106 | let finalName: string 107 | 108 | /* === 1. 处理演员名 === */ 109 | // av = refineActress(av) 110 | const actressString = av.actress.join(', ') 111 | 112 | /* === 2. 处理标题 === */ 113 | av = refineTitle(av) 114 | 115 | /* === 3. 处理系列名 === */ 116 | if (av && av.seriesName) { 117 | /* 系列作品 */ 118 | av.seriesName = av.seriesName.trim() 119 | 120 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 121 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 122 | 123 | if (numType1 || (!numType1 && numType2)) { 124 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 125 | const digitFirstSeries = DIGIT_FIRST_SERIES.find((x) => av.seriesName === x) 126 | 127 | if (digitFirstSeries) { 128 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)素人女优真实姓名 [时长] 129 | finalName = `${av.seriesName}${seriesEndSpace(av?.seriesName)}${numType1 || numType2}(${datify( 130 | av.date 131 | )})${actressString}(${av.code})${av.actressRealName} [${av.duration}]` 132 | } else { 133 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名(素人女优真实姓名)[时长] 134 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}(${ 135 | av.actressRealName 136 | })[${av.duration}]` 137 | } 138 | } else { 139 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)素人女优真实姓名 [时长] */ 140 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})${av.actressRealName} [${ 141 | av.duration 142 | }]` 143 | } 144 | } else { 145 | /* 非系列作品:【厂商】(日期)演员(番号)作品名(素人女优真实姓名)[时长] */ 146 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}(${av.actressRealName})[${ 147 | av.duration 148 | }]` 149 | } 150 | 151 | return `【${MAKER_TRANS?.[av.makerName] || av.makerName}】${finalName}.jpg`.replaceAll('/', '-') 152 | } 153 | -------------------------------------------------------------------------------- /src/style/main.less: -------------------------------------------------------------------------------- 1 | @background_color: yellow; 2 | 3 | body { 4 | background-color: @background_color; 5 | z-index: 20; 6 | } 7 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less' 2 | 3 | export interface AVWork { 4 | makerName: string // 厂商名 5 | workName: string // 作品名 6 | seriesName?: string // 系列名 7 | date: string // 发布日期 8 | actress: string[] // 演员列表 9 | actressRealName?: string // 素人女优的真实姓名 10 | code?: string // 番号 11 | imgUrl: string // 封面图片地址 12 | duration?: string // 时长 13 | resolutions?: string[] // 清晰度 14 | } 15 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' 2 | import adapter from 'axios-userscript-adapter' 3 | 4 | export function get> (url: string, config?: Exclude, 'adapter'>): Promise { 5 | return axios.get(url, { 6 | adapter, 7 | ...config 8 | }) 9 | } 10 | 11 | export function post> (url: string, data?: any, config?: Exclude, 'adapter'>): Promise { 12 | return axios.post(url, data, { 13 | adapter, 14 | ...config 15 | }) 16 | } 17 | 18 | // 代码暂停执行 19 | export function sleep(ms: number) { 20 | return new Promise((resolve) => setTimeout(resolve, ms)) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/check-digit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 检测系列作品的标题中是否包含纯数字编号 3 | * 4 | * @param {string} workName 作品名 5 | * @param {string} seriesName 系列名 6 | * @returns { string | undefined } 检测出的纯数字编号或 undefined(未检测出) 7 | */ 8 | export function checkDigit(workName: string, seriesName: string): string | undefined { 9 | const seriesNameNoSpace = seriesName.replaceAll(' ', '') 10 | 11 | /* 有纯数字编号也肯定是在标题中的系列名后跟着纯数字 */ 12 | const reg = new RegExp(`\\s*${seriesNameNoSpace}\\s*(\\d+)`, 'g') 13 | const matches = [...workName.replaceAll(' ', '').matchAll(reg)] 14 | 15 | for (const match of matches) { 16 | if (match.length >= 2) { 17 | // 说明匹配到了捕获组,将其返回(系列编号) 18 | return match[1] 19 | } 20 | } 21 | 22 | // 不包含纯数字 23 | return undefined 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/check-indicator.ts: -------------------------------------------------------------------------------- 1 | // 编号标识列表 2 | const INDICATORS = ['vol.', 'Vol.', 'VOL.', 'Case.', 'File.', 'FILE.', 'Part', 'Talk.', 'パート'] 3 | 4 | /** 5 | * 检测系列作品的标题中是否包含有标识的编号 6 | * 7 | * @param {string} workName 作品名 8 | * @returns {string | undefined} 检测出的标识和编号或 undefined 9 | */ 10 | export function checkIndicator(workName: string): string | undefined { 11 | // 是否包含编号标识 12 | const indicator = INDICATORS.find((d) => workName.includes(d)) 13 | 14 | if (indicator) { 15 | // 包含编号标识:返回标识和编号部分 16 | 17 | // 标识符和编号部分 18 | const regExp = new RegExp(indicator + '\\s{0,3}\\d+') 19 | const num = workName.match(regExp)?.[0] 20 | 21 | return num 22 | } 23 | 24 | // 不包含编号标识:返回 undefined 25 | return undefined 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/codify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将不同格式的番号标准化为统一格式 3 | * 4 | * @param {string} code 传入的未处理的番号 5 | * @returns {string} 标准化处理后的番号 6 | */ 7 | export function codify(code: string): string { 8 | /** 9 | * 无需处理 10 | * - 東京熱番号 11 | * - 包含连字符的番号 12 | */ 13 | if (/n\d+/.test(code) || /-/.test(code)) { 14 | return code 15 | } 16 | 17 | /* 有字母的番号 */ 18 | if (/[a-z,A-Z]/.test(code)) { 19 | const codeNum = code.match(/\d+/) // 数字 20 | const codeCap = code.match(/[A-Z,a-z]+/) // 字母 21 | 22 | let newCodeNum 23 | // 处理番号数字位大于3,有多个0的情况 24 | if (codeNum[0] && codeNum[0].length > 3) { 25 | newCodeNum = codeNum[0].slice(-3) 26 | code = code.replace(codeNum[0], newCodeNum) 27 | } 28 | return `${codeCap}-${codeNum}`.toUpperCase() 29 | } 30 | 31 | /* 其他情况 */ 32 | return code 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/datify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将不同格式的発売日标准化为统一格式 3 | * YYYY.MM.DD 4 | * 5 | * @param {string} date 未处理的日期字符串 6 | * @returns {string} 处理后的标准日期格式 7 | */ 8 | export function datify(date: string): string { 9 | let newDate 10 | 11 | // 符合标准格式:直接返回 12 | if (/\d{4}\.\d{2}\.\d{2}/.test(date)) { 13 | newDate = date 14 | return newDate 15 | } 16 | 17 | // 使用 “/” 作为分隔符的格式 18 | if (/\d+\/\d+\/\d+/.test(date)) { 19 | newDate = new Date(date) 20 | .toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }) 21 | .replaceAll('/', '.') 22 | return newDate 23 | } 24 | 25 | // 含有汉字的格式 26 | let year = date.match(/(\d{4})年/)?.[1] 27 | let month = date.match(/(\d{1,2})月/)?.[1] 28 | let day = date.match(/(\d{1,2})日/)?.[1] 29 | newDate = `${year}.${month?.length === 1 ? '0' + month : month}.${day?.length === 1 ? '0' + day : day}` 30 | return newDate 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/final.ts: -------------------------------------------------------------------------------- 1 | import { datify } from './datify' 2 | import { codify } from './codify' 3 | import { refineTitle } from './refine-title' 4 | import { refineActress } from './refine-actress' 5 | import { checkIndicator } from './check-indicator' 6 | import { checkDigit } from './check-digit' 7 | import { AVWork } from '../typings' 8 | 9 | /** 10 | * 是否需要移除系列名后的空格 11 | * @param list 需要移除的系列列表 12 | * @param seriesName 当前作品系列名 13 | */ 14 | function seriesEndSpace(list: string[], seriesName: string) { 15 | return list?.includes(seriesName) ? '' : ' ' 16 | } 17 | 18 | /** 19 | * 拼接最终文件名(无码) 20 | */ 21 | export function finalUncensored(av: AVWork, DIGIT_FIRST_SERIES: string[] = []) { 22 | let finalName: string 23 | 24 | /* === 1. 处理演员名 === */ 25 | av = refineActress(av) 26 | const actressString = av.actress.join(' ') 27 | 28 | /* === 2. 处理标题 === */ 29 | av = refineTitle(av) 30 | 31 | /* === 3. 处理系列名 === */ 32 | if (av && av.seriesName) { 33 | /* 系列作品 */ 34 | av.seriesName = av.seriesName.trim() 35 | 36 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 37 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 38 | 39 | if (numType1 || (!numType1 && numType2)) { 40 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 41 | const digitFirstSeries = DIGIT_FIRST_SERIES.find((x) => av.seriesName === x) 42 | 43 | if (digitFirstSeries) { 44 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 45 | finalName = `${av.seriesName} ${numType1 || numType2}(${datify(av.date)})${actressString}(${av.code})` 46 | } else { 47 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 48 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString} ` 49 | } 50 | } else { 51 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 52 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})` 53 | } 54 | } else { 55 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 56 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName} ` 57 | } 58 | 59 | return `【${av.makerName}】${finalName}[${av.duration}${ 60 | av?.resolutions ? `; ${av?.resolutions?.join(', ')}` : '' 61 | }].jpg` 62 | } 63 | 64 | /** 65 | * 拼接最终文件名(有码) 66 | */ 67 | export function finalCensored(av: AVWork, digitFirstSeriesList: string[] = [], noSeriesEndSpaceList: string[] = []) { 68 | let finalName: string 69 | 70 | /* === 1. 处理演员名 === */ 71 | av = refineActress(av) 72 | const actressString = av.actress.join(' ') 73 | 74 | /* === 2. 处理标题 === */ 75 | av = refineTitle(av) 76 | 77 | /* === 3. 处理番号 === */ 78 | av.code = codify(av.code) 79 | 80 | /* === 3. 处理系列名 === */ 81 | if (av && av.seriesName) { 82 | /* 系列作品 */ 83 | av.seriesName = av.seriesName.trim() 84 | 85 | const numType1 = checkIndicator(av.workName) // 标识 + 编号 86 | const numType2 = checkDigit(av.workName, av.seriesName) // 纯数字编号 87 | 88 | if (numType1 || (!numType1 && numType2)) { 89 | /* 包含编号标识 || 不包含编号标识但包含纯数字 */ 90 | const digitFirstSeries = digitFirstSeriesList.find((x) => av.seriesName === x) 91 | 92 | if (digitFirstSeries) { 93 | // 系列编号在前:【厂商】系列名 编号(日期)演员名(番号)[时长] 94 | finalName = `${av.seriesName}${seriesEndSpace(noSeriesEndSpaceList, av?.seriesName)}${ 95 | numType1 || numType2 96 | }(${datify(av.date)})${actressString}(${av.code})` 97 | } else { 98 | // 系列编号在后:【厂商】系列名(日期)编号(番号)演员名 [时长] 99 | finalName = `${av.seriesName}(${datify(av.date)})${numType1 || numType2}(${av.code})${actressString}` 100 | } 101 | } else { 102 | /* 不含编号标识:【厂商】系列名(日期)演员名(番号)[时长] */ 103 | finalName = `${av.seriesName}(${datify(av.date)})${actressString}(${av.code})` 104 | } 105 | } else { 106 | /* 非系列作品:【厂商】(日期)演员(番号)作品名 [时长] */ 107 | finalName = `(${datify(av.date)})${actressString}(${codify(av.code)})${av.workName}` 108 | } 109 | 110 | return `【${av.makerName}】${finalName}.jpg` 111 | } 112 | -------------------------------------------------------------------------------- /src/utils/first-letter-uppercase.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将句子转换为每个单词首字母大写、其他字母小写的格式 3 | */ 4 | 5 | export function firstLetterUppercase(original: string) { 6 | return original 7 | ?.split(/\s/) 8 | .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) 9 | ?.join(' ') 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/get-info.ts: -------------------------------------------------------------------------------- 1 | interface InfoKeyList { 2 | seriesKey?: string 3 | dateKey?: string 4 | actressKey?: string 5 | codeKey?: string 6 | durationKey?: string 7 | } 8 | 9 | const DEFAULT_KEY_LIST = { 10 | seriesKey: 'シリーズ', 11 | dateKey: '発売日', 12 | actressKey: '女優', 13 | codeKey: '品番', 14 | } 15 | 16 | /** 17 | * 从详情列表中查找返回对应信息 18 | * @param {Array} infoList 作品信息列表 19 | * @param {string} selector 列表项下信息值对应的 CSS 选择器 20 | * @param {InfoKeyList} keyList 查询的信息键列表 21 | */ 22 | export function getInfo( 23 | infoList: Array, 24 | selector: string, 25 | keyList: InfoKeyList = DEFAULT_KEY_LIST, 26 | actressSelector = '' 27 | ) { 28 | keyList = { ...DEFAULT_KEY_LIST, ...keyList } 29 | 30 | function getValue(key: string) { 31 | if (!key) { 32 | return 33 | } 34 | 35 | const infoItem = infoList?.find((info) => (info as HTMLElement)?.innerText?.includes(key)) 36 | 37 | let tempSelector = key === keyList.actressKey ? actressSelector || selector : selector 38 | return Array.from(infoItem?.querySelectorAll(tempSelector), (item) => (item as HTMLElement)?.innerText || '') 39 | } 40 | 41 | return { 42 | seriesName: getValue(keyList.seriesKey)?.[0], 43 | date: getValue(keyList.dateKey)?.[0], 44 | actress: getValue(keyList.actressKey), 45 | code: getValue(keyList.codeKey)?.[0]?.replace('DVD', ''), 46 | duration: getValue(keyList.durationKey)?.[0], 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/refine-actress.ts: -------------------------------------------------------------------------------- 1 | import { AVWork } from '../typings' 2 | 3 | /** 4 | * 美化演员列表 5 | * - 剔除头尾空格 6 | * - 剔除名字内空格(日本演员) 7 | * 8 | * @param {AVWork} av 未处理的 AV 对象 9 | * @returns {AVWork} 处理后的 AV 对象 10 | */ 11 | export function refineActress(av: AVWork): AVWork { 12 | av.actress = (av.actress as string[]).map((a) => { 13 | // 先剔除头尾空格 14 | let newA = a.trim() 15 | 16 | // 剔除内部空格(仅作用于非英文名演员、非素人演员) 17 | if (!newA.match(/[a-zA-Z]+/g) && !av.actressRealName) { 18 | newA = newA.replaceAll(/\s/g, '') 19 | } 20 | return newA 21 | }) 22 | 23 | return av 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/refine-title.ts: -------------------------------------------------------------------------------- 1 | import { AVWork } from '../typings' 2 | 3 | /** 4 | * 删除作品标题头尾包含的演员名 5 | * 6 | * @param {AVWork} av 未处理的 AV 对象 7 | * @returns {AVWork} 处理后的 AV 对象 8 | */ 9 | export function refineTitle(av: AVWork) { 10 | // 去头尾演员名 11 | let startAct = new RegExp('^' + av.actress, 'g') 12 | let endAct = new RegExp(av.actress + '$', 'g') 13 | let titleHasActress = startAct.test(av.workName) || endAct.test(av.workName) 14 | 15 | // 替换斜线 16 | av.workName = av.workName.replaceAll('/', ' ') 17 | 18 | if (titleHasActress) { 19 | av.workName = av.workName.replace(av.actress?.join(' '), '') 20 | } 21 | // 头尾去空格 22 | av.workName = av.workName.trim() 23 | return av 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "esModuleInterop": true, 5 | "noImplicitAny": true, 6 | "moduleResolution": "Node", 7 | "module": "ESNext", 8 | "target": "ES2021", 9 | "allowJs": true, 10 | "paths": { 11 | "@/*": ["./src/*"], 12 | }, 13 | }, 14 | "include": ["./src/**/*.ts"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------