Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de86c861d8 | |||
| af3b8a2a6b | |||
| 47d3d786ca | |||
| 1c755b4bd5 | |||
| 0cece2b194 | |||
| 840fc610f0 | |||
| 6e55fe9f34 | |||
| 1620b8e4c4 | |||
| f798bd3d8b | |||
| 65850f158c | |||
| 8f5991e65f | |||
| 5492e1df35 | |||
| 93fbd8768b | |||
| 246f079905 | |||
| b25285fcc2 | |||
| 7a2b31d328 | |||
| a4ac847f0d | |||
| 6b632b5502 | |||
| 86e1345684 | |||
| b91de0e442 | |||
| 62e47aac4c | |||
| 43fb92ac82 | |||
| de5ecb570f | |||
| 014c851a03 | |||
| eae07b1d3b | |||
| df06736722 | |||
| 25b90a0993 | |||
| ad1b58bf53 | |||
| 259118e48b | |||
| 145ac6bb12 | |||
| 7b581ff917 | |||
| 62423f8156 | |||
| 5fd539445e | |||
| 7c756cccef | |||
| 786826b434 | |||
| 56f41999b5 | |||
| 7423cf89ab | |||
| 8c6ee20b30 | |||
| 17ffa1acc0 | |||
| cec95c9f0a | |||
| eaf3d46e24 | |||
| c30badabbf | |||
| 40cdecc820 | |||
| 3c1088b6d6 | |||
| ac9a4c8f48 | |||
| b79ab2ee3f | |||
| ceef11bca6 | |||
| 5ce80eddb6 | |||
| 8bc3e229d7 | |||
| f939ce138c | |||
| ad89281927 | |||
| c9ae9e6b32 | |||
| ce7dd57d36 | |||
| 43ed1f8e3f | |||
| 9d1e796a20 | |||
| 80bfe5f979 | |||
| 13ee214026 | |||
| 08e38d752a | |||
| aef4d4ffee | |||
| a1b2c70fdd | |||
| c9b4525baf | |||
| 0b0666912e | |||
| 9dbb5681f7 | |||
| eaa7bdd736 | |||
| 0a65bbae5d | |||
| 82cfc1cabb | |||
| f4402ac57b | |||
| f6514f85dc | |||
| 1cf4c0ce39 | |||
| 3ffe7a63e9 | |||
| a532d9d209 | |||
| 9c1f2f6662 | |||
| 0b9ef9ff4c | |||
| 35f25abd8a | |||
| 70bed8c006 | |||
| dcfad6a3c0 | |||
| e7d1540b72 | |||
| 4bc09d74ff | |||
| 7ab45498e6 | |||
| da7ffd9c12 | |||
| b5d7ec111d | |||
| 9077eb836c | |||
| 6f3c2666ca | |||
| eaa3170f01 | |||
| 7e1c425676 | |||
| 7df980f881 | |||
| 7f0259e9cc | |||
| c7b2f99c43 | |||
| 4534238216 | |||
| 2085ef3063 | |||
| 7d3d69eae5 | |||
| 5c129e050b | |||
| 73163de408 | |||
| f18bdc4375 | |||
| 2d9a226378 | |||
| 6cd4094159 | |||
| d4c43ece23 | |||
| 9ac8d5d2bf | |||
| 049ef746f4 | |||
| 83d26211a0 | |||
| 680641707e | |||
| 27d4e092cf | |||
| 5b477b9e9e | |||
| eb332b70e0 | |||
| 824df9cae2 | |||
| 1fc73f324c | |||
| 8dcdb1f8a3 | |||
| 4fe1d1ea66 | |||
| 93b6f8c7b6 | |||
| a33e7c3903 | |||
| 4ea0180781 | |||
| 1d4850d7fb | |||
| cf77e6633e | |||
| c692fdf6c3 | |||
| 1901b177cc | |||
| ce60d77c8d | |||
| fbe1d43488 | |||
| ee8e75513d | |||
| f094fd1e7f | |||
| 14c30e0e58 | |||
| 872f1b9284 | |||
| 5680512573 | |||
| 2f6a43d98a | |||
| 1142c2c501 | |||
| c1502c0230 | |||
| 55cf680b0c | |||
| 6c0d0cdf74 | |||
| 32ad030838 | |||
| 9811c18407 | |||
| a9c6e01294 | |||
| b904d6aaec | |||
| 78958d925a | |||
| afba340028 | |||
| ec4b24149f | |||
| 8e1bd8596f | |||
| f8785f8db8 | |||
| c508c1dd1a | |||
| c78b593afa | |||
| 1b906804de | |||
| 62561e9826 | |||
| cbb9798174 | |||
| 2de6b6a1cc | |||
| 5893d1492e | |||
| 5906f29180 | |||
| 9b838e496e | |||
| 106a1fa9eb | |||
| ecfb2371f4 | |||
| fe3ae95a22 | |||
| 4ae352d5d2 | |||
| 67d56fc5ab | |||
| 039f9eb092 | |||
| df67e52e47 | |||
| e3f7c48f80 | |||
| 57b67ea55c | |||
| 21c3eb475f | |||
| 805f9435f3 | |||
| 213261fbbb | |||
| ceda93a8cf | |||
| 39ec4f863d | |||
| 4d9188221f | |||
| 30c8085964 | |||
| 97206279d8 | |||
| 8d3c8f4ad1 | |||
| bf4075ba1c | |||
| 5f5892242b | |||
| 29602d9fa2 | |||
| f2f1f963d4 | |||
| 56219c8fe4 | |||
| ba211c1360 | |||
| 2870a42cd4 | |||
| f8a2188760 | |||
| 5c4020a52f | |||
| 4c060366b8 | |||
| af219bde11 | |||
| caa279ce36 | |||
| 4651229a0d | |||
| 6c8916a9e6 | |||
| 8d831e2c9d | |||
| 1f3b4f5624 | |||
| 04c8afe616 | |||
| e53aed0910 | |||
| 520b63a8de | |||
| 0e4aa730eb | |||
| 07c7ddccad | |||
| 5ff7252ee9 |
@@ -1,44 +1,66 @@
|
||||
acodec
|
||||
adblocker
|
||||
ahooks
|
||||
aliyuncs
|
||||
antd
|
||||
aplus
|
||||
behaviour
|
||||
bili
|
||||
bilibili
|
||||
bodyparser
|
||||
bodys
|
||||
buildx
|
||||
cancle
|
||||
cliqz
|
||||
clsx
|
||||
commitlint
|
||||
compat
|
||||
Conpty
|
||||
consola
|
||||
conventionalcommits
|
||||
datetime
|
||||
DDTHH
|
||||
deeplink
|
||||
esmfile
|
||||
execa
|
||||
ghostery
|
||||
idtype
|
||||
immer
|
||||
inversify
|
||||
KHTML
|
||||
languagedetector
|
||||
localforage
|
||||
lucide
|
||||
mediago
|
||||
metas
|
||||
nocheck
|
||||
optimizelegibility
|
||||
outdir
|
||||
pinia
|
||||
Qrcode
|
||||
redownload
|
||||
Redownload
|
||||
reduxjs
|
||||
remusao
|
||||
rmvb
|
||||
shadcn
|
||||
Sider
|
||||
smaz
|
||||
stylelint
|
||||
svgr
|
||||
svgz
|
||||
tailwindcss
|
||||
TDAPP
|
||||
tiptap
|
||||
tldts
|
||||
tseslint
|
||||
typeorm
|
||||
vaul
|
||||
vitepress
|
||||
vuedraggable
|
||||
vuejs
|
||||
waline
|
||||
watting
|
||||
Watting
|
||||
xgplayer
|
||||
zustand
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# FIXME: APP_ 开头的会暴漏给前端
|
||||
APP_NAME=mediago
|
||||
APP_ID=mediago.ziying.site
|
||||
APP_COPYRIGHT=caorushizi
|
||||
APP_VERSION=2.2.3
|
||||
APP_VERSION=3.0.1
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
# Workflow's jobs
|
||||
jobs:
|
||||
@@ -23,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
os: [windows-latest, macos-latest, macos-13, ubuntu-latest]
|
||||
|
||||
# create steps
|
||||
steps:
|
||||
@@ -60,4 +61,4 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mediago-${{ matrix.os }}
|
||||
path: packages/main/release/mediago-setup-*
|
||||
path: packages/main/release/mediago-*
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Build and Push Multi-arch Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
standalone: true
|
||||
|
||||
- name: Build
|
||||
run: pnpm build:web-release
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Aliyun Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-beijing.aliyuncs.com
|
||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
registry.cn-beijing.aliyuncs.com/caorushizi/mediago:latest
|
||||
registry.cn-beijing.aliyuncs.com/caorushizi/mediago:${{ github.sha }}
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Deploy Downloader Docs.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
BUCKET: downloader-docs
|
||||
ENDPOINT: oss-cn-beijing.aliyuncs.com
|
||||
ACCESS_KEY: LTAI5tLckcUrBtj7bCiUYwWz
|
||||
ACCESS_KEY_SECRET: ${{ secrets.ACCESS_KEY_SECRET }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
|
||||
- name: Build with Vitepress
|
||||
run: pnpm run docs:build
|
||||
|
||||
- name: Install Alibaba Cloud OSSUTIL
|
||||
run: wget http://gosspublic.alicdn.com/ossutil/1.6.10/ossutil64 && chmod +x ossutil64
|
||||
|
||||
- name: Configure Alibaba Cloud OSSUTIL
|
||||
run: ./ossutil64 config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
|
||||
|
||||
- name: Upload the web folder to the chosen OSS bucket
|
||||
run: ./ossutil64 --config-file .ossutilconfig cp ${{ github.workspace }}/docs/.vitepress/dist oss://${BUCKET} -r -f
|
||||
@@ -0,0 +1,34 @@
|
||||
FROM m.daocloud.io/docker.io/library/node:20 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./packages/backend/dist /app
|
||||
|
||||
COPY ./packages/backend/package*.json ./
|
||||
|
||||
RUN npm install --omit=dev --registry=https://registry.npmmirror.com
|
||||
|
||||
RUN npm rebuild
|
||||
|
||||
# FROM m.daocloud.io/docker.io/library/debian:11-slim AS deb_extractor
|
||||
|
||||
# RUN cd /tmp && \
|
||||
# apt-get update && apt-get download libicu-dev && \
|
||||
# mkdir /dpkg && \
|
||||
# for deb in *.deb; do dpkg --extract $deb /dpkg || exit 10; done
|
||||
|
||||
# FROM gcr.io/distroless/nodejs20-debian12
|
||||
FROM m.daocloud.io/docker.io/library/node:20-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app /app
|
||||
|
||||
# COPY --from=deb_extractor /dpkg /
|
||||
RUN apt-get update && apt-get install -y libicu-dev ffmpeg
|
||||
|
||||
RUN npm install pm2 -g
|
||||
|
||||
EXPOSE 8899
|
||||
|
||||
CMD ["pm2-runtime", "server/index.js"]
|
||||
@@ -0,0 +1,141 @@
|
||||
<div align="center">
|
||||
<h1>MediaGo</h1>
|
||||
<a href="https://downloader.caorushizi.cn/en/guides.html?form=github">Quick start</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
## Intro
|
||||
|
||||
This project supports m3u8 video extraction tools, streaming media download, and m3u8 download.
|
||||
|
||||
- **✅ No need to capture packets**: You can easily sniff video resources on web pages using the built-in browser. Choose the resource you want to download from the sniffed resource list—simple and fast.
|
||||
- **📱 Mobile playback**: Easily switch between PC and mobile devices seamlessly. Once downloaded, you can watch the video on your phone.
|
||||
- **⚡️ Batch download**: Supports downloading multiple videos and live streams simultaneously, ensuring high bandwidth is fully utilized.
|
||||
- **🎉 Docker deployment supported**: Supports Docker deployment for the web version, making it convenient and quick.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To run the code, you'll need Node.js and pnpm. Node.js can be downloaded and installed from the official website, and pnpm can be installed via `npm i -g pnpm`.
|
||||
|
||||
## Run the code
|
||||
|
||||
```shell
|
||||
# Code download
|
||||
git clone https://github.com/caorushizi/mediago.git
|
||||
|
||||
# Installation dependency
|
||||
pnpm i
|
||||
|
||||
# Development environment
|
||||
pnpm dev
|
||||
|
||||
# Package run
|
||||
pnpm release
|
||||
|
||||
# Build a docker image
|
||||
docker buildx build -t caorushizi/mediago:latest .
|
||||
|
||||
# docker startup
|
||||
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
### v3.0.0 (Released on 2024.10.7)
|
||||
|
||||
#### Software Downloads
|
||||
|
||||
- [【mediago】 Windows (Installer) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 Windows (Portable) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macOS x64 (Intel) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 Linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
#### Domestic Downloads
|
||||
|
||||
- [【mediago】 Windows (Installer) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 Windows (Portable) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macOS x64 (Intel) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 Linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### One-click Docker Panel Deployment (Recommended)
|
||||
|
||||
1. Install the BT Panel by visiting [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago) and downloading the official version script.
|
||||
2. After installation, log into the BT Panel, click on `Docker` in the menu bar. The first time you enter, it will prompt you to install the `Docker` service. Click to install and follow the instructions to complete the installation.
|
||||
3. Once installed, find `MediaGo` in the application store, click install, configure domain names, and other basic information to complete the installation.
|
||||
|
||||
### Software Screenshots
|
||||
|
||||

|
||||
|
||||
### Major Updates
|
||||
|
||||
- Support for Docker deployment on the web version
|
||||
- Updated desktop UI
|
||||
|
||||
### Changelog
|
||||
|
||||
- Updated desktop UI
|
||||
- Support for Docker deployment on the web version
|
||||
- Added video playback, supporting both desktop and mobile playback
|
||||
- Fixed issue where the macOS version couldn't display the interface
|
||||
- Optimized the batch download interaction
|
||||
- Added portable version for Windows (no installation required)
|
||||
- Optimized the download list, supporting sniffing multiple videos on a page
|
||||
- Supported manual import/export of the favorites list
|
||||
- Supported export of the homepage download list
|
||||
- Optimized the interaction logic for the "New Download" form
|
||||
- Supported opening the app via UrlScheme and adding download tasks
|
||||
- Fixed several bugs and enhanced the user experience
|
||||
|
||||
## Software Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Tech Stack
|
||||
|
||||
- React <https://react.dev/>
|
||||
- Electron <https://www.electronjs.org>
|
||||
- Koa <https://koajs.com>
|
||||
- Vite <https://cn.vitejs.dev>
|
||||
- Ant Design <https://ant.design>
|
||||
- Tailwind CSS <https://tailwindcss.com>
|
||||
- Shadcn <https://ui.shadcn.com/>
|
||||
- Inversify <https://inversify.io>
|
||||
- TypeORM <https://typeorm.io>
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- N_m2u8DL-CLI from <https://github.com/nilaoda/N_m3u8DL-CLI>
|
||||
- N_m3u8DL-RE from <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
- mediago from <https://github.com/caorushizi/hls-downloader>
|
||||
@@ -0,0 +1,143 @@
|
||||
<div align="center">
|
||||
<h1>MediaGo</h1>
|
||||
<a href="https://downloader.caorushizi.cn/jp/guides.html?form=github">早く始めます</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/jp?form=github">公式サイトです</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/jp/documents.html?form=github">にやすりをかける</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
## Intro
|
||||
|
||||
本プロジェクトはm3u8ビデオ抽出ツール、ストリーミングダウンロード、m3u8ダウンロードをサポートしています。
|
||||
|
||||
- **✅ パケットキャプチャ不要**: ソフトウェアに内蔵されたブラウザを使用して、ウェブページ内のビデオリソースを簡単に検出し、検出したリソースリストからダウンロードしたいリソースを選択することで、シンプルかつ迅速にダウンロードできます。
|
||||
- **📱 モバイル再生**: PCとモバイルデバイス間で簡単にシームレスに切り替えが可能で、ダウンロードが完了した後はスマートフォンでビデオを視聴できます。
|
||||
- **⚡️ バッチダウンロード**: 複数のビデオやライブストリームリソースを同時にダウンロードでき、高速帯域幅を無駄にしません。
|
||||
- **🎉 Dockerデプロイメントサポート**: WebエンドをDockerでデプロイすることができ、簡単かつ便利です。
|
||||
|
||||
## Quickstart
|
||||
|
||||
コードを実行するには、Node.jsとpnpmが必要です。Node.jsは公式ウェブサイトからダウンロードしてインストールし、pnpmは`npm i -g pnpm`コマンドでインストールできます。
|
||||
|
||||
## コードの実行
|
||||
|
||||
```shell
|
||||
# コードのダウンロードです
|
||||
git clone https://github.com/caorushizi/mediago.git
|
||||
|
||||
# インストール依存症です
|
||||
pnpm i
|
||||
|
||||
# 開発環境です
|
||||
pnpm dev
|
||||
|
||||
# 梱包して運行します
|
||||
pnpm release
|
||||
|
||||
# dockerミラーリングを構築します
|
||||
docker buildx build -t caorushizi/mediago:latest .
|
||||
|
||||
# docker启动
|
||||
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
### v3.0.0 (2024.10.7 発売)
|
||||
|
||||
#### ソフトウェアダウンロード
|
||||
|
||||
- [【mediago】 windows(インストーラー版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(ポータブル版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
#### 国内ダウンロード
|
||||
|
||||
- [【mediago】 windows(インストーラー版) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(ポータブル版) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### docker 宝塔パネルワンクリックデプロイ(推奨)
|
||||
|
||||
1. 宝塔パネルをインストールし、[宝塔パネル](https://www.bt.cn/new/download.html?r=dk_mediago) の公式サイトから正式版のスクリプトを選択してインストールします。
|
||||
|
||||
2. インストール後、宝塔パネルにログインし、メニューから `Docker` をクリックします。初めてアクセスすると、`Docker` サービスをインストールするように指示されるので、「今すぐインストール」をクリックし、指示に従ってインストールを完了します。
|
||||
|
||||
3. インストールが完了したら、アプリストアで「MediaGo」を見つけ、インストールをクリックし、ドメイン名などの基本情報を設定してインストールを完了します。
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 重要な更新
|
||||
|
||||
- Web端のdockerデプロイをサポート
|
||||
- デスクトップ端のUIを更新
|
||||
|
||||
### 更新ログ
|
||||
|
||||
- デスクトップ端のUIを更新
|
||||
- Web端のdockerデプロイをサポート
|
||||
- 新たにビデオ再生機能を追加、デスクトップとモバイル端両方で再生可能
|
||||
- Macでの画面表示ができない問題を修正
|
||||
- バッチダウンロードのインタラクションを最適化
|
||||
- Windowsのポータブル版(インストール不要)を追加
|
||||
- ダウンロードリストの最適化、ページ内の複数のビデオリソースを嗅ぎ取る機能を追加
|
||||
- 手動でお気に入りリストのインポートとエクスポートをサポート
|
||||
- ホームページのダウンロードリストエクスポートをサポート
|
||||
- 「新規ダウンロード」フォームのインタラクションロジックを最適化
|
||||
- UrlSchemeでアプリを開き、ダウンロードタスクを追加する機能をサポート
|
||||
- バグの修正とユーザー体験の向上
|
||||
|
||||
## ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 技術スタック
|
||||
|
||||
- react <https://react.dev/>
|
||||
- electron <https://www.electronjs.org>
|
||||
- koa <https://koajs.com>
|
||||
- vite <https://cn.vitejs.dev>
|
||||
- antd <https://ant.design>
|
||||
- tailwindcss <https://tailwindcss.com>
|
||||
- shadcn <https://ui.shadcn.com/>
|
||||
- inversify <https://inversify.io>
|
||||
- typeorm <https://typeorm.io>
|
||||
|
||||
## 感謝
|
||||
|
||||
- N_m2u8DL-CLI は <https://github.com/nilaoda/N_m3u8DL-CLI> から来ています
|
||||
- N_m3u8DL-RE は <https://github.com/nilaoda/N_m3u8DL-RE> から来ています
|
||||
- mediago は <https://github.com/caorushizi/hls-downloader> から来ています
|
||||
@@ -1,5 +1,3 @@
|
||||
<img src="https://socialify.git.ci/caorushizi/mediago/image?font=Inter&forks=1&issues=1&language=1&name=1&owner=1&pattern=Circuit%20Board&pulls=1&stargazers=1&theme=Auto" alt="MediaDownloader"/>
|
||||
|
||||
<div align="center">
|
||||
<h1>MediaGo</h1>
|
||||
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
|
||||
@@ -10,12 +8,21 @@
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
|
||||
<a href="https://github.com/caorushizi/mediago/blob/master/README.en.md">English</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
@@ -27,6 +34,7 @@
|
||||
- **✅ 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- **📱 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **⚡️ 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
|
||||
- **🎉 支持 docker 部署**: 支持 docker 部署 web 端,方便快捷。
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -46,53 +54,84 @@ pnpm dev
|
||||
|
||||
# 打包运行
|
||||
pnpm release
|
||||
|
||||
# 构建 docker 镜像
|
||||
docker buildx build -t caorushizi/mediago:latest .
|
||||
|
||||
# docker 启动
|
||||
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
**v2.2.0 (2024.5.22 发布)**
|
||||
### v3.0.0 (2024.10.7 发布)
|
||||
|
||||
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
|
||||
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
|
||||
#### 软件下载
|
||||
|
||||
**更新日志**
|
||||
- [【mediago】 windows(安装版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(便携版) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
- 支持下载直播流
|
||||
- 支持哔哩哔哩视频下载
|
||||
- 优化沉浸式嗅探流程
|
||||
- 支持下载控制台输出
|
||||
- 修复了一些 bug
|
||||
### docker 宝塔面板一键部署(推荐)
|
||||
|
||||
**v2.0.2(2023.7.9 发布)**
|
||||
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/new/download.html?r=dk_mediago) 官网,选择正式版的脚本下载安装
|
||||
|
||||
- [windows mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.exe)
|
||||
- [macos mediago v2.0.2](https://github.com/caorushizi/mediago/releases/download/v2.0.2/media-downloader-setup-2.0.2.dmg)
|
||||
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
|
||||
|
||||
**更新日志**
|
||||
3. 安装完成后在应用商店中找到`MediaGo`,点击安装,配置域名等基本信息即可完成安装
|
||||
|
||||
- 暗黑模式
|
||||
- 更多下载配置
|
||||
- 支持请求标头自动带入
|
||||
- 支持开启广告过滤
|
||||
- 支持开启沉浸式嗅探
|
||||
- 支持切换手机和 PC 模式
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 重要更新
|
||||
|
||||
- 支持 docker 部署 web 端
|
||||
- 更新桌面端 UI
|
||||
|
||||
### 更新日志
|
||||
|
||||
- 更新桌面端 UI
|
||||
- 支持 docker 部署 web 端
|
||||
- 新增视频播放,支持桌面端和移动端播放
|
||||
- 修复 mac 打开无法展示界面的问题
|
||||
- 优化了批量下载的交互
|
||||
- 添加了 windows 的便携版(免安装哦)
|
||||
- 优化了下载列表,支持页面中多个视频的嗅探
|
||||
- 支持收藏列表手动导入导出
|
||||
- 支持首页下载列表导出
|
||||
- 优化了【新建下载】表单的交互逻辑
|
||||
- 支持 UrlScheme 打开应用,并添加下载任务
|
||||
- 修复了一些 bug 并提升用户体验
|
||||
|
||||
## 软件截图
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## 技术栈
|
||||
|
||||
- react <https://react.dev/>
|
||||
- electron <https://www.electronjs.org>
|
||||
- koa <https://koajs.com>
|
||||
- vite <https://cn.vitejs.dev>
|
||||
- antd <https://ant.design>
|
||||
- electron <https://www.electronjs.org>
|
||||
- tailwindcss <https://tailwindcss.com>
|
||||
- shadcn <https://ui.shadcn.com/>
|
||||
- inversify <https://inversify.io>
|
||||
- typeorm <https://typeorm.io>
|
||||
|
||||
## 鸣谢
|
||||
|
||||
N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
- N_m2u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
|
||||
- N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
- mediago 来自于 <https://github.com/caorushizi/hls-downloader>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.vitepress/dist
|
||||
.vitepress/cache
|
||||
@@ -1,62 +0,0 @@
|
||||
import { HeadConfig, defineConfig } from "vitepress";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
const head: HeadConfig[] = [
|
||||
["link", { rel: "shortcut icon", href: "/favicon.svg" }],
|
||||
];
|
||||
if (!isDev) {
|
||||
head.push([
|
||||
"script",
|
||||
{},
|
||||
`var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?eefcbd14f0323044aa0ca678cd278381";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();`,
|
||||
]);
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "MediaGo",
|
||||
description: "简单易用,快速下载",
|
||||
lastUpdated: true,
|
||||
head,
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "教程", link: "/guides" },
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: "开始",
|
||||
items: [
|
||||
{ text: "快速开始", link: "/guides" },
|
||||
{ text: "使用说明", link: "/documents" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "其他",
|
||||
items: [
|
||||
{ text: "旧版本", link: "/history" },
|
||||
{ text: "意见收集", link: "/proposal" },
|
||||
{ text: "支持列表", link: "/list" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/caorushizi/m3u8-downloader" },
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: '<a href="https://beian.miit.gov.cn">豫ICP备20012967号-2</a>',
|
||||
copyright: "Copyright © 2021-present caorushizi",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, ref, watchEffect } from "vue";
|
||||
import { init, WalineInstance } from "@waline/client";
|
||||
import "@waline/client/waline.css";
|
||||
import { useData, useRoute } from "vitepress";
|
||||
|
||||
const route = useRoute();
|
||||
const { isDark } = useData();
|
||||
|
||||
const commentsRef = ref<WalineInstance | null>(null);
|
||||
|
||||
watchEffect(() => {
|
||||
commentsRef.value?.update({
|
||||
dark: isDark.value,
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
commentsRef.value?.destroy();
|
||||
initWaline();
|
||||
}
|
||||
);
|
||||
|
||||
function initWaline() {
|
||||
commentsRef.value = init({
|
||||
el: "#waline",
|
||||
serverURL: "https://comments.ziying.site",
|
||||
dark: isDark.value,
|
||||
lang: "zh-CN",
|
||||
pageview: true,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initWaline();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="waline">hello world</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#waline {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
import { h } from "vue";
|
||||
import Theme from "vitepress/theme";
|
||||
import "./style/var.css";
|
||||
import Comments from "./components/Comments.vue";
|
||||
|
||||
export default {
|
||||
...Theme,
|
||||
Layout() {
|
||||
return h(Theme.Layout, null, {
|
||||
"doc-after": () => h(Comments),
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
:root {
|
||||
/* 标题 */
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: linear-gradient(
|
||||
135deg,
|
||||
#5e9ef3 10%,
|
||||
#2a82f6 100%
|
||||
);
|
||||
|
||||
/* 图标背景 */
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
135deg,
|
||||
#5e9ef3 10%,
|
||||
#2a82f6 100%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(150px);
|
||||
|
||||
/* brand按钮 */
|
||||
--vp-button-brand-border: #5e9ef3;
|
||||
--vp-button-brand-text: #f2f7fd;
|
||||
--vp-button-brand-bg: #2a82f6;
|
||||
|
||||
--vp-button-brand-active-border: #5e9ef3;
|
||||
--vp-button-brand-active-text: #fff;
|
||||
--vp-button-brand-active-bg: #409eff;
|
||||
|
||||
--vp-button-brand-hover-border: #5e9ef3;
|
||||
--vp-button-brand-hover-text: #fff;
|
||||
--vp-button-brand-hover-bg: #2a82f6;
|
||||
|
||||
--vp-c-brand: #2a82f6;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 使用说明
|
||||
|
||||
这个页面主要讲解的是设置页面的参数的含义
|
||||
|
||||

|
||||
|
||||
## 基础设置
|
||||
|
||||
::: tip
|
||||
下载器全局设置
|
||||
:::
|
||||
|
||||
1. 选择文件夹: 视频下载的路径
|
||||
|
||||
2. 下载器主题: 支持浅色和深色模式
|
||||
|
||||
3. 在新窗口中打开浏览器: 开启了之后浏览器页面会独立出一个窗口。
|
||||
|
||||

|
||||
|
||||
4. 下载完成提示: 开启之后下载完成会出现系统提示
|
||||
|
||||
## 浏览器设置
|
||||
|
||||
::: tip
|
||||
浏览器窗口相关的设置
|
||||
:::
|
||||
|
||||
1. 代理设置: 填写自己的代理地址
|
||||
|
||||
2. 代理开关: 打开开关后浏览器默认会走填写的代理地址,如果代理地址这个开关是打不开的
|
||||
|
||||
3. 开启广告过滤: 打开开关后会过滤页面中的广告
|
||||
|
||||
4. 以手机模式进入: 打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
|
||||
|
||||

|
||||
|
||||
5. 使用浏览器插件
|
||||
|
||||
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面下方的下载的链接来添加视频到下载列表 **(后面会有很强大的功能哦)**
|
||||
|
||||

|
||||
|
||||
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
|
||||
|
||||
## 下载设置
|
||||
|
||||
::: tip
|
||||
和下载相关的设置
|
||||
:::
|
||||
|
||||
1. 下载完成删除分片: 开启后下载完成后会将分片文件删除
|
||||
2. 最大同时下载数量: 最多同时可以下载多少视频文件,目前最大是 10,最小是 1
|
||||
3. 更多操作
|
||||
- 配置文件目录: 软件数据库路径、日志等等路径
|
||||
- 可执行文件目录: 下载器二进制文件的路径
|
||||
- 本地存储路径: 下载视频的本地路径
|
||||
4. 当前版本: 当前软件版本信息
|
||||
|
||||
## 其他问题
|
||||
|
||||
**Q: 关于直播视频的下载**
|
||||
|
||||
A: 软件支持直播下载,软件目前没有好的办法区分直播流,所以将所有的下载控制台开启。用户自行分辨下载数据源
|
||||
|
||||
**Q: 关于 MacOS 版本**
|
||||
|
||||
A: 目前 MacOS 版本属于非稳定版本,2023 年 5 月发布。需要在 Mac 的安全设置中打开来自身份不明开发者的 Mac App
|
||||

|
||||
|
||||
**Q: 关于旧版本**
|
||||
|
||||
A: 1.1.5 版本确实发布了很长时间,经过大量用户的验证,比较稳定。所以想要使用旧版软件的话请访问[此链接](/history.html)
|
||||
|
||||
**Q: 关于 Win7 用户**
|
||||
|
||||
A: v2.0.0 之后的版本已经放弃不支持 Win7 如果需要在 win7 上面使用同样需要下载 1.1.5 版本
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 快速开始
|
||||
|
||||
这篇文章是软件的简单说明,可以快速上手使用本本软件。
|
||||
|
||||
::: tip
|
||||
方便大家一起沟通交流, 可以加入 QQ 反馈群:
|
||||
|
||||
MediaGo 反馈群 1: 574209001
|
||||
|
||||
MediaGo 反馈群 2: 921619626
|
||||
:::
|
||||
|
||||
## 下载安装
|
||||
|
||||
**v2.2.0 (2024.5.22 发布)**
|
||||
|
||||
- [windows mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.exe)
|
||||
- [macos mediago v2.2.0](https://github.com/caorushizi/mediago/releases/download/v2.2.0/mediago-setup-2.2.0.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持下载直播流
|
||||
- 支持哔哩哔哩视频下载
|
||||
- 优化沉浸式嗅探流程
|
||||
- 支持下载控制台输出
|
||||
- 修复了一些 bug
|
||||
|
||||
**v2.0.1(2023.7.1 发布)**
|
||||
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 暗黑模式
|
||||
- 更多下载配置
|
||||
- 支持请求标头自动带入
|
||||
- 支持开启广告过滤
|
||||
- 支持开启沉浸式嗅探
|
||||
- 支持切换手机和 PC 模式
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持视频下载
|
||||
|
||||
## 操作说明
|
||||
|
||||
### 视频自动嗅探
|
||||
|
||||
1. 选择【资源提取】
|
||||
|
||||

|
||||
|
||||
2. 进入视频的网址
|
||||
|
||||

|
||||
|
||||
3. 开始下载,即可下载视频
|
||||
|
||||

|
||||
|
||||
### 手动下载
|
||||
|
||||
1. 点击页面右上角的【新建下载】
|
||||
|
||||

|
||||
|
||||
2. 在新建下载弹窗中填入【视频名称】和【m3u8 地址】即可
|
||||
|
||||

|
||||
|
||||
3. 在列表中点击下载,即可下载视频
|
||||
|
||||

|
||||
|
||||
## 附加功能
|
||||
|
||||
1. 下载完成后可以在下载列表中点击转换音频按钮将视频转换为音频
|
||||
|
||||

|
||||
|
||||
2. 下载完成后播放视频
|
||||
|
||||
- PC 播放
|
||||
|
||||

|
||||
|
||||
- 移动端播放
|
||||
|
||||

|
||||
|
||||
3. 批量下载
|
||||
|
||||

|
||||
|
||||
## 开始你的视频下载吧
|
||||
|
||||
是不是很简单,快去下载你的视频吧
|
||||
|
||||
::: warning
|
||||
本软件仅可用于学习交流
|
||||
:::
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 旧版本
|
||||
|
||||
::: info
|
||||
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
|
||||
|
||||
**这个链接的软件不会自动升级, 可放心使用**
|
||||
:::
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos mediago v1.1.5 无~~
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持视频下载
|
||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 311 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,30 +0,0 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "在线视频下载"
|
||||
text: "简单易用,快速下载"
|
||||
tagline: 简单易学,无需抓包,不需要安装插件
|
||||
image:
|
||||
src: /home.png
|
||||
alt: home
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /guides
|
||||
- theme: alt
|
||||
text: 使用说明
|
||||
link: /documents
|
||||
|
||||
features:
|
||||
- icon: ⏩
|
||||
title: 无需抓包
|
||||
details: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- icon: 📱
|
||||
title: 移动播放
|
||||
details: 可以轻松无缝的在PC和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- icon: ⚡️
|
||||
title: 支持批量下载
|
||||
details: 支持同时下载多个视频和直播资源,高速带宽不闲置。
|
||||
---
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 支持列表
|
||||
|
||||
::: tip
|
||||
大家有什么网站想要支持的在这里提
|
||||
:::
|
||||
|
||||
**模板**
|
||||
|
||||
---
|
||||
|
||||
- 网站首页: xxx
|
||||
- 视频(直播)列表页: xxx
|
||||
- 视频(直播)页: xxx
|
||||
|
||||
---
|
||||
|
||||
支持完成后我会在评论区回复
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 意见收集
|
||||
|
||||
::: info
|
||||
这里手机大家使用软件的问题,或者意见建议
|
||||
:::
|
||||
|
||||
如果软件报错,需要描述清楚在什么网站下载什么视频的时候报错
|
||||
|
||||
如果是建议请描述清楚你的需求
|
||||
|
||||
如果是意见请尽量提出来,我会尽量满足大家的需求
|
||||
@@ -1,40 +0,0 @@
|
||||
<svg t="1686326276609" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="6962" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="200" height="200">
|
||||
<path
|
||||
d="M110.933333 42.666667h716.8A68.266667 68.266667 0 0 1 896 110.933333v802.133334a68.266667 68.266667 0 0 1-68.266667 68.266666H110.933333A68.266667 68.266667 0 0 1 42.666667 913.066667V110.933333A68.266667 68.266667 0 0 1 110.933333 42.666667z"
|
||||
fill="#111314" fill-opacity=".72" p-id="6963"></path>
|
||||
<path
|
||||
d="M853.333333 42.666667h85.333334a68.266667 68.266667 0 0 1 68.266666 68.266666v802.133334A68.266667 68.266667 0 0 1 938.666667 981.333333h-85.333334a68.266667 68.266667 0 0 0 68.266667-68.266666V110.933333A68.266667 68.266667 0 0 0 853.333333 42.666667z"
|
||||
fill="#CACBCC" p-id="6964"></path>
|
||||
<path
|
||||
d="M162.133333 128h614.4a34.133333 34.133333 0 0 1 34.133334 34.133333v699.733334a34.133333 34.133333 0 0 1-34.133334 34.133333H162.133333a34.133333 34.133333 0 0 1-34.133333-34.133333V162.133333a34.133333 34.133333 0 0 1 34.133333-34.133333z"
|
||||
fill="#FFFFFF" p-id="6965"></path>
|
||||
<path
|
||||
d="M187.733333 170.666667h563.2c9.386667 0 17.066667 7.68 17.066667 17.066666v221.866667a17.066667 17.066667 0 0 1-17.066667 17.066667H187.733333a17.066667 17.066667 0 0 1-17.066666-17.066667V187.733333c0-9.386667 7.68-17.066667 17.066666-17.066666z"
|
||||
fill="#EDEEF0" p-id="6966"></path>
|
||||
<path
|
||||
d="M256 298.666667m17.066667 0l136.533333 0q17.066667 0 17.066667 17.066666l0 136.533334q0 17.066667-17.066667 17.066666l-136.533333 0q-17.066667 0-17.066667-17.066666l0-136.533334q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#FFC13C" p-id="6967"></path>
|
||||
<path
|
||||
d="M341.333333 392.533333a34.133333 34.133333 0 1 1 0-68.266666 34.133333 34.133333 0 0 1 0 68.266666z m65.109334 56.192H276.224C276.224 426.666667 276.224 401.066667 341.333333 401.066667s65.109333 25.6 65.109334 47.658666z"
|
||||
fill="#FFFFFF" p-id="6968"></path>
|
||||
<path
|
||||
d="M469.333333 341.333333m17.066667 0l136.533333 0q17.066667 0 17.066667 17.066667l0 8.533333q0 17.066667-17.066667 17.066667l-136.533333 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
|
||||
fill="#5BADFF" p-id="6969"></path>
|
||||
<path
|
||||
d="M512 554.666667m17.066667 0l179.2 0q17.066667 0 17.066666 17.066666l0 51.2q0 17.066667-17.066666 17.066667l-179.2 0q-17.066667 0-17.066667-17.066667l0-51.2q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#EDEEF0" p-id="6970"></path>
|
||||
<path
|
||||
d="M512 682.666667m17.066667 0l179.2 0q17.066667 0 17.066666 17.066666l0 51.2q0 17.066667-17.066666 17.066667l-179.2 0q-17.066667 0-17.066667-17.066667l0-51.2q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#EDEEF0" p-id="6971"></path>
|
||||
<path
|
||||
d="M213.333333 554.666667m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066666l0 8.533334q0 17.066667-17.066666 17.066666l-221.866667 0q-17.066667 0-17.066667-17.066666l0-8.533334q0-17.066667 17.066667-17.066666Z"
|
||||
fill="#EDEEF0" p-id="6972"></path>
|
||||
<path
|
||||
d="M213.333333 640m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066667l0 8.533333q0 17.066667-17.066666 17.066667l-221.866667 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
|
||||
fill="#EDEEF0" p-id="6973"></path>
|
||||
<path
|
||||
d="M213.333333 725.333333m17.066667 0l221.866667 0q17.066667 0 17.066666 17.066667l0 8.533333q0 17.066667-17.066666 17.066667l-221.866667 0q-17.066667 0-17.066667-17.066667l0-8.533333q0-17.066667 17.066667-17.066667Z"
|
||||
fill="#EDEEF0" p-id="6974"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 120 KiB |
@@ -8,16 +8,21 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx scripts/dev.ts && pnpm --parallel -F \"./packages/*\" run dev",
|
||||
"dev:web": "pnpm --parallel -F \"./packages/*\" run dev:web",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"build:plugin": "pnpm -F plugin run build",
|
||||
"build:main": "pnpm -F main run build",
|
||||
"build:web": "pnpm -F renderer run build:web",
|
||||
"build:backend": "pnpm -F backend run build",
|
||||
"build:renderer": "pnpm -F renderer run build",
|
||||
"build:mobile": "pnpm -F mobile run build",
|
||||
"build:web-release": "tsx scripts/web.ts",
|
||||
"rebuild:web": "pnpm -F backend rebuild",
|
||||
"rebuild:electron": "pnpm -F main run rebuild",
|
||||
"beta": "pnpm run build && pnpm -F mediago run pack",
|
||||
"release": "pnpm run build && pnpm -F main run release",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"types": "pnpm -F main run types",
|
||||
"types:renderer": "pnpm -F renderer run types",
|
||||
"types:watch": "pnpm -F main run types:watch",
|
||||
"prepare": "husky",
|
||||
"commit": "git-cz",
|
||||
@@ -38,24 +43,21 @@
|
||||
"@commitlint/core": "^19.3.0",
|
||||
"@commitlint/cz-commitlint": "^19.2.0",
|
||||
"@commitlint/format": "^19.3.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@waline/client": "^3.2.0",
|
||||
"@eslint/js": "^8.57.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cspell": "^8.8.3",
|
||||
"cz-git": "^1.9.2",
|
||||
"eslint": "^9.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"globals": "^15.3.0",
|
||||
"husky": "^9.0.11",
|
||||
"tsx": "^4.11.0",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"vitepress": "1.2.2",
|
||||
"vue": "^3.4.27",
|
||||
"zx": "^8.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@cliqz/adblocker@1.27.3": "patches/@cliqz__adblocker@1.27.3.patch"
|
||||
"@ghostery/adblocker@2.3.1": "patches/@ghostery__adblocker@2.3.1.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
@@ -0,0 +1,9 @@
|
||||
.bin
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
bin/Logs
|
||||
release
|
||||
types
|
||||
log
|
||||
bin
|
||||
@@ -0,0 +1 @@
|
||||
types/
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
# backend
|
||||
@@ -0,0 +1,16 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export { dev, build } from "./scripts";
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["dist"],
|
||||
"ext": "js",
|
||||
"exec": "npx cross-env NODE_ENV=development node dist/server/index.js"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.1.0",
|
||||
"description": "在线视频下载器",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "nodemon",
|
||||
"dev:web": "cross-env NODE_ENV=development gulp dev",
|
||||
"build": "cross-env NODE_ENV=production gulp build",
|
||||
"types": "tsc",
|
||||
"types:watch": "tsc -w",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": "eslint"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^8.57.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/gulp": "^4.0.17",
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa-send": "^4.1.6",
|
||||
"@types/koa-static": "^4.0.4",
|
||||
"@types/koa__cors": "^5.0.0",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/serve-handler": "^6.1.4",
|
||||
"@types/undertaker": "^1.2.11",
|
||||
"consola": "^3.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"del": "^7.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "0.21.4",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.57.0",
|
||||
"glob": "^10.4.1",
|
||||
"globals": "^15.3.0",
|
||||
"gulp": "^5.0.0",
|
||||
"lint-staged": "^15.2.5",
|
||||
"nodemon": "^3.1.4",
|
||||
"semver": "^7.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@koa/router": "^12.0.1",
|
||||
"axios": "^1.7.2",
|
||||
"better-sqlite3": "^9.3.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"dayjs": "^1.11.11",
|
||||
"execa": "^9.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"i18next": "^23.11.5",
|
||||
"inversify": "^6.0.2",
|
||||
"koa": "^2.15.3",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-pty": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"socket.io": "^4.8.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"typeorm": "0.3.20",
|
||||
"winston": "^3.14.2"
|
||||
},
|
||||
"keywords": []
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Env, isDev, mainResolve } from "./utils";
|
||||
import esbuild from "esbuild";
|
||||
|
||||
export function buildOptions(): esbuild.BuildOptions {
|
||||
const getDefine = (): Record<string, string> => {
|
||||
const vars: Record<string, string> = {
|
||||
"process.env.NODE_ENV": isDev ? '"development"' : '"production"',
|
||||
};
|
||||
if (isDev) {
|
||||
vars.__bin__ = `"${mainResolve("bin").replace(/\\/g, "\\\\")}"`;
|
||||
Object.assign(vars, Env.getInstance().loadDotEnvDefined());
|
||||
}
|
||||
|
||||
return vars;
|
||||
};
|
||||
|
||||
return {
|
||||
entryPoints: [mainResolve("src/index.ts")],
|
||||
bundle: true,
|
||||
sourcemap: process.env.NODE_ENV === "development",
|
||||
define: getDefine(),
|
||||
outdir: mainResolve("dist/server"),
|
||||
loader: { ".png": "file" },
|
||||
minify: process.env.NODE_ENV === "production",
|
||||
packages: "external",
|
||||
format: "esm",
|
||||
target: ["node20"],
|
||||
platform: "node",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { deleteSync } from "del";
|
||||
import { mainResolve, Env, isDev } from "./utils";
|
||||
import gulp from "gulp";
|
||||
import * as esbuild from "esbuild";
|
||||
import consola from "consola";
|
||||
import { buildOptions } from "./config";
|
||||
import fs from "fs";
|
||||
import spawn from "cross-spawn";
|
||||
|
||||
const env = Env.getInstance();
|
||||
env.loadDotEnvRuntime();
|
||||
|
||||
async function clean() {
|
||||
return deleteSync([mainResolve("dist/server")]);
|
||||
}
|
||||
|
||||
async function buildClean() {
|
||||
return deleteSync([mainResolve("dist")]);
|
||||
}
|
||||
|
||||
async function copyBin() {
|
||||
const source = mainResolve(
|
||||
"../main/bin",
|
||||
isDev ? process.platform : "linux",
|
||||
process.arch,
|
||||
);
|
||||
const target = mainResolve("dist/server/bin");
|
||||
fs.cpSync(source, target, {
|
||||
recursive: true,
|
||||
filter: (src) => {
|
||||
if (!isDev && src.endsWith("ffmpeg")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const copy = gulp.parallel(copyBin);
|
||||
|
||||
async function watchTask() {
|
||||
const main = await esbuild.context(buildOptions());
|
||||
await main.rebuild();
|
||||
|
||||
const watcher = gulp.watch(["./src"]);
|
||||
watcher
|
||||
.on("change", async () => {
|
||||
await main.rebuild();
|
||||
})
|
||||
.on("error", (error: any) => {
|
||||
consola.error(error);
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function startNodemon() {
|
||||
const nodemon = spawn("npm", ["start"], {
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
nodemon.stdout?.on("data", (data) => {
|
||||
consola.log(data.toString());
|
||||
});
|
||||
|
||||
// Listen for stderr data events
|
||||
nodemon.stderr?.on("data", (data) => {
|
||||
consola.error(`stderr: ${data.toString()}`);
|
||||
});
|
||||
|
||||
// Listens for closing events of the child process
|
||||
nodemon.on("close", (code) => {
|
||||
consola.log(`子进程退出,退出码 ${code}`);
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function buildTask() {
|
||||
await esbuild.build(buildOptions());
|
||||
}
|
||||
|
||||
// Development environment
|
||||
export const dev = gulp.series(clean, copy, watchTask, startNodemon);
|
||||
// Build packaging
|
||||
export const build = gulp.series(buildClean, copy, buildTask);
|
||||
@@ -0,0 +1,70 @@
|
||||
import dotenv from "dotenv";
|
||||
import { resolve } from "path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
const baseResolve = (...r: any[]) => resolve(__dirname, ...r);
|
||||
export const mainResolve = (...r: string[]) => baseResolve("..", ...r);
|
||||
export const rootResolve = (...r: string[]) => baseResolve("../../..", ...r);
|
||||
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
export const isLinux = process.platform === "linux";
|
||||
export const isMac = process.platform === "darwin";
|
||||
export const isWin = process.platform === "win32";
|
||||
|
||||
export class Env {
|
||||
env: Record<string, string> = {};
|
||||
nodeEnv = "";
|
||||
static instance: Env;
|
||||
|
||||
private constructor(nodeEnv = process.env.NODE_ENV) {
|
||||
const env = this.parseEnv(rootResolve(".env"));
|
||||
const modeEnv = this.parseEnv(rootResolve(`.env.${nodeEnv}`));
|
||||
const localEnv = this.parseEnv(rootResolve(`.env.${nodeEnv}.local`));
|
||||
|
||||
this.nodeEnv = nodeEnv || "development";
|
||||
this.env = { ...env, ...modeEnv, ...localEnv };
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!Env.instance) {
|
||||
Env.instance = new Env();
|
||||
}
|
||||
|
||||
return Env.instance;
|
||||
}
|
||||
|
||||
private parseEnv(path: string) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = dotenv.parse(fs.readFileSync(path));
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.keys(parsed).reduce((prev: any, curr) => {
|
||||
prev[curr] = parsed[curr];
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
loadDotEnvRuntime() {
|
||||
Object.keys(this.env).forEach((key) => {
|
||||
if (process.env[key] != null || !this.env[key]) return;
|
||||
process.env[key] = this.env[key];
|
||||
});
|
||||
}
|
||||
|
||||
loadDotEnvDefined() {
|
||||
return Object.keys(this.env).reduce<Record<string, string>>((prev, cur) => {
|
||||
prev[`process.env.${[cur]}`] = JSON.stringify(this.env[cur]);
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
get isDev() {
|
||||
return this.nodeEnv === "development";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "./types.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import RouterHandlerService from "./core/router.ts";
|
||||
import Koa from "koa";
|
||||
import cors from "@koa/cors";
|
||||
import bodyParser from "koa-bodyparser";
|
||||
import Logger from "./vendor/Logger.ts";
|
||||
import http from "http";
|
||||
import SocketIO from "./vendor/SocketIO.ts";
|
||||
import VideoRepository from "./repository/VideoRepository.ts";
|
||||
import { DownloadStatus } from "./interfaces.ts";
|
||||
import serve from "koa-static";
|
||||
import { STATIC_DIR } from "./const.ts";
|
||||
import send from "koa-send";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronApp extends Koa {
|
||||
constructor(
|
||||
@inject(TYPES.RouterHandlerService)
|
||||
private readonly router: RouterHandlerService,
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
@inject(TYPES.Logger)
|
||||
private readonly logger: Logger,
|
||||
@inject(TYPES.SocketIO)
|
||||
private readonly socket: SocketIO,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private async vendorInit() {
|
||||
await this.db.init();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.router.init();
|
||||
|
||||
// vendor
|
||||
await this.vendorInit();
|
||||
|
||||
this.use(cors())
|
||||
.use(bodyParser())
|
||||
.use(this.router.routes())
|
||||
.use(this.router.allowedMethods());
|
||||
this.use(serve(STATIC_DIR));
|
||||
|
||||
// Middleware that handles static files and front-end routing
|
||||
this.use(async (ctx, next) => {
|
||||
if (!ctx.path.startsWith("/api")) {
|
||||
try {
|
||||
await send(ctx, ctx.path, { root: STATIC_DIR });
|
||||
} catch (err: any) {
|
||||
if (err.status === 404) {
|
||||
await send(ctx, "index.html", { root: STATIC_DIR });
|
||||
}
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
const server = http.createServer(this.callback());
|
||||
|
||||
this.socket.initSocketIO(server);
|
||||
|
||||
server.listen(8899, () => {
|
||||
this.logger.info("Server running on port 8899");
|
||||
});
|
||||
}
|
||||
|
||||
// If there are still videos being downloaded after the restart, change the status to download failed
|
||||
async resetDownloadStatus(): Promise<void> {
|
||||
// If data in the downloading state still fails after the restart, all downloads fail
|
||||
const videos = await this.videoRepository.findWattingAndDownloadingVideos();
|
||||
const videoIds = videos.map((video) => video.id);
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
videoIds,
|
||||
DownloadStatus.Failed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import os from "os";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export const API_PREFIX = "/api";
|
||||
export const HOME_DIR = os.homedir();
|
||||
export const DOWNLOAD_DIR = `${HOME_DIR}/mediago`;
|
||||
export const STATIC_DIR = resolve(__dirname, "../app");
|
||||
export const BIN_DIR = resolve(__dirname, "./bin");
|
||||
export const DB_PATH = `${__dirname}/../mediago.db`;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import {
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
type Controller,
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { get, post } from "../helper/index.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import { Context } from "koa";
|
||||
import DownloaderService from "../services/DownloaderService.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import Logger from "../vendor/Logger.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DownloadController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.DownloaderService)
|
||||
private readonly downloaderService: DownloaderService,
|
||||
@inject(TYPES.DownloadService)
|
||||
private readonly downloadService: DownloadService,
|
||||
@inject(TYPES.Logger)
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@get("/")
|
||||
async getFavorites() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@post("add-download-item")
|
||||
async addDownloadItem(ctx: Context) {
|
||||
const video = ctx.request.body as DownloadItem;
|
||||
const item = await this.videoRepository.addVideo(video);
|
||||
return item;
|
||||
}
|
||||
|
||||
@post("add-download-items")
|
||||
async addDownloadItems(ctx: Context) {
|
||||
const videos = ctx.request.body as DownloadItem[];
|
||||
return this.downloaderService.addDownloadItems(videos);
|
||||
}
|
||||
|
||||
@post("get-download-items")
|
||||
async getDownloadItems(ctx: Context) {
|
||||
const pagination = ctx.request.body as DownloadItemPagination;
|
||||
const videos = await this.videoRepository.findVideos(pagination);
|
||||
return videos;
|
||||
}
|
||||
|
||||
@post("start-download")
|
||||
async startDownload(ctx: Context) {
|
||||
const { vid } = ctx.request.body as { vid: number };
|
||||
await this.downloaderService.startDownload(vid);
|
||||
}
|
||||
|
||||
@post("delete-download-item")
|
||||
async deleteDownloadItem(ctx: Context) {
|
||||
const { id } = ctx.request.body as { id: number };
|
||||
await this.videoRepository.deleteDownloadItem(id);
|
||||
}
|
||||
|
||||
@post("download-now")
|
||||
async downloadNow(ctx: Context) {
|
||||
const video = ctx.request.body as Omit<DownloadItem, "id">;
|
||||
await this.downloaderService.downloadNow(video);
|
||||
}
|
||||
|
||||
@post("download-items-now")
|
||||
async downloadItemsNow(ctx: Context) {
|
||||
const videos = ctx.request.body as Omit<DownloadItem, "id">[];
|
||||
// Add download
|
||||
const items = await this.downloaderService.addDownloadItems(videos);
|
||||
// Start downloading
|
||||
items.forEach((item) => this.downloaderService.startDownload(item.id));
|
||||
return items;
|
||||
}
|
||||
|
||||
@post("edit-download-now")
|
||||
async editDownloadNow(ctx: Context) {
|
||||
const video = ctx.request.body as DownloadItem;
|
||||
const item = await this.downloaderService.editDownloadItem(video);
|
||||
await this.downloaderService.startDownload(item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
@post("edit-download-item")
|
||||
async editDownloadItem(ctx: Context) {
|
||||
const video = ctx.request.body as DownloadItem;
|
||||
this.logger.info("editDownloadItem", video);
|
||||
return this.downloaderService.editDownloadItem(video);
|
||||
}
|
||||
|
||||
@post("stop-download")
|
||||
async stopDownload(ctx: Context) {
|
||||
const { id } = ctx.request.body as { id: number };
|
||||
|
||||
this.downloadService.stopTask(id);
|
||||
}
|
||||
|
||||
@post("get-video-folders")
|
||||
async getVideoFolders() {
|
||||
return this.downloaderService.getVideoFolders();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { ConfigParams, TYPES } from "../types.ts";
|
||||
import FavoriteRepository from "../repository/FavoriteRepository.ts";
|
||||
import { get, post } from "../helper/index.ts";
|
||||
import Logger from "../vendor/Logger.ts";
|
||||
import ConfigService from "../services/ConfigService.ts";
|
||||
import { Context } from "koa";
|
||||
import SocketIO from "../vendor/SocketIO.ts";
|
||||
|
||||
@injectable()
|
||||
export default class HomeController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.FavoriteRepository)
|
||||
private readonly favoriteRepository: FavoriteRepository,
|
||||
@inject(TYPES.Logger)
|
||||
private readonly logger: Logger,
|
||||
@inject(TYPES.ConfigService)
|
||||
private readonly config: ConfigService,
|
||||
@inject(TYPES.SocketIO)
|
||||
private readonly socket: SocketIO,
|
||||
) {}
|
||||
|
||||
@get("/")
|
||||
async getFavorites() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@post("get-app-store")
|
||||
async getAppStore() {
|
||||
const store = await this.config.getConfig();
|
||||
return store;
|
||||
}
|
||||
|
||||
@post("set-app-store")
|
||||
async setAppStore(ctx: Context) {
|
||||
const params = ctx.request.body as ConfigParams;
|
||||
this.config.setConfig(params);
|
||||
this.logger.info("set app store");
|
||||
return false;
|
||||
}
|
||||
|
||||
@post("socket-test")
|
||||
async socketTest(ctx: Context) {
|
||||
const { message } = ctx.request.body as { message: string };
|
||||
this.logger.info(message);
|
||||
|
||||
this.socket.emit("socket-test", message);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { inject, injectable, multiInject } from "inversify";
|
||||
import { Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import Router from "@koa/router";
|
||||
import { error, success } from "../helper/utils.ts";
|
||||
import { API_PREFIX } from "../const.ts";
|
||||
import path from "path";
|
||||
import Logger from "../vendor/Logger.ts";
|
||||
|
||||
@injectable()
|
||||
export default class RouterHandlerService extends Router {
|
||||
constructor(
|
||||
@multiInject(TYPES.Controller)
|
||||
private readonly controllers: Controller[],
|
||||
@inject(TYPES.Logger)
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private registerIpc(
|
||||
controller: Controller,
|
||||
propertyKey: string | symbol,
|
||||
): void {
|
||||
const property = controller[propertyKey];
|
||||
if (typeof property !== "function") return;
|
||||
|
||||
const httpMethod: "get" | "post" = Reflect.getMetadata(
|
||||
"http-method",
|
||||
controller,
|
||||
propertyKey,
|
||||
);
|
||||
if (!httpMethod) return;
|
||||
|
||||
const routerPath = Reflect.getMetadata(
|
||||
"router-path",
|
||||
controller,
|
||||
propertyKey,
|
||||
);
|
||||
if (!routerPath) return;
|
||||
|
||||
const finalPath = path
|
||||
.join(API_PREFIX, routerPath)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/\/$/, "");
|
||||
this[httpMethod](finalPath, async (context, next) => {
|
||||
try {
|
||||
let res = property.call(controller, context, next);
|
||||
if (res.then) {
|
||||
res = await res;
|
||||
}
|
||||
context.body = success(res);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(e);
|
||||
if (e instanceof Error) {
|
||||
context.body = error(e.message);
|
||||
} else {
|
||||
context.body = error(String(e));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
init(): void {
|
||||
for (const controller of this.controllers) {
|
||||
const Class = Object.getPrototypeOf(controller);
|
||||
Object.getOwnPropertyNames(Class).forEach((propertyKey) =>
|
||||
this.registerIpc(controller, propertyKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface Vendor {
|
||||
init(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
import { AppLanguage, AppTheme } from "../types.ts";
|
||||
import { DOWNLOAD_DIR } from "../const.ts";
|
||||
|
||||
@Entity({
|
||||
name: "config",
|
||||
})
|
||||
export class Config {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: DOWNLOAD_DIR,
|
||||
})
|
||||
local: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
enum: AppTheme,
|
||||
nullable: false,
|
||||
default: AppTheme.System,
|
||||
})
|
||||
theme: AppTheme;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
enum: AppLanguage,
|
||||
nullable: false,
|
||||
default: AppLanguage.System,
|
||||
})
|
||||
language: AppLanguage;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: "",
|
||||
})
|
||||
proxy: string;
|
||||
|
||||
@Column({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
nullable: false,
|
||||
})
|
||||
isLive: boolean;
|
||||
|
||||
@Column({
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
default: false,
|
||||
})
|
||||
downloadProxySwitch: boolean;
|
||||
|
||||
@Column({
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
default: true,
|
||||
})
|
||||
deleteSegments: boolean;
|
||||
|
||||
@Column({
|
||||
type: "int",
|
||||
nullable: false,
|
||||
default: 2,
|
||||
})
|
||||
maxRunner: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedDate: Date;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity({
|
||||
name: "favorite",
|
||||
})
|
||||
export class Favorite {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
})
|
||||
url: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
})
|
||||
icon?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedDate: Date;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { DownloadStatus, DownloadType } from "../interfaces.ts";
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
@Entity({
|
||||
name: "video",
|
||||
})
|
||||
export class Video {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
})
|
||||
folder?: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: DownloadType.m3u8,
|
||||
})
|
||||
type: DownloadType;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
})
|
||||
url: string;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: true,
|
||||
})
|
||||
headers?: string;
|
||||
|
||||
@Column({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
nullable: false,
|
||||
})
|
||||
isLive: boolean;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: DownloadStatus.Ready,
|
||||
})
|
||||
status: DownloadStatus;
|
||||
|
||||
@Column({
|
||||
type: "text",
|
||||
nullable: false,
|
||||
default: "",
|
||||
})
|
||||
log: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdDate: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedDate: Date;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable no-var */
|
||||
declare global {
|
||||
var __bin__: string;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const get = (route: string) => {
|
||||
return (target: any, propertyKey: string): void => {
|
||||
Reflect.defineMetadata("http-method", "get", target, propertyKey);
|
||||
Reflect.defineMetadata("router-path", route, target, propertyKey);
|
||||
};
|
||||
};
|
||||
|
||||
export const post = (route: string) => {
|
||||
return (target: any, propertyName: string): void => {
|
||||
Reflect.defineMetadata("http-method", "post", target, propertyName);
|
||||
Reflect.defineMetadata("router-path", route, target, propertyName);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { spawn } from "child_process";
|
||||
import { ffmpegPath } from "./variables.ts";
|
||||
|
||||
export const convertToAudio = async (
|
||||
input: string,
|
||||
output: string,
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ffmpeg = spawn(ffmpegPath, [
|
||||
"-y",
|
||||
"-v",
|
||||
"error",
|
||||
"-i",
|
||||
input,
|
||||
"-acodec",
|
||||
"mp3",
|
||||
"-format",
|
||||
"mp3",
|
||||
output,
|
||||
]);
|
||||
let errData = "";
|
||||
|
||||
ffmpeg.stderr.on("data", (data) => {
|
||||
errData += String(data);
|
||||
});
|
||||
|
||||
ffmpeg.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
ffmpeg.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(errData));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import os from "os";
|
||||
|
||||
export function getLocalIP() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
let localIP = "";
|
||||
|
||||
// Traverse the network interface
|
||||
for (const key in interfaces) {
|
||||
const iface = interfaces[key];
|
||||
if (!iface) continue;
|
||||
|
||||
// IPv4 addresses that are not loopback addresses are filtered out
|
||||
const filteredIface = iface.filter(
|
||||
(details) => details.family === "IPv4" && !details.internal,
|
||||
);
|
||||
|
||||
if (filteredIface.length > 0) {
|
||||
localIP = filteredIface[0].address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return localIP;
|
||||
}
|
||||
|
||||
export { sleep, formatHeaders } from "./utils.ts";
|
||||
export * from "./variables.ts";
|
||||
export { get, post } from "./decorator.ts";
|
||||
export { convertToAudio } from "./ffmpeg.ts";
|
||||
@@ -0,0 +1,37 @@
|
||||
import EventEmitter from "events";
|
||||
|
||||
export async function sleep(second = 1): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, second * 1000));
|
||||
}
|
||||
|
||||
export function formatHeaders(headers: Record<string, string>): string {
|
||||
if (!headers) return "";
|
||||
const formatted = Object.entries(headers)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join("\n");
|
||||
return formatted;
|
||||
}
|
||||
|
||||
export const event = new EventEmitter();
|
||||
|
||||
export interface IpcResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: Record<string, any> | null;
|
||||
}
|
||||
|
||||
export function success(data: Record<string, any>): IpcResponse {
|
||||
return {
|
||||
code: 0,
|
||||
message: "success",
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function error(message = "fail"): IpcResponse {
|
||||
return {
|
||||
code: -1,
|
||||
message,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { resolve } from "path";
|
||||
import { BIN_DIR } from "../const.ts";
|
||||
|
||||
export enum Platform {
|
||||
Windows = "win32",
|
||||
MacOS = "darwin",
|
||||
Linux = "linux",
|
||||
}
|
||||
|
||||
export const isMac = process.platform === Platform.MacOS;
|
||||
export const isWin = process.platform === Platform.Windows;
|
||||
export const isLinux = process.platform === Platform.Linux;
|
||||
|
||||
export function resolveBin(path: string) {
|
||||
if (isWin) {
|
||||
path += ".exe";
|
||||
}
|
||||
return resolve(BIN_DIR, path);
|
||||
}
|
||||
|
||||
export const appName = process.env.APP_NAME || "mediago";
|
||||
export const defaultScheme = "mediago";
|
||||
export const PERSIST_MEDIAGO = "persist:mediago";
|
||||
export const PERSIST_WEBVIEW = "persist:webview";
|
||||
export const PRIVACY_WEBVIEW = "webview";
|
||||
|
||||
// bin path
|
||||
export const ffmpegPath = resolveBin("ffmpeg");
|
||||
export const biliDownloaderBin = resolveBin("BBDown");
|
||||
export const m3u8DownloaderBin = resolveBin("N_m3u8DL-RE");
|
||||
|
||||
// user agent
|
||||
export const pcUA = "";
|
||||
export const mobileUA =
|
||||
"Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36";
|
||||
@@ -0,0 +1,114 @@
|
||||
import i18n from "i18next";
|
||||
|
||||
i18n.init({
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
fallbackLng: "zh",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: {
|
||||
en: {
|
||||
translation: {
|
||||
processIPC: "Processing ipc [{{channel}}] event exception: ",
|
||||
open: "Open",
|
||||
delete: "Delete",
|
||||
noMainWindow: "No main window found",
|
||||
copyLinkAddress: "Copy Link Address",
|
||||
select: "Select",
|
||||
download: "Download",
|
||||
refresh: "Refresh",
|
||||
openFolder: "Download Folder",
|
||||
openFile: "Open File",
|
||||
noFileFound: "No file found",
|
||||
exportFavorites: "Export Favorites",
|
||||
copy: "Copy",
|
||||
paste: "Paste",
|
||||
noTaskFound: "No task found",
|
||||
urlExist: "Url already exists",
|
||||
videoExistsPleaseChangeName:
|
||||
"Video name already exists, please change the video name",
|
||||
videoNotExists: "Video does not exist",
|
||||
unknownError: "Unknown error",
|
||||
unsupportedDownloadType: "Unsupported download type",
|
||||
captureVideoLinkInWindow: "Capture video link in window: {{url}}",
|
||||
browserViewNotFound: "Browser view not found",
|
||||
currentWindowNotFound: "Current window not found",
|
||||
proxyNotAllowEmpty: "[Proxy] Proxy cannot be empty",
|
||||
enableProxy: "[Proxy] Enable Proxy({{proxy}})",
|
||||
disableProxy: "[Proxy] Disable Proxy",
|
||||
adBlockerEnableFailed: "[AdBlocker] AdBlocker enable failed",
|
||||
enableADBlocker: "[AdBlocker] Enable AD Blocker",
|
||||
adBlockerDisableFailed: "[AdBlocker] AdBlocker disable failed",
|
||||
disableADBlocker: "[AdBlocker] Disable AD Blocker",
|
||||
ua: '[UA] User-Agent: "{{ua}}"',
|
||||
session: "[Session] {{mode}} Mode",
|
||||
currentEnvironmentIsDevelopment:
|
||||
"Current environment is development, start loading developer tools",
|
||||
loadDevToolsSuccess: "Load developer tools successfully",
|
||||
loadDevToolsFailed: "Failed to load developer tools",
|
||||
autoUpdateSuccess: "Auto update success",
|
||||
nextTimeWillAutoInstall: "Will be automatically installed next time",
|
||||
downloadSuccess: "Download success",
|
||||
videoDownloadSuccess: "{{name}} download success",
|
||||
downloadFailed: "Download failed",
|
||||
videoDownloadFailed: "{{name}} download failed",
|
||||
showMainWindow: "Show main window",
|
||||
exitApp: "Exit app",
|
||||
updateAvailable: "Update available.",
|
||||
updateNotAvailable: "No updates available.",
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
translation: {
|
||||
processIPC: "处理 ipc [{{channel}}] 事件时异常: ",
|
||||
open: "打开",
|
||||
delete: "删除",
|
||||
noMainWindow: "未找到主窗口",
|
||||
copyLinkAddress: "复制链接地址",
|
||||
select: "选择",
|
||||
download: "下载",
|
||||
refresh: "刷新",
|
||||
openFolder: "下载目录",
|
||||
openFile: "打开文件",
|
||||
noFileFound: "未找到文件",
|
||||
exportFavorites: "导出收藏",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
noTaskFound: "未找到任务",
|
||||
urlExist: "链接已存在",
|
||||
videoExistsPleaseChangeName: "视频名称已存在,请更改视频名称",
|
||||
videoNotExists: "视频不存在",
|
||||
unknownError: "未知错误",
|
||||
unsupportedDownloadType: "不支持的下载类型",
|
||||
captureVideoLinkInWindow: "在窗口中捕获视频链接: {{url}}",
|
||||
browserViewNotFound: "未找到 BrowserView",
|
||||
currentWindowNotFound: "未找到当前窗口",
|
||||
proxyNotAllowEmpty: "[Proxy] 代理不能为空",
|
||||
enableProxy: "[Proxy] 启用代理({{proxy}})",
|
||||
disableProxy: "[Proxy] 关闭代理",
|
||||
adBlockerEnableFailed: "[AdBlocker] AdBlocker 启用失败",
|
||||
enableADBlocker: "[AdBlocker] 启用 AD Blocker",
|
||||
adBlockerDisableFailed: "[AdBlocker] AdBlocker 关闭失败",
|
||||
disableADBlocker: "[AdBlocker] 关闭 AD Blocker",
|
||||
ua: '[UA] User-Agent: "{{ua}}"',
|
||||
session: "[Session] {{mode}} 模式",
|
||||
currentEnvironmentIsDevelopment:
|
||||
"当前环境为开发环境,开始加载开发者工具",
|
||||
loadDevToolsSuccess: "加载开发者工具成功",
|
||||
loadDevToolsFailed: "加载开发者工具失败",
|
||||
autoUpdateSuccess: "自动更新成功",
|
||||
nextTimeWillAutoInstall: "下次将自动安装",
|
||||
downloadSuccess: "下载成功",
|
||||
videoDownloadSuccess: "{{name}} 下载成功",
|
||||
downloadFailed: "下载失败",
|
||||
videoDownloadFailed: "{{name}} 下载失败",
|
||||
showMainWindow: "显示主窗口",
|
||||
exitApp: "退出应用",
|
||||
updateAvailable: "有可用更新。",
|
||||
updateNotAvailable: "没有可用更新。",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,7 @@
|
||||
declare module "*.svg";
|
||||
declare module "*.png";
|
||||
declare module "*.jpg";
|
||||
declare module "*.jpeg";
|
||||
declare module "*.gif";
|
||||
declare module "*.bmp";
|
||||
declare module "*.tiff";
|
||||
@@ -0,0 +1,7 @@
|
||||
import "reflect-metadata";
|
||||
import { container } from "./inversify.config.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import ElectronApp from "./app.ts";
|
||||
|
||||
const mediago = container.get<ElectronApp>(TYPES.ElectronApp);
|
||||
mediago.init();
|
||||
@@ -0,0 +1,73 @@
|
||||
export type Controller = Record<string | symbol, any>;
|
||||
|
||||
export interface DownloadItem {
|
||||
id: number;
|
||||
type: DownloadType;
|
||||
name: string;
|
||||
url: string;
|
||||
headers?: string;
|
||||
status?: DownloadStatus;
|
||||
isLive?: boolean;
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
export enum DownloadFilter {
|
||||
list = "list",
|
||||
done = "done",
|
||||
}
|
||||
|
||||
export interface DownloadItemPagination {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
filter?: DownloadFilter;
|
||||
}
|
||||
|
||||
export interface ConversionPagination {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface VideoResponse {
|
||||
total: number;
|
||||
list: DownloadItem[];
|
||||
}
|
||||
|
||||
export enum DownloadStatus {
|
||||
Ready = "ready",
|
||||
Watting = "watting",
|
||||
Downloading = "downloading",
|
||||
Stopped = "stopped",
|
||||
Success = "success",
|
||||
Failed = "failed",
|
||||
}
|
||||
|
||||
export type Task = {
|
||||
id: number;
|
||||
params: Omit<DownloadParams, "id" | "abortSignal" | "callback">;
|
||||
};
|
||||
|
||||
export interface DownloadProgress {
|
||||
id: number;
|
||||
type: string;
|
||||
percent: string;
|
||||
speed: string;
|
||||
isLive: boolean;
|
||||
}
|
||||
|
||||
export enum DownloadType {
|
||||
m3u8 = "m3u8",
|
||||
bilibili = "bilibili",
|
||||
}
|
||||
export interface DownloadParams {
|
||||
id: number;
|
||||
type: DownloadType;
|
||||
url: string;
|
||||
local: string;
|
||||
name: string;
|
||||
headers?: string;
|
||||
abortSignal: AbortController;
|
||||
proxy?: string;
|
||||
deleteSegments?: boolean;
|
||||
callback: (progress: DownloadProgress) => void;
|
||||
folder?: string;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import HomeController from "./controller/HomeController.ts";
|
||||
import { Container } from "inversify";
|
||||
import FavoriteRepository from "./repository/FavoriteRepository.ts";
|
||||
import ElectronApp from "./app.ts";
|
||||
import { Controller } from "./interfaces.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import RouterHandler from "./core/router.ts";
|
||||
import HomeService from "./services/HomeService.ts";
|
||||
import Logger from "./vendor/Logger.ts";
|
||||
import DownloadController from "./controller/DownloadController.ts";
|
||||
import VideoRepository from "./repository/VideoRepository.ts";
|
||||
import ConfigRepository from "./repository/ConfigRepository.ts";
|
||||
import ConfigService from "./services/ConfigService.ts";
|
||||
import DownloadService from "./services/DownloadService.ts";
|
||||
import SocketIO from "./vendor/SocketIO.ts";
|
||||
import DownloaderService from "./services/DownloaderService.ts";
|
||||
|
||||
const container = new Container({
|
||||
skipBaseClassChecks: true,
|
||||
defaultScope: "Singleton",
|
||||
autoBindInjectable: true,
|
||||
});
|
||||
|
||||
container
|
||||
.bind<ElectronApp>(TYPES.ElectronApp)
|
||||
.to(ElectronApp)
|
||||
.inSingletonScope();
|
||||
|
||||
// services
|
||||
container.bind<HomeService>(TYPES.HomeService).to(HomeService);
|
||||
container.bind<ConfigService>(TYPES.ConfigService).to(ConfigService);
|
||||
container.bind<DownloadService>(TYPES.DownloadService).to(DownloadService);
|
||||
container
|
||||
.bind<DownloaderService>(TYPES.DownloaderService)
|
||||
.to(DownloaderService);
|
||||
|
||||
// controller
|
||||
container.bind<Controller>(TYPES.Controller).to(HomeController);
|
||||
container.bind<Controller>(TYPES.Controller).to(DownloadController);
|
||||
|
||||
// repository
|
||||
container
|
||||
.bind<FavoriteRepository>(TYPES.FavoriteRepository)
|
||||
.to(FavoriteRepository);
|
||||
container.bind<VideoRepository>(TYPES.VideoRepository).to(VideoRepository);
|
||||
container.bind<ConfigRepository>(TYPES.ConfigRepository).to(ConfigRepository);
|
||||
|
||||
// vendor
|
||||
container.bind<TypeORM>(TYPES.TypeORM).to(TypeORM);
|
||||
container.bind<Logger>(TYPES.Logger).to(Logger);
|
||||
container.bind<SocketIO>(TYPES.SocketIO).to(SocketIO);
|
||||
|
||||
// core
|
||||
container
|
||||
.bind<RouterHandler>(TYPES.RouterHandlerService)
|
||||
.to(RouterHandler)
|
||||
.inSingletonScope();
|
||||
|
||||
export { container };
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Rectangle } from "electron";
|
||||
import { type DownloadType } from "interfaces";
|
||||
import { AppLanguage, AppTheme } from "./types";
|
||||
|
||||
declare interface EnvPath {
|
||||
binPath: string;
|
||||
dbPath: string;
|
||||
workspace: string;
|
||||
platform: string;
|
||||
local: string;
|
||||
}
|
||||
|
||||
declare interface BrowserWindowInitialVal {
|
||||
url?: string;
|
||||
sourceList?: WebSource[];
|
||||
}
|
||||
|
||||
declare interface WebSource {
|
||||
url: string;
|
||||
type: DownloadType;
|
||||
name: string;
|
||||
headers?: string;
|
||||
}
|
||||
|
||||
declare interface AppStore {
|
||||
// Local storage address
|
||||
local: string;
|
||||
// Download completion tone
|
||||
promptTone: boolean;
|
||||
// Proxy address
|
||||
proxy: string;
|
||||
// Whether to enable agent
|
||||
useProxy: boolean;
|
||||
// Delete the original file after downloading
|
||||
deleteSegments: boolean;
|
||||
// A new window opens the browser
|
||||
openInNewWindow: boolean;
|
||||
mainBounds?: Rectangle;
|
||||
browserBounds?: Rectangle;
|
||||
blockAds: boolean;
|
||||
// theme
|
||||
theme: AppTheme;
|
||||
// Using browser plugins
|
||||
useExtension: boolean;
|
||||
// Whether to use mobile UA
|
||||
isMobile: boolean;
|
||||
// Maximum number of simultaneous downloads
|
||||
maxRunner: number;
|
||||
// Language
|
||||
language: AppLanguage;
|
||||
// Show terminal or not
|
||||
showTerminal: boolean;
|
||||
// Privacy mode
|
||||
privacy: boolean;
|
||||
// Machine id
|
||||
machineId: string;
|
||||
// Download proxy Settings
|
||||
downloadProxySwitch: boolean;
|
||||
// Automatic update
|
||||
autoUpgrade: boolean;
|
||||
}
|
||||
|
||||
declare interface BrowserStore {
|
||||
url: string;
|
||||
sourceList: WebSource[];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { ConfigParams, TYPES } from "../types.ts";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
import { Repository } from "typeorm";
|
||||
import { Config } from "../entity/Config.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ConfigRepository {
|
||||
repository: Repository<Config>;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
) {
|
||||
this.repository = this.db.manager.getRepository(Config);
|
||||
}
|
||||
|
||||
async getConfig(): Promise<Config> {
|
||||
const config = await this.repository.findOneBy({ id: 1 });
|
||||
if (!config) {
|
||||
return await this.repository.save(new Config());
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
async setConfig({ key, val }: ConfigParams) {
|
||||
const config = await this.getConfig();
|
||||
this.repository.update(config.id, { [key]: val });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
|
||||
@injectable()
|
||||
export default class FavoriteRepository {
|
||||
constructor(
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
) {}
|
||||
|
||||
async findFavorites(): Promise<Favorite[]> {
|
||||
return await this.db.manager.find(Favorite, {
|
||||
order: {
|
||||
createdDate: "desc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addFavorite(favorite: Favorite): Promise<Favorite> {
|
||||
const exist = await this.db.manager.findOne(Favorite, {
|
||||
where: {
|
||||
url: favorite.url,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
throw new Error("网址已经存在");
|
||||
}
|
||||
|
||||
const item = new Favorite();
|
||||
item.title = favorite.title;
|
||||
item.url = favorite.url;
|
||||
item.icon = favorite.icon;
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
async removeFavorite(id: number): Promise<void> {
|
||||
await this.db.manager.getRepository(Favorite).delete(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Video } from "../entity/Video.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import {
|
||||
DownloadFilter,
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
DownloadStatus,
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import { In, Not } from "typeorm";
|
||||
import TypeORM from "../vendor/TypeORM.ts";
|
||||
import i18n from "../i18n/index.ts";
|
||||
|
||||
@injectable()
|
||||
export default class VideoRepository {
|
||||
constructor(
|
||||
@inject(TYPES.TypeORM)
|
||||
private readonly db: TypeORM,
|
||||
) {}
|
||||
|
||||
async addVideo(video: Omit<DownloadItem, "id">) {
|
||||
// Let's see if there's a video with the same name
|
||||
const exist = await this.findVideoByName(video.name);
|
||||
if (exist) {
|
||||
throw new Error(i18n.t("videoExistsPleaseChangeName"));
|
||||
}
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
item.type = video.type;
|
||||
video.headers && (item.headers = video.headers);
|
||||
video.folder && (item.folder = video.folder);
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
async addVideos(videos: Omit<DownloadItem, "id">[]) {
|
||||
// Check for videos with the same name
|
||||
const names = videos.map((item) => item.name);
|
||||
const existItems = await this.db.appDataSource
|
||||
.getRepository(Video)
|
||||
.findBy({ name: In(names) });
|
||||
if (existItems.length) {
|
||||
const existNames = existItems.map((item) => item.name);
|
||||
throw new Error(
|
||||
i18n.t("videoExistsPleaseChangeName") +
|
||||
"[" +
|
||||
existNames.join(", ") +
|
||||
"]",
|
||||
);
|
||||
}
|
||||
|
||||
const items = videos.map((video) => {
|
||||
const item = new Video();
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
item.type = video.type;
|
||||
video.headers && (item.headers = video.headers);
|
||||
video.folder && (item.folder = video.folder);
|
||||
return item;
|
||||
});
|
||||
return await this.db.manager.save(items);
|
||||
}
|
||||
|
||||
// Edit video
|
||||
async editVideo(video: DownloadItem) {
|
||||
const item = await this.db.appDataSource
|
||||
.getRepository(Video)
|
||||
.findOneBy({ id: video.id });
|
||||
if (!item) {
|
||||
throw new Error(i18n.t("videoNotExists"));
|
||||
}
|
||||
item.name = video.name;
|
||||
item.url = video.url;
|
||||
video.headers && (item.headers = video.headers);
|
||||
video.folder && (item.folder = video.folder);
|
||||
return await this.db.manager.save(item);
|
||||
}
|
||||
|
||||
// Find all Videos
|
||||
async findAllVideos() {
|
||||
return await this.db.appDataSource.getRepository(Video).find({
|
||||
order: {
|
||||
createdDate: "DESC",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findVideos(pagination: DownloadItemPagination) {
|
||||
const {
|
||||
current = 0,
|
||||
pageSize = 50,
|
||||
filter = DownloadFilter.list,
|
||||
} = pagination;
|
||||
const filterCondition =
|
||||
filter === DownloadFilter.done
|
||||
? DownloadStatus.Success
|
||||
: Not(DownloadStatus.Success);
|
||||
|
||||
const [items, count] = await this.db.appDataSource
|
||||
.getRepository(Video)
|
||||
.findAndCount({
|
||||
where: {
|
||||
status: filterCondition,
|
||||
},
|
||||
order: {
|
||||
createdDate: "ASC",
|
||||
},
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
return {
|
||||
total: count,
|
||||
list: items,
|
||||
};
|
||||
}
|
||||
|
||||
async findVideo(id: number) {
|
||||
const repository = this.db.appDataSource.getRepository(Video);
|
||||
const video = await repository.findOneBy({ id });
|
||||
|
||||
if (!video) {
|
||||
throw new Error(i18n.t("videoNotExists"));
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
async findVideoByName(name: string) {
|
||||
return this.db.appDataSource.getRepository(Video).findOneBy({
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
async changeVideoStatus(id: number | number[], status: DownloadStatus) {
|
||||
const ids = !Array.isArray(id) ? [id] : id;
|
||||
const videoRepository = this.db.appDataSource.getRepository(Video);
|
||||
const videos = await videoRepository.findBy({ id: In(ids) });
|
||||
for (const video of videos) {
|
||||
video.status = status;
|
||||
}
|
||||
await videoRepository.save(videos);
|
||||
}
|
||||
|
||||
async changeVideoIsLive(id: number) {
|
||||
const video = await this.findVideo(id);
|
||||
video.isLive = true;
|
||||
return this.db.manager.save(video);
|
||||
}
|
||||
|
||||
async findWattingAndDownloadingVideos() {
|
||||
return await this.db.appDataSource.getRepository(Video).find({
|
||||
where: {
|
||||
status: In([DownloadStatus.Downloading, DownloadStatus.Watting]),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDownloadItem(id: number) {
|
||||
return await this.db.appDataSource.getRepository(Video).delete(id);
|
||||
}
|
||||
|
||||
async findVideoByUrl(url: string) {
|
||||
return this.db.appDataSource.getRepository(Video).findOneBy({
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
async appendDownloadLog(id: number, message: string) {
|
||||
const video = await this.findVideo(id);
|
||||
video.log = video.log ? `${video.log}\n${message}` : message;
|
||||
return await this.db.manager.save(video);
|
||||
}
|
||||
|
||||
async getDownloadLog(id: number) {
|
||||
const video = await this.findVideo(id);
|
||||
return video.log;
|
||||
}
|
||||
|
||||
async getVideoFolders() {
|
||||
const videos = await this.db.appDataSource.getRepository(Video).find();
|
||||
const folders = new Set<string>();
|
||||
for (const video of videos) {
|
||||
if (video.folder) {
|
||||
folders.add(video.folder);
|
||||
}
|
||||
}
|
||||
return Array.from(folders);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { ConfigParams, TYPES } from "../types.ts";
|
||||
import ConfigRepository from "../repository/ConfigRepository.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ConfigService {
|
||||
constructor(
|
||||
@inject(TYPES.ConfigRepository)
|
||||
private readonly configRepository: ConfigRepository,
|
||||
) {}
|
||||
|
||||
async getConfig() {
|
||||
return await this.configRepository.getConfig();
|
||||
}
|
||||
|
||||
async setConfig({ key, val }: ConfigParams) {
|
||||
return await this.configRepository.setConfig({ key, val });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
import EventEmitter from "events";
|
||||
import { inject, injectable } from "inversify";
|
||||
import {
|
||||
DownloadParams,
|
||||
DownloadProgress,
|
||||
DownloadStatus,
|
||||
Task,
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import Logger from "../vendor/Logger.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import {
|
||||
Platform,
|
||||
biliDownloaderBin,
|
||||
m3u8DownloaderBin,
|
||||
} from "../helper/index.ts";
|
||||
import * as pty from "node-pty";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import i18n from "../i18n/index.ts";
|
||||
import ConfigService from "./ConfigService.ts";
|
||||
import path from "path";
|
||||
|
||||
interface DownloadContext {
|
||||
// Whether it is live
|
||||
isLive: boolean;
|
||||
// Download progress
|
||||
percent: string;
|
||||
// Download speed
|
||||
speed: string;
|
||||
// Ready
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadOptions {
|
||||
abortSignal: AbortController;
|
||||
encoding?: string;
|
||||
onMessage?: (ctx: DownloadContext, message: string) => void;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface Schema {
|
||||
args: Record<string, { argsName: string[] | null }>;
|
||||
consoleReg: {
|
||||
percent: string;
|
||||
speed: string;
|
||||
error: string;
|
||||
start: string;
|
||||
isLive: string;
|
||||
};
|
||||
bin: string;
|
||||
platform: string[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
// FIXME: Multilingual regular expressions
|
||||
const processList: Schema[] = [
|
||||
{
|
||||
type: "m3u8",
|
||||
platform: [Platform.MacOS, Platform.Linux, Platform.Windows],
|
||||
bin: m3u8DownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
argsName: null,
|
||||
},
|
||||
localDir: {
|
||||
argsName: ["--tmp-dir", "--save-dir"],
|
||||
},
|
||||
name: {
|
||||
argsName: ["--save-name"],
|
||||
},
|
||||
headers: {
|
||||
argsName: ["--header"],
|
||||
},
|
||||
deleteSegments: {
|
||||
argsName: ["--del-after-done"],
|
||||
},
|
||||
proxy: {
|
||||
argsName: ["--custom-proxy"],
|
||||
},
|
||||
__common__: {
|
||||
argsName: [
|
||||
"--no-log",
|
||||
"--auto-select",
|
||||
"--ui-language",
|
||||
"zh-CN",
|
||||
"--live-real-time-merge",
|
||||
],
|
||||
},
|
||||
},
|
||||
consoleReg: {
|
||||
percent: "([\\d.]+)%",
|
||||
speed: "([\\d.]+[GMK]Bps)",
|
||||
error: "ERROR",
|
||||
start: "保存文件名:",
|
||||
isLive: "检测到直播流",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "bilibili",
|
||||
platform: [Platform.Linux, Platform.MacOS, Platform.Windows],
|
||||
bin: biliDownloaderBin,
|
||||
args: {
|
||||
url: {
|
||||
argsName: null,
|
||||
},
|
||||
localDir: {
|
||||
argsName: ["--work-dir"],
|
||||
},
|
||||
name: {
|
||||
argsName: ["--file-pattern"],
|
||||
},
|
||||
},
|
||||
consoleReg: {
|
||||
speed: "([\\d.]+\\s[GMK]B/s)",
|
||||
percent: "([\\d.]+)%",
|
||||
error: "ERROR",
|
||||
start: "开始下载",
|
||||
isLive: "检测到直播流",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@injectable()
|
||||
export default class DownloadService extends EventEmitter {
|
||||
private queue: Task[] = [];
|
||||
|
||||
private active: Task[] = [];
|
||||
|
||||
private limit: number = 2;
|
||||
|
||||
private signal: Record<number, AbortController> = {};
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.Logger)
|
||||
private readonly logger: Logger,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.ConfigService)
|
||||
private readonly storeService: ConfigService,
|
||||
) {
|
||||
super();
|
||||
|
||||
// this.storeService.getConfig().then(({ maxRunner }) => {
|
||||
// this.limit = maxRunner;
|
||||
// });
|
||||
}
|
||||
|
||||
async addTask(task: Task) {
|
||||
this.queue.push(task);
|
||||
this.runTask();
|
||||
}
|
||||
|
||||
async stopTask(id: number) {
|
||||
if (this.signal[id]) {
|
||||
this.signal[id].abort();
|
||||
}
|
||||
}
|
||||
|
||||
async execute(task: Task) {
|
||||
try {
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Downloading,
|
||||
);
|
||||
this.emit("download-start", task.id);
|
||||
|
||||
this.logger.info(`taskId: ${task.id} start`);
|
||||
const controller = new AbortController();
|
||||
this.signal[task.id] = controller;
|
||||
|
||||
const callback = (progress: DownloadProgress) => {
|
||||
if (progress.type === "progress") {
|
||||
this.emit("download-progress", progress);
|
||||
} else if (progress.type === "ready") {
|
||||
this.emit("download-ready-start", progress);
|
||||
if (progress.isLive) {
|
||||
this.removeTask(progress.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const params: DownloadParams = {
|
||||
...task.params,
|
||||
id: task.id,
|
||||
abortSignal: controller,
|
||||
callback,
|
||||
};
|
||||
|
||||
const { proxy, downloadProxySwitch } =
|
||||
await this.storeService.getConfig();
|
||||
if (downloadProxySwitch && proxy) {
|
||||
params.proxy = proxy;
|
||||
}
|
||||
|
||||
await this.process(params);
|
||||
delete this.signal[task.id];
|
||||
this.logger.info(`taskId: ${task.id} success`);
|
||||
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Success,
|
||||
);
|
||||
this.emit("download-success", task.id);
|
||||
} catch (err: any) {
|
||||
if (err.message === "AbortError") {
|
||||
this.logger.info(`taskId: ${task.id} stopped`);
|
||||
// Download pause
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Stopped,
|
||||
);
|
||||
this.emit("download-stop", task.id);
|
||||
} else {
|
||||
this.logger.error(`taskId: ${task.id} failed`, err);
|
||||
// Download failure
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
task.id,
|
||||
DownloadStatus.Failed,
|
||||
);
|
||||
this.emit("download-failed", task.id, err);
|
||||
}
|
||||
} finally {
|
||||
this.removeTask(task.id);
|
||||
|
||||
// Transmission complete
|
||||
if (this.queue.length === 0 && this.active.length === 0) {
|
||||
// this.emit("download-finish");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeTask(id: number) {
|
||||
// Process the currently active task
|
||||
const doneId = this.active.findIndex((i) => i.id === id);
|
||||
this.active.splice(doneId, 1);
|
||||
// Process completed tasks
|
||||
if (this.active.length < this.limit) {
|
||||
this.runTask();
|
||||
}
|
||||
}
|
||||
|
||||
runTask() {
|
||||
while (this.active.length < this.limit && this.queue.length > 0) {
|
||||
const task = this.queue.shift();
|
||||
if (task) {
|
||||
this.active.push(task);
|
||||
this.execute(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _execa(
|
||||
binPath: string,
|
||||
args: string[],
|
||||
params: DownloadOptions,
|
||||
): Promise<void> {
|
||||
const { abortSignal, onMessage, id } = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const ptyProcess = pty.spawn(binPath, args, {
|
||||
name: "xterm-color",
|
||||
cols: 500,
|
||||
rows: 500,
|
||||
useConpty: false,
|
||||
});
|
||||
|
||||
if (onMessage) {
|
||||
const ctx: DownloadContext = {
|
||||
ready: false,
|
||||
isLive: false,
|
||||
percent: "",
|
||||
speed: "",
|
||||
};
|
||||
ptyProcess.onData((data) => {
|
||||
try {
|
||||
const txt = stripAnsi(data);
|
||||
this.emit("download-message", id, data);
|
||||
onMessage(ctx, txt);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
abortSignal.signal.addEventListener("abort", () => {
|
||||
ptyProcess.kill();
|
||||
reject(new Error("AbortError"));
|
||||
});
|
||||
|
||||
ptyProcess.onExit(({ exitCode }) => {
|
||||
if (exitCode === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(i18n.t("unknownError")));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async downloader(params: DownloadParams, schema: Schema): Promise<void> {
|
||||
const {
|
||||
id,
|
||||
abortSignal,
|
||||
url,
|
||||
local,
|
||||
name,
|
||||
deleteSegments,
|
||||
headers,
|
||||
callback,
|
||||
proxy,
|
||||
folder,
|
||||
} = params;
|
||||
|
||||
const spawnParams = [];
|
||||
for (const key of Object.keys(schema.args)) {
|
||||
const { argsName } = schema.args[key];
|
||||
if (key === "url") {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
spawnParams.push(url);
|
||||
}
|
||||
if (key === "localDir") {
|
||||
let finalLocal = local;
|
||||
if (folder) {
|
||||
finalLocal = path.join(local, folder);
|
||||
}
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, finalLocal));
|
||||
}
|
||||
if (key === "name") {
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, name));
|
||||
}
|
||||
|
||||
if (key === "headers" && headers) {
|
||||
const headerArr: string[] = headers?.split("\n") || [];
|
||||
headerArr.forEach((headerStr) => {
|
||||
spawnParams.push("--header", headerStr);
|
||||
});
|
||||
}
|
||||
|
||||
if (key === "deleteSegments" && deleteSegments) {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
}
|
||||
|
||||
if (key === "proxy" && proxy) {
|
||||
argsName && argsName.forEach((i) => spawnParams.push(i, proxy));
|
||||
}
|
||||
|
||||
if (key === "__common__") {
|
||||
argsName && spawnParams.push(...argsName);
|
||||
}
|
||||
}
|
||||
|
||||
const { consoleReg } = schema;
|
||||
const isLiveReg = RegExp(consoleReg.isLive, "g");
|
||||
const startDownloadReg = RegExp(consoleReg.start, "g");
|
||||
const errorReg = RegExp(consoleReg.error, "g");
|
||||
const speedReg = RegExp(consoleReg.speed, "g");
|
||||
const percentReg = RegExp(consoleReg.percent, "g");
|
||||
|
||||
const onMessage = (ctx: DownloadContext, message: string) => {
|
||||
// Resolve whether it is a live resource
|
||||
if (isLiveReg.test(message)) {
|
||||
ctx.isLive = true;
|
||||
}
|
||||
// Parse download progress
|
||||
const [, percent] = percentReg.exec(message) || [];
|
||||
if (percent && Number(ctx.percent || 0) < Number(percent)) {
|
||||
ctx.percent = percent;
|
||||
}
|
||||
// Parsing download speed
|
||||
const [, speed] = speedReg.exec(message) || [];
|
||||
if (speed) {
|
||||
ctx.speed = speed;
|
||||
}
|
||||
|
||||
if (startDownloadReg.test(message)) {
|
||||
callback({
|
||||
id,
|
||||
type: "ready",
|
||||
isLive: ctx.isLive,
|
||||
speed: "",
|
||||
percent: "",
|
||||
});
|
||||
ctx.ready = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorReg.test(message)) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (ctx.ready && (ctx.percent || ctx.speed)) {
|
||||
callback({
|
||||
id,
|
||||
type: "progress",
|
||||
percent: ctx.percent || "",
|
||||
speed: ctx.speed || "",
|
||||
isLive: ctx.isLive,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.logger.debug("download params: ", spawnParams.join(" "));
|
||||
await this._execa(schema.bin, spawnParams, {
|
||||
id,
|
||||
abortSignal,
|
||||
onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
async process(params: DownloadParams): Promise<void> {
|
||||
const program = processList
|
||||
.filter((i) => i.platform.includes(process.platform))
|
||||
.filter((i) => i.type === params.type);
|
||||
|
||||
if (program.length === 0) {
|
||||
return Promise.reject(new Error(i18n.t("unsupportedDownloadType")));
|
||||
}
|
||||
|
||||
const schema = program[0];
|
||||
await this.downloader(params, schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import ConfigRepository from "../repository/ConfigRepository.ts";
|
||||
import { DownloadItem, DownloadStatus, Task } from "../interfaces.ts";
|
||||
import DownloadService from "./DownloadService.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DownloaderService {
|
||||
constructor(
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.ConfigRepository)
|
||||
private readonly store: ConfigRepository,
|
||||
@inject(TYPES.DownloadService)
|
||||
private readonly downloadService: DownloadService,
|
||||
) {}
|
||||
|
||||
async startDownload(vid: number) {
|
||||
// Find the video you want to download
|
||||
const video = await this.videoRepository.findVideo(vid);
|
||||
const { name, url, headers, type, folder } = video;
|
||||
const { local, deleteSegments } = await this.store.getConfig();
|
||||
|
||||
const task: Task = {
|
||||
id: vid,
|
||||
params: {
|
||||
url,
|
||||
type,
|
||||
local,
|
||||
name,
|
||||
headers,
|
||||
deleteSegments,
|
||||
folder,
|
||||
},
|
||||
};
|
||||
await this.videoRepository.changeVideoStatus(vid, DownloadStatus.Watting);
|
||||
this.downloadService.addTask(task);
|
||||
}
|
||||
|
||||
async downloadNow(video: Omit<DownloadItem, "id">) {
|
||||
// Add download
|
||||
const item = await this.addDownloadItem(video);
|
||||
// Start downloading
|
||||
await this.startDownload(item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
async addDownloadItem(video: Omit<DownloadItem, "id">) {
|
||||
const item = await this.videoRepository.addVideo(video);
|
||||
return item;
|
||||
}
|
||||
|
||||
async addDownloadItems(videos: Omit<DownloadItem, "id">[]) {
|
||||
const items = await this.videoRepository.addVideos(videos);
|
||||
return items;
|
||||
}
|
||||
|
||||
async editDownloadItem(video: DownloadItem) {
|
||||
return this.videoRepository.editVideo(video);
|
||||
}
|
||||
|
||||
async getVideoFolders() {
|
||||
return this.videoRepository.getVideoFolders();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import EventEmitter from "events";
|
||||
import { injectable } from "inversify";
|
||||
|
||||
@injectable()
|
||||
export default class HomeService extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Config } from "./entity/Config.ts";
|
||||
|
||||
export const TYPES = {
|
||||
ElectronApp: Symbol.for("ElectronApp"),
|
||||
RouterHandlerService: Symbol.for("RouterHandlerService"),
|
||||
Controller: Symbol.for("Controller"),
|
||||
// repository
|
||||
VideoRepository: Symbol.for("VideoRepository"),
|
||||
FavoriteRepository: Symbol.for("FavoriteRepository"),
|
||||
ConversionRepository: Symbol.for("ConversionRepository"),
|
||||
ConfigRepository: Symbol.for("ConfigRepository"),
|
||||
// windows
|
||||
BrowserWindow: Symbol.for("BrowserWindow"),
|
||||
MainWindow: Symbol.for("MainWindow"),
|
||||
PlayerWindow: Symbol.for("PlayerWindow"),
|
||||
// services
|
||||
HomeService: Symbol.for("HomeService"),
|
||||
DownloadService: Symbol.for("DownloadService"),
|
||||
SessionService: Symbol.for("SessionService"),
|
||||
ProtocolService: Symbol.for("ProtocolService"),
|
||||
VideoService: Symbol.for("VideoService"),
|
||||
SniffingHelper: Symbol.for("SniffingHelper"),
|
||||
ConfigService: Symbol.for("ConfigService"),
|
||||
DownloaderService: Symbol.for("DownloaderService"),
|
||||
// vendor
|
||||
TypeORM: Symbol.for("TypeORM"),
|
||||
Logger: Symbol.for("Logger"),
|
||||
SocketIO: Symbol.for("SocketIO"),
|
||||
};
|
||||
|
||||
export enum AppTheme {
|
||||
System = "system",
|
||||
Light = "light",
|
||||
Dark = "dark",
|
||||
}
|
||||
|
||||
export enum AppLanguage {
|
||||
System = "system",
|
||||
ZH = "zh",
|
||||
EN = "en",
|
||||
}
|
||||
|
||||
export interface ConfigParams {
|
||||
key: keyof Config;
|
||||
val: Config[keyof Config];
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { injectable } from "inversify";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import winston from "winston";
|
||||
|
||||
@injectable()
|
||||
export default class Logger implements Vendor {
|
||||
logger: winston.Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.json(),
|
||||
defaultMeta: { service: "user-service" },
|
||||
transports: [
|
||||
//
|
||||
// - Write all logs with importance level of `error` or less to `error.log`
|
||||
// - Write all logs with importance level of `info` or less to `combined.log`
|
||||
//
|
||||
new winston.transports.File({
|
||||
filename: "./log/error.log",
|
||||
level: "error",
|
||||
}),
|
||||
new winston.transports.File({ filename: "./log/combined.log" }),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// If we're not in production then log to the `console` with the format:
|
||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||
//
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
this.logger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
info(...args: unknown[]) {
|
||||
return this.logger.info(args);
|
||||
}
|
||||
|
||||
warn(...args: unknown[]) {
|
||||
return this.logger.warn(args);
|
||||
}
|
||||
|
||||
error(...args: unknown[]) {
|
||||
return this.logger.error(args);
|
||||
}
|
||||
|
||||
debug(...args: unknown[]) {
|
||||
return this.logger.debug(args);
|
||||
}
|
||||
|
||||
async init() {}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import http from "http";
|
||||
import { Server } from "socket.io";
|
||||
import { TYPES } from "../types.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import { DownloadProgress } from "../interfaces.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import Logger from "./Logger.ts";
|
||||
import ConfigService from "../services/ConfigService.ts";
|
||||
|
||||
@injectable()
|
||||
export default class SocketIO implements Vendor {
|
||||
io: Server;
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.DownloadService)
|
||||
private readonly downloadService: DownloadService,
|
||||
@inject(TYPES.VideoRepository)
|
||||
private readonly videoRepository: VideoRepository,
|
||||
@inject(TYPES.Logger)
|
||||
private readonly logger: Logger,
|
||||
@inject(TYPES.ConfigService)
|
||||
private readonly store: ConfigService,
|
||||
) {}
|
||||
|
||||
async init() {}
|
||||
|
||||
async initSocketIO(server: http.Server) {
|
||||
this.io = new Server(server, {
|
||||
/* options */
|
||||
cors: {
|
||||
origin: "*",
|
||||
},
|
||||
});
|
||||
|
||||
this.io.engine.on("headers", (headers) => {
|
||||
headers["Access-Control-Allow-Private-Network"] = true;
|
||||
});
|
||||
|
||||
this.io.on("connection", () => {});
|
||||
|
||||
this.downloadService.on("download-ready-start", this.onDownloadReadyStart);
|
||||
this.downloadService.on("download-progress", this.onDownloadProgress);
|
||||
this.downloadService.on("download-success", this.onDownloadSuccess);
|
||||
this.downloadService.on("download-failed", this.onDownloadFailed);
|
||||
this.downloadService.on("download-start", this.onDownloadStart);
|
||||
this.downloadService.on("download-stop", this.onDownloadStop);
|
||||
this.downloadService.on("download-message", this.receiveMessage);
|
||||
}
|
||||
|
||||
async emit(event: string, ...data: any[]) {
|
||||
this.io.emit(event, {}, ...data);
|
||||
}
|
||||
|
||||
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
|
||||
if (isLive) {
|
||||
await this.videoRepository.changeVideoIsLive(id);
|
||||
this.emit("change-video-is-live", { id });
|
||||
}
|
||||
};
|
||||
|
||||
onDownloadProgress = (progress: DownloadProgress) => {
|
||||
this.emit("download-progress", progress);
|
||||
};
|
||||
|
||||
onDownloadSuccess = async (id: number) => {
|
||||
this.emit("download-success", id);
|
||||
};
|
||||
|
||||
onDownloadFailed = async (id: number, err: unknown) => {
|
||||
this.logger.error("download failed: ", err);
|
||||
this.emit("download-failed", id);
|
||||
};
|
||||
|
||||
onDownloadStart = async (id: number) => {
|
||||
this.emit("download-start", id);
|
||||
};
|
||||
|
||||
onDownloadStop = async (id: number) => {
|
||||
this.emit("download-stop", id);
|
||||
};
|
||||
|
||||
receiveMessage = async (id: number, message: string) => {
|
||||
// Write the log to the database
|
||||
await this.videoRepository.appendDownloadLog(id, message);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { injectable } from "inversify";
|
||||
import { DataSource, EntityManager } from "typeorm";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import { Vendor } from "../core/vendor.ts";
|
||||
import { Video } from "../entity/Video.ts";
|
||||
import { Config } from "../entity/Config.ts";
|
||||
import { DB_PATH } from "../const.ts";
|
||||
|
||||
@injectable()
|
||||
export default class DatabaseService implements Vendor {
|
||||
appDataSource: DataSource;
|
||||
|
||||
constructor() {
|
||||
this.appDataSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
database: DB_PATH,
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [Favorite, Video, Config],
|
||||
migrations: [],
|
||||
subscribers: [],
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.appDataSource.initialize();
|
||||
}
|
||||
|
||||
get manager(): EntityManager {
|
||||
return this.appDataSource.manager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"strictPropertyInitialization": false,
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./types",
|
||||
"outDir": "app/output/build/main",
|
||||
"allowImportingTsExtensions": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ES2015",
|
||||
"lib": ["ES2015"],
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
},
|
||||
"include": ["gulpfile.ts", "scripts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Main Process
|
||||
@@ -1,15 +1,25 @@
|
||||
{
|
||||
"name": "mediago",
|
||||
"version": "2.2.3",
|
||||
"version": "3.0.1",
|
||||
"description": "在线视频下载器",
|
||||
"main": "main/index.js",
|
||||
"author": "caorushizi",
|
||||
"homepage": "https://downloader.caorushizi.cn/",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "^1.27.3",
|
||||
"@cliqz/adblocker-electron-preload": "^1.27.3",
|
||||
"node-pty": "^1.0.0"
|
||||
"@ghostery/adblocker": "^2.3.1",
|
||||
"@ghostery/adblocker-content": "^2.3.1",
|
||||
"@ghostery/adblocker-electron": "^2.3.1",
|
||||
"@ghostery/adblocker-electron-preload": "^2.3.1",
|
||||
"@ghostery/adblocker-extended-selectors": "^2.3.1",
|
||||
"@remusao/guess-url-type": "^1.3.0",
|
||||
"@remusao/small": "^1.3.0",
|
||||
"@remusao/smaz": "^1.10.0",
|
||||
"@remusao/smaz-compress": "^1.10.0",
|
||||
"@remusao/smaz-decompress": "^1.10.0",
|
||||
"@remusao/trie": "^1.5.0",
|
||||
"node-pty": "^1.0.0",
|
||||
"tldts-experimental": "^6.1.70"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "30.0.8"
|
||||
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,3 @@
|
||||
provider: generic
|
||||
updaterCacheDirName: mediago-updater
|
||||
url: "http://127.0.0.1:9000/release"
|
||||
@@ -10,6 +10,7 @@
|
||||
"pack": "cross-env NODE_ENV=development gulp release",
|
||||
"build": "cross-env NODE_ENV=production gulp build",
|
||||
"release": "cross-env NODE_ENV=production gulp release",
|
||||
"rebuild": "electron-rebuild -f -w",
|
||||
"types": "tsc",
|
||||
"types:watch": "tsc -w",
|
||||
"lint-staged": "lint-staged"
|
||||
@@ -21,8 +22,10 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@eslint/js": "^8.57.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/electron-devtools-installer": "^2.2.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/gulp": "^4.0.17",
|
||||
@@ -31,6 +34,8 @@
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/serve-handler": "^6.1.4",
|
||||
"@types/undertaker": "^1.2.11",
|
||||
"consola": "^3.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"del": "^7.1.0",
|
||||
@@ -40,7 +45,7 @@
|
||||
"electron-updater": "^6.1.8",
|
||||
"esbuild": "^0.21.4",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^9.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"glob": "^10.4.1",
|
||||
"globals": "^15.3.0",
|
||||
"gulp": "^5.0.0",
|
||||
@@ -50,27 +55,31 @@
|
||||
"typescript-eslint": "^7.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cliqz/adblocker-electron": "1.27.3",
|
||||
"@cliqz/adblocker-electron-preload": "1.27.3",
|
||||
"@ghostery/adblocker-electron": "^2.3.1",
|
||||
"axios": "^1.7.2",
|
||||
"better-sqlite3": "^10.0.0",
|
||||
"better-sqlite3": "^9.6.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.11",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-is-dev": "^3.0.1",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^9.0.0",
|
||||
"execa": "^9.1.0",
|
||||
"express": "^4.19.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"i18next": "^23.11.5",
|
||||
"inversify": "^6.0.2",
|
||||
"lint-staged": "^15.2.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"node-pty": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"serve-handler": "^6.1.5",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"typeorm": "0.3.20"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ const external = [
|
||||
"nock",
|
||||
"aws-sdk",
|
||||
"mock-aws-s3",
|
||||
"@cliqz/adblocker-electron-preload",
|
||||
"@ghostery/adblocker-electron",
|
||||
"tldts-experimental",
|
||||
"node-pty",
|
||||
];
|
||||
|
||||
@@ -39,7 +40,7 @@ function getConfig(): esbuild.BuildOptions {
|
||||
function buildOptions(
|
||||
entry: string,
|
||||
platform: esbuild.Platform,
|
||||
target: string
|
||||
target: string,
|
||||
): esbuild.BuildOptions {
|
||||
return {
|
||||
...getConfig(),
|
||||
@@ -63,7 +64,8 @@ export function getReleaseConfig(): Configuration {
|
||||
buildVersion: process.env.APP_VERSION,
|
||||
appId: process.env.APP_ID,
|
||||
copyright: process.env.APP_COPYRIGHT,
|
||||
artifactName: "${productName}-setup-${arch}-${buildVersion}.${ext}",
|
||||
artifactName:
|
||||
"${productName}-setup-${platform}-${arch}-${buildVersion}.${ext}",
|
||||
npmRebuild: true,
|
||||
directories: {
|
||||
output: "./release",
|
||||
@@ -81,7 +83,11 @@ export function getReleaseConfig(): Configuration {
|
||||
to: "plugin",
|
||||
},
|
||||
{
|
||||
from: "./app/bin/",
|
||||
from: "./app/mobile",
|
||||
to: "mobile",
|
||||
},
|
||||
{
|
||||
from: "./bin/${platform}/${arch}",
|
||||
to: "bin",
|
||||
},
|
||||
],
|
||||
@@ -92,8 +98,13 @@ export function getReleaseConfig(): Configuration {
|
||||
target: "nsis",
|
||||
arch: ["x64"],
|
||||
},
|
||||
"portable",
|
||||
],
|
||||
},
|
||||
portable: {
|
||||
artifactName:
|
||||
"${productName}-portable-${platform}-${arch}-${buildVersion}.${ext}",
|
||||
},
|
||||
dmg: {
|
||||
contents: [
|
||||
{
|
||||
@@ -114,7 +125,7 @@ export function getReleaseConfig(): Configuration {
|
||||
target: [
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["x64", "arm64"],
|
||||
arch: [process.arch === "arm64" ? "arm64" : "x64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import pkg from "../app/package.json";
|
||||
import * as builder from "electron-builder";
|
||||
import { globSync } from "glob";
|
||||
import fs from "fs";
|
||||
import { exec } from "child_process";
|
||||
|
||||
const env = Env.getInstance();
|
||||
env.loadDotEnvRuntime();
|
||||
@@ -17,10 +18,25 @@ async function clean() {
|
||||
return deleteSync([
|
||||
mainResolve("app/build"),
|
||||
mainResolve("app/bin"),
|
||||
mainResolve("app/mobile"),
|
||||
mainResolve("app/plugin"),
|
||||
mainResolve("release"),
|
||||
]);
|
||||
}
|
||||
|
||||
function compileTypescript() {
|
||||
exec("npx tsc", (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`Stderr: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function copySqlite() {
|
||||
const path = "build/Release/better_sqlite3.node";
|
||||
const source = mainResolve("node_modules/better-sqlite3", path);
|
||||
@@ -29,13 +45,13 @@ async function copySqlite() {
|
||||
}
|
||||
|
||||
async function copyBin() {
|
||||
const source = mainResolve("bin", process.platform);
|
||||
const source = mainResolve("bin", process.platform, process.arch);
|
||||
const target = mainResolve("app/bin");
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
async function chmodBin() {
|
||||
// 遍历文件夹下的所有文件,将文件的权限设置为 777
|
||||
// Go through all the files in the folder and set the permissions on the files to 777
|
||||
if (isWin) return;
|
||||
|
||||
const files = globSync(mainResolve("app/bin/*"));
|
||||
@@ -44,7 +60,14 @@ async function chmodBin() {
|
||||
}
|
||||
}
|
||||
|
||||
const copy = gulp.parallel(copyBin, copySqlite);
|
||||
async function copyUpdateConfig() {
|
||||
const source = mainResolve("dev-app-update.yml");
|
||||
const target = mainResolve("app/build/main/dev-app-update.yml");
|
||||
fs.cpSync(source, target, { recursive: true });
|
||||
}
|
||||
|
||||
const devCopy = gulp.parallel(copyBin, copySqlite, copyUpdateConfig);
|
||||
const buildCopy = gulp.parallel(copySqlite);
|
||||
|
||||
async function watchTask() {
|
||||
const app = new ElectronApp();
|
||||
@@ -59,6 +82,8 @@ async function watchTask() {
|
||||
app.start();
|
||||
})
|
||||
.on("change", async () => {
|
||||
compileTypescript();
|
||||
|
||||
await main.rebuild();
|
||||
await preload.rebuild();
|
||||
app.restart();
|
||||
@@ -90,9 +115,9 @@ async function pack() {
|
||||
await builder.build({ config });
|
||||
}
|
||||
|
||||
// 开发环境
|
||||
export const dev = gulp.series(clean, copy, chmodBin, watchTask);
|
||||
// 构建打包
|
||||
export const build = gulp.series(clean, copy, chmodBin, buildTask);
|
||||
// Development environment
|
||||
export const dev = gulp.series(devCopy, chmodBin, watchTask);
|
||||
// Build packaging
|
||||
export const build = gulp.series(clean, buildCopy, chmodBin, buildTask);
|
||||
// release
|
||||
export const release = gulp.series(pack);
|
||||
|
||||
@@ -5,7 +5,7 @@ import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import electron from "electron";
|
||||
|
||||
const baseResolve = (...r) => resolve(__dirname, ...r);
|
||||
const baseResolve = (...r: any[]) => resolve(__dirname, ...r);
|
||||
export const mainResolve = (...r: string[]) => baseResolve("..", ...r);
|
||||
export const rootResolve = (...r: string[]) => baseResolve("../../..", ...r);
|
||||
|
||||
@@ -81,7 +81,7 @@ export class Env {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.keys(parsed).reduce((prev, curr) => {
|
||||
return Object.keys(parsed).reduce((prev: any, curr) => {
|
||||
prev[curr] = parsed[curr];
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { DownloadStatus } from "./interfaces.ts";
|
||||
import { TYPES } from "./types.ts";
|
||||
import { Menu, Tray, app, nativeImage, nativeTheme } from "electron";
|
||||
import { Menu, Tray, app, nativeImage, nativeTheme, Event } from "electron";
|
||||
import TrayIcon from "./tray-icon.png";
|
||||
import TrayIconLight from "./tray-icon-light.png";
|
||||
import path from "path";
|
||||
import MainWindow from "./windows/MainWindow.ts";
|
||||
import WebviewService from "./services/WebviewService.ts";
|
||||
@@ -13,6 +14,9 @@ import ElectronUpdater from "./vendor/ElectronUpdater.ts";
|
||||
import TypeORM from "./vendor/TypeORM.ts";
|
||||
import ProtocolService from "./core/protocol.ts";
|
||||
import IpcHandlerService from "./core/ipc.ts";
|
||||
import { VideoService } from "./services/VideoService.ts";
|
||||
import i18n from "./i18n/index.ts";
|
||||
import { isMac } from "./helper/variables.ts";
|
||||
|
||||
@injectable()
|
||||
export default class ElectronApp {
|
||||
@@ -34,11 +38,14 @@ export default class ElectronApp {
|
||||
@inject(TYPES.ElectronDevtools)
|
||||
private readonly devTools: ElectronDevtools,
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.VideoService)
|
||||
private readonly videoService: VideoService,
|
||||
) {}
|
||||
|
||||
private async serviceInit(): Promise<void> {
|
||||
this.mainWindow.init();
|
||||
this.videoService.init();
|
||||
}
|
||||
|
||||
private async vendorInit() {
|
||||
@@ -61,6 +68,7 @@ export default class ElectronApp {
|
||||
});
|
||||
|
||||
this.initAppTheme();
|
||||
this.initLanguage();
|
||||
this.resetDownloadStatus();
|
||||
|
||||
this.initTray();
|
||||
@@ -71,29 +79,45 @@ export default class ElectronApp {
|
||||
nativeTheme.themeSource = theme;
|
||||
}
|
||||
|
||||
initLanguage(): void {
|
||||
const language = this.store.get("language");
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
|
||||
initTray() {
|
||||
const iconPath = path.resolve(__dirname, TrayIcon);
|
||||
const iconPath = path.resolve(__dirname, isMac ? TrayIconLight : TrayIcon);
|
||||
const icon = nativeImage.createFromPath(iconPath);
|
||||
const tray = new Tray(icon);
|
||||
tray.setToolTip("在线视频下载");
|
||||
tray.setToolTip("Media Go");
|
||||
tray.addListener("click", () => {
|
||||
this.mainWindow.init();
|
||||
});
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: "显示主窗口", click: () => this.mainWindow.init() },
|
||||
{ label: "退出 app", role: "quit" },
|
||||
{
|
||||
label: i18n.t("showMainWindow"),
|
||||
click: () => this.mainWindow.init(),
|
||||
},
|
||||
{
|
||||
label: i18n.t("exitApp"),
|
||||
role: "quit",
|
||||
},
|
||||
]);
|
||||
tray.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
// 如果重启后还有正在下载的视频,就将状态改成下载失败
|
||||
// If there are still videos being downloaded after the restart, change the status to download failed
|
||||
async resetDownloadStatus(): Promise<void> {
|
||||
// 重启后如果还有 downloading 状态的数据, 全部重置为失败
|
||||
// If data in the downloading state still fails after the restart, all downloads fail
|
||||
const videos = await this.videoRepository.findWattingAndDownloadingVideos();
|
||||
const videoIds = videos.map((video) => video.id);
|
||||
await this.videoRepository.changeVideoStatus(
|
||||
videoIds,
|
||||
DownloadStatus.Failed
|
||||
DownloadStatus.Failed,
|
||||
);
|
||||
}
|
||||
|
||||
secondInstance = (event: Event, commandLine: string[]) => {
|
||||
const url = commandLine.pop() || "";
|
||||
this.mainWindow.showWindow(url);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { IpcMainEvent } from "electron/main";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper/index.ts";
|
||||
import { handle, videoPattern } from "../helper/index.ts";
|
||||
import {
|
||||
type Controller,
|
||||
DownloadItem,
|
||||
DownloadItemPagination,
|
||||
Task,
|
||||
DownloadStatus,
|
||||
VideoStat,
|
||||
ListPagination,
|
||||
} from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
@@ -14,6 +16,8 @@ import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import DownloadService from "../services/DownloadService.ts";
|
||||
import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
|
||||
@injectable()
|
||||
export default class DownloadController implements Controller {
|
||||
@@ -27,7 +31,7 @@ export default class DownloadController implements Controller {
|
||||
@inject(TYPES.MainWindow)
|
||||
private readonly mainWindow: MainWindow,
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webviewService: WebviewService
|
||||
private readonly webviewService: WebviewService,
|
||||
) {}
|
||||
|
||||
@handle("show-download-dialog")
|
||||
@@ -36,14 +40,14 @@ export default class DownloadController implements Controller {
|
||||
this.webviewService.sendToWindow(
|
||||
"show-download-dialog",
|
||||
data,
|
||||
image.toDataURL()
|
||||
image?.toDataURL(),
|
||||
);
|
||||
}
|
||||
|
||||
@handle("add-download-item")
|
||||
async addDownloadItem(e: IpcMainEvent, video: Omit<DownloadItem, "id">) {
|
||||
const item = await this.videoRepository.addVideo(video);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
// This sends a message to the page notifying it of the update
|
||||
this.mainWindow.send("download-item-notifier", item);
|
||||
return item;
|
||||
}
|
||||
@@ -51,7 +55,7 @@ export default class DownloadController implements Controller {
|
||||
@handle("add-download-items")
|
||||
async addDownloadItems(e: IpcMainEvent, videos: Omit<DownloadItem, "id">[]) {
|
||||
const items = await this.videoRepository.addVideos(videos);
|
||||
// 这里向页面发送消息,通知页面更新
|
||||
// This sends a message to the page notifying it of the update
|
||||
this.mainWindow.send("download-item-notifier", items);
|
||||
return items;
|
||||
}
|
||||
@@ -71,36 +75,57 @@ export default class DownloadController implements Controller {
|
||||
|
||||
@handle("download-now")
|
||||
async downloadNow(e: IpcMainEvent, video: Omit<DownloadItem, "id">) {
|
||||
// 添加下载项
|
||||
// Add download
|
||||
const item = await this.addDownloadItem(e, video);
|
||||
// 开始下载
|
||||
// Start downloading
|
||||
await this.startDownload(e, item.id);
|
||||
return item;
|
||||
}
|
||||
|
||||
@handle("download-items-now")
|
||||
async downloadItemsNow(e: IpcMainEvent, videos: Omit<DownloadItem, "id">[]) {
|
||||
// 添加下载项
|
||||
// Add download
|
||||
const items = await this.addDownloadItems(e, videos);
|
||||
// 开始下载
|
||||
// Start downloading
|
||||
items.forEach((item) => this.startDownload(e, item.id));
|
||||
return items;
|
||||
}
|
||||
|
||||
@handle("get-download-items")
|
||||
async getDownloadItems(e: IpcMainEvent, pagination: DownloadItemPagination) {
|
||||
async getDownloadItems(
|
||||
e: IpcMainEvent,
|
||||
pagination: DownloadItemPagination,
|
||||
): Promise<ListPagination> {
|
||||
const videos = await this.videoRepository.findVideos(pagination);
|
||||
return videos;
|
||||
|
||||
const result: ListPagination = {
|
||||
total: videos.total,
|
||||
list: [],
|
||||
};
|
||||
|
||||
const local = this.store.get("local");
|
||||
for (const video of videos.list) {
|
||||
const final: VideoStat = { ...video };
|
||||
if (video.status === DownloadStatus.Success) {
|
||||
const pattern = path.join(local, `${video.name}.{${videoPattern}}`);
|
||||
const files = await glob(pattern);
|
||||
final.exists = files.length > 0;
|
||||
final.file = files[0];
|
||||
}
|
||||
result.list.push(final);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@handle("start-download")
|
||||
async startDownload(e: IpcMainEvent, vid: number) {
|
||||
// 查找将要下载的视频
|
||||
// Find the video you want to download
|
||||
const video = await this.videoRepository.findVideo(vid);
|
||||
const { name, url, headers, type } = video;
|
||||
const { name, url, headers, type, folder } = video;
|
||||
const local = this.store.get("local");
|
||||
|
||||
// 从配置中添加参数
|
||||
// Add parameters from the configuration
|
||||
const deleteSegments = this.store.get("deleteSegments");
|
||||
|
||||
const task: Task = {
|
||||
@@ -112,6 +137,7 @@ export default class DownloadController implements Controller {
|
||||
name,
|
||||
headers,
|
||||
deleteSegments,
|
||||
folder,
|
||||
},
|
||||
};
|
||||
await this.videoRepository.changeVideoStatus(vid, DownloadStatus.Watting);
|
||||
|
||||
@@ -9,12 +9,17 @@ import {
|
||||
clipboard,
|
||||
} from "electron";
|
||||
import { Favorite } from "../entity/Favorite.ts";
|
||||
import { convertToAudio, db, workspace } from "../helper/index.ts";
|
||||
import {
|
||||
convertToAudio,
|
||||
db,
|
||||
videoPattern,
|
||||
workspace,
|
||||
} from "../helper/index.ts";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { AppStore, EnvPath } from "../main.ts";
|
||||
import path from "path";
|
||||
import { handle, getLocalIP } from "../helper/index.ts";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { DownloadStatus, type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import fs from "fs-extra";
|
||||
import MainWindow from "../windows/MainWindow.ts";
|
||||
@@ -26,6 +31,10 @@ import VideoRepository from "../repository/VideoRepository.ts";
|
||||
import ConversionRepository from "../repository/ConversionRepository.ts";
|
||||
import { machineId } from "node-machine-id";
|
||||
import { nanoid } from "nanoid";
|
||||
import { glob } from "glob";
|
||||
import i18n from "../i18n/index.ts";
|
||||
import ElectronLogger from "../vendor/ElectronLogger.ts";
|
||||
import ElectronUpdater from "../vendor/ElectronUpdater.ts";
|
||||
|
||||
@injectable()
|
||||
export default class HomeController implements Controller {
|
||||
@@ -45,7 +54,11 @@ export default class HomeController implements Controller {
|
||||
@inject(TYPES.WebviewService)
|
||||
private readonly webviewService: WebviewService,
|
||||
@inject(TYPES.ConversionRepository)
|
||||
private readonly conversionRepository: ConversionRepository
|
||||
private readonly conversionRepository: ConversionRepository,
|
||||
@inject(TYPES.ElectronLogger)
|
||||
private readonly logger: ElectronLogger,
|
||||
@inject(TYPES.ElectronUpdater)
|
||||
private readonly updater: ElectronUpdater,
|
||||
) {}
|
||||
|
||||
@handle("get-env-path")
|
||||
@@ -89,14 +102,14 @@ export default class HomeController implements Controller {
|
||||
};
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: "打开",
|
||||
label: i18n.t("open"),
|
||||
click: () => {
|
||||
send("open");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "删除",
|
||||
label: i18n.t("delete"),
|
||||
click: () => {
|
||||
send("delete");
|
||||
},
|
||||
@@ -110,7 +123,7 @@ export default class HomeController implements Controller {
|
||||
@handle("select-download-dir")
|
||||
async selectDownloadDir(): Promise<string> {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject("未找到主窗口");
|
||||
if (!window) return Promise.reject(i18n.t("noMainWindow"));
|
||||
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
properties: ["openDirectory"],
|
||||
@@ -152,6 +165,18 @@ export default class HomeController implements Controller {
|
||||
if (key === "privacy") {
|
||||
this.webviewService.setDefaultSession(val);
|
||||
}
|
||||
// language
|
||||
if (key === "language") {
|
||||
i18n.changeLanguage(val);
|
||||
}
|
||||
// allowBeta
|
||||
if (key === "allowBeta") {
|
||||
this.updater.changeAllowBeta(val);
|
||||
}
|
||||
// audio muted mode
|
||||
if (key === "audioMuted") {
|
||||
this.webviewService.setAudioMuted(val);
|
||||
}
|
||||
|
||||
this.store.set(key, val);
|
||||
}
|
||||
@@ -177,38 +202,63 @@ export default class HomeController implements Controller {
|
||||
const item = await this.videoRepository.findVideo(id);
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: "拷贝链接地址",
|
||||
label: i18n.t("copyLinkAddress"),
|
||||
click: () => {
|
||||
clipboard.writeText(item.url || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "选择",
|
||||
label: i18n.t("select"),
|
||||
click: () => {
|
||||
send("select");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "下载",
|
||||
label: i18n.t("download"),
|
||||
click: () => {
|
||||
send("download");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "刷新",
|
||||
label: i18n.t("refresh"),
|
||||
click: () => {
|
||||
send("refresh");
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "删除",
|
||||
label: i18n.t("delete"),
|
||||
click: () => {
|
||||
send("delete");
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (item.status === DownloadStatus.Success) {
|
||||
const local = this.store.get("local");
|
||||
const pattern = path.join(local, `${item.name}.{${videoPattern}}`);
|
||||
const files = await glob(pattern);
|
||||
const exists = files.length > 0;
|
||||
if (exists) {
|
||||
const file = files[0];
|
||||
template.unshift(
|
||||
{
|
||||
label: i18n.t("openFolder"),
|
||||
click: () => {
|
||||
shell.showItemInFolder(file);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.t("openFile"),
|
||||
click: () => {
|
||||
shell.openPath(file);
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
menu.popup();
|
||||
}
|
||||
@@ -225,7 +275,7 @@ export default class HomeController implements Controller {
|
||||
if (exist) {
|
||||
return await convertToAudio(input, output);
|
||||
} else {
|
||||
return Promise.reject("未找到文件,可能是文件已经删除");
|
||||
return Promise.reject(i18n.t("noFileFound"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,9 +286,9 @@ export default class HomeController implements Controller {
|
||||
|
||||
@handle("combine-to-home-page")
|
||||
async combineToHomePage() {
|
||||
// 关闭浏览器窗口
|
||||
// Close browser window
|
||||
this.browserWindow.hideWindow();
|
||||
// 修改设置中的属性
|
||||
// Modify the properties in the Settings
|
||||
this.store.set("openInNewWindow", false);
|
||||
}
|
||||
|
||||
@@ -266,7 +316,7 @@ export default class HomeController implements Controller {
|
||||
@handle("select-file")
|
||||
async selectFile() {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject("未找到主窗口");
|
||||
if (!window) return Promise.reject(i18n.t("noMainWindow"));
|
||||
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
properties: ["openFile"],
|
||||
@@ -294,4 +344,86 @@ export default class HomeController implements Controller {
|
||||
return newId;
|
||||
}
|
||||
}
|
||||
|
||||
@handle("export-favorites")
|
||||
async exportFavorites() {
|
||||
const favorites = await this.favoriteRepository.findFavorites();
|
||||
const json = JSON.stringify(
|
||||
favorites.map((i) => ({
|
||||
title: i.title,
|
||||
url: i.url,
|
||||
icon: i.icon,
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject(i18n.t("noMainWindow"));
|
||||
|
||||
const result = await dialog.showSaveDialog(window, {
|
||||
title: i18n.t("exportFavorites"),
|
||||
defaultPath: "favorites.json",
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
await fs.writeFile(result.filePath, json);
|
||||
}
|
||||
}
|
||||
|
||||
@handle("import-favorites")
|
||||
async importFavorites() {
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject(i18n.t("noMainWindow"));
|
||||
|
||||
const result = await dialog.showOpenDialog(window, {
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
const filePath = result.filePaths[0];
|
||||
const json = await fs.readJSON(filePath);
|
||||
await this.favoriteRepository.importFavorites(json);
|
||||
}
|
||||
}
|
||||
|
||||
@handle("check-update")
|
||||
async checkUpdate() {
|
||||
this.updater.manualUpdate();
|
||||
}
|
||||
|
||||
@handle("start-update")
|
||||
async startUpdate() {
|
||||
this.updater.startDownload();
|
||||
}
|
||||
|
||||
@handle("install-update")
|
||||
async installUpdate() {
|
||||
this.updater.install();
|
||||
}
|
||||
|
||||
@handle("export-download-list")
|
||||
async exportDownloadList() {
|
||||
const videos = await this.videoRepository.findAllVideos();
|
||||
|
||||
const txt = videos.map((video) => `${video.url} ${video.name}`).join("\n");
|
||||
const window = this.mainWindow.window;
|
||||
if (!window) return Promise.reject(i18n.t("noMainWindow"));
|
||||
|
||||
const result = await dialog.showSaveDialog(window, {
|
||||
title: i18n.t("exportDownloadList"),
|
||||
defaultPath: "download-list.txt",
|
||||
filters: [{ name: "Text", extensions: ["txt"] }],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
await fs.writeFile(result.filePath, txt);
|
||||
}
|
||||
}
|
||||
|
||||
@handle("get-video-folders")
|
||||
async getVideoFolders() {
|
||||
return this.videoRepository.getVideoFolders();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { inject, injectable } from "inversify";
|
||||
import { handle } from "../helper/index.ts";
|
||||
import { type Controller } from "../interfaces.ts";
|
||||
import { TYPES } from "../types.ts";
|
||||
import PlayerWindow from "../windows/PlayerWindow.ts";
|
||||
|
||||
@injectable()
|
||||
export default class PlayerController implements Controller {
|
||||
constructor(
|
||||
@inject(TYPES.PlayerWindow)
|
||||
private readonly playerWindow: PlayerWindow,
|
||||
) {}
|
||||
|
||||
@handle("open-player-window")
|
||||
async openPlayerWindow() {
|
||||
this.playerWindow.showWindow();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { TYPES } from "../types.ts";
|
||||
import WebviewService from "../services/WebviewService.ts";
|
||||
import ElectronStore from "../vendor/ElectronStore.ts";
|
||||
import { SniffingHelper } from "../services/SniffingHelperService.ts";
|
||||
import i18n from "../i18n/index.ts";
|
||||
|
||||
@injectable()
|
||||
export default class WebviewController implements Controller {
|
||||
@@ -20,7 +21,7 @@ export default class WebviewController implements Controller {
|
||||
@inject(TYPES.ElectronStore)
|
||||
private readonly store: ElectronStore,
|
||||
@inject(TYPES.SniffingHelper)
|
||||
private readonly sniffingHelper: SniffingHelper
|
||||
private readonly sniffingHelper: SniffingHelper,
|
||||
) {}
|
||||
|
||||
@handle("set-webview-bounds")
|
||||
@@ -37,11 +38,11 @@ export default class WebviewController implements Controller {
|
||||
async webviewUrlContextMenu() {
|
||||
const template: Array<MenuItemConstructorOptions | MenuItem> = [
|
||||
{
|
||||
label: "复制",
|
||||
label: i18n.t("copy"),
|
||||
role: "copy",
|
||||
},
|
||||
{
|
||||
label: "粘贴",
|
||||
label: i18n.t("paste"),
|
||||
role: "paste",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -43,7 +43,7 @@ export default class IpcHandlerService {
|
||||
}
|
||||
return success(res);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`处理 ipc [${channel}] 事件时异常: `, e);
|
||||
this.logger.error(`process ipc [${channel}] failed: `, e);
|
||||
if (e instanceof Error) {
|
||||
return error(e.message);
|
||||
} else {
|
||||
|
||||