diff --git a/.vscode/settings.json b/.vscode/settings.json index 4b10a06..1dfc313 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,5 @@ "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "cSpell.words": ["shadcn", "weibo"] + "cSpell.words": ["authtoken", "Frpc", "shadcn", "weibo"] } diff --git a/README-CN.md b/README-CN.md index c14ff8c..b9d4576 100644 --- a/README-CN.md +++ b/README-CN.md @@ -5,6 +5,8 @@ ## ✋🏻 简介 这是一个基于`React` `Ffmpeg` `Electron` `Shadcn`的**直播录制软件**。支持监控直播,可以帮助用户简单便捷的对直播进行录制并保存为MP4格式的视频。 +Fideo 官方网站:[https://www.fideo.site/cn](https://www.fideo.site/cn) + ## 已支持平台 - [x] [YouTube](https://www.youtube.com/) - [x] [Twitch](https://www.twitch.tv/) diff --git a/README.md b/README.md index 089fcf6..e2dbf04 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,12 @@ [中文文档](https://github.com/chenfan0/fideo-live-record/blob/main/README-CN.md) + ## ✋🏻Introduction This is a **live recording software** based on `React`, `Ffmpeg`, `Electron`, `Shadcn`. It supports monitoring live streams, making it simple and convenient for users to record live streams and save them as MP4 videos. +Fideo official website: [https://www.fideo.site/en](https://www.fideo.site/en) + ## Supported Platforms - [x] [YouTube](https://www.youtube.com/) - [x] [Twitch](https://www.twitch.tv/) diff --git a/package.json b/package.json index f9b4c69..a1ad6b2 100644 --- a/package.json +++ b/package.json @@ -39,17 +39,21 @@ "base-64": "^1.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cross-spawn": "^7.0.3", "crypto-js": "^4.2.0", "dayjs": "^1.11.11", "debug": "^4.3.5", "download": "^8.0.0", "electron-updater": "^6.1.7", "execa": "5.1.1", + "fastify": "^5.0.0", "fluent-ffmpeg": "^2.1.3", "i18next": "^23.11.5", "localforage": "^1.10.0", "lucide-react": "^0.383.0", "mitt": "^3.0.1", + "nanoid": "^5.0.7", + "react-confetti": "^6.1.0", "react-hook-form": "^7.51.5", "react-i18next": "^14.1.2", "react-use": "^17.5.0", @@ -57,6 +61,8 @@ "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "utf8": "^3.0.0", + "validator": "^13.12.0", + "ws": "^8.18.0", "zod": "^3.23.8", "zustand": "^4.5.2" }, @@ -64,11 +70,13 @@ "@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1", + "@types/cross-spawn": "^6.0.6", "@types/download": "^8.0.5", "@types/fluent-ffmpeg": "^2.1.24", "@types/node": "^18.19.9", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", + "@types/ws": "^8.5.12", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.19", "bumpp": "^9.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a5b7a8..427c87c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cross-spawn: + specifier: ^7.0.3 + version: 7.0.3 crypto-js: specifier: ^4.2.0 version: 4.2.0 @@ -74,6 +77,9 @@ importers: execa: specifier: 5.1.1 version: 5.1.1 + fastify: + specifier: ^5.0.0 + version: 5.0.0 fluent-ffmpeg: specifier: ^2.1.3 version: 2.1.3 @@ -89,6 +95,12 @@ importers: mitt: specifier: ^3.0.1 version: 3.0.1 + nanoid: + specifier: ^5.0.7 + version: 5.0.7 + react-confetti: + specifier: ^6.1.0 + version: 6.1.0(react@18.3.1) react-hook-form: specifier: ^7.51.5 version: 7.51.5(react@18.3.1) @@ -110,6 +122,12 @@ importers: utf8: specifier: ^3.0.0 version: 3.0.0 + validator: + specifier: ^13.12.0 + version: 13.12.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -126,6 +144,9 @@ importers: '@electron-toolkit/tsconfig': specifier: ^1.0.1 version: 1.0.1(@types/node@18.19.34) + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 '@types/download': specifier: ^8.0.5 version: 8.0.5 @@ -141,6 +162,9 @@ importers: '@types/react-dom': specifier: ^18.2.18 version: 18.3.0 + '@types/ws': + specifier: ^8.5.12 + version: 8.5.12 '@vitejs/plugin-react': specifier: ^4.2.1 version: 4.3.1(vite@5.2.13(@types/node@18.19.34)) @@ -663,6 +687,18 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fastify/ajv-compiler@4.0.1': + resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==} + + '@fastify/error@4.0.0': + resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==} + + '@fastify/fast-json-stringify-compiler@5.0.1': + resolution: {integrity: sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==} + + '@fastify/merge-json-schemas@0.1.1': + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} + '@floating-ui/core@1.6.2': resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} @@ -1468,6 +1504,9 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1534,6 +1573,9 @@ packages: '@types/verror@1.10.10': resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==} + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -1611,6 +1653,13 @@ packages: '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1625,6 +1674,14 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -1633,6 +1690,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1741,6 +1801,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.4.19: resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} engines: {node: ^10 || ^12 || >=14} @@ -1752,6 +1816,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.0.0: + resolution: {integrity: sha512-UbYrOXgE/I+knFG+3kJr9AgC7uNo8DG+FGGODpH9Bj1O1kL/QDjBXnTem9leD3VdQKtaHjV3O85DQ7hHh4IIHw==} + axios@1.7.2: resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} @@ -1817,6 +1884,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builder-util-runtime@9.2.3: resolution: {integrity: sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==} engines: {node: '>=12.0.0'} @@ -1977,6 +2047,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -2315,6 +2389,14 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2340,6 +2422,9 @@ packages: resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} engines: {'0': node >=0.6.0} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2353,18 +2438,37 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.0.0: + resolution: {integrity: sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} fast-loops@1.1.3: resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + fast-uri@2.4.0: + resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} + + fast-uri@3.0.2: + resolution: {integrity: sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==} + fastest-stable-stringify@2.0.2: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + fastify@5.0.0: + resolution: {integrity: sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -2410,6 +2514,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-my-way@9.1.0: + resolution: {integrity: sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==} + engines: {node: '>=14'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2449,6 +2557,10 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2717,6 +2829,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -2913,9 +3029,15 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2957,6 +3079,9 @@ packages: lie@3.1.1: resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + light-my-request@6.1.0: + resolution: {integrity: sha512-+NFuhlOGoEwxeQfJ/pobkVFxcnKyDtiX847hLjuB/IzBxIl3q4VJeFI8uRCgb3AlTWL1lgOr+u5+8QdUcr33ng==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -3139,6 +3264,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.0.7: + resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3218,6 +3348,10 @@ packages: ohash@1.1.3: resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3332,6 +3466,16 @@ packages: resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} engines: {node: '>=0.10.0'} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.4.0: + resolution: {integrity: sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==} + hasBin: true + pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -3408,6 +3552,13 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@4.0.0: + resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -3423,6 +3574,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -3440,6 +3595,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -3447,6 +3605,12 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-confetti@6.1.0: + resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} + engines: {node: '>=10.18'} + peerDependencies: + react: ^16.3.0 || ^17.0.1 || ^18.0.0 + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -3544,10 +3708,18 @@ packages: readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -3563,6 +3735,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -3587,6 +3763,10 @@ packages: responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -3595,6 +3775,9 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -3629,6 +3812,13 @@ packages: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-regex2@4.0.0: + resolution: {integrity: sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3645,6 +3835,9 @@ packages: resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} engines: {node: '>=0.10.0'} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + seek-bzip@1.0.6: resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} hasBin: true @@ -3669,6 +3862,9 @@ packages: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} + set-cookie-parser@2.7.0: + resolution: {integrity: sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3719,6 +3915,9 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + sonic-boom@4.1.0: + resolution: {integrity: sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==} + sort-keys-length@1.0.1: resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} engines: {node: '>=0.10.0'} @@ -3746,6 +3945,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -3799,6 +4002,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3888,6 +4094,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} @@ -3920,6 +4129,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} @@ -3945,6 +4158,9 @@ packages: tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tween-functions@1.2.0: + resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4053,6 +4269,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + verror@1.10.1: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} @@ -4128,6 +4348,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -4597,6 +4829,22 @@ snapshots: '@eslint/js@8.57.0': {} + '@fastify/ajv-compiler@4.0.1': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.2 + + '@fastify/error@4.0.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.1': + dependencies: + fast-json-stringify: 6.0.0 + + '@fastify/merge-json-schemas@0.1.1': + dependencies: + fast-deep-equal: 3.1.3 + '@floating-ui/core@1.6.2': dependencies: '@floating-ui/utils': 0.2.2 @@ -5366,6 +5614,10 @@ snapshots: '@types/node': 18.19.34 '@types/responselike': 1.0.3 + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 18.19.34 + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -5444,6 +5696,10 @@ snapshots: '@types/verror@1.10.10': optional: true + '@types/ws@8.5.12': + dependencies: + '@types/node': 18.19.34 + '@types/yauzl@2.10.3': dependencies: '@types/node': 18.19.34 @@ -5552,6 +5808,12 @@ snapshots: '@xobotyi/scrollbar-width@1.9.5': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + abstract-logging@2.0.1: {} + acorn-jsx@5.3.2(acorn@8.11.3): dependencies: acorn: 8.11.3 @@ -5564,6 +5826,10 @@ snapshots: transitivePeerDependencies: - supports-color + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -5575,6 +5841,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-regex@6.0.1: {} @@ -5724,6 +5997,8 @@ snapshots: at-least-node@1.0.0: {} + atomic-sleep@1.0.0: {} + autoprefixer@10.4.19(postcss@8.4.38): dependencies: browserslist: 4.23.1 @@ -5738,6 +6013,11 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + avvio@9.0.0: + dependencies: + '@fastify/error': 4.0.0 + fastq: 1.17.1 + axios@1.7.2(debug@4.3.5): dependencies: follow-redirects: 1.15.6(debug@4.3.5) @@ -5808,6 +6088,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + builder-util-runtime@9.2.3: dependencies: debug: 4.3.5 @@ -6013,6 +6298,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.7.2: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -6553,6 +6840,10 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + + events@3.3.0: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -6599,6 +6890,8 @@ snapshots: extsprintf@1.4.1: optional: true + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -6613,14 +6906,52 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.0.0: + dependencies: + '@fastify/merge-json-schemas': 0.1.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-deep-equal: 3.1.3 + fast-uri: 2.4.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} fast-loops@1.1.3: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + fast-shallow-equal@1.0.0: {} + fast-uri@2.4.0: {} + + fast-uri@3.0.2: {} + fastest-stable-stringify@2.0.2: {} + fastify@5.0.0: + dependencies: + '@fastify/ajv-compiler': 4.0.1 + '@fastify/error': 4.0.0 + '@fastify/fast-json-stringify-compiler': 5.0.1 + abstract-logging: 2.0.1 + avvio: 9.0.0 + fast-json-stringify: 6.0.0 + find-my-way: 9.1.0 + light-my-request: 6.1.0 + pino: 9.4.0 + process-warning: 4.0.0 + proxy-addr: 2.0.7 + rfdc: 1.4.1 + secure-json-parse: 2.7.0 + semver: 7.6.2 + toad-cache: 3.7.0 + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -6659,6 +6990,12 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-my-way@9.1.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6702,6 +7039,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + forwarded@0.2.0: {} + fraction.js@4.3.7: {} from2@2.3.0: @@ -7019,6 +7358,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} + is-array-buffer@3.0.4: dependencies: call-bind: 1.0.7 @@ -7187,8 +7528,14 @@ snapshots: json-buffer@3.0.1: {} + json-schema-ref-resolver@1.0.1: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: @@ -7234,6 +7581,12 @@ snapshots: dependencies: immediate: 3.0.6 + light-my-request@6.1.0: + dependencies: + cookie: 0.7.2 + process-warning: 4.0.0 + set-cookie-parser: 2.7.0 + lilconfig@2.1.0: {} lilconfig@3.1.2: {} @@ -7390,6 +7743,8 @@ snapshots: nanoid@3.3.7: {} + nanoid@5.0.7: {} + natural-compare@1.4.0: {} node-addon-api@1.7.2: @@ -7470,6 +7825,8 @@ snapshots: ohash@1.1.3: {} + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -7558,6 +7915,27 @@ snapshots: pinkie@2.0.4: {} + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.4.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.1.0 + thread-stream: 3.1.0 + pirates@4.0.6: {} pkg-types@1.1.1: @@ -7623,6 +8001,10 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@4.0.0: {} + + process@0.11.10: {} + progress@2.0.3: {} promise-retry@2.0.1: @@ -7641,6 +8023,11 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} pump@3.0.0: @@ -7658,6 +8045,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + quick-lru@5.1.1: {} rc9@2.1.2: @@ -7665,6 +8054,11 @@ snapshots: defu: 6.1.4 destr: 2.0.3 + react-confetti@6.1.0(react@18.3.1): + dependencies: + react: 18.3.1 + tween-functions: 1.2.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -7778,10 +8172,20 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@4.5.2: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 + real-require@0.2.0: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -7803,6 +8207,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resize-observer-polyfill@1.5.1: {} resolve-alpn@1.2.1: {} @@ -7829,10 +8235,14 @@ snapshots: dependencies: lowercase-keys: 2.0.0 + ret@0.5.0: {} + retry@0.12.0: {} reusify@1.0.4: {} + rfdc@1.4.1: {} + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -7894,6 +8304,12 @@ snapshots: es-errors: 1.3.0 is-regex: 1.1.4 + safe-regex2@4.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sanitize-filename@1.6.3: @@ -7908,6 +8324,8 @@ snapshots: screenfull@5.2.0: {} + secure-json-parse@2.7.0: {} + seek-bzip@1.0.6: dependencies: commander: 2.20.3 @@ -7926,6 +8344,8 @@ snapshots: type-fest: 0.13.1 optional: true + set-cookie-parser@2.7.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -7979,6 +8399,10 @@ snapshots: smart-buffer@4.2.0: optional: true + sonic-boom@4.1.0: + dependencies: + atomic-sleep: 1.0.0 + sort-keys-length@1.0.1: dependencies: sort-keys: 1.1.2 @@ -8002,6 +8426,8 @@ snapshots: source-map@0.6.1: {} + split2@4.2.0: {} + sprintf-js@1.1.3: optional: true @@ -8078,6 +8504,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8202,6 +8632,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + throttle-debounce@3.0.1: {} through@2.3.8: {} @@ -8224,6 +8658,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toggle-selection@1.0.6: {} trim-repeated@1.0.0: @@ -8244,6 +8680,8 @@ snapshots: tslib@2.6.3: {} + tween-functions@1.2.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -8350,6 +8788,8 @@ snapshots: util-deprecate@1.0.2: {} + validator@13.12.0: {} + verror@1.10.1: dependencies: assert-plus: 1.0.0 @@ -8430,6 +8870,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + xmlbuilder@15.1.1: {} xtend@4.0.2: {} diff --git a/resources/index.html b/resources/index.html new file mode 100644 index 0000000..d7cb5c3 --- /dev/null +++ b/resources/index.html @@ -0,0 +1,815 @@ + + + + + + + + + + + + Fideo Web Control + + + + + + + + +
+ + +
+
+
+

{{ streamConfig.title }}

+
+ + + + + +
+
+ {{ streamConfig.roomUrl }} + +
+ +
+
+
{{ ffmpegProgressInfo[streamConfig.id].timemark }}
+
+ {{ ffmpegProgressInfo[streamConfig.id].targetSize / 1024 + 'M' }} +
+
+
+
+ + + +
+
+
×
+

+ {{ activeStreamConfig.type === 'create' ? '创建' : '编辑' }} +

+ +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ +
+
+
+
+
+ +
+
+
×
+
{{ dialogObj.title }}
+
{{ dialogObj.description }}
+ +
+
+
+ + + + diff --git a/src/const.ts b/src/const.ts index 4da0a3f..73413a8 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,6 +1,7 @@ export const NAV_BY_DEFAULT_BROWSER = 'NAV_BY_DEFAULT_BROWSER' export const SELECT_DIR = 'SELECT_DIR' export const GET_LIVE_URLS = 'GET_LIVE_URLS' +export const GET_ROOM_INFO = 'GET_ROOM_INFO' export const START_STREAM_RECORD = 'START_STREAM_RECORD' export const STOP_STREAM_RECORD = 'STOP_STREAM_RECORD' export const STREAM_RECORD_END = 'STREAM_RECORD_END' @@ -24,3 +25,29 @@ export const FORCE_CLOSE_WINDOW = 'FORCE_CLOSE_WINDOW' export const RECORD_DUMMY_PROCESS = { kill: () => {} } export const RECORD_END_NOT_USER_STOP = 'RECORD_END_NOT_USER_STOP' + +export const START_FRPC_PROCESS = 'START_FRPC_PROCESS' +export const STOP_FRPC_PROCESS = 'STOP_FRPC_PROCESS' + +export const FRPC_PROCESS_ERROR = 'FRPC_PROCESS_ERROR' + +export const FRP_DOMAIN = 'web-control.fideo.site' +export const API_DOMAIN = 'api-web-control.fideo.site' + +export const START_WEB_CONTROL = 'START_WEB_CONTROL' + +export const WEBSOCKET_MESSAGE_TYPE = { + UPDATE_STREAM_CONFIG_LIST: 'UPDATE_STREAM_CONFIG_LIST', + + UPDATE_FFMPEG_PROGRESS_INFO: 'UPDATE_FFMPEG_PROGRESS_INFO', + SHOW_TOAST: 'SHOW_TOAST', + + START_RECORD_STREAM: 'START_RECORD_STREAM', + PAUSE_RECORD_STREAM: 'PAUSE_RECORD_STREAM', + REMOVE_STREAM_CONFIG: 'REMOVE_STREAM_CONFIG', + UPDATE_STREAM_CONFIG: 'UPDATE_STREAM_CONFIG', + ADD_STREAM_CONFIG: 'ADD_STREAM_CONFIG', + + GET_LIVE_URLS: 'GET_LIVE_URLS', + UPDATE_LIVE_URLS: 'UPDATE_LIVE_URLS' +} diff --git a/src/main/crawler/capture-error.js b/src/main/crawler/capture-error.js index 42db523..612640e 100644 --- a/src/main/crawler/capture-error.js +++ b/src/main/crawler/capture-error.js @@ -14,21 +14,15 @@ export function captureError(fn) { const realArgs = args.slice(0, args.length - 1) try { const res = await fn.apply(this, realArgs) - writeLog(`Fetch Live Res: ${JSON.stringify(res, null, 2)}`) + writeLog(`Fetch Res: ${JSON.stringify(res, null, 2)}`) return res } catch (e) { const message = e.message - writeLog(`Fetch Live Error: ${message}`) + writeLog(`Fetch Error: ${message}`) log('error:', message) - // if (message.includes('timeout')) { - // return { - // code: CRAWLER_ERROR_CODE.TIMEOUT - // } - // } - if (message === ERROR_MESSAGE.INVALID_PROXY) { return { code: CRAWLER_ERROR_CODE.INVALID_PROXY diff --git a/src/main/crawler/index.js b/src/main/crawler/index.js index 77ae8c2..0bacfa6 100644 --- a/src/main/crawler/index.js +++ b/src/main/crawler/index.js @@ -1,85 +1,201 @@ import debug from 'debug' -import { getBilibiliLiveUrlsPlugin } from './plugins/bilibili' -import { getCCLiveUrlsPlugin } from './plugins/cc' -import { getDouYinLiveUrlsPlugin } from './plugins/douyin' -import { getDouyuLiveUrlsPlugin } from './plugins/douyu' -import { getHuyaLiveUrlsPlugin } from './plugins/huya' -import { getKuaishouLiveUrlsPlugin } from './plugins/kuaishou' -import { getYoutubeLiveUrlsPlugin } from './plugins/youtube' -import { getTwitchLiveUrlsPlugin } from './plugins/twitch' -import { getTiktokLiveUrlsPlugin } from './plugins/tiktok' -import { getWeiboLiveUrlsPlugin } from './plugins/weibo' -import { getHuaJiaoLiveUrlsPlugin } from './plugins/huajiao' -import { getTaobaoLiveUrlsPlugin } from './plugins/taobao' -import { getBigoLiveUrlsPlugin } from './plugins/bigo' -import { getYYLiveUrlsPlugin } from './plugins/yy' -import { getJDLiveUrlsPlugin } from './plugins/jd' -import { getMomoLiveUrlsPlugin } from './plugins/momo' -import { getShiGuangLiveUrlsPlugin } from './plugins/shiguang' -import { getVvLiveUrlsPlugin } from './plugins/vv' -import { getChangLiaoLiveUrlsPlugin } from './plugins/changliao' -import { get17LiveUrlsPlugin } from './plugins/17live' -import { getXhsUrlsPlugin } from './plugins/xhs' +import { getBilibiliLiveUrlsPlugin, getBilibiliRoomInfoPlugin } from './plugins/bilibili' +import { getCCLiveUrlsPlugin, getCCRoomInfoPlugin } from './plugins/cc' +import { getDouYinLiveUrlsPlugin, getDouYinRoomInfoPlugin } from './plugins/douyin' +import { getDouyuLiveUrlsPlugin, getDouyuRoomInfoPlugin } from './plugins/douyu' +import { getHuyaLiveUrlsPlugin, getHuyaRoomInfoPlugin } from './plugins/huya' +import { getKuaishouLiveUrlsPlugin, getKuaishouRoomInfoPlugin } from './plugins/kuaishou' +import { getYoutubeLiveUrlsPlugin, getYoutubeRoomInfoPlugin } from './plugins/youtube' +import { getTwitchLiveUrlsPlugin, getTwitchRoomInfoPlugin } from './plugins/twitch' +import { getTiktokLiveUrlsPlugin, getTiktokRoomInfoPlugin } from './plugins/tiktok' +import { getWeiboLiveUrlsPlugin, getWeiboRoomInfoPlugin } from './plugins/weibo' +import { getHuaJiaoLiveUrlsPlugin, getHuaJiaoRoomInfoPlugin } from './plugins/huajiao' +import { getTaobaoLiveUrlsPlugin, getTaobaoRoomInfoPlugin } from './plugins/taobao' +import { getBigoLiveUrlsPlugin, getBigoRoomInfoPlugin } from './plugins/bigo' +import { getYYLiveUrlsPlugin, getYYRoomInfoPlugin } from './plugins/yy' +import { getJDLiveUrlsPlugin, getJDRoomInfoPlugin } from './plugins/jd' +import { getMomoLiveUrlsPlugin, getMomoRoomInfoPlugin } from './plugins/momo' +import { getShiGuangLiveUrlsPlugin, getShiGuangRoomInfoPlugin } from './plugins/shiguang' +import { getVvLiveUrlsPlugin, getVvRoomInfoPlugin } from './plugins/vv' +import { getChangLiaoLiveUrlsPlugin, getChangLiaoRoomInfoPlugin } from './plugins/changliao' +import { get17LiveUrlsPlugin, get17LiveRoomInfoPlugin } from './plugins/17live' +import { getXhsUrlsPlugin, getXhsRoomInfoPlugin } from './plugins/xhs' +import { getKilakilaLiveUrlsPlugin, getKilakilaRoomInfoPlugin } from './plugins/kilakila' +import { getAcFunLiveUrlsPlugin, getAcFunRoomInfoPlugin } from './plugins/acfun' import { CRAWLER_ERROR_CODE } from '../../code' const log = debug('fideo-crawler') const hostnameToPlatformCrawlerFnMap = { - 'www.youtube.com': getYoutubeLiveUrlsPlugin, - 'youtube.com': getYoutubeLiveUrlsPlugin, + 'www.youtube.com': { + getLiveUrlsFn: getYoutubeLiveUrlsPlugin, + getRoomInfoFn: getYoutubeRoomInfoPlugin + }, + 'youtube.com': { + getLiveUrlsFn: getYoutubeLiveUrlsPlugin, + getRoomInfoFn: getYoutubeRoomInfoPlugin + }, - 'www.twitch.tv': getTwitchLiveUrlsPlugin, - 'twitch.tv': getTwitchLiveUrlsPlugin, + 'www.twitch.tv': { + getLiveUrlsFn: getTwitchLiveUrlsPlugin, + getRoomInfoFn: getTwitchRoomInfoPlugin + }, + 'twitch.tv': { + getLiveUrlsFn: getTwitchLiveUrlsPlugin, + getRoomInfoFn: getTwitchRoomInfoPlugin + }, - 'www.tiktok.com': getTiktokLiveUrlsPlugin, - 'tiktok.com': getTiktokLiveUrlsPlugin, + 'www.tiktok.com': { + getLiveUrlsFn: getTiktokLiveUrlsPlugin, + getRoomInfoFn: getTiktokRoomInfoPlugin + }, + 'tiktok.com': { + getLiveUrlsFn: getTiktokLiveUrlsPlugin, + getRoomInfoFn: getTiktokRoomInfoPlugin + }, - 'live.douyin.com': getDouYinLiveUrlsPlugin, - 'v.douyin.com': getDouYinLiveUrlsPlugin, + 'live.douyin.com': { + getLiveUrlsFn: getDouYinLiveUrlsPlugin, + getRoomInfoFn: getDouYinRoomInfoPlugin + }, + 'v.douyin.com': { + getLiveUrlsFn: getDouYinLiveUrlsPlugin, + getRoomInfoFn: getDouYinRoomInfoPlugin + }, - 'live.kuaishou.com': getKuaishouLiveUrlsPlugin, - 'live.bilibili.com': getBilibiliLiveUrlsPlugin, + 'live.kuaishou.com': { + getLiveUrlsFn: getKuaishouLiveUrlsPlugin, + getRoomInfoFn: getKuaishouRoomInfoPlugin + }, - 'cc.163.com': getCCLiveUrlsPlugin, + 'live.bilibili.com': { + getLiveUrlsFn: getBilibiliLiveUrlsPlugin, + getRoomInfoFn: getBilibiliRoomInfoPlugin + }, - 'www.huajiao.com': getHuaJiaoLiveUrlsPlugin, - 'huajiao.com': getHuaJiaoLiveUrlsPlugin, + 'cc.163.com': { + getLiveUrlsFn: getCCLiveUrlsPlugin, + getRoomInfoFn: getCCRoomInfoPlugin + }, - 'weibo.com': getWeiboLiveUrlsPlugin, - 'www.weibo.com': getWeiboLiveUrlsPlugin, + 'www.huajiao.com': { + getLiveUrlsFn: getHuaJiaoLiveUrlsPlugin, + getRoomInfoFn: getHuaJiaoRoomInfoPlugin + }, - 'www.douyu.com': getDouyuLiveUrlsPlugin, - 'douyu.com': getDouyuLiveUrlsPlugin, + 'huajiao.com': { + getLiveUrlsFn: getHuaJiaoLiveUrlsPlugin, + getRoomInfoFn: getHuaJiaoRoomInfoPlugin + }, - 'tbzb.taobao.com': getTaobaoLiveUrlsPlugin, + 'weibo.com': { + getLiveUrlsFn: getWeiboLiveUrlsPlugin, + getRoomInfoFn: getWeiboRoomInfoPlugin + }, + 'www.weibo.com': { + getLiveUrlsFn: getWeiboLiveUrlsPlugin, + getRoomInfoFn: getWeiboRoomInfoPlugin + }, - 'www.bigo.tv': getBigoLiveUrlsPlugin, - 'bigo.tv': getBigoLiveUrlsPlugin, + 'www.douyu.com': { + getLiveUrlsFn: getDouyuLiveUrlsPlugin, + getRoomInfoFn: getDouyuRoomInfoPlugin + }, + 'douyu.com': { + getLiveUrlsFn: getDouyuLiveUrlsPlugin, + getRoomInfoFn: getDouyuRoomInfoPlugin + }, - 'www.yy.com': getYYLiveUrlsPlugin, - 'yy.com': getYYLiveUrlsPlugin, + 'tbzb.taobao.com': { + getLiveUrlsFn: getTaobaoLiveUrlsPlugin, + getRoomInfoFn: getTaobaoRoomInfoPlugin + }, - 'www.huya.com': getHuyaLiveUrlsPlugin, - 'huya.com': getHuyaLiveUrlsPlugin, + 'www.bigo.tv': { + getLiveUrlsFn: getBigoLiveUrlsPlugin, + getRoomInfoFn: getBigoRoomInfoPlugin + }, + 'bigo.tv': { + getLiveUrlsFn: getBigoLiveUrlsPlugin, + getRoomInfoFn: getBigoRoomInfoPlugin + }, - 'lives.jd.com': getJDLiveUrlsPlugin, + 'www.yy.com': { + getLiveUrlsFn: getYYLiveUrlsPlugin, + getRoomInfoFn: getYYRoomInfoPlugin + }, + 'yy.com': { + getLiveUrlsFn: getYYLiveUrlsPlugin, + getRoomInfoFn: getYYRoomInfoPlugin + }, - 'web.immomo.com': getMomoLiveUrlsPlugin, + 'www.huya.com': { + getLiveUrlsFn: getHuyaLiveUrlsPlugin, + getRoomInfoFn: getHuyaRoomInfoPlugin + }, + 'huya.com': { + getLiveUrlsFn: getHuyaLiveUrlsPlugin, + getRoomInfoFn: getHuyaRoomInfoPlugin + }, - 'www.rengzu.com': getShiGuangLiveUrlsPlugin, + 'lives.jd.com': { + getLiveUrlsFn: getJDLiveUrlsPlugin, + getRoomInfoFn: getJDRoomInfoPlugin + }, - 'h5webcdn-pro.vvxqiu.com': getVvLiveUrlsPlugin, + 'web.immomo.com': { + getLiveUrlsFn: getMomoLiveUrlsPlugin, + getRoomInfoFn: getMomoRoomInfoPlugin + }, - 'www.tlclw.com': getChangLiaoLiveUrlsPlugin, - 'tlclw.com': getChangLiaoLiveUrlsPlugin, + 'www.rengzu.com': { + getLiveUrlsFn: getShiGuangLiveUrlsPlugin, + getRoomInfoFn: getShiGuangRoomInfoPlugin + }, - 'www.17.live': get17LiveUrlsPlugin, - '17.live': get17LiveUrlsPlugin, + 'h5webcdn-pro.vvxqiu.com': { + getLiveUrlsFn: getVvLiveUrlsPlugin, + getRoomInfoFn: getVvRoomInfoPlugin + }, - 'www.xiaohongshu.com': getXhsUrlsPlugin, - 'xiaohongshu.com': getXhsUrlsPlugin + 'www.tlclw.com': { + getLiveUrlsFn: getChangLiaoLiveUrlsPlugin, + getRoomInfoFn: getChangLiaoRoomInfoPlugin + }, + 'tlclw.com': { + getLiveUrlsFn: getChangLiaoLiveUrlsPlugin, + getRoomInfoFn: getChangLiaoRoomInfoPlugin + }, + + 'www.17.live': { + getLiveUrlsFn: get17LiveUrlsPlugin, + getRoomInfoFn: get17LiveRoomInfoPlugin + }, + '17.live': { + getLiveUrlsFn: get17LiveUrlsPlugin, + getRoomInfoFn: get17LiveRoomInfoPlugin + }, + + 'www.xiaohongshu.com': { + getLiveUrlsFn: getXhsUrlsPlugin, + getRoomInfoFn: getXhsRoomInfoPlugin + }, + 'xiaohongshu.com': { + getLiveUrlsFn: getXhsUrlsPlugin, + getRoomInfoFn: getXhsRoomInfoPlugin + }, + + 'live.kilakila.cn': { + getLiveUrlsFn: getKilakilaLiveUrlsPlugin, + getRoomInfoFn: getKilakilaRoomInfoPlugin + }, + + 'live.acfun.cn': { + getLiveUrlsFn: getAcFunLiveUrlsPlugin, + getRoomInfoFn: getAcFunRoomInfoPlugin + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -118,7 +234,7 @@ export async function getLiveUrls(info, writeLog) { } } - const getLiveUrlsFn = hostnameToPlatformCrawlerFnMap[host] + const getLiveUrlsFn = hostnameToPlatformCrawlerFnMap[host]?.getLiveUrlsFn // TODO: 判断是否是支持的平台但是url不对,提供更好的错误提示 if (!getLiveUrlsFn) { @@ -131,3 +247,29 @@ export async function getLiveUrls(info, writeLog) { log('res:', res) return res } + +export async function getRoomInfo(info, writeLog) { + const { roomUrl, proxy, cookie } = info + let host + try { + host = new URL(roomUrl).host + } catch (e) { + console.error(e) + return { + code: CRAWLER_ERROR_CODE.INVALID_URL + } + } + + const getRoomInfoFn = hostnameToPlatformCrawlerFnMap[host]?.getRoomInfoFn + + // TODO: 判断是否是支持的平台但是url不对,提供更好的错误提示 + if (!getRoomInfoFn) { + return { + code: CRAWLER_ERROR_CODE.NOT_SUPPORT + } + } + + const res = await getRoomInfoFn(roomUrl, { proxy, cookie }, writeLog) + log('res:', res) + return res +} diff --git a/src/main/crawler/plugins/17live.js b/src/main/crawler/plugins/17live.js index eaa5f9a..660fab9 100644 --- a/src/main/crawler/plugins/17live.js +++ b/src/main/crawler/plugins/17live.js @@ -44,4 +44,33 @@ async function baseGet17LiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGet17LiveRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + log('roomId:', roomId, 'cookie:', cookie, 'proxy:', proxy) + + const res = ( + await request(`https://wap-api.17app.co/api/v1/lives/${roomId}/info`, { + headers: { + cookie, + 'User-Agent': MOBILE_USER_AGENT + }, + proxy + }) + ).data + + const name = res.userInfo.displayName + + log('name:', name) + + return { + code: SUCCESS_CODE, + roomInfo: { + name + } + } +} + export const get17LiveUrlsPlugin = captureError(baseGet17LiveUrlsPlugin) +export const get17LiveRoomInfoPlugin = captureError(baseGet17LiveRoomInfoPlugin) diff --git a/src/main/crawler/plugins/acfun.js b/src/main/crawler/plugins/acfun.js new file mode 100644 index 0000000..52eaa47 --- /dev/null +++ b/src/main/crawler/plugins/acfun.js @@ -0,0 +1,115 @@ +import debug from 'debug' + +import { request, DESKTOP_USER_AGENT } from '../base-request.js' +import { captureError } from '../capture-error.js' + +import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' + +const log = debug('fideo-crawler-acFun') + +function getRoomIdByUrl(url) { + return new URL(url).pathname.split('/')[2] +} + +async function baseGetAcFunLiveUrlsPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + log('roomId:', roomId, 'cookie:', cookie, 'proxy:', proxy) + + const setCookie = await request(roomUrl, { + headers: { + 'User-Agent': DESKTOP_USER_AGENT + } + }).then((res) => res.headers.get('Set-Cookie')) + + const { userId, 'acfun.api.visitor_st': visitorSt } = ( + await request(`https://id.app.acfun.cn/rest/app/visitor/login`, { + method: 'POST', + headers: { + Cookie: cookie ? `${setCookie};${cookie}` : `${setCookie}`, + 'User-Agent': DESKTOP_USER_AGENT, + Referer: 'https://live.acfun.cn', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + proxy, + data: 'sid=acfun.api.visitor' + }) + ).data + + const res = ( + await request( + `https://api.kuaishouzt.com/rest/zt/live/web/startPlay?subBiz=mainApp&kpn=ACFUN_APP&kpf=PC_WEB&userId=${userId}&acfun.api.visitor_st=${visitorSt}`, + { + method: 'POST', + headers: { + cookie: cookie ? `${setCookie};${cookie}` : `${setCookie}`, + 'User-Agent': DESKTOP_USER_AGENT, + Referer: 'https://live.acfun.cn', + 'Content-Type': 'application/x-www-form-urlencoded' + }, + proxy, + data: `authorId=${roomId}&pullStreamType=FLV` + } + ) + ).data + + if (res.result !== 1) { + return { + code: CRAWLER_ERROR_CODE.NOT_URLS + } + } + const videoPlayRes = JSON.parse(res.data.videoPlayRes) + + const representations = videoPlayRes.liveAdaptiveManifest[0].adaptationSet.representation + + let maxBitrate = 0 + let maxBitrateUrl = '' + for (const representation of representations) { + const { url, bitrate } = representation + + if (bitrate > maxBitrate) { + maxBitrate = bitrate + maxBitrateUrl = url + } + } + + if (!maxBitrateUrl) { + return { + code: CRAWLER_ERROR_CODE.NOT_URLS + } + } + + return { + code: SUCCESS_CODE, + liveUrls: [maxBitrateUrl] + } +} + +async function baseGetAcFunRoomInfoPlugin(roomUrl, others = {}) { + const { proxy, cookie } = others + const roomId = getRoomIdByUrl(roomUrl) + + const res = ( + await request(`https://live.acfun.cn/rest/pc-direct/user/userInfo?userId=${roomId}`, { + headers: { + 'User-Agent': DESKTOP_USER_AGENT, + referer: 'https://live.acfun.cn', + cookie + }, + proxy + }) + ).data + + log('name:', res.profile.name) + + return { + code: SUCCESS_CODE, + roomInfo: { + name: res.profile.name + } + } +} + +export const getAcFunLiveUrlsPlugin = captureError(baseGetAcFunLiveUrlsPlugin) +export const getAcFunRoomInfoPlugin = captureError(baseGetAcFunRoomInfoPlugin) diff --git a/src/main/crawler/plugins/bigo.js b/src/main/crawler/plugins/bigo.js index 4a642f9..00a2107 100644 --- a/src/main/crawler/plugins/bigo.js +++ b/src/main/crawler/plugins/bigo.js @@ -1,6 +1,6 @@ import debug from 'debug' -import { request } from '../base-request.js' +import { DESKTOP_USER_AGENT, request } from '../base-request.js' import { captureError } from '../capture-error.js' import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' @@ -47,4 +47,31 @@ async function baseGetBigoLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetBigoRoomInfoPlugin(roomUrl, others = {}) { + const { proxy, cookie } = others + + log('roomUrl:', roomUrl, 'cookie:', cookie, 'proxy:', proxy) + + const html = ( + await request(roomUrl, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + }, + proxy + }) + ).data + + const flag = '.nickname="' + const startIndex = html.indexOf(flag) + flag.length + const endIndex = html.indexOf('";', startIndex) + const name = html.slice(startIndex, endIndex) + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getBigoLiveUrlsPlugin = captureError(baseGetBigoLiveUrlsPlugin) +export const getBigoRoomInfoPlugin = captureError(baseGetBigoRoomInfoPlugin) diff --git a/src/main/crawler/plugins/bilibili.js b/src/main/crawler/plugins/bilibili.js index 443c485..8c4c51a 100644 --- a/src/main/crawler/plugins/bilibili.js +++ b/src/main/crawler/plugins/bilibili.js @@ -1,6 +1,6 @@ import debug from 'debug' -import { request } from '../base-request.js' +import { request, DESKTOP_USER_AGENT } from '../base-request.js' import { captureError } from '../capture-error.js' import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' @@ -69,4 +69,29 @@ async function baseGetBilibiliLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetBilibiliRoomInfoPlugin(roomUrl, others = {}) { + const { proxy, cookie } = others + + const html = ( + await request(roomUrl, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + }, + proxy + }) + ).data + + const flag = 'uname":"' + const startIndex = html.indexOf(flag) + flag.length + const endIndex = html.indexOf('","', startIndex) + const name = html.slice(startIndex, endIndex) + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getBilibiliLiveUrlsPlugin = captureError(baseGetBilibiliLiveUrlsPlugin) +export const getBilibiliRoomInfoPlugin = captureError(baseGetBilibiliRoomInfoPlugin) diff --git a/src/main/crawler/plugins/cc.js b/src/main/crawler/plugins/cc.js index 4ea749c..c7daec4 100644 --- a/src/main/crawler/plugins/cc.js +++ b/src/main/crawler/plugins/cc.js @@ -62,4 +62,27 @@ async function baseGetCCLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetCCRoomInfoPlugin(roomUrl, others = {}) { + const { proxy, cookie } = others + const html = ( + await request(roomUrl, { + headers: { + cookie + }, + proxy + }) + ).data + + const flag = '"nickname":"' + const startIndex = html.indexOf(flag) + flag.length + const endIndex = html.indexOf('","', startIndex) + const name = html.slice(startIndex, endIndex) + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getCCLiveUrlsPlugin = captureError(baseGetCCLiveUrlsPlugin) +export const getCCRoomInfoPlugin = captureError(baseGetCCRoomInfoPlugin) diff --git a/src/main/crawler/plugins/changliao.js b/src/main/crawler/plugins/changliao.js index b1f497b..acce6b6 100644 --- a/src/main/crawler/plugins/changliao.js +++ b/src/main/crawler/plugins/changliao.js @@ -1,6 +1,6 @@ import debug from 'debug' -import { request, MOBILE_USER_AGENT } from '../base-request.js' +import { request, MOBILE_USER_AGENT, DESKTOP_USER_AGENT } from '../base-request.js' import { captureError } from '../capture-error.js' import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' @@ -87,4 +87,30 @@ async function baseGetChangLiaoLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetChangLiaoRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + const res = ( + await request( + `https://www.tlclw.com/ashx/UI/room/base.ashx?v=1.0.1&giftNum=9&roomid=${roomId}`, + { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + }, + proxy + } + ) + ).data + + const name = res.roomname + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getChangLiaoLiveUrlsPlugin = captureError(baseGetChangLiaoLiveUrlsPlugin) +export const getChangLiaoRoomInfoPlugin = captureError(baseGetChangLiaoRoomInfoPlugin) diff --git a/src/main/crawler/plugins/douyin.js b/src/main/crawler/plugins/douyin.js index f2163d0..ff4cec7 100644 --- a/src/main/crawler/plugins/douyin.js +++ b/src/main/crawler/plugins/douyin.js @@ -7,7 +7,8 @@ import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' const log = debug('fideo-crawler-douyin') -async function baseGetMobileDouYinLiveUrlsPlugin(roomUrl, others = {}) { +async function getMobileData(roomUrl, others = {}) { + const { proxy, cookie } = others // 这里直接用fetch然后取消重定向,获取location const location = ( await fetch(roomUrl, { @@ -18,10 +19,6 @@ async function baseGetMobileDouYinLiveUrlsPlugin(roomUrl, others = {}) { const url = new URL(location) const roomId = url.pathname.split('/').pop() - const { proxy, cookie } = others - - log('roomId:', roomId, 'cookie:', cookie, 'proxy:', proxy) - const secUserId = url.searchParams.get('sec_user_id') const data = ( @@ -37,7 +34,36 @@ async function baseGetMobileDouYinLiveUrlsPlugin(roomUrl, others = {}) { ) ).data - // console.dir(data, { depth: null }) + return data +} + +async function getDesktopData(roomUrl, others = {}) { + const roomId = new URL(roomUrl).pathname.split('/')[1] + const { proxy, cookie } = others + const baseUrl = 'https://live.douyin.com/' + const fetchRoomUrl = `${baseUrl}${roomId}` + const fetchUrl = `${baseUrl}webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&language=zh-CN&enter_from=web_live&cookie_enabled=true&screen_width=1728&screen_height=1117&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Chrome&browser_version=116.0.0.0&web_rid=${roomId}` + + const [res1, res2] = await Promise.all([request(baseUrl), request(fetchRoomUrl)]) + const setCookie = `${res1.headers.get('set-cookie')};${res2.headers.get('set-cookie')};${cookie ? cookie : ''}` + const data = ( + await request(fetchUrl, { + headers: { + cookie: setCookie + }, + proxy + }) + ).data + + return data +} + +async function baseGetMobileDouYinLiveUrlsPlugin(roomUrl, others = {}) { + const { proxy, cookie } = others + + log('roomUrl:', roomUrl, 'cookie:', cookie, 'proxy:', proxy) + + const data = await getMobileData(roomUrl, others) const pullData = data.data.room.stream_url.live_core_sdk_data.pull_data const status = data.data.room.status @@ -73,20 +99,9 @@ async function baseGetDesktopDouYinLiveUrlsPlugin(roomUrl, others = {}) { log('roomId:', roomId, 'cookie:', cookie, 'proxy:', proxy) - const baseUrl = 'https://live.douyin.com/' - const fetchRoomUrl = `${baseUrl}${roomId}` - const fetchUrl = `${baseUrl}webcast/room/web/enter/?aid=6383&app_name=douyin_web&live_id=1&device_platform=web&language=zh-CN&enter_from=web_live&cookie_enabled=true&screen_width=1728&screen_height=1117&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Chrome&browser_version=116.0.0.0&web_rid=${roomId}` + const data = await getDesktopData(roomUrl, others) - const [res1, res2] = await Promise.all([request(baseUrl), request(fetchRoomUrl)]) - const setCookie = `${res1.headers.get('set-cookie')};${res2.headers.get('set-cookie')};${cookie ? cookie : ''}` - const pullData = ( - await request(fetchUrl, { - headers: { - cookie: setCookie - }, - proxy - }) - ).data.data.data[0].stream_url.live_core_sdk_data.pull_data + const pullData = data.data.data[0].stream_url.live_core_sdk_data.pull_data const streamData = JSON.parse(pullData.stream_data).data @@ -116,4 +131,22 @@ async function baseGetDouYinLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetDouYinRoomInfoPlugin(roomUrl, others = {}) { + const isDesktopUrl = new URL(roomUrl).host === 'live.douyin.com' + let name = '' + if (isDesktopUrl) { + const data = await getDesktopData(roomUrl, others) + name = data.data.data[0].owner.nickname + } else { + const data = await getMobileData(roomUrl, others) + name = data.data.room.owner.nickname + } + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getDouYinLiveUrlsPlugin = captureError(baseGetDouYinLiveUrlsPlugin) +export const getDouYinRoomInfoPlugin = captureError(baseGetDouYinRoomInfoPlugin) diff --git a/src/main/crawler/plugins/douyu.js b/src/main/crawler/plugins/douyu.js index b9981a2..ad3d18c 100644 --- a/src/main/crawler/plugins/douyu.js +++ b/src/main/crawler/plugins/douyu.js @@ -96,4 +96,27 @@ async function baseGetDouyuLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetDouyuRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + const data = ( + await request(`https://www.douyu.com/betard/${roomId}`, { + proxy, + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + } + }) + ).data + + const name = data.room.nickname + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getDouyuLiveUrlsPlugin = captureError(baseGetDouyuLiveUrlsPlugin) +export const getDouyuRoomInfoPlugin = captureError(baseGetDouyuRoomInfoPlugin) diff --git a/src/main/crawler/plugins/huajiao.js b/src/main/crawler/plugins/huajiao.js index 6eea3fa..2c5dc3f 100644 --- a/src/main/crawler/plugins/huajiao.js +++ b/src/main/crawler/plugins/huajiao.js @@ -1,6 +1,6 @@ import debug from 'debug' -import { request } from '../base-request.js' +import { DESKTOP_USER_AGENT, request } from '../base-request.js' import { captureError } from '../capture-error.js' import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' @@ -26,6 +26,12 @@ async function baseGetHuaJiaoLiveUrlsPlugin(roomUrl, others = {}) { }) ).data + if (htmlContent.includes('正在重播')) { + return { + code: CRAWLER_ERROR_CODE.NOT_URLS + } + } + const scriptContentRegex = /]*>([\s\S]*?)<\/script>/gi const matches = htmlContent.match(scriptContentRegex) @@ -62,7 +68,7 @@ async function baseGetHuaJiaoLiveUrlsPlugin(roomUrl, others = {}) { ) ).data - const liveUrls = [liveUrlData.data.main, liveUrlData.data.h264_url] + const liveUrls = [liveUrlData.data.main, liveUrlData.data.h264_url].filter(Boolean) if (liveUrls.length === 0) { return { @@ -76,4 +82,31 @@ async function baseGetHuaJiaoLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetHuaJiaoRoomInfoPlugin(roomUrl, others = {}) { + const { proxy, cookie } = others + const htmlContent = ( + await request(roomUrl, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + }, + proxy + }) + ).data + + const flag = '

' + const jsNicknameIndex = htmlContent.indexOf('js-nickname') + const startIndex = htmlContent.indexOf(flag, jsNicknameIndex) + flag.length + + const endIndex = htmlContent.indexOf('

', startIndex) + + const name = htmlContent.slice(startIndex, endIndex) + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getHuaJiaoLiveUrlsPlugin = captureError(baseGetHuaJiaoLiveUrlsPlugin) +export const getHuaJiaoRoomInfoPlugin = captureError(baseGetHuaJiaoRoomInfoPlugin) diff --git a/src/main/crawler/plugins/huya.js b/src/main/crawler/plugins/huya.js index 28f4e44..b1c9c90 100644 --- a/src/main/crawler/plugins/huya.js +++ b/src/main/crawler/plugins/huya.js @@ -113,4 +113,33 @@ async function baseGetHuyaLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetHuyaRoomInfoPlugin(roomUrl, others = {}) { + + const { proxy, cookie } = others + + const htmlContent = ( + await request(roomUrl, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + }, + proxy + }) + ).data + + const startFlag = '">' + const hostNameIndex = htmlContent.indexOf('host-name') + + const startIndex = htmlContent.indexOf(startFlag, hostNameIndex) + startFlag.length + const endIndex = htmlContent.indexOf('', startIndex) + + const name = htmlContent.slice(startIndex, endIndex) + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getHuyaLiveUrlsPlugin = captureError(baseGetHuyaLiveUrlsPlugin) +export const getHuyaRoomInfoPlugin = captureError(baseGetHuyaRoomInfoPlugin) diff --git a/src/main/crawler/plugins/jd.js b/src/main/crawler/plugins/jd.js index 82cc2b7..b815e3d 100644 --- a/src/main/crawler/plugins/jd.js +++ b/src/main/crawler/plugins/jd.js @@ -55,4 +55,36 @@ async function baseGetJDLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetJDRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + const body = { + liveId: roomId, + pageId: 'Mlive_LiveRoom' + } + + const json = ( + await request( + `https://api.m.jd.com/api?appid=h5-live&functionId=liveDetailToM&body=${encodeURIComponent(JSON.stringify(body))}`, + { + proxy, + headers: { + cookie, + 'User-Agent': MOBILE_USER_AGENT, + Referer: `https://lives.jd.com/` + } + } + ) + ).data + + const { name } = json.data.author + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getJDLiveUrlsPlugin = captureError(baseGetJDLiveUrlsPlugin) +export const getJDRoomInfoPlugin = captureError(baseGetJDRoomInfoPlugin) diff --git a/src/main/crawler/plugins/kilakila.js b/src/main/crawler/plugins/kilakila.js new file mode 100644 index 0000000..b412ea7 --- /dev/null +++ b/src/main/crawler/plugins/kilakila.js @@ -0,0 +1,98 @@ +import debug from 'debug' + +import { request, DESKTOP_USER_AGENT } from '../base-request.js' +import { captureError } from '../capture-error.js' + +import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' + +const log = debug('fideo-crawler-kilakila') + +function getRoomIdByUrl(url) { + return new URL(url).searchParams.get('id') +} + +async function baseGetKilakilaLiveUrlsPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + log('roomId:', roomId, 'cookie:', cookie, 'proxy:', proxy) + + const res = ( + await request(`https://live.kilakila.cn/LiveRoom/getRoomInfo?roomId=${roomId}`, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + } + }) + ).data + + const b = res.b + if (b.liveStartStr !== '直播中') { + return { + code: CRAWLER_ERROR_CODE.NOT_URLS + } + } + const fetchRoomId = b.roomId + + if (Number(fetchRoomId) === Number(roomId)) { + return { + code: SUCCESS_CODE, + liveUrls: [b.flvPlayUrl, b.hlsPlayUrl] + } + } + + const newRes = ( + await request(`https://live.kilakila.cn/LiveRoom/getRoomInfo?roomId=${fetchRoomId}`, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + } + }) + ).data + + const newB = newRes.b + if (newB.liveStartStr !== '直播中') { + return { + code: CRAWLER_ERROR_CODE.NOT_URLS + } + } + + const liveUrls = [newB.flvPlayUrl, newB.hlsPlayUrl].filter(Boolean) + + if (liveUrls.length === 0) { + return { + code: CRAWLER_ERROR_CODE.NOT_URLS + } + } + + return { + code: SUCCESS_CODE, + liveUrls + } +} + +async function baseGetKilakilaRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others + + log('roomId:', roomId, 'cookie:', cookie, 'proxy:', proxy) + + const res = ( + await request(`https://live.kilakila.cn/LiveRoom/getRoomInfo?roomId=${roomId}`, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + } + }) + ).data + + const name = res.b.userInfo.nickname + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + +export const getKilakilaLiveUrlsPlugin = captureError(baseGetKilakilaLiveUrlsPlugin) +export const getKilakilaRoomInfoPlugin = captureError(baseGetKilakilaRoomInfoPlugin) diff --git a/src/main/crawler/plugins/kuaishou.js b/src/main/crawler/plugins/kuaishou.js index 30acbe6..3469b17 100644 --- a/src/main/crawler/plugins/kuaishou.js +++ b/src/main/crawler/plugins/kuaishou.js @@ -66,4 +66,10 @@ async function baseGetKuaishouLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetKuaishouRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getKuaishouLiveUrlsPlugin = captureError(baseGetKuaishouLiveUrlsPlugin) +export const getKuaishouRoomInfoPlugin = captureError(baseGetKuaishouRoomInfoPlugin) diff --git a/src/main/crawler/plugins/momo.js b/src/main/crawler/plugins/momo.js index 0035e99..c96507e 100644 --- a/src/main/crawler/plugins/momo.js +++ b/src/main/crawler/plugins/momo.js @@ -56,4 +56,10 @@ async function baseGetMomoLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetMomoRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getMomoLiveUrlsPlugin = captureError(baseGetMomoLiveUrlsPlugin) +export const getMomoRoomInfoPlugin = captureError(baseGetMomoRoomInfoPlugin) diff --git a/src/main/crawler/plugins/shiguang.js b/src/main/crawler/plugins/shiguang.js index 8f61217..4e7770c 100644 --- a/src/main/crawler/plugins/shiguang.js +++ b/src/main/crawler/plugins/shiguang.js @@ -87,4 +87,10 @@ async function baseGetShiGuangLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetShiGuangRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getShiGuangLiveUrlsPlugin = captureError(baseGetShiGuangLiveUrlsPlugin) +export const getShiGuangRoomInfoPlugin = captureError(baseGetShiGuangRoomInfoPlugin) diff --git a/src/main/crawler/plugins/taobao.js b/src/main/crawler/plugins/taobao.js index 355e005..1a2f8b9 100644 --- a/src/main/crawler/plugins/taobao.js +++ b/src/main/crawler/plugins/taobao.js @@ -336,4 +336,10 @@ async function baseGetTaobaoLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetTaobaoRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getTaobaoLiveUrlsPlugin = captureError(baseGetTaobaoLiveUrlsPlugin) +export const getTaobaoRoomInfoPlugin = captureError(baseGetTaobaoRoomInfoPlugin) diff --git a/src/main/crawler/plugins/tiktok.js b/src/main/crawler/plugins/tiktok.js index 9fbb3c7..8395088 100644 --- a/src/main/crawler/plugins/tiktok.js +++ b/src/main/crawler/plugins/tiktok.js @@ -87,4 +87,10 @@ async function baseGetTiktokLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetTiktokRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getTiktokLiveUrlsPlugin = captureError(baseGetTiktokLiveUrlsPlugin) +export const getTiktokRoomInfoPlugin = captureError(baseGetTiktokRoomInfoPlugin) diff --git a/src/main/crawler/plugins/twitch.js b/src/main/crawler/plugins/twitch.js index a537a47..ed14375 100644 --- a/src/main/crawler/plugins/twitch.js +++ b/src/main/crawler/plugins/twitch.js @@ -106,4 +106,10 @@ async function baseGetTwitchLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetTwitchRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getTwitchLiveUrlsPlugin = captureError(baseGetTwitchLiveUrlsPlugin) +export const getTwitchRoomInfoPlugin = captureError(baseGetTwitchRoomInfoPlugin) diff --git a/src/main/crawler/plugins/vv.js b/src/main/crawler/plugins/vv.js index a44b09a..9185f5f 100644 --- a/src/main/crawler/plugins/vv.js +++ b/src/main/crawler/plugins/vv.js @@ -43,4 +43,10 @@ async function baseGetVvLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetVvRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getVvLiveUrlsPlugin = captureError(baseGetVvLiveUrlsPlugin) +export const getVvRoomInfoPlugin = captureError(baseGetVvRoomInfoPlugin) diff --git a/src/main/crawler/plugins/weibo.js b/src/main/crawler/plugins/weibo.js index 8510956..4b77d4d 100644 --- a/src/main/crawler/plugins/weibo.js +++ b/src/main/crawler/plugins/weibo.js @@ -57,4 +57,10 @@ async function baseGetWeiboLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetWeiboRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getWeiboLiveUrlsPlugin = captureError(baseGetWeiboLiveUrlsPlugin) +export const getWeiboRoomInfoPlugin = captureError(baseGetWeiboRoomInfoPlugin) diff --git a/src/main/crawler/plugins/xhs.js b/src/main/crawler/plugins/xhs.js index 4fb2139..01eee71 100644 --- a/src/main/crawler/plugins/xhs.js +++ b/src/main/crawler/plugins/xhs.js @@ -61,4 +61,10 @@ async function baseGetXhsUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetXhsRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getXhsUrlsPlugin = captureError(baseGetXhsUrlsPlugin) +export const getXhsRoomInfoPlugin = captureError(baseGetXhsRoomInfoPlugin) diff --git a/src/main/crawler/plugins/youtube.js b/src/main/crawler/plugins/youtube.js index 29445aa..532b622 100644 --- a/src/main/crawler/plugins/youtube.js +++ b/src/main/crawler/plugins/youtube.js @@ -1,6 +1,6 @@ import debug from 'debug' -import { request } from '../base-request.js' +import { DESKTOP_USER_AGENT, request } from '../base-request.js' import { captureError } from '../capture-error.js' import { CRAWLER_ERROR_CODE, SUCCESS_CODE } from '../../../code' @@ -22,8 +22,7 @@ async function baseGetYoutubeLiveUrlsPlugin(roomUrl, others = {}) { proxy, headers: { cookie, - 'User-Agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' + 'User-Agent': DESKTOP_USER_AGENT } }) ).data @@ -58,4 +57,30 @@ async function baseGetYoutubeLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetYoutubeRoomInfoPlugin(roomUrl, others = {}) { + const { cookie, proxy } = others + + const html = ( + await request(roomUrl, { + headers: { + cookie, + 'User-Agent': DESKTOP_USER_AGENT + }, + proxy + }) + ).data + + const flag = '', startIndex) + const name = html.slice(startIndex, endIndex) + + return { + code: SUCCESS_CODE, + roomInfo: { name } + } +} + export const getYoutubeLiveUrlsPlugin = captureError(baseGetYoutubeLiveUrlsPlugin) +export const getYoutubeRoomInfoPlugin = captureError(baseGetYoutubeRoomInfoPlugin) diff --git a/src/main/crawler/plugins/yy.js b/src/main/crawler/plugins/yy.js index 4068ed7..bbfc202 100644 --- a/src/main/crawler/plugins/yy.js +++ b/src/main/crawler/plugins/yy.js @@ -99,4 +99,10 @@ async function baseGetYYLiveUrlsPlugin(roomUrl, others = {}) { } } +async function baseGetYYRoomInfoPlugin(roomUrl, others = {}) { + const roomId = getRoomIdByUrl(roomUrl) + const { proxy, cookie } = others +} + export const getYYLiveUrlsPlugin = captureError(baseGetYYLiveUrlsPlugin) +export const getYYRoomInfoPlugin = captureError(baseGetYYRoomInfoPlugin) diff --git a/src/main/download-dep/index.ts b/src/main/download-dep/index.ts new file mode 100644 index 0000000..79199a1 --- /dev/null +++ b/src/main/download-dep/index.ts @@ -0,0 +1,173 @@ +import os from 'node:os' +import path from 'node:path' +import fsp from 'node:fs/promises' + +import download from 'download' + +import debug from 'debug' +const log = debug('fideo-download-dep') + +export let downloadReq = { + destroy: () => {} +} + +export const downloadDepProgressInfo: IDownloadDepProgressInfo = { + title: '', + downloading: false, + progress: 0, + showRetry: false +} + +const isMac = os.platform() === 'darwin' +const isArm = ['arm64', 'arm'].includes(os.arch()) + +const ffmpegMacUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/ffmpeg-mac.zip' +const ffmpegWinUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/ffmpeg-win.zip' + +const frpMacArmUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/frp-mac-arm64.zip' +const frpMacAmdUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/frp-mac-amd64.zip' +const frpWinArmUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/frp-win-arm64.zip' +const frpWinAmdUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/frp-win-amd64.zip' + +async function checkFileExist(filepath: string) { + return fsp + .access(filepath, fsp.constants.F_OK) + .then(() => true) + .catch(() => false) +} +export function checkFfmpegExist(dirname: string) { + const ffmpegPath = isMac + ? path.resolve(dirname, 'ffmpeg-mac/ffmpeg') + : path.resolve(dirname, 'ffmpeg-win/ffmpeg.exe') + return checkFileExist(ffmpegPath) +} + +export function checkFfprobeExist(dirname: string) { + const ffprobePath = isMac + ? path.resolve(dirname, 'ffmpeg-mac/ffprobe') + : path.resolve(dirname, 'ffmpeg-win/ffprobe.exe') + return checkFileExist(ffprobePath) +} + +export function checkFrpcExist(dirname: string) { + const frpPath = isMac + ? isArm + ? path.resolve(dirname, 'frp-mac-arm64/frpc') + : path.resolve(dirname, 'frp-mac-amd64/frpc') + : isArm + ? path.resolve(dirname, 'frp-win-arm64/frpc.exe') + : path.resolve(dirname, 'frp-win-amd64/frpc.exe') + return checkFileExist(frpPath) +} + +async function makeSureFfmpegDependenciesExist(dirname: string) { + const [ffmpegExist, ffprobeExist] = await Promise.all([ + checkFfmpegExist(dirname), + checkFfprobeExist(dirname) + ]) + + if (ffmpegExist && ffprobeExist) { + return true + } + + let _resolve: (value: unknown) => void, _reject: (reason?: any) => void + const p = new Promise((resolve, reject) => { + _resolve = resolve + _reject = reject + }) + const downloadUrl = isMac ? ffmpegMacUrl : ffmpegWinUrl + + downloadDepProgressInfo.downloading = true + download(downloadUrl, dirname, { extract: true }) + .on('request', (req) => { + downloadReq = req + }) + .on('downloadProgress', ({ percent }) => { + downloadDepProgressInfo.title = 'FFMPEG' + downloadDepProgressInfo.progress = percent + log(`ffmpeg download progress: ${percent}`) + }) + .on('error', (error) => { + downloadDepProgressInfo.showRetry = true + downloadDepProgressInfo.downloading = false + downloadDepProgressInfo.progress = 0 + log(error.message) + _reject() + }) + .then(() => { + downloadDepProgressInfo.title = '' + downloadDepProgressInfo.downloading = false + downloadDepProgressInfo.progress = 0 + + _resolve(true) + }) + .catch(() => { + downloadDepProgressInfo.title = '' + downloadDepProgressInfo.showRetry = true + downloadDepProgressInfo.downloading = false + downloadDepProgressInfo.progress = 0 + _reject() + }) + + return p +} + +async function makeSureFrpDependenciesExist(dirname: string) { + const frpExist = await checkFrpcExist(dirname) + if (frpExist) { + return true + } + + let _resolve: (value: unknown) => void, _reject: (reason?: any) => void + const p = new Promise((resolve, reject) => { + _resolve = resolve + _reject = reject + }) + + const downloadUrl = isMac + ? isArm + ? frpMacArmUrl + : frpMacAmdUrl + : isArm + ? frpWinArmUrl + : frpWinAmdUrl + + downloadDepProgressInfo.downloading = true + download(downloadUrl, dirname, { extract: true }) + .on('request', (req) => { + downloadReq = req + }) + .on('downloadProgress', ({ percent }) => { + downloadDepProgressInfo.title = 'FRPC' + downloadDepProgressInfo.progress = percent + log(`frpc download progress: ${percent}`) + }) + .on('error', (error) => { + downloadDepProgressInfo.showRetry = true + downloadDepProgressInfo.downloading = false + downloadDepProgressInfo.progress = 0 + log(error.message) + _reject() + }) + .then(() => { + downloadDepProgressInfo.title = '' + downloadDepProgressInfo.downloading = false + downloadDepProgressInfo.progress = 0 + + _resolve(true) + }) + .catch(() => { + downloadDepProgressInfo.title = '' + downloadDepProgressInfo.showRetry = true + downloadDepProgressInfo.downloading = false + downloadDepProgressInfo.progress = 0 + _reject() + }) + + return p +} + +export async function makeSureDependenciesExist(dirname: string) { + await makeSureFfmpegDependenciesExist(dirname) + await makeSureFrpDependenciesExist(dirname) +} diff --git a/src/main/ffmpeg/index.ts b/src/main/ffmpeg/index.ts index af82c86..19bfa4b 100644 --- a/src/main/ffmpeg/index.ts +++ b/src/main/ffmpeg/index.ts @@ -1,58 +1,18 @@ import module from 'module' import os from 'node:os' import path from 'node:path' -import fsp from 'node:fs/promises' import type Ffmpeg from 'fluent-ffmpeg' -import debug from 'debug' - -import download from 'download' - -const log = debug('fideo-ffmpeg') - const require = module.createRequire(import.meta.url) const ffmpeg = require('fluent-ffmpeg') as typeof Ffmpeg & { ffmpegPath: string ffprobePath: string } -export const isMac = os.platform() === 'darwin' - -const ffmpegMacUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/ffmpeg-mac.zip' -const ffmpegWinUrl = 'https://gitlab.com/chenfan0/ffmpeg-resource/-/raw/main/ffmpeg-win.zip' +const isMac = os.platform() === 'darwin' -export async function checkFfmpegExist(dirname: string) { - const ffmpegPath = isMac - ? path.resolve(dirname, 'ffmpeg-mac/ffmpeg') - : path.resolve(dirname, 'ffmpeg-win/ffmpeg.exe') - return fsp - .access(ffmpegPath, fsp.constants.F_OK) - .then(() => true) - .catch(() => false) -} - -export async function checkFfprobeExist(dirname: string) { - const ffprobePath = isMac - ? path.resolve(dirname, 'ffmpeg-mac/ffprobe') - : path.resolve(dirname, 'ffmpeg-win/ffprobe.exe') - return fsp - .access(ffprobePath, fsp.constants.F_OK) - .then(() => true) - .catch(() => false) -} - -export const downloadDepProgressInfo: IDownloadDepProgressInfo = { - downloading: false, - progress: 0, - showRetry: false -} - -export let downloadReq = { - destroy: () => {} -} - -const setFfmpegAndFfprobePath = (dirname: string) => { +export const setFfmpegAndFfprobePath = (dirname: string) => { const ffmpegPath = isMac ? path.resolve(dirname, 'ffmpeg-mac/ffmpeg') : path.resolve(dirname, 'ffmpeg-win/ffmpeg.exe') @@ -65,57 +25,6 @@ const setFfmpegAndFfprobePath = (dirname: string) => { ffmpeg.ffprobePath = ffprobePath } -export async function makeSureDependenciesExist( - dirname: string, - isFfmpegExist: boolean, - isFFprobeExist: boolean -) { - if (isFfmpegExist && isFFprobeExist) { - setFfmpegAndFfprobePath(dirname) - return true - } - - let _resolve: (value: unknown) => void, _reject: (reason?: any) => void - const p = new Promise((resolve, reject) => { - _resolve = resolve - _reject = reject - }) - const downloadUrl = isMac ? ffmpegMacUrl : ffmpegWinUrl - - downloadDepProgressInfo.downloading = true - download(downloadUrl, dirname, { extract: true }) - .on('request', (req) => { - downloadReq = req - }) - .on('downloadProgress', ({ percent }) => { - downloadDepProgressInfo.progress = percent - log(`ffmpeg download progress: ${percent}`) - }) - .on('error', (error) => { - downloadDepProgressInfo.showRetry = true - downloadDepProgressInfo.downloading = false - downloadDepProgressInfo.progress = 0 - log(error.message) - _reject() - }) - .then(() => { - downloadDepProgressInfo.downloading = false - downloadDepProgressInfo.progress = 0 - - setFfmpegAndFfprobePath(dirname) - - _resolve(true) - }) - .catch(() => { - downloadDepProgressInfo.showRetry = true - downloadDepProgressInfo.downloading = false - downloadDepProgressInfo.progress = 0 - _reject() - }) - - return p -} - // const isDev = import.meta.env.MODE === 'development' // const ffmpegPath = isDev // ? path.join(__dirname, '../../resources/ffmpeg', 'ffmpeg') diff --git a/src/main/ffmpeg/record.ts b/src/main/ffmpeg/record.ts index bc4f3b3..e59bf82 100644 --- a/src/main/ffmpeg/record.ts +++ b/src/main/ffmpeg/record.ts @@ -22,48 +22,48 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) export const recordStreamFfmpegProgressInfo: IFfmpegProgressInfo = {} // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const setRecordStreamFfmpegProgressInfo = (title: string, progress: any) => { - recordStreamFfmpegProgressInfo[title] = progress +export const setRecordStreamFfmpegProgressInfo = (id: string, progress: any) => { + recordStreamFfmpegProgressInfo[id] = progress } export const recordStreamFfmpegProcessMap = {} -export const setRecordStreamFfmpegProcessMap = (title: string, process: any) => { - recordStreamFfmpegProcessMap[title] = process +export const setRecordStreamFfmpegProcessMap = (id: string, process: any) => { + recordStreamFfmpegProcessMap[id] = process } -export const killRecordStreamFfmpegProcess = (title: string) => { - const process = recordStreamFfmpegProcessMap[title] +export const killRecordStreamFfmpegProcess = (id: string) => { + const process = recordStreamFfmpegProcessMap[id] const isDummy = process === RECORD_DUMMY_PROCESS || !process process?.kill('SIGKILL') - delete recordStreamFfmpegProgressInfo[title] - delete recordStreamFfmpegProcessMap[title] + delete recordStreamFfmpegProgressInfo[id] + delete recordStreamFfmpegProcessMap[id] - killDetectStreamProcess(title) + killDetectStreamProcess(id) return isDummy } const detectStreamProcessMap = {} const streamResolutionMap = {} -const setDetectStreamProcessMap = (title: string, process: execa.ExecaChildProcess) => { - detectStreamProcessMap[title] = process +const setDetectStreamProcessMap = (id: string, process: execa.ExecaChildProcess) => { + detectStreamProcessMap[id] = process } -const killDetectStreamProcess = (title: string) => { - const process = detectStreamProcessMap[title] +const killDetectStreamProcess = (id: string) => { + const process = detectStreamProcessMap[id] process?.kill('SIGKILL') - delete detectStreamProcessMap[title] - delete streamResolutionMap[title] + delete detectStreamProcessMap[id] + delete streamResolutionMap[id] } const resolutionChangeSet = new Set() -const addResolutionChangeSet = (title: string) => { - resolutionChangeSet.add(title) +const addResolutionChangeSet = (id: string) => { + resolutionChangeSet.add(id) } -const removeResolutionChangeSet = (title: string) => { - resolutionChangeSet.delete(title) +const removeResolutionChangeSet = (id: string) => { + resolutionChangeSet.delete(id) } const checkFileExist = async (filepath: string) => { @@ -139,10 +139,10 @@ async function convert( } async function detectStreamResolution(streamConfig: IStreamConfig) { - const { liveUrls, line, cookie, proxy, title } = streamConfig + const { liveUrls, line, cookie, proxy, id } = streamConfig // 检测之前先清除之前的检测数据 - removeResolutionChangeSet(title) + removeResolutionChangeSet(id) const process = execa(ffmpeg.ffprobePath, [ '-v', @@ -163,19 +163,20 @@ async function detectStreamResolution(streamConfig: IStreamConfig) { ]) process.on('error', (error) => { - console.log('detect stream resolution error: ', error) - delete detectStreamProcessMap[title] + log('detect stream resolution error: ', error) + + delete detectStreamProcessMap[id] }) process.stdout?.on('data', (data) => { const stringData = data.toString() as string const [width, height] = stringData.split(',').map((item) => item.replace('\n', '')) - const prevResolution = streamResolutionMap[title] + const prevResolution = streamResolutionMap[id] log('width, height: ', width, height) if (!prevResolution) { - streamResolutionMap[title] = { + streamResolutionMap[id] = { width, height } @@ -183,23 +184,23 @@ async function detectStreamResolution(streamConfig: IStreamConfig) { const { width: prevWidth, height: prevHeight } = prevResolution if (prevWidth !== width || prevHeight !== height) { - log('resolution change: ', title, width, height) + log('resolution change: ', id, width, height) - addResolutionChangeSet(title) + addResolutionChangeSet(id) - killRecordStreamFfmpegProcess(title) + killRecordStreamFfmpegProcess(id) return } - streamResolutionMap[title] = { + streamResolutionMap[id] = { width, height } } }) - setDetectStreamProcessMap(title, process) + setDetectStreamProcessMap(id, process) } export async function recordStream( @@ -232,7 +233,8 @@ export async function recordStream( title, segmentTime, convertToMP4, - detectResolution + detectResolution, + id } = streamConfig writeLog(title, `RecordStream Config: ${JSON.stringify(streamConfig, null, 2)}`) @@ -251,7 +253,7 @@ export async function recordStream( fs.mkdirSync(baseOutput) } - if (recordStreamFfmpegProcessMap[title] !== RECORD_DUMMY_PROCESS) { + if (recordStreamFfmpegProcessMap[id] !== RECORD_DUMMY_PROCESS) { writeLog(title, 'Record Stream is Killed') _resolve({ code: FFMPEG_ERROR_CODE.USER_KILL_PROCESS @@ -267,7 +269,7 @@ export async function recordStream( ffmpegProcess.inputOptions(['-re']) - setRecordStreamFfmpegProcessMap(title, ffmpegProcess) + setRecordStreamFfmpegProcessMap(id, ffmpegProcess) ffmpegProcess.inputOption( '-headers', @@ -306,7 +308,10 @@ export async function recordStream( }) }) .on('progress', (progress) => { - setRecordStreamFfmpegProgressInfo(title, progress) + setRecordStreamFfmpegProgressInfo(id, { + targetSize: progress.targetSize, + timemark: progress.timemark + }) log('record live progress: ', progress) }) .on('end', async (...args) => { @@ -315,7 +320,7 @@ export async function recordStream( log('record live end: ', msg) writeLog(title, `Record Live End: ${msg}`) - killRecordStreamFfmpegProcess(title) + killRecordStreamFfmpegProcess(id) cb?.(SUCCESS_CODE) await convert(convertSource, writeLog.bind(null, title), convertToMP4) @@ -326,9 +331,9 @@ export async function recordStream( log('record live error: ', errMsg) writeLog(title, `Record Live Error: ${errMsg}`) - const isResolutionChange = resolutionChangeSet.has(title) + const isResolutionChange = resolutionChangeSet.has(id) // 清空数据 - killRecordStreamFfmpegProcess(title) + killRecordStreamFfmpegProcess(id) let errCode!: number diff --git a/src/main/frpc/index.ts b/src/main/frpc/index.ts new file mode 100644 index 0000000..e7fc687 --- /dev/null +++ b/src/main/frpc/index.ts @@ -0,0 +1,258 @@ +import fsp from 'node:fs/promises' +import { join } from 'node:path' +import os from 'node:os' +import type { ChildProcess } from 'node:child_process' + +import { app, BrowserWindow } from 'electron' +import { is } from '@electron-toolkit/utils' +import Fastify from 'fastify' +import WebSocket from 'ws' +import spawn from 'cross-spawn' +import { FRP_DOMAIN, FRPC_PROCESS_ERROR, WEBSOCKET_MESSAGE_TYPE } from '../../const' + +import debug from 'debug' + +const log = debug('fideo-frpc') + +const isMac = os.platform() === 'darwin' +const isArm = ['arm64', 'arm'].includes(os.arch()) + +export let frpcObj: { + frpcProcess: ChildProcess + stopFrpcLocalServer: () => void +} | null = null + +export function stopFrpc() { + frpcObj?.frpcProcess.kill() + frpcObj?.stopFrpcLocalServer() + frpcObj = null +} + +async function startFrpcLocalServer( + code: string +): Promise<{ port: string; stopFrpcLocalServer: () => void }> { + let resolve!: (value: unknown) => void, reject!: (reason?: any) => void + + let port: string + + const p = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + + const fastify = Fastify() + + fastify.get(`/${code}`, async (_, reply) => { + const filePath = is.dev + ? join(__dirname, '../../resources/index.html') + : join(process.resourcesPath, 'index.html') + + try { + const htmlContent = (await fsp.readFile(filePath, 'utf-8')) + .toString() + .replace( + '$$WEBSOCKET_URL$$', + !is.dev ? `ws://localhost:${port}` : `wss://${FRP_DOMAIN}/${code}` + ) + + reply.code(200).header('Content-Type', 'text/html').send(htmlContent) + } catch (err) { + log('err: ', err) + reply.code(500).header('Content-Type', 'text/plain').send('Internal Server Error') + } + }) + + fastify.get('/health', async (_, reply) => { + reply.code(200).header('Content-Type', 'text/plain').send('OK') + }) + + fastify.setNotFoundHandler((_, reply) => { + reply.code(404).header('Content-Type', 'text/plain').send('Not Found') + }) + + const server = fastify.server + const wss = new WebSocket.Server({ + server, + perMessageDeflate: true + }) + + let streamConfigList: IStreamConfig[] = [] + + wss.on('connection', (ws) => { + log('WebSocket client connected') + + ws.send( + JSON.stringify({ + type: 'UPDATE_STREAM_CONFIG_LIST', + data: streamConfigList + }) + ) + + ws.on('message', (message) => { + let messageObj + try { + messageObj = JSON.parse(message.toString()) + } catch { + messageObj = {} + } + const { type, data } = messageObj + + switch (type) { + case WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG_LIST: + streamConfigList = data as IStreamConfig[] + break + case WEBSOCKET_MESSAGE_TYPE.REMOVE_STREAM_CONFIG: + streamConfigList = streamConfigList.filter((streamConfig) => streamConfig.id !== data) + break + case WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG: + streamConfigList = streamConfigList.map((streamConfig) => + streamConfig.id === data.id ? data : streamConfig + ) + break + case WEBSOCKET_MESSAGE_TYPE.ADD_STREAM_CONFIG: + streamConfigList.unshift(data as IStreamConfig) + break + } + + wss.clients.forEach((client) => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send( + JSON.stringify({ + type, + data + }) + ) + } + }) + }) + + ws.on('close', () => { + log('WebSocket client disconnected') + }) + }) + + const stopFrpcLocalServer = () => { + fastify.close() + wss.close() + } + + fastify.listen({ port: 0, host: '0.0.0.0' }, async (err, address) => { + if (err) { + log(err) + reject() + stopFrpcLocalServer() + return + } + + port = new URL(address).port + log(`Server is listening on ${address}`) + + resolve({ + port, + stopFrpcLocalServer + }) + }) + + return p as any +} + +let frpcProcessTimer: NodeJS.Timeout + +export async function startFrpcProcess( + code: string, + writeLog: (title: string, content: string) => void, + win: BrowserWindow +) { + try { + writeLog('frpc', 'code: ' + code) + log('code: ', code) + const userPath = app.getPath('userData') + const { port, stopFrpcLocalServer } = await startFrpcLocalServer(code) + writeLog('frpc', 'port: ' + port) + log('port: ', port) + + const frpcConfig = ` + serverAddr = "${FRP_DOMAIN}" + auth.token = "fideo-frp" + loginFailExit = false + [transport] + heartbeatInterval = 60 + heartbeatTimeout = 180 + tcpMuxKeepaliveInterval = -1 + dialServerTimeout = 30 + [[proxies]] + name = "${code}" + type = "http" + localPort = ${port} + customDomains = ["${FRP_DOMAIN}"] + locations = ["/${code}"] + healthCheck.type = "http" + healthCheck.path = "/health" + ` + const frpcConfigPath = join(userPath, 'frpc.toml') + + await fsp.writeFile(frpcConfigPath, frpcConfig, { encoding: 'utf-8' }) + + writeLog('frpc', 'frpcConfigPath: ' + frpcConfigPath) + + const frpcPath = isMac + ? isArm + ? join(userPath, 'frp-mac-arm64/frpc') + : join(userPath, 'frp-mac-amd64/frpc') + : isArm + ? join(userPath, 'frp-win-arm64/frpc.exe') + : join(userPath, 'frp-win-amd64/frpc.exe') + + const frpcProcess = spawn(frpcPath, ['-c', frpcConfigPath]) + + const frpcProcessCheck = () => { + if (!frpcProcess) return false + try { + process.kill(frpcProcess.pid!, 0) + return true + } catch (err) { + return false + } + } + + frpcProcess.stdout?.on('data', (data) => { + const str = data.toString() + writeLog('frpc', 'frpcProcess stdout: ' + str) + log('frpcProcess stdout: ', str) + if (str.includes('Fideo FRPS ERROR: ')) { + stopFrpc() + win.webContents.send(FRPC_PROCESS_ERROR, str) + } + }) + + frpcProcess.stdout?.on('error', (err) => { + writeLog('frpc', 'frpcProcess stdout error: ' + err) + log('frpcProcess stdout error: ', err) + stopFrpc() + win.webContents.send(FRPC_PROCESS_ERROR, err) + }) + + frpcProcessTimer = setInterval(() => { + const isAlive = frpcProcessCheck() + if (!isAlive) { + writeLog('frpc', 'frpcProcess isAlive: ' + isAlive) + stopFrpcLocalServer() + clearInterval(frpcProcessTimer) + } + }, 3000) + + frpcObj = { + frpcProcess, + stopFrpcLocalServer + } + return { + status: true, + code, + port + } + } catch { + return { + status: false + } + } +} diff --git a/src/main/index.ts b/src/main/index.ts index d2175f0..c8546f2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -21,6 +21,7 @@ import { FFMPEG_PROGRESS_INFO, FORCE_CLOSE_WINDOW, GET_LIVE_URLS, + GET_ROOM_INFO, MAXIMIZE_RESTORE_WINDOW, MINIMIZE_WINDOW, NAV_BY_DEFAULT_BROWSER, @@ -30,12 +31,14 @@ import { SELECT_DIR, SHOW_NOTIFICATION, SHOW_UPDATE_DIALOG, + START_FRPC_PROCESS, START_STREAM_RECORD, + STOP_FRPC_PROCESS, STOP_STREAM_RECORD, STREAM_RECORD_END, USER_CLOSE_WINDOW } from '../const' -import { getLiveUrls } from './crawler/index' +import { getLiveUrls, getRoomInfo } from './crawler/index' import { FFMPEG_ERROR_CODE, SUCCESS_CODE } from '../code' import { recordStream, @@ -44,15 +47,18 @@ import { killRecordStreamFfmpegProcess, setRecordStreamFfmpegProcessMap } from './ffmpeg/record' +import { setFfmpegAndFfprobePath } from './ffmpeg' import { - makeSureDependenciesExist, - downloadDepProgressInfo, checkFfmpegExist, checkFfprobeExist, + checkFrpcExist, + downloadDepProgressInfo, + makeSureDependenciesExist, downloadReq -} from './ffmpeg' +} from './download-dep' import { writeLogWrapper } from './log/index' +import { startFrpcProcess, stopFrpc, frpcObj } from './frpc' export const writeLog = writeLogWrapper(app.getPath('userData')) @@ -196,7 +202,7 @@ async function createTray() { { label: isChinese ? '退出' : 'Quit', click: () => { - if (!isAllFfmpegProcessEnd()) { + if (!isAllFfmpegProcessEnd() || frpcObj !== null) { win?.show() } win?.webContents.send(USER_CLOSE_WINDOW) @@ -222,16 +228,19 @@ function showNotification(title: string, body: string) { async function handleMakeSureDependenciesExist() { const userDataPath = app.getPath('userData') - const [isFFmpegExist, isFfprobeExist] = await Promise.all([ + const [isFFmpegExist, isFfprobeExist, isFrpcExist] = await Promise.all([ checkFfmpegExist(userDataPath), - checkFfprobeExist(userDataPath) + checkFfprobeExist(userDataPath), + checkFrpcExist(userDataPath) ]) - if (!isFFmpegExist || !isFfprobeExist) { + if (!isFFmpegExist || !isFfprobeExist || !isFrpcExist) { startDownloadDepTimerWhenFirstDownloadDepStart() } - makeSureDependenciesExist(userDataPath, isFFmpegExist, isFfprobeExist) + + makeSureDependenciesExist(userDataPath) .then(() => { + setFfmpegAndFfprobePath(userDataPath) stopDownloadDepTimerWhenAllDownloadDepEnd() }) .catch(() => { @@ -243,6 +252,11 @@ async function handleMakeSureDependenciesExist() { // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(async () => { + if (app.isPackaged) { + app.setLoginItemSettings({ + openAtLogin: true + }) + } // Set app user model id for windows electronApp.setAppUserModelId('site.fideo.app') @@ -268,13 +282,21 @@ app.whenReady().then(async () => { } ) + ipcMain.handle( + GET_ROOM_INFO, + async (_, info: { roomUrl: string; proxy?: string; cookie?: string }) => { + const { roomUrl, proxy, cookie } = info + return getRoomInfo({ roomUrl, proxy, cookie }, writeLog.bind(null, 'Get Room Info')) + } + ) + ipcMain.handle(NAV_BY_DEFAULT_BROWSER, (_, url: string) => { shell.openExternal(url) }) ipcMain.handle(START_STREAM_RECORD, async (_, streamConfigStr: string) => { const streamConfig = JSON.parse(streamConfigStr) as IStreamConfig - const { roomUrl, proxy, cookie, title } = streamConfig + const { roomUrl, proxy, cookie, title, id } = streamConfig /** * When requesting the live stream address, @@ -283,7 +305,7 @@ app.whenReady().then(async () => { * Prevent clicking the stop recording button while requesting the live stream address, * causing the page to display that the recording has stopped, but the ffmpeg process is still running */ - setRecordStreamFfmpegProcessMap(title, RECORD_DUMMY_PROCESS) + setRecordStreamFfmpegProcessMap(id, RECORD_DUMMY_PROCESS) const { code: liveUrlsCode, liveUrls } = await getLiveUrls( { roomUrl, proxy, cookie }, @@ -301,7 +323,7 @@ app.whenReady().then(async () => { streamConfig, writeLog, (code: number, errMsg?: string) => { - win?.webContents.send(STREAM_RECORD_END, title, code, errMsg) + win?.webContents.send(STREAM_RECORD_END, id, code, errMsg) clearTimerWhenAllFfmpegProcessEnd() } ) @@ -313,7 +335,7 @@ app.whenReady().then(async () => { } }) - ipcMain.handle(STOP_STREAM_RECORD, async (_, title: string) => { + ipcMain.handle(STOP_STREAM_RECORD, async (_, id: string) => { /** * If the ffmpeg process is RECORD_DUMMY_PROCESS when stopping recording, * need to send the STREAM_RECORD_END event to display the information that the recording has stopped on the page. @@ -322,9 +344,8 @@ app.whenReady().then(async () => { * At this time, you do not need to send the STREAM_RECORD_END event, * because the ffmpeg process will send the STREAM_RECORD_END event when it is finished running. */ - const shouldSend = killRecordStreamFfmpegProcess(title) - shouldSend && - win?.webContents.send(STREAM_RECORD_END, title, FFMPEG_ERROR_CODE.USER_KILL_PROCESS) + const shouldSend = killRecordStreamFfmpegProcess(id) + shouldSend && win?.webContents.send(STREAM_RECORD_END, id, FFMPEG_ERROR_CODE.USER_KILL_PROCESS) clearTimerWhenAllFfmpegProcessEnd() return { @@ -366,12 +387,27 @@ app.whenReady().then(async () => { stillRecordStreamKeys.forEach((key) => { killRecordStreamFfmpegProcess(key) }) + downloadReq.destroy() clearTimerWhenAllFfmpegProcessEnd() stopDownloadDepTimerWhenAllDownloadDepEnd() + + stopFrpc() + win?.destroy() }) + ipcMain.handle(START_FRPC_PROCESS, async (_, code: string) => { + if (frpcObj) { + return false + } + return await startFrpcProcess(code, writeLog, win!) + }) + + ipcMain.handle(STOP_FRPC_PROCESS, async () => { + stopFrpc() + }) + await createWindow() await createTray() diff --git a/src/main/log/index.ts b/src/main/log/index.ts index 313ef39..60dc541 100644 --- a/src/main/log/index.ts +++ b/src/main/log/index.ts @@ -4,7 +4,6 @@ import dayjs from 'dayjs' export function writeLogWrapper(baseDir: string) { const logPrefixPath = path.resolve(baseDir, 'logs') - console.log('logPrefixPath:', logPrefixPath) return async (title: string, content: string) => { const logPath = path.resolve(logPrefixPath, title) return fsp diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 8c227c4..9182a0a 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -2,6 +2,7 @@ import { ElectronAPI } from '@electron-toolkit/preload' declare global { interface Window { + socket: WebSocket electron: ElectronAPI api: { isDarwin: boolean @@ -13,6 +14,11 @@ declare global { proxy?: string cookie?: string }) => Promise<{ code: number; liveUrls: string[] }> + getRoomInfo: (info: { + roomUrl: string + proxy?: string + cookie?: string + }) => Promise<{ code: number; roomInfo: IRoomInfo }> navByDefaultBrowser: (url: string) => void startStreamRecord: (streamConfig: string) => Promise<{ code: number }> stopStreamRecord: (title: string) => Promise<{ code: number }> @@ -23,11 +29,15 @@ declare global { forceCloseWindow: () => void retryDownloadDep: () => void - onStreamRecordEnd: (callback: (title: string, code: number, errMsg?: string) => void) => void + startFrpcProcess: (code: string) => Promise<{ status: boolean; code?: string; port?: number }> + stopFrpcProcess: () => void + + onStreamRecordEnd: (callback: (id: string, code: number, errMsg?: string) => void) => void onFFmpegProgressInfo: (callback: (info: IFfmpegProgressInfo) => void) => void onDownloadDepProgressInfo: (callback: (info: IDownloadDepProgressInfo) => void) => void onUserCloseWindow: (callback: () => void) => void onAppUpdate: (callback: () => void) => void + onFrpcProcessError: (callback: (err: string) => void) => void } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 0e3c1f0..561d8c4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -5,6 +5,9 @@ import { DOWNLOAD_DEP_PROGRESS_INFO, FFMPEG_PROGRESS_INFO, FORCE_CLOSE_WINDOW, + FRPC_PROCESS_ERROR, + GET_LIVE_URLS, + GET_ROOM_INFO, MAXIMIZE_RESTORE_WINDOW, MINIMIZE_WINDOW, NAV_BY_DEFAULT_BROWSER, @@ -13,7 +16,9 @@ import { SELECT_DIR, SHOW_NOTIFICATION, SHOW_UPDATE_DIALOG, + START_FRPC_PROCESS, START_STREAM_RECORD, + STOP_FRPC_PROCESS, STOP_STREAM_RECORD, STREAM_RECORD_END, USER_CLOSE_WINDOW @@ -25,7 +30,9 @@ const api = { selectDir: () => ipcRenderer.invoke(SELECT_DIR), openLogsDir: () => ipcRenderer.invoke(OPEN_LOGS_DIR), getLiveUrls: (info: { roomUrl: string; proxy?: string; cookie?: string; title: string }) => - ipcRenderer.invoke('GET_LIVE_URLS', info), + ipcRenderer.invoke(GET_LIVE_URLS, info), + getRoomInfo: (info: { roomUrl: string; proxy?: string; cookie?: string }) => + ipcRenderer.invoke(GET_ROOM_INFO, info), navByDefaultBrowser: (url: string) => ipcRenderer.invoke(NAV_BY_DEFAULT_BROWSER, url), startStreamRecord: (streamConfig: string) => ipcRenderer.invoke(START_STREAM_RECORD, streamConfig), @@ -39,6 +46,9 @@ const api = { forceCloseWindow: () => ipcRenderer.invoke(FORCE_CLOSE_WINDOW), retryDownloadDep: () => ipcRenderer.invoke(RETRY_DOWNLOAD_DEP), + startFrpcProcess: (code: string) => ipcRenderer.invoke(START_FRPC_PROCESS, code), + stopFrpcProcess: () => ipcRenderer.invoke(STOP_FRPC_PROCESS), + onStreamRecordEnd: (callback: (title: string, code: number, errMsg?: string) => void) => { ipcRenderer.on(STREAM_RECORD_END, (_, title, code, errMsg) => { callback(title, code, errMsg) @@ -65,6 +75,11 @@ const api = { ipcRenderer.on(SHOW_UPDATE_DIALOG, () => { callback() }) + }, + onFrpcProcessError: (callback: (err: string) => void) => { + ipcRenderer.on(FRPC_PROCESS_ERROR, (_, err) => { + callback(err) + }) } } diff --git a/src/renderer/index.html b/src/renderer/index.html index 8fb75f4..1c5f25c 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -6,7 +6,7 @@ Electron + content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://api.xunhupay.com; connect-src 'self' https://xizhi.qqoq.net *;" /> diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index bda320c..e2693be 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -12,14 +12,19 @@ import DownloadingDep from '@/components/DownloadingDep/DownloadingDep' import { useStreamConfigStore } from './store/useStreamConfigStore' import { useDefaultSettingsStore } from './store/useDefaultSettingsStore' import { useNavSelectedStatusStore } from './store/useNavSelectedStatusStore' +import { useWebControlSettingStore } from './store/useWebControlSettingStore' import { useDownloadDepInfoStore } from './store/useDownloadDepStore' +import Loading from './components/Loading' +import Confetti from './components/Confetti' +import { StreamStatus } from './lib/utils' function App(): JSX.Element { const [showUpdateDialog, setShowUpdateDialog] = useState(false) const { i18n, t } = useTranslation() - const { initData: initStreamConfigData } = useStreamConfigStore((state) => ({ - initData: state.initialData + const { initData: initStreamConfigData, updateStreamConfig } = useStreamConfigStore((state) => ({ + initData: state.initialData, + updateStreamConfig: state.updateStreamConfig })) const { initData: initDefaultSettingsData, defaultSettingsConfig } = useDefaultSettingsStore( (state) => ({ @@ -27,6 +32,9 @@ function App(): JSX.Element { defaultSettingsConfig: state.defaultSettingsConfig }) ) + const { initData: initWebControlSettingData } = useWebControlSettingStore((state) => ({ + initData: state.initData + })) const { initData: initNavSelectedStatus } = useNavSelectedStatusStore((state) => ({ initData: state.initData })) @@ -34,9 +42,42 @@ function App(): JSX.Element { (state) => state ) + const [closeWindowDialogOpen, setCloseWindowDialogOpen] = useState(false) + const [closeWindowText, setCloseWindowText] = useState('stream_config.confirm_force_close_window') + + const handleForceCloseWindow = async () => { + const { streamConfigList } = useStreamConfigStore.getState() + const { webControlSetting } = useWebControlSettingStore.getState() + setCloseWindowDialogOpen(false) + + for (const streamConfig of streamConfigList) { + if (streamConfig.status !== StreamStatus.NOT_STARTED) { + await updateStreamConfig( + { ...streamConfig, status: StreamStatus.NOT_STARTED }, + streamConfig.id + ) + } + } + + if (webControlSetting.enableWebControl) { + window.api.stopFrpcProcess() + } + + window.api.forceCloseWindow() + } + useMount(() => { const titleBar = document.getElementById('title-bar') - window.api.isDarwin && titleBar && (titleBar.style.opacity = '0') + const minButton = document.getElementById('min-button') + const restoreButton = document.getElementById('restore-button') + const closeButton = document.getElementById('close-button') + + if (window.api.isDarwin && titleBar) { + titleBar.style.opacity = '0' + minButton!.style.visibility = 'hidden' + restoreButton!.style.visibility = 'hidden' + closeButton!.style.visibility = 'hidden' + } window.api.onAppUpdate(() => { setShowUpdateDialog(true) @@ -45,12 +86,38 @@ function App(): JSX.Element { window.api.onDownloadDepProgressInfo((progressInfo) => { updateUpdateDownloadProgressInfo(progressInfo) }) + + window.api.onUserCloseWindow(() => { + const { streamConfigList } = useStreamConfigStore.getState() + const { downloadDepProgressInfo } = useDownloadDepInfoStore.getState() + const { webControlSetting } = useWebControlSettingStore.getState() + + const stillWorkStream = streamConfigList.find( + (streamConfig) => streamConfig.status !== StreamStatus.NOT_STARTED + ) + const stillDownloadDep = downloadDepProgressInfo.downloading + const stillWorkWebControl = webControlSetting.enableWebControl + + if (!stillWorkStream && !stillDownloadDep && !stillWorkWebControl) { + window.api.forceCloseWindow() + return + } + if (stillDownloadDep) { + setCloseWindowText('downloading_dep.confirm_force_close_window_with_downloading_dep') + } + if (stillWorkWebControl) { + setCloseWindowText('web_control_setting.confirm_force_close_window_with_web_control') + } + + setCloseWindowDialogOpen(true) + }) }) useEffect(() => { initStreamConfigData() initDefaultSettingsData() initNavSelectedStatus() + initWebControlSettingData() }, []) useEffect(() => { @@ -77,10 +144,22 @@ function App(): JSX.Element { ) } /> + {(downloadDepProgressInfo.downloading || downloadDepProgressInfo.showRetry) && ( )} + + + + ) } diff --git a/src/renderer/src/assets/images/dark/phone.svg b/src/renderer/src/assets/images/dark/phone.svg new file mode 100644 index 0000000..8752a07 --- /dev/null +++ b/src/renderer/src/assets/images/dark/phone.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/assets/images/light/phone.svg b/src/renderer/src/assets/images/light/phone.svg new file mode 100644 index 0000000..5ac78ad --- /dev/null +++ b/src/renderer/src/assets/images/light/phone.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/components/Confetti.tsx b/src/renderer/src/components/Confetti.tsx new file mode 100644 index 0000000..d95a647 --- /dev/null +++ b/src/renderer/src/components/Confetti.tsx @@ -0,0 +1,15 @@ +import ReactConfetti from 'react-confetti' +import { useWindowSize } from 'react-use' + +import { useConfettiStore } from '@renderer/store/useConfettiStore' + +export default function Confetti() { + const { width, height } = useWindowSize() + const { showConfetti, numberOfPieces } = useConfettiStore() + + return ( +
+ {showConfetti && } +
+ ) +} diff --git a/src/renderer/src/components/DownloadingDep/DownloadingDep.tsx b/src/renderer/src/components/DownloadingDep/DownloadingDep.tsx index 70fadfc..8b49314 100644 --- a/src/renderer/src/components/DownloadingDep/DownloadingDep.tsx +++ b/src/renderer/src/components/DownloadingDep/DownloadingDep.tsx @@ -15,6 +15,7 @@ export default function DownloadingDep() { const handleRetryDownloadDep = () => { updateUpdateDownloadProgressInfo({ + title: '', showRetry: false, downloading: true, progress: 0 @@ -29,7 +30,9 @@ export default function DownloadingDep() { {downloadDepProgressInfo.showRetry ? t('downloading_dep.retry_title') - : t('downloading_dep.downloading_title')} + : t('downloading_dep.downloading_title', { + title: downloadDepProgressInfo.title + })} {downloadDepProgressInfo.downloading && ( diff --git a/src/renderer/src/components/Loading.tsx b/src/renderer/src/components/Loading.tsx new file mode 100644 index 0000000..6582a9f --- /dev/null +++ b/src/renderer/src/components/Loading.tsx @@ -0,0 +1,15 @@ +import { useLoadingStore } from '@/store/useLoadingStore' + +export default function Loading() { + const { loading } = useLoadingStore() + + return ( + <> + {loading && ( +
+
+
+ )} + + ) +} diff --git a/src/renderer/src/components/NavBar/NavBar.tsx b/src/renderer/src/components/NavBar/NavBar.tsx index 17ed3ea..469b35b 100644 --- a/src/renderer/src/components/NavBar/NavBar.tsx +++ b/src/renderer/src/components/NavBar/NavBar.tsx @@ -5,17 +5,21 @@ import Theme from '@/components/NavBar/components/Theme' import UseThemeIcon from '@/components/UseThemeIcon' import StreamConfigSheet from '@/components/StreamConfigSheet' +import DefaultSettingSheet from './components/DefaultSettingSheet' +import WebControlSettingSheet from './components/WebControlSettingSheet' + import darkAddIcon from '@/assets/images/dark/add.svg' import lightAddIcon from '@/assets/images/light/add.svg' import darkSettingIcon from '@/assets/images/dark/setting.svg' import lightSettingIcon from '@/assets/images/light/setting.svg' import darkLogo from '@/assets/images/dark/logo.png' import lightLogo from '@/assets/images/light/logo.png' -import DefaultSettingSheet from './components/DefaultSettingSheet' import darkQQIcon from '@/assets/images/dark/qq.svg' import lightQQIcon from '@/assets/images/light/qq.svg' import darkDiscordIcon from '@/assets/images/dark/discord.svg' import lightDiscordIcon from '@/assets/images/light/discord.svg' +import darkPhoneIcon from '@/assets/images/dark/phone.svg' +import lightPhoneIcon from '@/assets/images/light/phone.svg' import { Select, @@ -38,6 +42,8 @@ export default function NavBar() { const [createSheetOpen, setCreateSheetOpen] = useState(false) const [settingSheetOpen, setSettingSheetOpen] = useState(false) + // + const [webControlSheetOpen, setWebControlSheetOpen] = useState(false) const handleLogoClick = () => { window.api.navByDefaultBrowser('https://www.fideo.site') @@ -77,6 +83,14 @@ export default function NavBar() { handleClick={handleDiscordClick} tooltipContent={t('nav_bar.discord')} /> + + setWebControlSheetOpen(true)} + />
@@ -125,6 +139,11 @@ export default function NavBar() { /> + +
) } diff --git a/src/renderer/src/components/NavBar/components/WebControlSettingSheet.tsx b/src/renderer/src/components/NavBar/components/WebControlSettingSheet.tsx new file mode 100644 index 0000000..32609c5 --- /dev/null +++ b/src/renderer/src/components/NavBar/components/WebControlSettingSheet.tsx @@ -0,0 +1,496 @@ +import { useEffect, useState } from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { useTranslation } from 'react-i18next' +import validator from 'validator' + +import { Button } from '@/shadcn/ui/button' +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/shadcn/ui/form' +import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/shadcn/ui/sheet' +import { Input } from '@/shadcn/ui/input' +import { Switch } from '@/shadcn/ui/switch' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shadcn/ui/tooltip' +import { + Dialog as ShadcnDialog, + DialogContent as ShadcnDialogContent, + DialogHeader as ShadcnDialogHeader, + DialogTitle as ShadcnDialogTitle +} from '@/shadcn/ui/dialog' +import { useWebControlSettingStore } from '@/store/useWebControlSettingStore' +import { useStreamConfigStore } from '@/store/useStreamConfigStore' +import { useLoadingStore } from '@/store/useLoadingStore' +import { useConfettiStore } from '@/store/useConfettiStore' +import { useToast } from '@/hooks/useToast' + +import { closeWebSocket, createWebSocket, sendMessage } from '@/lib/websocket' +import emitter from '@/lib/bus' +import { START_WEB_CONTROL, WEBSOCKET_MESSAGE_TYPE } from '../../../../../const' +import { errorCodeToI18nMessage, SUCCESS_CODE } from '../../../../../code' + +const formSchema = z.object({ + webControlPath: z.string(), + enableWebControl: z.boolean(), + email: z.string() +}) + +interface StreamConfigSheetProps { + sheetOpen: boolean + setSheetOpen: (status: boolean) => void +} + +const initialTitle = 'Fideo网页访问激活码(一个月)' +const initialMoney = 9.99 + +let intervalCheckOrderStatusTimer: NodeJS.Timeout | null = null + +export default function WebControlSettingSheet(props: StreamConfigSheetProps) { + const { t } = useTranslation() + const { setLoading } = useLoadingStore() + const { setShowConfetti } = useConfettiStore() + const { sheetOpen, setSheetOpen } = props + + const { webControlSetting, setWebControlSetting } = useWebControlSettingStore((state) => state) + const { updateStreamConfig, removeStreamConfig, addStreamConfig } = useStreamConfigStore( + (state) => state + ) + + const [dialogOpen, setDialogOpen] = useState(false) + const [qrcode, setQrcode] = useState('') + const [title, setTitle] = useState(initialTitle) + const [money, setMoney] = useState(initialMoney) + + const { toast } = useToast() + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + ...webControlSetting + } + }) + + useEffect(() => { + async function handleStartWebControl(webControlPath: string, timeout = 1000 * 10) { + setWebControlSetting({ ...form.getValues(), enableWebControl: true }) + const isSuccess = await startFrpc(webControlPath) + if (!isSuccess) { + setWebControlSetting({ ...form.getValues(), enableWebControl: false }) + + toast({ + title: t('web_control_setting.start_web_control_failed'), + description: t('web_control_setting.will_retry', { time: timeout / 1000 }), + variant: 'destructive' + }) + setTimeout(() => { + handleStartWebControl(webControlPath, timeout + 1000 * 10) + }, timeout) + } + } + + emitter.on(START_WEB_CONTROL, handleStartWebControl as any) + return () => { + emitter.off(START_WEB_CONTROL, handleStartWebControl as any) + } + }, []) + + useEffect(() => { + form.reset({ ...webControlSetting }) + }, [webControlSetting]) + + useEffect(() => { + window.api.onFrpcProcessError((err) => { + toast({ + title: t('web_control_setting.frpc_process_error'), + description: err, + variant: 'destructive' + }) + setWebControlSetting({ ...form.getValues(), enableWebControl: false }) + closeWebSocket() + }) + }, []) + + const handleSetSheetOpen = async (status: boolean, trigger = false) => { + const formValues = form.getValues() as IWebControlSetting + if (trigger) { + setWebControlSetting(formValues) + } + + setQrcode('') + setSheetOpen(status) + form.reset() + } + + const handleClosePayingDialog = (status: boolean) => { + setDialogOpen(status) + if (!status) { + if (intervalCheckOrderStatusTimer) { + clearTimeout(intervalCheckOrderStatusTimer) + } + + setQrcode('') + setTitle(initialTitle) + setMoney(initialMoney) + } + } + + function intervalCheckOrderStatus(orderId: number) { + fetch('https://api-web-control.fideo.site/api/pay/check', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + orderId + }) + }).then((res) => { + res + .json() + .then(({ code, data: webControlPath }) => { + if (code === 200) { + form.setValue('webControlPath', webControlPath) + setWebControlSetting(form.getValues()) + setDialogOpen(false) + setShowConfetti(true) + toast({ + title: t('web_control_setting.get_web_control_path_success'), + description: t('web_control_setting.get_web_control_path_success_desc') + }) + } else { + if (intervalCheckOrderStatusTimer) { + clearTimeout(intervalCheckOrderStatusTimer) + } + + intervalCheckOrderStatusTimer = setTimeout(() => { + intervalCheckOrderStatus(orderId) + }, 1000) + } + }) + .catch(() => { + if (intervalCheckOrderStatusTimer) { + clearTimeout(intervalCheckOrderStatusTimer) + } + + intervalCheckOrderStatusTimer = setTimeout(() => { + intervalCheckOrderStatus(orderId) + }, 1000) + }) + }) + } + + const handleGetWebControlPath = async () => { + const email = form.getValues('email') + if (!email) { + form.setError('email', { message: t('web_control_setting.email_required') }) + return + } + + if (!validator.isEmail(email)) { + form.setError('email', { message: t('web_control_setting.email_invalid') }) + return + } + + form.clearErrors('email') + setLoading(true) + + try { + const res = await fetch('https://api-web-control.fideo.site/api/pay/wx', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email + }) + }) + const { code, data } = (await res.json()) as { + code: number + data: { orderId: number; qrcode: string; title: string; money: number } + } + + if (code !== 200) { + toast({ + title: t('web_control_setting.get_web_control_path_failed'), + description: t('web_control_setting.get_web_control_path_failed_desc'), + variant: 'destructive' + }) + return + } + const { orderId, qrcode, title, money } = data + + setQrcode(qrcode) + setTitle(title) + setMoney(money) + + setDialogOpen(true) + intervalCheckOrderStatusTimer = setTimeout(() => { + intervalCheckOrderStatus(orderId) + }, 2000) + } catch (error) { + console.error(error) + + setDialogOpen(false) + setLoading(false) + setTitle(initialTitle) + setMoney(initialMoney) + toast({ + title: t('web_control_setting.get_web_control_path_failed'), + description: t('web_control_setting.get_web_control_path_failed_desc'), + variant: 'destructive' + }) + } finally { + setLoading(false) + } + } + + const websocketOnMessage = (event: MessageEvent) => { + const messageObj = JSON.parse(event.data) + + const { type, data } = messageObj + + switch (type) { + case WEBSOCKET_MESSAGE_TYPE.START_RECORD_STREAM: + document.getElementById(data + '_play')?.click() + break + case WEBSOCKET_MESSAGE_TYPE.PAUSE_RECORD_STREAM: + document.getElementById(data + '_pause')?.click() + break + case WEBSOCKET_MESSAGE_TYPE.REMOVE_STREAM_CONFIG: + removeStreamConfig(data) + break + case WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG: + updateStreamConfig(data, data.id) + break + case WEBSOCKET_MESSAGE_TYPE.ADD_STREAM_CONFIG: + addStreamConfig(data) + break + case WEBSOCKET_MESSAGE_TYPE.GET_LIVE_URLS: + { + const { roomUrl, proxy, cookie, title } = data + window.api + .getLiveUrls({ + roomUrl, + proxy, + cookie, + title + }) + .then(({ code, liveUrls }) => { + if (code !== SUCCESS_CODE) { + toast({ + title, + description: t(errorCodeToI18nMessage(code, 'error.get_line.')), + variant: 'destructive' + }) + } + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.UPDATE_LIVE_URLS, + data: liveUrls || [] + }) + }) + } + break + } + } + + const startFrpc = async (webControlPath: string) => { + setLoading(true) + const { status: isSuccess, code, port } = await window.api.startFrpcProcess(webControlPath) + const formValues = form.getValues() + + if (isSuccess) { + createWebSocket(port!, code!, websocketOnMessage) + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG_LIST, + data: useStreamConfigStore.getState().streamConfigList + }) + } else { + window.api.stopFrpcProcess() + closeWebSocket() + } + + setWebControlSetting({ ...formValues, enableWebControl: isSuccess }) + + const prefix = 'web_control_setting.start_web_control' + toast({ + title: isSuccess ? t(`${prefix}_success`) : t(`${prefix}_failed`), + description: isSuccess ? t(`${prefix}_success_desc`) : t(`${prefix}_failed_desc`), + variant: isSuccess ? 'default' : 'destructive' + }) + + setLoading(false) + return isSuccess + } + + const handleToggleWebControl = async (status: boolean, field: any) => { + form.clearErrors('webControlPath') + if (status) { + const webControlPath = form.getValues('webControlPath') + if (!webControlPath) { + form.setError('webControlPath', { + message: t('web_control_setting.web_control_path_required') + }) + return + } + + const isSuccess = await startFrpc(webControlPath) + + if (isSuccess) { + field.onChange(status) + } + return + } + + window.api.stopFrpcProcess() + closeWebSocket() + field.onChange(status) + + toast({ + title: t('web_control_setting.stop_web_control_success'), + description: t('web_control_setting.stop_web_control_success_desc') + }) + } + + return ( + <> + handleSetSheetOpen(status)}> + + + {t('web_control_setting.title')} + +
+
+
+ + ( + + {t('web_control_setting.web_control_path')} + +
+ + +
+
+ +
+ )} + /> + ( + + {t('web_control_setting.email')} + +
+ +
+
+ +
+ )} + /> + ( + + + + + + {t('web_control_setting.enable_web_control')} + + + +

+ {t('web_control_setting.enable_web_control_tooltip')} +

+
+
+
+ handleToggleWebControl(status, field)} + className="flex" + /> +
+ )} + /> + {form.getValues('enableWebControl') && form.getValues('webControlPath') && ( + ( + + {t('web_control_setting.web_control_address')} + + + + + + + + +
+ {`https://web-control.fideo.site/${form.getValues('webControlPath')}`} +
+
+
+
+
+
+ )} + /> + )} + + +
+
+ + + +
+
+ handleClosePayingDialog(status)}> + + + +

{title}

+

{`¥${money}`}

+
+
+
+ {qrcode && } +
+
+
+ + ) +} diff --git a/src/renderer/src/components/StreamConfigList/StreamConfigList.tsx b/src/renderer/src/components/StreamConfigList/StreamConfigList.tsx index 17c2d4a..c3a5973 100644 --- a/src/renderer/src/components/StreamConfigList/StreamConfigList.tsx +++ b/src/renderer/src/components/StreamConfigList/StreamConfigList.tsx @@ -1,8 +1,7 @@ -import { useMemo, useState } from 'react' +import { useMemo } from 'react' import { useMount } from 'react-use' import { useTranslation } from 'react-i18next' -import Dialog from '@/components/Dialog' import StreamConfigCard from './components/StreamConfigCard' import { @@ -18,7 +17,6 @@ import { useStreamConfigStore } from '@/store/useStreamConfigStore' import { useDefaultSettingsStore } from '@renderer/store/useDefaultSettingsStore' import { useNavSelectedStatusStore } from '@renderer/store/useNavSelectedStatusStore' import { useFfmpegProgressInfoStore } from '@/store/useFfmpegProgressInfoStore' -import { useDownloadDepInfoStore } from '@/store/useDownloadDepStore' import { StreamStatus, useXizhiToPushNotification } from '@renderer/lib/utils' import { useToast } from '@renderer/hooks/useToast' @@ -28,21 +26,17 @@ const maxRetryTimes = 3 const alreadyCallbackOneTimeSet = new Set() export default function StreamConfigList() { - const [closeWindowDialogOpen, setCloseWindowDialogOpen] = useState(false) - const [closeWindowText, setCloseWindowText] = useState('stream_config.confirm_force_close_window') - const navSelectedStatus = useNavSelectedStatusStore((state) => state.navSelectedStatus) - const { streamConfigList, updateStreamConfig } = useStreamConfigStore((state) => ({ - streamConfigList: state.streamConfigList, - updateStreamConfig: state.updateStreamConfig + const { streamConfigList } = useStreamConfigStore((state) => ({ + streamConfigList: state.streamConfigList })) - const selectedStreamConfigTitleList = useMemo(() => { + const selectedStreamConfigIdList = useMemo(() => { if (navSelectedStatus === '-1') { - return streamConfigList.map((stream) => stream.title) + return streamConfigList.map((stream) => stream.id) } return streamConfigList .filter((streamConfig) => streamConfig.status === Number(navSelectedStatus)) - .map((stream) => stream.title) + .map((stream) => stream.id) }, [streamConfigList, navSelectedStatus]) const { updateFfmpegProgressInfo } = useFfmpegProgressInfoStore((state) => state) @@ -50,32 +44,15 @@ export default function StreamConfigList() { const { toast } = useToast() const { t } = useTranslation() - const handleForceCloseWindow = async () => { - setCloseWindowDialogOpen(false) - - for (const streamConfig of streamConfigList) { - if (streamConfig.status !== StreamStatus.NOT_STARTED) { - await updateStreamConfig( - { ...streamConfig, status: StreamStatus.NOT_STARTED }, - streamConfig.title - ) - } - } - - window.api.forceCloseWindow() - } - useMount(() => { window.api.onFFmpegProgressInfo((progressInfo) => { updateFfmpegProgressInfo(progressInfo) }) - window.api.onStreamRecordEnd(async (title, code, errMsg) => { + window.api.onStreamRecordEnd(async (id, code, errMsg) => { const { streamConfigList, updateStreamConfig } = useStreamConfigStore.getState() const xiZhiKey = useDefaultSettingsStore.getState().defaultSettingsConfig.xizhiKey - const index = streamConfigList.findIndex((streamConfig) => streamConfig.title === title) - - console.log('onStreamRecordEnd:', title, code, errMsg) + const index = streamConfigList.findIndex((streamConfig) => streamConfig.id === id) if (index === -1) { return @@ -106,7 +83,7 @@ export default function StreamConfigList() { ) { await updateStreamConfig( { ...streamConfig, status: StreamStatus.NOT_STARTED }, - streamConfig.title + streamConfig.id ) toast({ @@ -121,24 +98,24 @@ export default function StreamConfigList() { * 会回调两次该函数,第一次是开始转换为mp4文件之前,第二次是转换后 */ - if (!alreadyCallbackOneTimeSet.has(title)) { - alreadyCallbackOneTimeSet.add(title) + if (!alreadyCallbackOneTimeSet.has(id)) { + alreadyCallbackOneTimeSet.add(id) // 第一次回调,除了当前状态是录制中并且需要转换为mp4文件的情况需要进行处理,其他情况都不需要处理 if (streamConfig.status === StreamStatus.RECORDING && streamConfig.convertToMP4) { await updateStreamConfig( { ...streamConfig, status: StreamStatus.VIDEO_FORMAT_CONVERSION }, - streamConfig.title + streamConfig.id ) } return } // 第二次回调,删除当前title - alreadyCallbackOneTimeSet.delete(title) + alreadyCallbackOneTimeSet.delete(id) - unknownErrorRetryTimesMap[title] = unknownErrorRetryTimesMap[title] || 0 - unknownErrorRetryTimesMap[title] += 1 + unknownErrorRetryTimesMap[id] = unknownErrorRetryTimesMap[id] || 0 + unknownErrorRetryTimesMap[id] += 1 if (!message) { message = errorCodeToI18nMessage(code, 'error.stop_record.') @@ -158,39 +135,19 @@ export default function StreamConfigList() { await updateStreamConfig( { ...streamConfig, status: StreamStatus.NOT_STARTED }, - streamConfig.title + streamConfig.id ) - if (isStopByUser || unknownErrorRetryTimesMap[title] >= maxRetryTimes) { - unknownErrorRetryTimesMap[title] = 0 + if (isStopByUser || unknownErrorRetryTimesMap[id] >= maxRetryTimes) { + unknownErrorRetryTimesMap[id] = 0 return } if (isStopByResolutionChange || isStopByStreamEnd) { - unknownErrorRetryTimesMap[title] = 0 - } - - emitter.emit(RECORD_END_NOT_USER_STOP, streamConfig.title) - }) - - window.api.onUserCloseWindow(() => { - const { streamConfigList } = useStreamConfigStore.getState() - const { downloadDepProgressInfo } = useDownloadDepInfoStore.getState() - - const stillWorkStream = streamConfigList.find( - (streamConfig) => streamConfig.status !== StreamStatus.NOT_STARTED - ) - const stillDownloadDep = downloadDepProgressInfo.downloading - - if (!stillWorkStream && !stillDownloadDep) { - window.api.forceCloseWindow() - return - } - if (stillDownloadDep) { - setCloseWindowText('downloading_dep.confirm_force_close_window_with_downloading_dep') + unknownErrorRetryTimesMap[id] = 0 } - setCloseWindowDialogOpen(true) + emitter.emit(RECORD_END_NOT_USER_STOP, streamConfig.id) }) }) @@ -200,22 +157,14 @@ export default function StreamConfigList() {
{streamConfigList.map((streamConfig) => (
))}
} - ) } diff --git a/src/renderer/src/components/StreamConfigList/components/MoveCardDropDownMenu.tsx b/src/renderer/src/components/StreamConfigList/components/MoveCardDropDownMenu.tsx index c48f51c..d3b9299 100644 --- a/src/renderer/src/components/StreamConfigList/components/MoveCardDropDownMenu.tsx +++ b/src/renderer/src/components/StreamConfigList/components/MoveCardDropDownMenu.tsx @@ -19,12 +19,12 @@ import lightDot from '@/assets/images/light/dot.svg' export function MoveCardDropdownMenu({ streamConfig }: { streamConfig: IStreamConfig }) { const { t } = useTranslation() - const { streamConfigList, replaceStreamConfig } = useStreamConfigStore((state) => ({ + const { streamConfigList, replaceStreamConfigList } = useStreamConfigStore((state) => ({ streamConfigList: state.streamConfigList, - replaceStreamConfig: state.replaceStreamConfig + replaceStreamConfigList: state.replaceStreamConfigList })) - const index = streamConfigList.findIndex((stream) => stream.title === streamConfig.title) + const index = streamConfigList.findIndex((stream) => stream.id === streamConfig.id) const handleMoveToTop = () => { const newStreamConfigList = [ @@ -32,7 +32,7 @@ export function MoveCardDropdownMenu({ streamConfig }: { streamConfig: IStreamCo ...streamConfigList.slice(0, index), ...streamConfigList.slice(index + 1) ] - replaceStreamConfig(newStreamConfigList) + replaceStreamConfigList(newStreamConfigList) } const handleMoveUp = () => { const newStreamConfigList = [ @@ -41,7 +41,7 @@ export function MoveCardDropdownMenu({ streamConfig }: { streamConfig: IStreamCo streamConfigList[index - 1], ...streamConfigList.slice(index + 1) ] - replaceStreamConfig(newStreamConfigList) + replaceStreamConfigList(newStreamConfigList) } const handleMoveDown = () => { const newStreamConfigList = [ @@ -50,7 +50,7 @@ export function MoveCardDropdownMenu({ streamConfig }: { streamConfig: IStreamCo streamConfigList[index], ...streamConfigList.slice(index + 2) ] - replaceStreamConfig(newStreamConfigList) + replaceStreamConfigList(newStreamConfigList) } return ( diff --git a/src/renderer/src/components/StreamConfigList/components/OperationBar.tsx b/src/renderer/src/components/StreamConfigList/components/OperationBar.tsx index 7a4fb05..15b521b 100644 --- a/src/renderer/src/components/StreamConfigList/components/OperationBar.tsx +++ b/src/renderer/src/components/StreamConfigList/components/OperationBar.tsx @@ -52,7 +52,7 @@ export default function OperationBar(props: OperationBarProps) { })) const handleConfirmDelete = async () => { - await removeStreamConfig(streamConfig.title) + await removeStreamConfig(streamConfig.id) setDeleteDialogOpen(false) } @@ -60,7 +60,7 @@ export default function OperationBar(props: OperationBarProps) { isFirst && (await updateStreamConfig( { ...streamConfig, status: StreamStatus.PREPARING_TO_RECORD }, - streamConfig.title + streamConfig.id )) const { code } = await window.api.startStreamRecord(JSON.stringify(streamConfig)).catch(() => { @@ -69,18 +69,13 @@ export default function OperationBar(props: OperationBarProps) { const currentStatus = useStreamConfigStore .getState() - .streamConfigList.find((item) => item.title === streamConfig.title)?.status + .streamConfigList.find((item) => item.id === streamConfig.id)?.status if (currentStatus === StreamStatus.NOT_STARTED) { return } - console.log('handleStartRecord code:', code) - if (code === SUCCESS_CODE) { - await updateStreamConfig( - { ...streamConfig, status: StreamStatus.RECORDING }, - streamConfig.title - ) + await updateStreamConfig({ ...streamConfig, status: StreamStatus.RECORDING }, streamConfig.id) toast({ title: streamConfig.title, description: t('start_record') @@ -115,7 +110,7 @@ export default function OperationBar(props: OperationBarProps) { } await updateStreamConfig( { ...streamConfig, status: StreamStatus.MONITORING }, - streamConfig.title + streamConfig.id ) return } @@ -127,10 +122,7 @@ export default function OperationBar(props: OperationBarProps) { const errMessage = crawlerErrorCodeToI18nMessage(code, 'error.start_record.') - await updateStreamConfig( - { ...streamConfig, status: StreamStatus.NOT_STARTED }, - streamConfig.title - ) + await updateStreamConfig({ ...streamConfig, status: StreamStatus.NOT_STARTED }, streamConfig.id) toast({ title: streamConfig.title, description: t(errMessage), @@ -149,7 +141,7 @@ export default function OperationBar(props: OperationBarProps) { } const handlePauseClick = async () => { - await window.api.stopStreamRecord(streamConfig.title) + await window.api.stopStreamRecord(streamConfig.id) clearTimeout(timer.current) } @@ -178,8 +170,8 @@ export default function OperationBar(props: OperationBarProps) { } useEffect(() => { - const handleRecordEndNotUserStop = async (title: string) => { - if (title !== streamConfig.title) return + const handleRecordEndNotUserStop = async (id: string) => { + if (id !== streamConfig.id) return if (timer.current) { clearTimeout(timer.current) } @@ -220,6 +212,7 @@ export default function OperationBar(props: OperationBarProps) { dark={darkPlayIcon} light={lightPlayIcon} handleClick={handlePlayClick} + id={streamConfig.id + '_play'} /> ) : ( )}
-

+

{streamConfig.title}

{streamConfig.status !== StreamStatus.NOT_STARTED && ( @@ -55,12 +58,12 @@ export default function StreamConfigCard({ streamConfig }: StreamConfigCardProps
- {ffmpegProgressInfo[streamConfig.title] && ( + {ffmpegProgressInfo[streamConfig.id] && ( <> -
{ffmpegProgressInfo[streamConfig.title]?.timemark}
+
{ffmpegProgressInfo[streamConfig.id]?.timemark}
{(streamConfig.segmentTime === '0' || streamConfig.segmentTime === '') && - ffmpegProgressInfo[streamConfig.title].targetSize / 1024 + 'M'} + ffmpegProgressInfo[streamConfig.id].targetSize / 1024 + 'M'}
)} diff --git a/src/renderer/src/components/StreamConfigSheet.tsx b/src/renderer/src/components/StreamConfigSheet.tsx index 21f7bd0..f7ef686 100644 --- a/src/renderer/src/components/StreamConfigSheet.tsx +++ b/src/renderer/src/components/StreamConfigSheet.tsx @@ -18,8 +18,10 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shad import { useStreamConfigStore } from '@renderer/store/useStreamConfigStore' import { useDefaultSettingsStore } from '@renderer/store/useDefaultSettingsStore' import { checkUrlValid } from '@renderer/lib/utils' +import { nanoid } from 'nanoid' const formSchema = z.object({ + id: z.string(), title: z.string(), roomUrl: z.string(), filename: z.string(), @@ -36,6 +38,7 @@ const formSchema = z.object({ }) const defaultStreamConfig: IStreamConfig = { + id: '', title: '', roomUrl: '', filename: '', @@ -245,8 +248,10 @@ export default function StreamConfigSheet(props: StreamConfigSheetProps) { } formValues.liveUrls = liveUrls if (type === 'edit') { - await updateStreamConfig(formValues, formValues.title) + await updateStreamConfig(formValues, formValues.id) } else { + // 创建时,id为空 + formValues.id = nanoid() await addStreamConfig(formValues) } } diff --git a/src/renderer/src/components/UseThemeIcon.tsx b/src/renderer/src/components/UseThemeIcon.tsx index 373510a..cb9de89 100644 --- a/src/renderer/src/components/UseThemeIcon.tsx +++ b/src/renderer/src/components/UseThemeIcon.tsx @@ -8,11 +8,12 @@ interface UseThemeIconProps { light: string handleClick?: () => void className?: string + id?: string tooltipContent?: string } export default function UseThemeIcon(props: UseThemeIconProps) { - const { dark, light, handleClick, className = '', tooltipContent } = props + const { dark, light, handleClick, className = '', tooltipContent, id } = props const [localTheme] = useLocalStorage('theme', 'light') const theme = useThemeStore((state) => state.theme || (localTheme as 'dark' | 'light')) @@ -24,6 +25,7 @@ export default function UseThemeIcon(props: UseThemeIconProps) { @@ -35,6 +37,7 @@ export default function UseThemeIcon(props: UseThemeIconProps) { src={theme === 'dark' ? dark : light} className={className ? className : 'w-[24px] h-[24px] cursor-pointer select-none'} onClick={handleClick} + id={id} /> ) } diff --git a/src/renderer/src/hooks/useToast.ts b/src/renderer/src/hooks/useToast.ts index f680fcc..fafbe8c 100644 --- a/src/renderer/src/hooks/useToast.ts +++ b/src/renderer/src/hooks/useToast.ts @@ -1,4 +1,6 @@ import { useToast as _useToast } from '@/shadcn/ui/use-toast' +import { sendMessage } from '@/lib/websocket' +import { WEBSOCKET_MESSAGE_TYPE } from '../../../const' export function useToast() { const { toast: _toast } = _useToast() @@ -13,7 +15,15 @@ export function useToast() { variant?: 'default' | 'destructive' | null }) { const isHide = document.hidden - // const isHide = true + + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.SHOW_TOAST, + data: { + title, + description, + variant + } + }) if (!isHide) { _toast({ diff --git a/src/renderer/src/lib/utils.ts b/src/renderer/src/lib/utils.ts index 4bac1bd..68f70cd 100644 --- a/src/renderer/src/lib/utils.ts +++ b/src/renderer/src/lib/utils.ts @@ -1,5 +1,3 @@ - - import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' @@ -34,4 +32,3 @@ export function useXizhiToPushNotification(options: { const titleAndContent = options.content ? `${options.title} - ${options.content}` : options.title fetch(`${options.key}?title=${encodeURIComponent(titleAndContent)}`) } - diff --git a/src/renderer/src/lib/websocket.ts b/src/renderer/src/lib/websocket.ts new file mode 100644 index 0000000..b4fd23f --- /dev/null +++ b/src/renderer/src/lib/websocket.ts @@ -0,0 +1,56 @@ +let websocket: WebSocket | null = null + +export function getWebsocket() { + return websocket +} + +interface IMessage { + type: string + data: any +} +const messageQueue: IMessage[] = [] +export function sendMessage(message: IMessage) { + if (!websocket) { + messageQueue.push(message) + return + } + if (websocket.readyState === WebSocket.OPEN) { + websocket.send(JSON.stringify(message)) + } else { + messageQueue.push(message) + } +} + +export function createWebSocket( + port: number, + code: string, + onMessage: (event: MessageEvent) => void +) { + websocket = new WebSocket(`ws://localhost:${port}/${code}`) + + websocket.onopen = () => { + while (messageQueue.length) { + const message = messageQueue.shift() + if (message) { + websocket!.send(JSON.stringify(message)) + } + } + } + + websocket.onmessage = onMessage + + websocket.onclose = (event) => { + if (event.code === 1000) { + websocket = null + } else { + createWebSocket(port, code, onMessage) + } + } +} + +export function closeWebSocket() { + if (websocket) { + websocket.close(1000) + websocket = null + } +} diff --git a/src/renderer/src/locales/locales.js b/src/renderer/src/locales/locales.js index a28527d..a963404 100644 --- a/src/renderer/src/locales/locales.js +++ b/src/renderer/src/locales/locales.js @@ -20,6 +20,20 @@ export default { logs_dir: 'Logs Directory', logs_dir_placeholder: 'Open Logs Directory' }, + web_control_setting: { + title: 'Web Control Setting', + web_control_path: 'Activation Code', + web_control_path_required: 'Activation Code cannot be empty', + web_control_path_placeholder: 'Can be obtained by clicking the get button', + get_web_control_path: 'Get', + email: 'Email', + email_placeholder: 'Please enter the email (for receiving activation code)', + email_required: 'Email cannot be empty', + email_invalid: 'Email is invalid', + enable_web_control: 'Enable Web Control', + confirm_force_close_window_with_web_control: + 'Web control is running, do you want to force close the window?' + }, stream_config: { confirm: 'Confirm', confirm_delete: 'Are you sure you want to delete this recording item? ({{title}})', @@ -121,7 +135,7 @@ export default { get_update_detail: 'View Update Details' }, downloading_dep: { - downloading_title: 'Downloading Dependencies. . .', + downloading_title: 'Downloading {{title}}. . .', retry_title: 'Download Dependencies Failed', confirm_force_close_window_with_downloading_dep: 'Dependencies are being downloaded, force close the window?', @@ -137,7 +151,36 @@ export default { toggle_theme: '切换主题', qq: '加入QQ群', discord: '加入Discord', - go_to_website: '前往官网' + go_to_website: '前往官网', + web_control: '网页操作' + }, + web_control_setting: { + title: '网页操作配置', + web_control_path: '激活码', + web_control_path_required: '激活码不能为空', + web_control_path_placeholder: '可通过点击获取按钮获取', + get_web_control_path: '获取', + email: '邮箱', + email_placeholder: '请输入邮箱(用于接收激活码)', + email_required: '邮箱不能为空', + email_invalid: '邮箱不合法', + enable_web_control: '开启网页访问功能', + enable_web_control_tooltip: '启用后可在手机通过网页操作录制', + get_web_control_path_success: '获取激活码成功', + get_web_control_path_success_desc: '激活码已发送至邮箱, 请注意查收', + get_web_control_path_failed: '获取激活码失败', + get_web_control_path_failed_desc: '请稍后重试', + start_web_control_success: '网页访问功能已开启', + start_web_control_success_desc: '你可以使用浏览器访问对应地址进行操作', + start_web_control_failed: '开启网页访问功能失败', + start_web_control_failed_desc: '请稍后重试', + frpc_process_error: 'frpc进程发生错误', + stop_web_control_success: '网页访问功能已关闭', + stop_web_control_success_desc: '你可以再次点击开启按钮开启', + stop_web_control_failed: '网页访问功能失败', + web_control_address: '网页访问地址', + confirm_force_close_window_with_web_control: '网页访问功能正在运行, 是否强制关闭窗口?', + will_retry: '将会在{{time}}秒后重试' }, default_settings: { title: '默认配置', @@ -246,7 +289,7 @@ export default { get_update_detail: '查看更新详情' }, downloading_dep: { - downloading_title: '正在下载依赖中. . .', + downloading_title: '正在下载 {{title}} 中. . .', retry_title: '下载依赖失败', confirm_force_close_window_with_downloading_dep: '当前正在下载依赖,是否强制关闭窗口?', retry: '重试' diff --git a/src/renderer/src/store/useConfettiStore.ts b/src/renderer/src/store/useConfettiStore.ts new file mode 100644 index 0000000..aebb3f1 --- /dev/null +++ b/src/renderer/src/store/useConfettiStore.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand' + +interface ConfettiStore { + showConfetti: boolean + numberOfPieces: number + setShowConfetti: (showConfetti: boolean) => void + setNumberOfPieces: (numberOfPieces: number) => void +} + +export const useConfettiStore = create((set) => ({ + showConfetti: false, + numberOfPieces: 500, + setShowConfetti: (showConfetti) => { + set({ showConfetti }) + let numberOfPieces = 500 + let timer: NodeJS.Timeout + if (showConfetti) { + timer = setInterval(() => { + numberOfPieces -= 10 + set({ numberOfPieces }) + if (numberOfPieces <= 0) { + clearInterval(timer) + set({ showConfetti: false, numberOfPieces: 500 }) + } + }, 100) + } + }, + setNumberOfPieces: (numberOfPieces) => set({ numberOfPieces }) +})) diff --git a/src/renderer/src/store/useDownloadDepStore.ts b/src/renderer/src/store/useDownloadDepStore.ts index e671312..3c5f467 100644 --- a/src/renderer/src/store/useDownloadDepStore.ts +++ b/src/renderer/src/store/useDownloadDepStore.ts @@ -7,6 +7,7 @@ interface IDownloadDepStore { export const useDownloadDepInfoStore = create((set) => ({ downloadDepProgressInfo: { + title: '', showRetry: false, downloading: false, progress: 0 diff --git a/src/renderer/src/store/useFfmpegProgressInfoStore.ts b/src/renderer/src/store/useFfmpegProgressInfoStore.ts index 52a613a..dc40afb 100644 --- a/src/renderer/src/store/useFfmpegProgressInfoStore.ts +++ b/src/renderer/src/store/useFfmpegProgressInfoStore.ts @@ -1,11 +1,26 @@ import { create } from 'zustand' +import { sendMessage } from '@/lib/websocket' +import { WEBSOCKET_MESSAGE_TYPE } from '../../../const' interface IRecordingProgressInfoStore { ffmpegProgressInfo: IFfmpegProgressInfo updateFfmpegProgressInfo: (newInfo: IFfmpegProgressInfo) => void } -export const useFfmpegProgressInfoStore = create((set) => ({ +export const useFfmpegProgressInfoStore = create((set, get) => ({ ffmpegProgressInfo: {}, - updateFfmpegProgressInfo: (newInfo) => set({ ffmpegProgressInfo: newInfo }) + updateFfmpegProgressInfo: (newInfo) => { + const raw = get().ffmpegProgressInfo + + if (Object.keys(raw).length === 0 && Object.keys(newInfo).length === 0) { + return + } + + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.UPDATE_FFMPEG_PROGRESS_INFO, + data: newInfo + }) + + set({ ffmpegProgressInfo: newInfo }) + } })) diff --git a/src/renderer/src/store/useLoadingStore.ts b/src/renderer/src/store/useLoadingStore.ts new file mode 100644 index 0000000..c89b766 --- /dev/null +++ b/src/renderer/src/store/useLoadingStore.ts @@ -0,0 +1,11 @@ +import { create } from 'zustand' + +interface LoadingState { + loading: boolean + setLoading: (value: boolean) => void +} + +export const useLoadingStore = create((set) => ({ + loading: false, + setLoading: (value: boolean) => set({ loading: value }) +})) diff --git a/src/renderer/src/store/useStreamConfigStore.ts b/src/renderer/src/store/useStreamConfigStore.ts index 727c856..fb043ed 100644 --- a/src/renderer/src/store/useStreamConfigStore.ts +++ b/src/renderer/src/store/useStreamConfigStore.ts @@ -1,6 +1,10 @@ import { create } from 'zustand' import localForage from 'localforage' +import { sendMessage } from '@/lib/websocket' +import { WEBSOCKET_MESSAGE_TYPE } from '../../../const' + +import { nanoid } from 'nanoid' interface IStreamConfigStore { streamConfigList: IStreamConfig[] @@ -8,45 +12,79 @@ interface IStreamConfigStore { addStreamConfig: (streamConfig: IStreamConfig) => Promise updateStreamConfig: (streamConfig: IStreamConfig, title: string) => Promise removeStreamConfig: (title: string) => Promise - replaceStreamConfig: (newStreamConfigList: IStreamConfig[]) => Promise + replaceStreamConfigList: (newStreamConfigList: IStreamConfig[]) => Promise } export const useStreamConfigStore = create((set, get) => ({ streamConfigList: [], streamConfigSheetOpen: false, initialData: async () => { - const streamConfigList = await localForage.getItem('streamConfigList') - if (streamConfigList) { - set(() => ({ streamConfigList })) + let streamConfigList = await localForage.getItem('streamConfigList') + + if (!streamConfigList) return + + let shouldUpdate = false + streamConfigList = streamConfigList.map((streamConfig) => { + if (!streamConfig.id) { + shouldUpdate = true + streamConfig.id = nanoid() + } + return streamConfig + }) + + if (shouldUpdate) { + await localForage.setItem('streamConfigList', streamConfigList) } + + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG_LIST, + data: streamConfigList + }) + set(() => ({ streamConfigList: streamConfigList! })) }, addStreamConfig: async (streamConfig: IStreamConfig) => { const newStreamConfigList = [streamConfig, ...get().streamConfigList] await localForage.setItem('streamConfigList', newStreamConfigList) + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.ADD_STREAM_CONFIG, + data: streamConfig + }) set(() => { return { streamConfigList: newStreamConfigList } }) }, - updateStreamConfig: async (newStreamConfig: IStreamConfig, title: string) => { + updateStreamConfig: async (newStreamConfig: IStreamConfig, id: string) => { const newStreamConfigList = get().streamConfigList.map((streamConfig) => - streamConfig.title === title ? newStreamConfig : streamConfig + streamConfig.id === id ? newStreamConfig : streamConfig ) await localForage.setItem('streamConfigList', newStreamConfigList) + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG, + data: newStreamConfig + }) set(() => { return { streamConfigList: newStreamConfigList } }) }, - replaceStreamConfig: async (newStreamConfigList: IStreamConfig[]) => { + replaceStreamConfigList: async (newStreamConfigList: IStreamConfig[]) => { await localForage.setItem('streamConfigList', newStreamConfigList) + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.UPDATE_STREAM_CONFIG_LIST, + data: newStreamConfigList + }) set(() => { return { streamConfigList: newStreamConfigList } }) }, - removeStreamConfig: async (title: string) => { + removeStreamConfig: async (id: string) => { const newStreamConfigList = get().streamConfigList.filter( - (streamConfig) => streamConfig.title !== title + (streamConfig) => streamConfig.id !== id ) await localForage.setItem('streamConfigList', newStreamConfigList) + sendMessage({ + type: WEBSOCKET_MESSAGE_TYPE.REMOVE_STREAM_CONFIG, + data: id + }) set(() => { return { streamConfigList: newStreamConfigList } }) diff --git a/src/renderer/src/store/useWebControlSettingStore.ts b/src/renderer/src/store/useWebControlSettingStore.ts new file mode 100644 index 0000000..9605281 --- /dev/null +++ b/src/renderer/src/store/useWebControlSettingStore.ts @@ -0,0 +1,36 @@ +import { create } from 'zustand' +import localForage from 'localforage' + +import emitter from '@renderer/lib/bus' +import { START_WEB_CONTROL } from '../../../const' + +interface IWebControlSettingStore { + webControlSetting: IWebControlSetting + setWebControlSetting: (setting: IWebControlSetting) => Promise + initData: () => void +} + +export const useWebControlSettingStore = create((set) => ({ + webControlSetting: { + webControlPath: '', + enableWebControl: false, + email: '' + }, + initData: async () => { + const webControlSetting = await localForage.getItem('webControlSetting') + + if (webControlSetting) { + const { enableWebControl, webControlPath } = webControlSetting + if (enableWebControl && webControlPath) { + emitter.emit(START_WEB_CONTROL, webControlPath) + } + set(() => ({ webControlSetting })) + } + }, + setWebControlSetting: async (setting: IWebControlSetting) => { + await localForage.setItem('webControlSetting', setting) + set(() => { + return { webControlSetting: setting } + }) + } +})) diff --git a/type.d.ts b/type.d.ts index a830ea6..dd90626 100644 --- a/type.d.ts +++ b/type.d.ts @@ -1,4 +1,5 @@ interface IStreamConfig { + id: string title: string roomUrl: string filename: string @@ -23,17 +24,20 @@ interface IDefaultDefaultSettingsConfig { type IFfmpegProgressInfo = Record< string, { - frames: number - currentFps: number - currentKbps: number targetSize: number timemark: string - percent?: number } > interface IDownloadDepProgressInfo { + title: string showRetry: boolean downloading: boolean progress: number } + +interface IWebControlSetting { + webControlPath: string + enableWebControl: boolean + email: string +}