├── tsconfig.json ├── package.json ├── lives.ts ├── README.md ├── pages.ts ├── t4.ts ├── .github └── workflows │ └── deploy.yml ├── vod.ts ├── vod_utils.ts ├── .gitignore ├── ts ├── zbkys.ts ├── xiaoyakankan.ts ├── mxvod.ts └── duonaovod.ts ├── utils.ts └── x18 ├── yjbav.ts └── vv99kk.ts /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "lib": [ "ESNext", "DOM" ], 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "utils": ["./utils.ts"], 13 | "utils/*": ["./utils/*"] 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kitty", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "kitty-parse -o result.json" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/d1y/kitty.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "type": "module", 17 | "bugs": { 18 | "url": "https://github.com/d1y/kitty/issues" 19 | }, 20 | "homepage": "https://github.com/d1y/kitty#readme", 21 | "dependencies": { 22 | "@types/bun": "^1.3.1", 23 | "@types/kitty": "https://gitpkg.vercel.app/waifu-project/movie/JS/types?dev", 24 | "cheerio": "^1.1.2", 25 | "kitty": "https://gitpkg.vercel.app/waifu-project/movie/JS/cli?dev", 26 | "semver": "^7.7.3", 27 | "typescript": "^5.9.2", 28 | "@types/semver": "^7.7.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lives.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs" 2 | interface ILIve { 3 | repo?: string 4 | path?: string 5 | branch?: string 6 | name: string 7 | url?: string 8 | type: 0 | 1 9 | } 10 | 11 | const data = [ 12 | { name: "vbskycn/tv", repo: "vbskycn/iptv", path: "tv/iptv4.m3u", branch: "master", type: 0 }, 13 | { name: "kimwang1978/tv", repo: "kimwang1978/collect-tv-txt", path: "bbxx_lite.m3u", branch: "main", type: 0 }, 14 | { name: "Guovin/tv", repo: "Guovin/iptv-api", path: "output/ipv4/result.m3u", branch: "gd", type: 0 }, 15 | { name: "mytv-android/tv", repo: "mytv-android/China-TV-Live-M3U8", path: "iptv.m3u", branch: "main", type: 0 }, 16 | ] 17 | 18 | const realData = data.map(item => { 19 | return { 20 | ...item, 21 | url: item.url || `https://raw.githubusercontent.com/${item.repo}/${item.branch || 'main'}/${item.path}` 22 | } 23 | }) 24 | 25 | const pipe = { 26 | lives: realData.map(item => { 27 | return { 28 | name: item.name, 29 | url: item.url, 30 | type: item.type, 31 | } 32 | }) 33 | } 34 | 35 | const outputFile = process.argv[2] || 'output.json' 36 | writeFileSync(outputFile, JSON.stringify(pipe, null, 2)) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @d1y/kitty 2 | 3 | 仅用于展示小猫源编写,方便参考 4 | 5 | ![Image 350x314.gif](https://s2.loli.net/2025/10/12/EVSOIbTzfWkoNB7.gif) 6 | 7 | ```bash 8 | bun install 9 | bunx kitty-parse -o result.json 10 | ``` 11 | 12 | 订阅地址: 13 | 14 | - JS: https://d1y.github.io/kitty/x.json 15 | - vod: https://d1y.github.io/kitty/vod.json 16 | - xvod: https://d1y.github.io/kitty/xvod.json 17 | - t4: https://d1y.github.io/kitty/t4.json 18 | - live: https://d1y.github.io/kitty/lives.json 19 | 20 | ```log 21 | https://d1y.github.io/kitty/x.json 22 | https://d1y.github.io/kitty/vod.json 23 | https://d1y.github.io/kitty/xvod.json 24 | https://d1y.github.io/kitty/t4.json 25 | https://d1y.github.io/kitty/lives.json 26 | ``` 27 | 28 | 加密 29 | 30 | ```log 31 | bZ2QxeS9raXR0eSNnaC1wYWdlcy94Lmpzb24= 32 | bZ2QxeS9raXR0eSNnaC1wYWdlcy92b2QuanNvbg== 33 | bZ2QxeS9raXR0eSNnaC1wYWdlcy94dm9kLmpzb24= 34 | bZ2QxeS9raXR0eSNnaC1wYWdlcy90NC5qc29u 35 | bZ2QxeS9raXR0eSNnaC1wYWdlcy9saXZlcy5qc29u 36 | ``` 37 | 38 | 国内镜像加速 39 | 40 | ```log 41 | https://ghfast.top/https://raw.githubusercontent.com/d1y/kitty/refs/heads/gh-pages/x.json 42 | https://ghfast.top/https://raw.githubusercontent.com/d1y/kitty/refs/heads/gh-pages/vod.json 43 | https://ghfast.top/https://raw.githubusercontent.com/d1y/kitty/refs/heads/gh-pages/xvod.json 44 | https://ghfast.top/https://raw.githubusercontent.com/d1y/kitty/refs/heads/gh-pages/t4.json 45 | https://ghfast.top/https://raw.githubusercontent.com/d1y/kitty/refs/heads/gh-pages/lives.json 46 | ``` -------------------------------------------------------------------------------- /pages.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync } from "fs" 2 | 3 | function getChinaDate() { 4 | const chinaDate = new Intl.DateTimeFormat('zh-CN', { 5 | timeZone: 'Asia/Shanghai', 6 | year: 'numeric', 7 | month: '2-digit', 8 | day: '2-digit' 9 | }).format(new Date()); 10 | return chinaDate.replace(/^(\d{2})/, "").replace(/\//g, '/') 11 | } 12 | 13 | const kPageUrl = `https://gist.githubusercontent.com/d1y/4d0551fc8105c9d57f85da8cbbdc8b2e/raw/fabu.html` 14 | 15 | const config = { 16 | sponsorship: "https://s2.loli.net/2025/09/24/ByRvOsQhWzKLXNo.jpg", 17 | sourceTotal: 0, 18 | update: getChinaDate(), 19 | baseUrl: "https://d1y.github.io/kitty", 20 | } 21 | 22 | function getSourceTotal() { 23 | let total = 0 24 | const files = ["vod.json", "xvod.json", "x.json", "t4.json"] 25 | for (const file of files) { 26 | try { 27 | const list: any[] = JSON.parse(readFileSync(file, "utf8")) 28 | total += list.length 29 | } catch (error) { 30 | console.error(error) 31 | } 32 | } 33 | return total 34 | } 35 | 36 | ;(async ()=> { 37 | const total = getSourceTotal() 38 | let text = await (await fetch(kPageUrl)).text() 39 | for (const [key, value] of Object.entries(config)) { 40 | let realValue = ''+value 41 | if (key == "sourceTotal") { 42 | realValue = `${total}` 43 | } 44 | text = text.replaceAll(`$$${key}`, realValue) 45 | } 46 | const file = process.argv[2] 47 | writeFileSync(file, text) 48 | })() -------------------------------------------------------------------------------- /t4.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs' 2 | 3 | // 从 TG 群和 @CxiaoyuN 那里抄过来的 4 | // 目的是为了测试 t4 的效果 5 | // 不是 @d1y 本人的 drpy-node 实例, 所以不要来骚扰我(@d1y) 6 | const t4 = [ 7 | { 8 | "id": "py_DianYingTanTang", 9 | "name": "🫐电影天堂(T4)", 10 | "api": "https://py.doube.eu.org/spider?site=DianYingTanTang", 11 | "nsfw": false, 12 | }, 13 | { 14 | "id": "py_Dm84", 15 | "name": "🍋动漫巴士(T4)", 16 | "api": "https://py.doube.eu.org/spider?site=Dm84", 17 | "nsfw": false, 18 | }, 19 | ].map(item => { 20 | const { id, name, api, nsfw } = item 21 | return { 22 | id, 23 | name, 24 | api, 25 | nsfw, 26 | type: 1, 27 | extra: { 28 | template: "t4", 29 | } 30 | } 31 | }) 32 | 33 | const file = process.argv[2] 34 | writeFileSync(file, JSON.stringify(t4, null, 2)) 35 | 36 | // ;(async ()=> { 37 | // const resp: { spiders: Array<{ 38 | // api: string 39 | // key: string 40 | // name: string 41 | // type: 4 42 | // }> } = await (await fetch("https://learnpython.ggff.net/api/list_spiders")).json() 43 | // const data = resp.spiders.map(item=> { 44 | // const { api, key, name } = item 45 | // return { 46 | // id: key, 47 | // name, 48 | // api, 49 | // nsfw: false, 50 | // type: 1, 51 | // extra: { 52 | // template: "t4", 53 | // } 54 | // } 55 | // }) 56 | // const file2 = process.argv[3] 57 | // writeFileSync(file2, JSON.stringify(data, null, 2)) 58 | // })() -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | workflow_dispatch: 7 | 8 | permissions: write-all 9 | 10 | env: 11 | VOD_CATES: "0" 12 | 13 | jobs: 14 | build-and-deploy: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup Bun 24 | uses: oven-sh/setup-bun@v1 25 | with: 26 | bun-version: latest 27 | 28 | - name: Install dependencies 29 | run: bun install 30 | 31 | - name: Generate configuration file 32 | run: | 33 | bunx kitty-parse -o x.json 34 | bun run vod.ts vod.json xvod.json 35 | bun run t4.ts t4.json 36 | bun run lives.ts lives.json 37 | bun run pages.ts index.html 38 | 39 | - name: Prepare deployment directory 40 | run: | 41 | mkdir -p deploy 42 | cp x.json vod.json xvod.json t4.json lives.json index.html deploy/ 43 | 44 | - name: Deploy to GitHub Pages 45 | uses: peaceiris/actions-gh-pages@v3 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | publish_dir: deploy 49 | publish_branch: gh-pages 50 | force_orphan: true 51 | keep_files: false 52 | commit_message: 'Deploy: Update configuration file' 53 | user_name: 'github-actions[bot]' 54 | user_email: 'github-actions[bot]@users.noreply.github.com' -------------------------------------------------------------------------------- /vod.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs' 2 | import { join } from 'path' 3 | import { getAvailableCategoryWithCfg } from "./vod_utils" 4 | import { req } from "utils" 5 | 6 | const vods = [ 7 | { 8 | id: "niuniuziyuan", 9 | name: "牛牛视频", 10 | api: "https://api.niuniuzy.me/api.php/provide/vod", 11 | nsfw: false, 12 | logo: "https://api.niuniuzy.me/template/niuniuzy/static/images/logo.png", 13 | type: 0, 14 | extra: { 15 | gfw: false, 16 | }, 17 | }, 18 | { 19 | id: "feifanziyuan", 20 | name: "非凡资源", 21 | api: "http://cj.ffzyapi.com/api.php/provide/vod/at/xml", 22 | logo: "http://cj.ffzyapi.com/template/ffzy/static/picture/logo.png", 23 | nsfw: false, 24 | type: 0, 25 | extra: { 26 | gfw: false, 27 | }, 28 | }, 29 | ]; 30 | 31 | const nsfwVods: Iconfig[] = [ 32 | { 33 | id: "Xxibaoziyuan", 34 | name: "X细胞资源", 35 | api: "https://www.xxibaozyw.com/api.php/provide/vod", 36 | nsfw: true, 37 | type: 0, 38 | extra: { 39 | gfw: false, 40 | }, 41 | }, 42 | { 43 | id: "91shipin", 44 | name: "91视频", 45 | api: "https://91av.cyou/api.php/provide/vod/", 46 | nsfw: true, 47 | type: 0, 48 | extra: { 49 | gfw: true, 50 | }, 51 | }, 52 | ] 53 | 54 | // from args context 55 | const args = process.argv.slice(2) 56 | const vodFile = args[0] 57 | const nsfwVodFile = args[1] 58 | const file1 = join(process.cwd(), vodFile) 59 | const file2 = join(process.cwd(), nsfwVodFile); 60 | 61 | (async () => { 62 | writeFileSync(file1, JSON.stringify(vods, null, 2)) 63 | writeFileSync(file2, JSON.stringify(nsfwVods, null, 2)) 64 | })() -------------------------------------------------------------------------------- /vod_utils.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio" 2 | import { req } from "utils" 3 | 4 | function isXML(text: string): boolean { 5 | return text.startsWith(" { 13 | const { api } = cfg 14 | const text = await req(api) 15 | if (isXML(text)) { 16 | const $ = load(text, { xmlMode: true }) 17 | return $("class ty").toArray().map(item => { 18 | return { 19 | text: $(item).text().trim(), 20 | id: $(item).attr("id") ?? "", 21 | } 22 | }) 23 | } else if (isJSON(text)) { 24 | const obj: { 25 | class: Array<{ 26 | type_id: number 27 | type_name: string 28 | }> 29 | } = JSON.parse(text) 30 | return obj.class.map(item => { 31 | return { 32 | text: item.type_name, 33 | id: item.type_id.toString(), 34 | } 35 | }) 36 | } 37 | return [] 38 | } 39 | 40 | async function getVideosCountWithCategory(cfg: Iconfig, cate: ICategory): Promise { 41 | const { api } = cfg 42 | const { id } = cate 43 | const text = await req(api, { 44 | params: { 45 | ac: "videolist", 46 | pg: 1, 47 | t: id, 48 | } 49 | }) 50 | if (isXML(text)) { 51 | const $ = load(text, { xmlMode: true }) 52 | return +($("list").attr("pagecount") ?? 0) 53 | } else if (isJSON(text)) { 54 | try { 55 | const obj: { pagecount: number } = JSON.parse(text) 56 | return +(obj.pagecount ?? 0) 57 | } catch (error) { 58 | return 0 59 | } 60 | } 61 | return 0 62 | } 63 | 64 | export async function getAvailableCategoryWithCfg(cfg: Iconfig) { 65 | console.debug(`[info] start task with ${cfg.name}`) 66 | const cates = await getCategory(cfg) 67 | console.debug(`[info] get ${cates.length} categories`) 68 | const result: ICategory[] = [] 69 | for (const cate of cates) { 70 | console.debug(`[info] check category ${cate.text}`) 71 | const count = await getVideosCountWithCategory(cfg, cate) 72 | console.debug(`[info] ${cate.text} has ${count} videos`) 73 | if (count >= 20) { 74 | result.push(cate) 75 | } 76 | } 77 | return result 78 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional stylelint cache 57 | .stylelintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variable files 69 | .env 70 | .env.* 71 | !.env.example 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | .parcel-cache 76 | 77 | # Next.js build output 78 | .next 79 | out 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | .output 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # vuepress v2.x temp and cache directory 96 | .temp 97 | .cache 98 | 99 | # Sveltekit cache directory 100 | .svelte-kit/ 101 | 102 | # vitepress build output 103 | **/.vitepress/dist 104 | 105 | # vitepress cache directory 106 | **/.vitepress/cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # Firebase cache directory 121 | .firebase/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v3 130 | .pnp.* 131 | .yarn/* 132 | !.yarn/patches 133 | !.yarn/plugins 134 | !.yarn/releases 135 | !.yarn/sdks 136 | !.yarn/versions 137 | 138 | # Vite files 139 | vite.config.js.timestamp-* 140 | vite.config.ts.timestamp-* 141 | .vite/ 142 | 143 | bun.lock 144 | package-lock.json 145 | result.json 146 | *.html -------------------------------------------------------------------------------- /ts/zbkys.ts: -------------------------------------------------------------------------------- 1 | import { kitty, req, createTestEnv } from 'utils' 2 | 3 | export default class zbkys implements Handle { 4 | getConfig() { 5 | return { 6 | id: "qnys-zbkys", 7 | name: "真不卡影院", 8 | api: "https://m.dgytlt.com", 9 | nsfw: false, 10 | type: 1, 11 | extra: { 12 | gfw: false, 13 | searchLimit: 10, 14 | } 15 | } 16 | } 17 | async getCategory() { 18 | return [ 19 | { text: "电影", id: "1" }, 20 | { text: "电视剧", id: "2" }, 21 | { text: "综艺", id: "3" }, 22 | { text: "综艺", id: "3" }, 23 | { text: "动漫", id: "4" }, 24 | ] 25 | } 26 | async getHome() { 27 | const cate = env.get("category") 28 | const page = env.get("page") 29 | const url = `${env.baseUrl}/vodshow/${cate}--------${page}---.html` 30 | const html = await req(url) 31 | const $ = kitty.load(html) 32 | return $(".stui-vodlist li").toArray().map(item => { 33 | const a = $(item).find("a.stui-vodlist__thumb") 34 | const id = a.attr("href") ?? "" 35 | const title = a.attr("title") ?? "" 36 | const cover = a.attr("data-original") ?? "" 37 | const remark = a.find(".pic-text.text-right").text() ?? "" 38 | return { id, title, cover, remark } 39 | }) 40 | } 41 | async getDetail() { 42 | const id = env.get("movieId") 43 | const url = `${env.baseUrl}${id}` 44 | const html = await req(url) 45 | const $ = kitty.load(html) 46 | const tabs = $(".nav.nav-tabs li").toArray().map(item => { 47 | return $(item).text() ?? "" 48 | }) 49 | const map = $(".stui-panel_bd div.tab-pane").toArray().map(item => { 50 | return $(item).find("a").toArray().map(_ => { 51 | const text = $(_).text() ?? "" 52 | const id = $(_).attr("href") ?? "" 53 | return { id, text } 54 | }) 55 | }) 56 | const playlist = tabs.map((title, index) => { 57 | const videos = map[index] 58 | return { title, videos } 59 | }) 60 | const a = $(".stui-pannel-box .stui-vodlist__thumb.picture.v-thumb") 61 | const title = a.attr("title") ?? "" 62 | const cover = a.find("img").attr("data-original") ?? "" 63 | const desc = $(".detail.col-pd").text() ?? "" 64 | return { id, title, cover, desc, playlist } 65 | } 66 | 67 | async getSearch() { 68 | const page = env.get("page") 69 | const wd = env.get("keyword") 70 | const url = `${env.baseUrl}/vodsearch/${wd}----------${page}---.html` 71 | const html = await req(url) 72 | const $ = kitty.load(html) 73 | return $(".stui-vodlist__media li").toArray().map(item => { 74 | const a = $(item).find(".v-thumb.stui-vodlist__thumb") 75 | const title = a.attr("title") ?? "" 76 | const cover = a.attr("data-original") ?? "" 77 | const id = a.attr("href") ?? "" 78 | const remark = a.find(".pic-text.text-right").text() ?? "" 79 | return { id, title, cover, remark } 80 | }) 81 | } 82 | 83 | async parseIframe() { 84 | return kitty.utils.getM3u8WithIframe(env) 85 | } 86 | } 87 | 88 | const env = createTestEnv("https://m.dgytlt.com") 89 | const tv = new zbkys(); 90 | (async () => { 91 | const cates = await tv.getCategory() 92 | env.set("category", cates[0].id) 93 | env.set("page", 1) 94 | const home = await tv.getHome() 95 | env.set("keyword", "我能") 96 | const search = await tv.getSearch() 97 | env.set("movieId", search[0].id) 98 | const detail = await tv.getDetail() 99 | env.set("iframe", detail.playlist![0].videos[0].id) 100 | const realM3u8 = await tv.parseIframe() 101 | debugger 102 | })() -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs' 2 | import { load } from 'cheerio' 3 | import compare from 'semver/functions/compare' 4 | 5 | // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 6 | 7 | export async function req( 8 | urlOrOptions: string | KittyRequestOptions, 9 | options?: Partial 10 | ): Promise { 11 | let finalOptions: KittyRequestOptions 12 | 13 | if (typeof urlOrOptions === 'string') { 14 | // req(url) | req(url, options) 15 | finalOptions = { 16 | url: urlOrOptions, 17 | method: 'GET', 18 | headers: {}, 19 | params: {}, 20 | ...options 21 | } 22 | } else { 23 | // req(options) 24 | finalOptions = { 25 | method: 'GET', 26 | headers: {}, 27 | params: {}, 28 | ...urlOrOptions 29 | } 30 | } 31 | 32 | if (!finalOptions.url) { 33 | throw new Error('URL is required') 34 | } 35 | 36 | let url = finalOptions.url 37 | let body: string | undefined 38 | 39 | if (!finalOptions.headers) { 40 | finalOptions.headers = {} 41 | } 42 | if (!finalOptions.data) { 43 | finalOptions.data = {} 44 | } 45 | 46 | if (finalOptions.params && Object.keys(finalOptions.params).length > 0) { 47 | if (finalOptions.method === 'GET') { 48 | const urlObj = new URL(url) 49 | Object.entries(finalOptions.params).forEach(([key, value]) => { 50 | urlObj.searchParams.set(key, String(value)) 51 | }) 52 | url = urlObj.toString() 53 | } 54 | } 55 | 56 | if (Object.keys(finalOptions.data).length >= 1) { 57 | if (finalOptions.bodyType === 'form') { 58 | finalOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded' 59 | body = new URLSearchParams(finalOptions.data as Record).toString() 60 | } else { 61 | finalOptions.headers['Content-Type'] = 'application/json' 62 | body = JSON.stringify(finalOptions.data) 63 | } 64 | } 65 | 66 | const response = await fetch(url, { 67 | method: finalOptions.method, 68 | headers: finalOptions.headers, 69 | body: body 70 | }) 71 | 72 | return await response.text() 73 | } 74 | 75 | export const kitty: Kitty = { 76 | load, 77 | utils: { 78 | async getM3u8WithIframe(env) { 79 | const iframe = env.get("iframe") 80 | const html = await req(`${env.baseUrl}${iframe}`) 81 | return this.getM3u8WithStr(html) 82 | }, 83 | getM3u8WithStr(str: string) { 84 | const m3u8 = str.match(/"url"\s*:\s*"([^"]+\.m3u8)"/)![1] 85 | const realM3u8 = m3u8.replaceAll("\\/", "/") 86 | return realM3u8 87 | } 88 | }, 89 | async md5(str: string) { 90 | return Buffer.from(Bun.MD5.hash(str).buffer).toString('hex') 91 | }, 92 | async version_compare(old: string, _new: string) { 93 | const cleanOld = old.startsWith('v') ? old.slice(1) : old 94 | const cleanNew = _new.startsWith('v') ? _new.slice(1) : _new 95 | return compare(cleanOld, cleanNew) >= 0 96 | }, 97 | VERSION: 'v2.6.0', 98 | } 99 | 100 | type safeSet = (key: KittyEnvParams, value: any) => void 101 | 102 | export function toEnv(env: { baseUrl: string, params?: Partial> }) { 103 | return { 104 | baseUrl: env.baseUrl ?? "", 105 | params: env.params ?? {}, 106 | get(key, defaultValue) { 107 | return this.params[key] ?? defaultValue 108 | }, 109 | set(key, value) { 110 | this.params[key] = value 111 | } 112 | } 113 | } 114 | 115 | export function createTestEnv(baseUrl: string, params: Partial> = {}) { 116 | return toEnv({ baseUrl, params }) 117 | } 118 | 119 | export function write(code: string, file: string) { 120 | writeFileSync(file, code, { encoding: 'utf-8' }) 121 | } -------------------------------------------------------------------------------- /ts/xiaoyakankan.ts: -------------------------------------------------------------------------------- 1 | import { kitty, req, createTestEnv } from 'utils' 2 | 3 | export default class xiaoyakankan implements Handle { 4 | getConfig() { 5 | return { 6 | id: 'xiaoyakankan', 7 | name: '小鸭看看', 8 | api: "https://xiaoyakankan.com", 9 | type: 1, 10 | nsfw: false, 11 | extra: { 12 | gfw: true 13 | } 14 | } 15 | } 16 | async getCategory() { 17 | return [ 18 | { text: "首页", id: "/" }, 19 | { text: "电影", id: "10" }, 20 | { text: "连续剧", id: "11" }, 21 | { text: "综艺", id: "12" }, 22 | { text: "动漫", id: "13" }, 23 | { text: "福利", id: "15" }, 24 | ] 25 | } 26 | async getHome() { 27 | const cate = env.get("category") 28 | if (cate == "/") { 29 | const $ = kitty.load(await req(env.baseUrl)) 30 | const titles = $(".m4-main .m4-meta").toArray().map(item => { 31 | return $(item).find("h3").text().trim() 32 | }) 33 | const videos = $(".m4-main .m4-list").toArray().map(item => { 34 | return $(item).find(".item").toArray().map(subItem => { 35 | const id = $(subItem).find("a.link").attr("href") ?? "" 36 | const title = $(subItem).find("a.title").text().trim() 37 | const remark = $(subItem).find(".tag2").text() ?? "" 38 | let cover = $(subItem).find("img.img").attr("data-src") ?? "" 39 | if (!!cover && cover.startsWith("//")) { 40 | cover = `https:${cover}` 41 | } 42 | return { id, title, cover, remark } 43 | }) 44 | }) 45 | const list = titles.map((title, index) => { 46 | return { 47 | type: "list", 48 | title, 49 | videos: videos[index] 50 | } 51 | }) 52 | return { 53 | type: "complex", 54 | data: [ 55 | { 56 | type: "markdown", extra: { 57 | markdown: ` 58 | > 欢迎使用小猫影视(${kitty.VERSION}) 59 | > 该源仅做测试使用,不可用于其他用途 60 | > 飞机交流群: https://t.me/catmovie1145 61 | > 小猫其他指南: https://xmpro.netlify.app 62 | ` 63 | } 64 | }, 65 | ...list, 66 | ] 67 | } 68 | } 69 | const page = env.get("page") 70 | const url = `${env.baseUrl}/cat/${cate}-${page}.html` 71 | const html = await req(url) 72 | const $ = kitty.load(html) 73 | return $(".m4-list .item").toArray().map(item => { 74 | const img = $(item).find("img.img") 75 | const id = $(item).find("a.link").attr("href") ?? "" 76 | const title = img.attr("alt") ?? "" 77 | let cover = img.attr("data-src") ?? "" 78 | if (!!cover && cover.startsWith("//")) { 79 | cover = `https:${cover}` 80 | } 81 | const remark = $(item).find(".tag1").text() ?? "" 82 | return { id, title, cover, remark, playlist: [] } 83 | }) 84 | } 85 | async getDetail() { 86 | const id = env.get("movieId") 87 | const url = `${env.baseUrl}${id}` 88 | const html = await req(url) 89 | const $ = kitty.load(html) 90 | const div = $(".m4-vod") 91 | const img = div.find("img.img") 92 | let cover = img.attr("src") ?? "" 93 | if (!!cover && cover.startsWith("//")) { 94 | cover = `https:${cover}` 95 | } 96 | let desc = $(".more .info:last-of-type").text() ?? "" 97 | const kPrefix = "简介:" 98 | if (desc.startsWith(kPrefix)) { 99 | desc = desc.replace(kPrefix, "") 100 | } else { 101 | desc = "" 102 | } 103 | const playlist: IPlaylist[] = [] 104 | for (const script of $("body script").toArray()) { 105 | let cx = $(script).text() ?? "" 106 | if (!cx || !cx.includes("var pp")) continue 107 | cx = cx.replace("var pp=", "") 108 | if (cx.endsWith(";")) cx = cx.slice(0, -1)//删除结尾的分号 109 | const unsafeJSObj: { 110 | lines: Array< 111 | [string, string, string, string[]] 112 | > 113 | } = eval(`(${cx})`) 114 | for (const line of unsafeJSObj.lines) { 115 | const _id = line[0] 116 | const text = line[1] 117 | const urls = line[3] 118 | const videos = $(`div[data-vod='${_id}'] .list a`).toArray().map((item, index) => { 119 | const text = $(item).text().trim() 120 | const idx = +($(item).attr("data-sou_idx") ?? "0") 121 | const realUrl = urls[idx] 122 | return { text, url: realUrl } 123 | }) 124 | playlist.push({ title: text, videos }) 125 | } 126 | } 127 | return { cover, desc, playlist } 128 | } 129 | } 130 | 131 | const env = createTestEnv("https://xiaoyakankan.com") 132 | const xy = new xiaoyakankan(); 133 | (async () => { 134 | const cate = await xy.getCategory() 135 | env.set("category", cate[1].id) 136 | env.set("page", 1) 137 | const home = await xy.getHome() 138 | if (!Array.isArray(home)) return 139 | env.set("movieId", home[0].id) 140 | const detail = await xy.getDetail() 141 | debugger 142 | })() -------------------------------------------------------------------------------- /x18/yjbav.ts: -------------------------------------------------------------------------------- 1 | // import { kitty, req, createTestEnv } from 'utils' 2 | 3 | export default class Yjbav implements Handle { 4 | getConfig() { 5 | return { 6 | id: "yjbav", 7 | name: "一级棒", 8 | type: 1, 9 | nsfw: true, 10 | api: "https://yjb.one" 11 | } 12 | } 13 | async getCategory() { 14 | const table = [ 15 | { 16 | "id": "/", 17 | "text": "首页" 18 | }, 19 | { 20 | "id": "21", 21 | "text": "JUU1JTlCJUJEJUU0JUJBJUE3JUU4JTg3JUFBJUU2JThCJThE" 22 | }, 23 | { 24 | "id": "22", 25 | "text": "JUU3JUJEJTkxJUU3JUJBJUEyJUU0JUI4JUJCJUU2JTkyJUFE" 26 | }, 27 | { 28 | "id": "24", 29 | "text": "JUU4JTg3JUFBJUU2JThCJThEJUU3JUIyJUJFJUU5JTgwJTg5" 30 | }, 31 | { 32 | "id": "25", 33 | "text": "JUU1JTlCJUJEJUU0JUJBJUE3JUU0JUJDJUEwJUU1JUFBJTky" 34 | }, 35 | { 36 | "id": "26", 37 | "text": "JUU2JTk3JUE1JUU2JTlDJUFDJUU2JTk3JUEwJUU3JUEwJTgx" 38 | }, 39 | { 40 | "id": "27", 41 | "text": "JUU2JTk3JUE1JUU2JTlDJUFDJUU2JTlDJTg5JUU3JUEwJTgx" 42 | }, 43 | { 44 | "id": "28", 45 | "text": "JUU2JTlDJTg5JUU3JUEwJTgxJUU3JUIyJUJFJUU5JTgwJTg5" 46 | }, 47 | { 48 | "id": "34", 49 | "text": "JUU0JUJBJTlBJUU2JUI0JUIyJUU3JUIyJUJFJUU5JTgwJTg5" 50 | }, 51 | { 52 | "id": "29", 53 | "text": "JUU1JUIwJThGJUU0JUJDJTk3JUU1JThGJUEzJUU1JTkxJUIz" 54 | }, 55 | { 56 | "id": "30", 57 | "text": "JUU2JUFDJUE3JUU3JUJFJThFJUU3JUIyJUJFJUU5JTgwJTg5" 58 | }, 59 | { 60 | "id": "31", 61 | "text": "JUU2JTg4JTkwJUU0JUJBJUJBJUU1JThBJUE4JUU2JUJDJUFC" 62 | }, 63 | { 64 | "id": "32", 65 | "text": "JUU3JUJCJThGJUU1JTg1JUI4JUU0JUI4JTg5JUU3JUJBJUE3" 66 | }, 67 | { 68 | "id": "33", 69 | "text": "QWklRTYlOTglOEUlRTYlOTglOUY=" 70 | } 71 | ] 72 | return table.map(item => { 73 | const { id, text } = item 74 | if (id == "/") return item 75 | const a = atob(text) 76 | const b = decodeURIComponent(a) 77 | return { id, text: b } 78 | }) 79 | } 80 | async getHome() { 81 | const cate = env.get("category") 82 | const page = env.get("page") 83 | if (cate == "/") { 84 | const $ = kitty.load(await req(env.baseUrl)) 85 | const titles = $(".category-count").toArray().map(item => { 86 | return $(item).text().replace("观看更多", "").trim() 87 | }) 88 | const videos = $(".post-list").toArray().map(item => { 89 | return $(item).find("div.col-md-2").toArray().map(item => { 90 | const id = $(item).find("a").attr("href") ?? "" 91 | const cover = env.baseUrl + ($(item).find("img").attr("data-original") ?? "") 92 | const title = $(item).find(".entry-title").text().trim() 93 | const remark = $(item).find(".type-text").text().trim() 94 | return { id, title, cover, remark } 95 | }) 96 | }) 97 | const list = titles.map((title, index) => { 98 | return { 99 | type: "list", 100 | title, 101 | videos: videos[index] 102 | } 103 | }) 104 | return { 105 | type: "complex", 106 | data: [ 107 | { 108 | type: "markdown", extra: { 109 | markdown: ` 110 | > 欢迎使用小猫影视(${kitty.VERSION}) 111 | > 该源仅做测试使用,不可用于其他用途 112 | > 飞机交流群: https://t.me/catmovie1145 113 | > 小猫其他指南: https://xmpro.netlify.app 114 | ` 115 | } 116 | }, 117 | ...list 118 | ] 119 | } 120 | } 121 | const url = `${env.baseUrl}/vodtype/${cate}-${page}/` 122 | const $ = kitty.load(await req(url)) 123 | return $(".post-list .col-md-3").toArray().map(item => { 124 | const a = $(item).find("a") 125 | const img = a.find("img") 126 | const id = a.attr("href") ?? "" 127 | let cover = img.attr("data-original") ?? "" 128 | cover = `${env.baseUrl}${cover}` 129 | const title = img.attr("alt") ?? "" 130 | return { id, cover, title } 131 | }) 132 | } 133 | async getDetail() { 134 | const id = env.get("movieId") 135 | const url = `${env.baseUrl}${id}` 136 | const html = await req(url) 137 | const $ = kitty.load(html) 138 | const m3u8 = kitty.utils.getM3u8WithStr(html) 139 | const title = $(".breadcrumb").text().trim() 140 | return { 141 | id, 142 | title, 143 | playlist: [ 144 | { 145 | title: "默认", videos: [ 146 | { text: "😍播放", url: m3u8 } 147 | ] 148 | } 149 | ] 150 | } 151 | } 152 | } 153 | 154 | // TEST 155 | // const env = createTestEnv("https://yjb.one") 156 | // const call = new Yjbav(); 157 | // (async ()=> { 158 | // const cates = await call.getCategory() 159 | // env.set("category", cates[0].id) 160 | // env.set("page", 1) 161 | // const home = await call.getHome() 162 | // env.set("movieId", home[0].id) 163 | // const detail = await call.getDetail() 164 | // debugger 165 | // })() -------------------------------------------------------------------------------- /ts/mxvod.ts: -------------------------------------------------------------------------------- 1 | import { kitty, req, createTestEnv } from 'utils' 2 | 3 | export default class mxvod implements Handle { 4 | getConfig() { 5 | return { 6 | id: 'mxvod', 7 | name: 'MXVOD', 8 | api: "https://www.mxvod.com", 9 | nsfw: false, 10 | type: 1, 11 | extra: { 12 | gfw: false, 13 | searchLimit: 10, 14 | } 15 | } 16 | } 17 | async getCategory() { 18 | return [ 19 | { text: '首页', id: "/" }, 20 | { text: '电影', id: "dianyin" }, 21 | { text: '电视剧', id: "dianshiju" }, 22 | { text: '综艺', id: "zongyi" }, 23 | { text: '动漫', id: "dongman" }, 24 | { text: '短剧', id: "duanju" }, 25 | { text: '电影解说', id: "dianyingjieshuo" }, 26 | { text: '直播', id: "live" }, 27 | { text: '体育', id: "tiyu" }, 28 | ] 29 | } 30 | async getHome() { 31 | const cate = env.get('category') 32 | const page = env.get('page') 33 | if (cate == "/") { 34 | const $ = kitty.load(await req(env.baseUrl)) 35 | const banner = $(".swiper-container .swiper-slide").toArray().map(item => { 36 | const bb = $(item).find(".banner") 37 | const id = bb.attr("href") ?? "" 38 | const title = bb.attr("data-name") ?? "" 39 | const remark = bb.attr("data-fname") ?? "" 40 | const cover = env.baseUrl + bb.attr("style")!.match((/background:\s*url\(([^)]+)\)/))![1] 41 | return { id, title, cover, remark } 42 | }) 43 | const list = $(".content .module").toArray().map(item => { 44 | if ($(item).hasClass("homepage_homepage_channelnav")) return null 45 | const title = $($(item).find(".module-title").toArray()[0]).text().trim() 46 | const videos = $(item).find(".module-items .module-item").toArray().map(item => { 47 | const a = $(item).find("a.module-item-title") 48 | const id = a.attr("href") ?? "" 49 | const title = a.text().trim() 50 | const cover = env.baseUrl + ($(item).find(".module-item-pic img").attr("data-src") ?? "") 51 | const remark = $(item).find(".module-item-caption").text().trim() 52 | return { id, title, cover, remark } 53 | }) 54 | if (!videos.length) return null 55 | return { type: "list", title: title, videos } 56 | }).filter(item => !!item) 57 | return { 58 | type: "complex", 59 | data: [ 60 | { type: "banner", videos: banner }, 61 | { 62 | type: "markdown", extra: { 63 | markdown: ` 64 | > 欢迎使用小猫影视(${kitty.VERSION}) 65 | > 该源仅做测试使用,不可用于其他用途 66 | > 飞机交流群: https://t.me/catmovie1145 67 | > 小猫其他指南: https://xmpro.netlify.app 68 | ` 69 | } 70 | }, 71 | ...list, 72 | ], 73 | } 74 | } 75 | const url = `${env.baseUrl}/vodshow/${cate}--------${page}---.html` 76 | const $ = kitty.load(await req(url)) 77 | return $($(".module .module-list").toArray()[0]).find(".module-items .module-item").toArray().map(item => { 78 | const a = $(item).find("a") 79 | const img = $(item).find("img") 80 | const id = a.attr("href") ?? "" 81 | let cover = img.attr("data-src") ?? "" 82 | cover = `${env.baseUrl}${cover}` 83 | const title = img.attr("alt") ?? "" 84 | const remark = $(item).find('.module-item-caption').text() ?? "" 85 | return { id, title, cover, remark, playlist: [] } 86 | }) 87 | } 88 | async getDetail() { 89 | const id = env.get("movieId") 90 | const url = `${env.baseUrl}${id}` 91 | const $ = kitty.load(await req(url)) 92 | const desc = ($($(".video-info-header .txtone").toArray().at(-1)).text() ?? "").trim() 93 | const tabs = $('.play-source-tab a, .module-tab-item').toArray().map(item => { 94 | const name = $(item).attr("data-dropdown-value") ?? $(item).find("span").attr("data-dropdown-value") 95 | return name 96 | }) 97 | const playlistTable = $(".module-player-list").toArray().map(item => { 98 | let id = $(item).attr("id") ?? "" 99 | id = id.replace("glist-", "") 100 | const list = $(item).find(".sort-item a").toArray().map(item => { 101 | const text = ($(item).text() ?? "").trim() 102 | const id = $(item).attr("href") ?? "" 103 | return { text, id } 104 | }) 105 | return { id: +id, list } 106 | }) 107 | const playlist = tabs.map((item, index) => { 108 | return { 109 | title: item, 110 | videos: playlistTable[index].list 111 | } 112 | }) 113 | return { desc, playlist } 114 | } 115 | async getSearch() { 116 | const wd = env.get("keyword") 117 | const page = env.get("page") 118 | const url = `${env.baseUrl}/vodsearch/${wd}----------${page}---.html` 119 | const $ = kitty.load(await req(url)) 120 | return $(".module-search-item").toArray().map(item => { 121 | const a = $(item).find("a") 122 | const img = $(item).find("img") 123 | const id = a.attr("href") ?? "" 124 | const title = a.attr("title") ?? "" 125 | let cover = img.attr("data-src") ?? "" 126 | cover = `${env.baseUrl}${cover}` 127 | return { id, title, cover, remark: "", desc: "", playlist: [] } 128 | }) 129 | } 130 | async parseIframe() { 131 | return kitty.utils.getM3u8WithIframe(env) 132 | } 133 | } 134 | 135 | const env = createTestEnv("https://www.mxvod.com") 136 | const tv = new mxvod(); 137 | (async () => { 138 | const cates = await tv.getCategory() 139 | env.set("category", cates[0].id) 140 | env.set("page", 2) 141 | const home = await tv.getHome() 142 | env.set('keyword', '我能') 143 | const search = await tv.getSearch() 144 | env.set("movieId", search[0].id) 145 | const detail = await tv.getDetail() 146 | env.set("iframe", detail.playlist![0].videos[0].id) 147 | const realM3u8 = await tv.parseIframe() 148 | debugger 149 | })() -------------------------------------------------------------------------------- /ts/duonaovod.ts: -------------------------------------------------------------------------------- 1 | import { kitty, req, createTestEnv } from 'utils' 2 | 3 | export default class duonaovod implements Handle { 4 | getConfig() { 5 | return { 6 | id: 'duonaovod', 7 | name: '多瑙影院', 8 | api: "https://www.duonaovod.com", 9 | nsfw: false, 10 | type: 1, 11 | extra: { 12 | gfw: false, 13 | searchLimit: 12, 14 | } 15 | } 16 | } 17 | 18 | async getCategory() { 19 | return [ 20 | { 21 | "text": "首页", 22 | "id": "/" 23 | }, 24 | { 25 | "text": "电影", 26 | "id": "1" 27 | }, 28 | { 29 | "text": "电视剧", 30 | "id": "2" 31 | }, 32 | { 33 | "text": "综艺", 34 | "id": "3" 35 | }, 36 | { 37 | "text": "动漫", 38 | "id": "4" 39 | }, 40 | { 41 | "text": "短剧", 42 | "id": "57" 43 | }, 44 | { 45 | "text": "动作片", 46 | "id": "6" 47 | }, 48 | { 49 | "text": "喜剧片", 50 | "id": "7" 51 | }, 52 | { 53 | "text": "爱情片", 54 | "id": "8" 55 | }, 56 | { 57 | "text": "科幻片", 58 | "id": "9" 59 | }, 60 | { 61 | "text": "恐怖片", 62 | "id": "10" 63 | }, 64 | { 65 | "text": "剧情片", 66 | "id": "11" 67 | }, 68 | { 69 | "text": "奇幻片", 70 | "id": "30" 71 | }, 72 | { 73 | "text": "战争片", 74 | "id": "12" 75 | }, 76 | { 77 | "text": "犯罪片", 78 | "id": "54" 79 | }, 80 | { 81 | "text": "动漫电影", 82 | "id": "55" 83 | }, 84 | { 85 | "text": "伦理片", 86 | "id": "34" 87 | }, 88 | { 89 | "text": "国产剧", 90 | "id": "13" 91 | }, 92 | { 93 | "text": "港台剧", 94 | "id": "14" 95 | }, 96 | { 97 | "text": "日韩剧", 98 | "id": "15" 99 | } 100 | ] 101 | } 102 | 103 | async getHome() { 104 | const cate = env.get('category') 105 | const page = env.get('page') 106 | if (cate == "/") { 107 | const $ = kitty.load(await req(env.baseUrl)) 108 | const cards = $(".conch-ctwrap .container").toArray().map(item => { 109 | const _title = $(item).find(".hl-rb-title").toArray() 110 | if (!_title.length) return null 111 | const title = $(_title[0]).text().trim() 112 | const isCard = $(item).find(".hl-vod-list").hasClass("swiper-wrapper") 113 | const list = $(item).find(".hl-row-box .hl-list-wrap .hl-list-item").toArray() 114 | const table = list.map(subItem => { 115 | const a = $(subItem).find("a") 116 | const id = a?.attr("href") ?? "" 117 | const cover = a?.attr("data-original") ?? "" 118 | const title = a?.attr("title") ?? "" 119 | const remark = a?.find(".remarks")?.text().trim() 120 | return { id, cover, title, remark } 121 | }) 122 | if (!table.length) return null 123 | return { 124 | type: isCard ? "card" : "list", 125 | title, 126 | videos: table, 127 | } 128 | }).filter(item => !!item) 129 | return { 130 | type: 'complex', 131 | data: [ 132 | { 133 | type: "markdown", extra: { 134 | markdown: ` 135 | > 欢迎使用小猫影视(${kitty.VERSION}) 136 | > 该源仅做测试使用,不可用于其他用途 137 | > 飞机交流群: https://t.me/catmovie1145 138 | > 小猫其他指南: https://xmpro.netlify.app 139 | ` 140 | } 141 | }, 142 | ...cards 143 | ], 144 | } 145 | } 146 | const url = `${env.baseUrl}/index.php/vod/type/id/${cate}/page/${page}.html` 147 | const $ = kitty.load(await req(url)) 148 | return $(".hl-vod-list li").toArray().map(item => { 149 | const a = $(item).find("a") 150 | const id = a.attr("href") ?? "" 151 | const cover = a.attr("data-original") ?? "" 152 | const title = a.attr("title") ?? "" 153 | const remark = $(item).find(".hl-lc-1.remarks").text() ?? "" 154 | return { id, title, cover, remark } 155 | }) 156 | } 157 | async getDetail() { 158 | const id = env.get("movieId") 159 | const url = `${env.baseUrl}${id}` 160 | const $ = kitty.load(await req(url)) 161 | let desc = $(".hl-col-xs-12.blurb").text().trim().replace("简介:", "") 162 | if (desc == "暂无简介") desc = "" 163 | const tabs = $(".hl-plays-from a").toArray().map(item => { 164 | return $(item).text().trim() 165 | }) 166 | const _videos = $(".hl-tabs-box").toArray().map((item) => { 167 | return $(item).find("li a").toArray().map(item => { 168 | const id = $(item).attr("href") ?? "" 169 | const text = $(item).text() ?? "" 170 | return { id, text } 171 | }) 172 | }) 173 | const playlist = tabs.map((title, index) => { 174 | const videos = _videos[index] 175 | return { title, videos } 176 | }) 177 | return { desc, playlist } 178 | } 179 | async getSearch() { 180 | const wd = env.get("keyword") 181 | const page = env.get("page") 182 | const url = `${env.baseUrl}/index.php/vod/search/page/${page}/wd/${wd}.html` 183 | const $ = kitty.load(await req(url)) 184 | return $(".hl-one-list li").toArray().map(item => { 185 | const a = $(item).find("a") 186 | const id = a.attr("href") ?? "" 187 | const cover = a.attr("data-original") ?? "" 188 | const title = a.attr("title") ?? "" 189 | return { id, cover, title, remark: "" } 190 | }) 191 | } 192 | async parseIframe() { 193 | const iframe = env.get("iframe") 194 | const html = await req(`${env.baseUrl}${iframe}`) 195 | const $ = kitty.load(html) 196 | const script = $("script").toArray().filter(item => { 197 | const text = $(item).text().trim() 198 | if (text.startsWith("var player_aaaa")) return true 199 | })[0] 200 | let code = $(script).text().trim().replace("var player_aaaa=", "") 201 | code = `(${code})` 202 | 203 | const unsafeObj: { url: string, encrypt: '1' | '2' } = eval(code) 204 | 205 | // https://www.duonaovod.com/static/js/player.js?t=a20250923 206 | if (unsafeObj.encrypt == '1') { 207 | unsafeObj.url = unescape(unsafeObj.url); 208 | } else if (unsafeObj.encrypt == '2') { 209 | unsafeObj.url = unescape(atob(unsafeObj.url)); 210 | } 211 | return unsafeObj.url 212 | } 213 | } 214 | 215 | const env = createTestEnv("https://www.duonaovod.com") 216 | const tv = new duonaovod(); 217 | (async () => { 218 | const cates = await tv.getCategory() 219 | env.set("category", cates[1].id) 220 | env.set("page", 1) 221 | const home = await tv.getHome() 222 | env.set('keyword', '我能') 223 | const search = await tv.getSearch() 224 | env.set("movieId", search[0].id) 225 | const detail = await tv.getDetail() 226 | env.set("iframe", detail.playlist![0].videos[0].id) 227 | const realM3u8 = await tv.parseIframe() 228 | debugger 229 | })() -------------------------------------------------------------------------------- /x18/vv99kk.ts: -------------------------------------------------------------------------------- 1 | // import { kitty, req, createTestEnv } from 'utils' 2 | 3 | interface IGetInfoBody { 4 | RecordsPage: 20 5 | command: "WEB_GET_INFO" 6 | content: string 7 | languageType: "CN" 8 | pageNumber: number 9 | typeId: number | string 10 | typeMid: 1 11 | type?: 1 12 | } 13 | 14 | interface ICard { 15 | id: string 16 | type_Mid: 1 17 | typeName: string 18 | vod_class: string 19 | vod_name: string 20 | vod_pic: string 21 | vod_server_id: number 22 | vod_url: string 23 | } 24 | 25 | interface IGetDetailBody { 26 | command: "WEB_GET_INFO_DETAIL" 27 | id: string 28 | languageType: "CN" 29 | type_Mid: "1" 30 | } 31 | 32 | interface IInfoResponse { 33 | data: { 34 | count: string 35 | pageAllNumber: string 36 | pageNumber: string 37 | resultList: Array 38 | } 39 | } 40 | 41 | interface IDetailResponse { 42 | data: { 43 | result: ICard 44 | } 45 | } 46 | 47 | export default class VV99KK implements Handle { 48 | getConfig() { 49 | return { 50 | id: 'vv99kk', 51 | name: '熊猫视频', 52 | api: 'https://spiderscloudcn2.51111666.com', 53 | type: 1, 54 | nsfw: true, 55 | extra: { 56 | gfw: false, 57 | } 58 | } 59 | } 60 | async getCategory() { 61 | const table = [ 62 | { 63 | "id": "6", 64 | "text": "OTElRTQlQkMlQTAlRTUlQUElOTI=" 65 | }, 66 | { 67 | "id": "7", 68 | "text": "JUU3JUIyJUJFJUU0JUI4JTlDJUU0JUJDJUEwJUU1JUFBJTky" 69 | }, 70 | { 71 | "id": "8", 72 | "text": "JUU5JUJBJUJCJUU4JUIxJTg2JUU0JUJDJUEwJUU1JUFBJTky" 73 | }, 74 | { 75 | "id": "9", 76 | "text": "JUU5JUJBJUJCJUU4JUIxJTg2JUU2JTk4JUEwJUU3JTk0JUJC" 77 | }, 78 | { 79 | "id": "10", 80 | "text": "JUU5JUJBJUJCJUU4JUIxJTg2JUU3JThDJUFCJUU3JTg4JUFB" 81 | }, 82 | { 83 | "id": "11", 84 | "text": "JUU4JTlDJTlDJUU2JUExJTgzJUU0JUJDJUEwJUU1JUFBJTky" 85 | }, 86 | { 87 | "id": "12", 88 | "text": "JUU1JUE0JUE5JUU3JUJFJThFJUU0JUJDJUEwJUU1JUFBJTky" 89 | }, 90 | { 91 | "id": "13", 92 | "text": "JUU2JTk4JTlGJUU3JUE5JUJBJUU0JUJDJUEwJUU1JUFBJTky" 93 | }, 94 | { 95 | "id": "14", 96 | "text": "JUU1JTgxJUI3JUU2JThCJThEJUU4JTg3JUFBJUU2JThCJThE" 97 | }, 98 | { 99 | "id": "15", 100 | "text": "JUU2JTk3JUE1JUU5JTlGJUE5JUU4JUE3JTg2JUU5JUEyJTkx" 101 | }, 102 | { 103 | "id": "16", 104 | "text": "JUU2JUFDJUE3JUU3JUJFJThFJUU2JTgwJUE3JUU3JTg4JUIx" 105 | }, 106 | { 107 | "id": "17", 108 | "text": "JUU2JTk5JUJBJUU4JTgzJUJEJUU2JThEJUEyJUU4JTg0JUI4" 109 | }, 110 | { 111 | "id": "18", 112 | "text": "JUU3JUJCJThGJUU1JTg1JUI4JUU0JUI4JTg5JUU3JUJBJUE3" 113 | }, 114 | { 115 | "id": "19", 116 | "text": "JUU3JUJEJTkxJUU3JUJBJUEyJUU0JUI4JUJCJUU2JTkyJUFE" 117 | }, 118 | { 119 | "id": "20", 120 | "text": "JUU1JThGJUIwJUU2JUI5JUJFJUU4JUJFJUEzJUU1JUE2JUI5" 121 | }, 122 | { 123 | "id": "21", 124 | "text": "b25seWZhbnM=" 125 | }, 126 | { 127 | "id": "22", 128 | "text": "JUU0JUI4JUFEJUU2JTk2JTg3JUU1JUFEJTk3JUU1JUI5JTk1" 129 | }, 130 | { 131 | "id": "23", 132 | "text": "JUU3JUJCJThGJUU1JTg1JUI4JUU3JUI0JUEwJUU0JUJBJUJB" 133 | }, 134 | { 135 | "id": "24", 136 | "text": "JUU5JUFCJTk4JUU2JUI4JTg1JUU2JTk3JUEwJUU3JUEwJTgx" 137 | }, 138 | { 139 | "id": "25", 140 | "text": "JUU3JUJFJThFJUU5JUEyJTlDJUU1JUI3JUE4JUU0JUI5JUIz" 141 | }, 142 | { 143 | "id": "26", 144 | "text": "JUU0JUI4JTlEJUU4JUEyJTlDJUU1JTg4JUI2JUU2JTlDJThE" 145 | }, 146 | { 147 | "id": "27", 148 | "text": "U00lRTclQjMlQkIlRTUlODglOTc=" 149 | }, 150 | { 151 | "id": "28", 152 | "text": "JUU2JUFDJUE3JUU3JUJFJThFJUU3JUIzJUJCJUU1JTg4JTk3" 153 | }, 154 | { 155 | "id": "29", 156 | "text": "SCVFNSU4QiU5NSVFNyU5NSVBQg==" 157 | } 158 | ] 159 | return table.map(item => { 160 | const { id, text } = item 161 | const a = atob(text) 162 | const b = decodeURIComponent(a) 163 | return { id, text: b } 164 | }) 165 | } 166 | async getHome() { 167 | const cate = env.get("category") 168 | const page = env.get("page") 169 | const unsafeObj: IInfoResponse = JSON.parse(await req(`${env.baseUrl}/forward`, { 170 | method: "POST", 171 | noCache: true, 172 | data: { 173 | RecordsPage: 20, 174 | command: "WEB_GET_INFO", 175 | content: "", 176 | languageType: "CN", 177 | pageNumber: page, 178 | typeId: cate, 179 | typeMid: 1, 180 | } 181 | })) 182 | return unsafeObj.data.resultList.map(item => { 183 | return { 184 | id: item.id, 185 | cover: item.vod_pic, 186 | title: item.vod_name, 187 | remark: item.vod_class, 188 | } 189 | }) 190 | } 191 | async getDetail() { 192 | const id = env.get("movieId") 193 | const response: IDetailResponse = JSON.parse(await req(`${env.baseUrl}/forward`, { 194 | method: "POST", 195 | noCache: true, 196 | data: { 197 | command: "WEB_GET_INFO_DETAIL", 198 | id, 199 | languageType: "CN", 200 | type_Mid: "1", 201 | } 202 | })) 203 | const _ = response.data.result 204 | 205 | const initObj: { 206 | data: { 207 | macVodLinkMap: Record> 208 | } 209 | } = JSON.parse(await req(`${env.baseUrl}/getDataInit`, { 210 | method: "POST", 211 | data: { 212 | age: 31, 213 | city: "New York", 214 | name: "John" 215 | } 216 | })) 217 | 218 | const xl1 = initObj.data.macVodLinkMap 219 | 220 | let playUrl = "" 221 | let xl: any = false 222 | const num = Math.floor(Math.random() * 2 + 1); 223 | // var playImgUrl = ""; 224 | if (null != xl) { 225 | // playImgUrl = xl1[response.data.result.vod_server_id].PIC_LINK_1 + response.data.result.vod_pic; 226 | if (xl == 1) { 227 | playUrl = xl1[response.data.result.vod_server_id].LINK_1 + response.data.result.vod_url; 228 | } else if (xl == 2) { 229 | playUrl = xl1[response.data.result.vod_server_id].LINK_2 + response.data.result.vod_url; 230 | } else if (xl == 3) { 231 | playUrl = xl1[response.data.result.vod_server_id].LINK_3 + response.data.result.vod_url; 232 | } else { 233 | if (num == 1) { 234 | playUrl = xl1[response.data.result.vod_server_id].LINK_1 + response.data.result.vod_url; 235 | console.log(1); 236 | } else if (num == 2) { 237 | playUrl = xl1[response.data.result.vod_server_id].LINK_2 + response.data.result.vod_url; 238 | console.log(2); 239 | } else { 240 | playUrl = xl1[response.data.result.vod_server_id].LINK_1 + response.data.result.vod_url; 241 | console.log(3); 242 | } 243 | } 244 | } 245 | 246 | return { 247 | id, 248 | title: _.vod_name, 249 | playlist: [ 250 | { 251 | title: "默认", 252 | videos: [ 253 | { 254 | url: playUrl, 255 | text: "播放", 256 | } 257 | ] 258 | } 259 | ] 260 | }; 261 | } 262 | async getSearch() { 263 | const wd = env.get("keyword") 264 | const page = env.get("page") 265 | const unsafeObj: IInfoResponse = JSON.parse(await req(`${env.baseUrl}/forward`, { 266 | method: "POST", 267 | noCache: true, 268 | data: { 269 | RecordsPage: 20, 270 | command: "WEB_GET_INFO", 271 | content: wd, 272 | languageType: "CN", 273 | pageNumber: page, 274 | type: 1, 275 | typeMid: 1, 276 | typeId: 0, 277 | } 278 | })) 279 | return unsafeObj.data.resultList.map(item => { 280 | return { 281 | id: item.id, 282 | cover: item.vod_pic, 283 | title: item.vod_name, 284 | remark: item.vod_class, 285 | } 286 | }) 287 | } 288 | } 289 | 290 | // TEST 291 | // const env = createTestEnv(`https://spiderscloudcn2.51111666.com`) 292 | // const call = new VV99KK(); 293 | // (async () => { 294 | // const cates = await call.getCategory() 295 | // env.set("category", cates[0].id) 296 | // env.set("page", 1) 297 | // const home = await call.getHome() 298 | // env.set("movieId", home[0].id) 299 | // const detail = await call.getDetail() 300 | // env.set("keyword", "黑丝") 301 | // const search = await call.getSearch() 302 | // debugger 303 | // })() --------------------------------------------------------------------------------