chore(repo): convert text files to CRLF line endings

This commit is contained in:
caorushizi
2025-10-30 22:41:34 +08:00
committed by 曹儒士子
parent f92f1affc6
commit 9e0752cafc
174 changed files with 10670 additions and 10577 deletions
+6 -6
View File
@@ -1,7 +1,7 @@
src/
vite.config.ts
postcss.config.js
tailwind.config.js
index.html
public/
src/
vite.config.ts
postcss.config.js
tailwind.config.js
index.html
public/
*.md
+15 -15
View File
@@ -1,15 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
+4 -4
View File
@@ -1,4 +1,4 @@
# FIXME: APP_ 开头的会暴漏给前端
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
# FIXME: APP_ 开头的会暴漏给前端
APP_NAME=mediago
APP_ID=mediago.ziying.site
APP_COPYRIGHT=caorushizi
+56 -56
View File
@@ -1,56 +1,56 @@
# GitHub Copilot Instruction: Code Review and Optimization
## Role and Objective
You are a **top-tier software architect and performance optimization expert**.
Your goal is to help me — a senior TypeScript full-stack engineer — elevate the quality of my code to a new level.
When reviewing my code, **do not explain basic concepts**. I need **precise, deep, and forward-thinking insights**.
Your core mission is **optimization**, including but not limited to:
- **Performance improvement**: Identify and optimize performance bottlenecks, reduce unnecessary computation and resource consumption.
- **Code refactoring**: Suggest more elegant and efficient implementations to improve code structure.
- **Design patterns**: Identify opportunities to apply or refine design patterns to enhance scalability and maintainability.
- **Best practices**: Ensure the code adheres to the latest best practices for the TypeScript ecosystem (Node.js, React, API layers, build pipelines).
- **Potential risks**: Anticipate and highlight deep issues such as concurrency problems, security vulnerabilities, or resource leaks.
---
## Review Perspective and Principles
1. **High-Standard Review**
Review the code as if it were going to **production for millions of users** and needs to be **maintained long-term**.
2. **Deep Analysis, Not Surface Advice**
Dont focus on trivial issues like typos or syntax sugar.
Instead, explain **why** a refactor or change matters — e.g.:
> “Switching from a synchronous file read to `fs.promises` can free the event loop, improving throughput under load.”
3. **Performance First**
- Evaluate time and space complexity; suggest algorithmic or structural improvements.
- Examine I/O, database queries, and network calls for efficiency — recommend batching, caching, or async processing.
- Recommend appropriate data structures or API strategies for scalability (e.g., pagination, streaming responses).
4. **Architecture and Design**
- Follow **SOLID** principles. Explicitly identify violations and propose refactoring approaches.
- Encourage **composition over inheritance** and **dependency injection**.
- Suggest modularization and clear separation between layers (e.g., API, service, repository, UI).
5. **Code Style and Standards**
- Code must be clear, consistent, and self-explanatory.
- Follow the projects existing conventions unless the change brings substantial clarity or performance gain.
- For complex logic, suggest adding comments explaining the **rationale (“why”)**, not just the **action (“what”)**.
---
## Specific Instructions
- **When reviewing code, include the optimized code snippet directly**, with short comments highlighting key changes and their reasoning.
- **If you find potential bugs or unhandled edge cases**, explicitly point them out and provide a fix suggestion.
- **Avoid subjective stylistic comments** unless they impact clarity, performance, or maintainability.
- **When I ask “Can this code be optimized?”**, provide a holistic evaluation covering performance, readability, scalability, and maintainability.
---
At the end of every response, please add:
> “AI-generated suggestions may contain errors; use your own judgment when applying them.”
# GitHub Copilot Instruction: Code Review and Optimization
## Role and Objective
You are a **top-tier software architect and performance optimization expert**.
Your goal is to help me — a senior TypeScript full-stack engineer — elevate the quality of my code to a new level.
When reviewing my code, **do not explain basic concepts**. I need **precise, deep, and forward-thinking insights**.
Your core mission is **optimization**, including but not limited to:
- **Performance improvement**: Identify and optimize performance bottlenecks, reduce unnecessary computation and resource consumption.
- **Code refactoring**: Suggest more elegant and efficient implementations to improve code structure.
- **Design patterns**: Identify opportunities to apply or refine design patterns to enhance scalability and maintainability.
- **Best practices**: Ensure the code adheres to the latest best practices for the TypeScript ecosystem (Node.js, React, API layers, build pipelines).
- **Potential risks**: Anticipate and highlight deep issues such as concurrency problems, security vulnerabilities, or resource leaks.
---
## Review Perspective and Principles
1. **High-Standard Review**
Review the code as if it were going to **production for millions of users** and needs to be **maintained long-term**.
2. **Deep Analysis, Not Surface Advice**
Dont focus on trivial issues like typos or syntax sugar.
Instead, explain **why** a refactor or change matters — e.g.:
> “Switching from a synchronous file read to `fs.promises` can free the event loop, improving throughput under load.”
3. **Performance First**
- Evaluate time and space complexity; suggest algorithmic or structural improvements.
- Examine I/O, database queries, and network calls for efficiency — recommend batching, caching, or async processing.
- Recommend appropriate data structures or API strategies for scalability (e.g., pagination, streaming responses).
4. **Architecture and Design**
- Follow **SOLID** principles. Explicitly identify violations and propose refactoring approaches.
- Encourage **composition over inheritance** and **dependency injection**.
- Suggest modularization and clear separation between layers (e.g., API, service, repository, UI).
5. **Code Style and Standards**
- Code must be clear, consistent, and self-explanatory.
- Follow the projects existing conventions unless the change brings substantial clarity or performance gain.
- For complex logic, suggest adding comments explaining the **rationale (“why”)**, not just the **action (“what”)**.
---
## Specific Instructions
- **When reviewing code, include the optimized code snippet directly**, with short comments highlighting key changes and their reasoning.
- **If you find potential bugs or unhandled edge cases**, explicitly point them out and provide a fix suggestion.
- **Avoid subjective stylistic comments** unless they impact clarity, performance, or maintainability.
- **When I ask “Can this code be optimized?”**, provide a holistic evaluation covering performance, readability, scalability, and maintainability.
---
At the end of every response, please add:
> “AI-generated suggestions may contain errors; use your own judgment when applying them.”
+5 -5
View File
@@ -1,6 +1,6 @@
{
"rules": {
"no-unused-vars": "error",
"no-console": "warn"
}
{
"rules": {
"no-unused-vars": "error",
"no-console": "warn"
}
}
+3 -3
View File
@@ -1,3 +1,3 @@
# Ignore artifacts:
build
coverage
# Ignore artifacts:
build
coverage
+1 -1
View File
@@ -1,2 +1,2 @@
plugins:
plugins:
- "@prettier/plugin-oxc"
+5 -5
View File
@@ -1,5 +1,5 @@
{
"cSpell.words": ["intlify", "unplugin", "vitepress", "waline"],
"typescript.experimental.useTsgo": true,
"oxc.enable": true
}
{
"cSpell.words": ["intlify", "unplugin", "vitepress", "waline"],
"typescript.experimental.useTsgo": true,
"oxc.enable": true
}
+16 -16
View File
@@ -1,16 +1,16 @@
# Repository Guidelines
## Project Structure & Module Organization
MediaGo is a pnpm/turborepo monorepo. Feature apps live in `apps/` (`frontend-main`, `frontend-mobile`, `backend-web`, `backend-electron`) for the user surfaces and API. Reusable logic stays in `packages/` (`shared` for cross-runtime helpers, `backend` for orchestration, `main` for Electron packaging). Long-form docs and assets sit in `docs/`, `images/`, and `docker/`. End-to-end checks live in `tests/`.
## Build, Test, and Development Commands
Run `pnpm install` once per clone. Use `pnpm dev` for the unified desktop + web experience, or scope to `pnpm dev:web` / `pnpm dev:electron`. `pnpm build` triggers the production Turborepo pipeline; `pnpm build:web-release` plus `pnpm build:docker` produce the deployable web bundle. Keep the codebase healthy with `pnpm lint`, `pnpm lint:fix`, `pnpm format`, and verify types through `pnpm types`.
## Coding Style & Naming Conventions
Target modern TypeScript with ES modules, two-space indentation, UTF-8, and LF endings per `.editorconfig`. Components, hooks, and services adopt PascalCase (e.g. `UserPreferencesPanel.tsx`). Utilities and helpers stay camelCase, and constants use SCREAMING_SNAKE_CASE. Always run `pnpm format` before committing; reserve comments for clarifying complex logic.
## Testing Guidelines
Integration suites live under `tests/*.test.ts` and execute via `pnpm test` using the Node `tsx` runner. Name files descriptively like `download.queue.integration.test.ts`. Mock external services, prefer shared fixtures in `tests/fixtures/`, and cover happy path, recovery, and edge behaviors when touching runtime code.
## Commit & Pull Request Guidelines
Follow Conventional Commits (e.g. `feat(frontend-main): add download queue UI`) and use `pnpm commit` (Commitizen) to stay compliant. Pull requests should summarize the change, link issues with `Closes #123`, note local test runs (`pnpm test`), and attach screenshots or recordings for UI updates. Call out new environment variables, migrations, or follow-up tasks so reviewers can reproduce the setup quickly.
# Repository Guidelines
## Project Structure & Module Organization
MediaGo is a pnpm/turborepo monorepo. Feature apps live in `apps/` (`frontend-main`, `frontend-mobile`, `backend-web`, `backend-electron`) for the user surfaces and API. Reusable logic stays in `packages/` (`shared` for cross-runtime helpers, `backend` for orchestration, `main` for Electron packaging). Long-form docs and assets sit in `docs/`, `images/`, and `docker/`. End-to-end checks live in `tests/`.
## Build, Test, and Development Commands
Run `pnpm install` once per clone. Use `pnpm dev` for the unified desktop + web experience, or scope to `pnpm dev:web` / `pnpm dev:electron`. `pnpm build` triggers the production Turborepo pipeline; `pnpm build:web-release` plus `pnpm build:docker` produce the deployable web bundle. Keep the codebase healthy with `pnpm lint`, `pnpm lint:fix`, `pnpm format`, and verify types through `pnpm types`.
## Coding Style & Naming Conventions
Target modern TypeScript with ES modules, two-space indentation, UTF-8, and LF endings per `.editorconfig`. Components, hooks, and services adopt PascalCase (e.g. `UserPreferencesPanel.tsx`). Utilities and helpers stay camelCase, and constants use SCREAMING_SNAKE_CASE. Always run `pnpm format` before committing; reserve comments for clarifying complex logic.
## Testing Guidelines
Integration suites live under `tests/*.test.ts` and execute via `pnpm test` using the Node `tsx` runner. Name files descriptively like `download.queue.integration.test.ts`. Mock external services, prefer shared fixtures in `tests/fixtures/`, and cover happy path, recovery, and edge behaviors when touching runtime code.
## Commit & Pull Request Guidelines
Follow Conventional Commits (e.g. `feat(frontend-main): add download queue UI`) and use `pnpm commit` (Commitizen) to stay compliant. Pull requests should summarize the change, link issues with `Closes #123`, note local test runs (`pnpm test`), and attach screenshots or recordings for UI updates. Call out new environment variables, migrations, or follow-up tasks so reviewers can reproduce the setup quickly.
+141 -141
View File
@@ -1,141 +1,141 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/en/guides.html?form=github">Quick start</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</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.
- **✅&nbsp; 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.
- **📱&nbsp; Mobile playback**: Easily switch between PC and mobile devices seamlessly. Once downloaded, you can watch the video on your phone.
- **⚡️&nbsp; Batch download**: Supports downloading multiple videos and live streams simultaneously, ensuring high bandwidth is fully utilized.
- **🎉&nbsp; 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
![Homepage](https://static.ziying.site/images/home.png)
### 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
![Homepage](https://static.ziying.site/images/home.png)
![Homepage-dark](https://static.ziying.site/images/home-dark.png)
![Settings Page](https://static.ziying.site/images/settings.png)
![Resource Extraction](https://static.ziying.site/images/browser.png)
## 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>
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/en/guides.html?form=github">Quick start</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</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.
- **✅&nbsp; 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.
- **📱&nbsp; Mobile playback**: Easily switch between PC and mobile devices seamlessly. Once downloaded, you can watch the video on your phone.
- **⚡️&nbsp; Batch download**: Supports downloading multiple videos and live streams simultaneously, ensuring high bandwidth is fully utilized.
- **🎉&nbsp; 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
![Homepage](https://static.ziying.site/images/home.png)
### 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
![Homepage](https://static.ziying.site/images/home.png)
![Homepage-dark](https://static.ziying.site/images/home-dark.png)
![Settings Page](https://static.ziying.site/images/settings.png)
![Resource Extraction](https://static.ziying.site/images/browser.png)
## 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>
+143 -143
View File
@@ -1,143 +1,143 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/jp/guides.html?form=github">早く始めます</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp?form=github">公式サイトです</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp/documents.html?form=github">にやすりをかける</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</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ダウンロードをサポートしています。
- **✅&nbsp; パケットキャプチャ不要**: ソフトウェアに内蔵されたブラウザを使用して、ウェブページ内のビデオリソースを簡単に検出し、検出したリソースリストからダウンロードしたいリソースを選択することで、シンプルかつ迅速にダウンロードできます。
- **📱&nbsp; モバイル再生**: PCとモバイルデバイス間で簡単にシームレスに切り替えが可能で、ダウンロードが完了した後はスマートフォンでビデオを視聴できます。
- **⚡️&nbsp; バッチダウンロード**: 複数のビデオやライブストリームリソースを同時にダウンロードでき、高速帯域幅を無駄にしません。
- **🎉&nbsp; 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 arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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 arm64Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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」を見つけ、インストールをクリックし、ドメイン名などの基本情報を設定してインストールを完了します。
### ソフトウェアスクリーンショット
![ホームページ](https://static.ziying.site/images/home.png)
### 重要な更新
- Web端のdockerデプロイをサポート
- デスクトップ端のUIを更新
### 更新ログ
- デスクトップ端のUIを更新
- Web端のdockerデプロイをサポート
- 新たにビデオ再生機能を追加、デスクトップとモバイル端両方で再生可能
- Macでの画面表示ができない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windowsのポータブル版(インストール不要)を追加
- ダウンロードリストの最適化、ページ内の複数のビデオリソースを嗅ぎ取る機能を追加
- 手動でお気に入りリストのインポートとエクスポートをサポート
- ホームページのダウンロードリストエクスポートをサポート
- 「新規ダウンロード」フォームのインタラクションロジックを最適化
- UrlSchemeでアプリを開き、ダウンロードタスクを追加する機能をサポート
- バグの修正とユーザー体験の向上
## ソフトウェアスクリーンショット
![ホームページ](https://static.ziying.site/images/home.png)
![ホームページ(ダークモード)](https://static.ziying.site/images/home-dark.png)
![設定ページ](https://static.ziying.site/images/settings.png)
![リソース抽出](https://static.ziying.site/images/browser.png)
## 技術スタック
- 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> から来ています
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/jp/guides.html?form=github">早く始めます</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp?form=github">公式サイトです</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/jp/documents.html?form=github">にやすりをかける</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</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ダウンロードをサポートしています。
- **✅&nbsp; パケットキャプチャ不要**: ソフトウェアに内蔵されたブラウザを使用して、ウェブページ内のビデオリソースを簡単に検出し、検出したリソースリストからダウンロードしたいリソースを選択することで、シンプルかつ迅速にダウンロードできます。
- **📱&nbsp; モバイル再生**: PCとモバイルデバイス間で簡単にシームレスに切り替えが可能で、ダウンロードが完了した後はスマートフォンでビデオを視聴できます。
- **⚡️&nbsp; バッチダウンロード**: 複数のビデオやライブストリームリソースを同時にダウンロードでき、高速帯域幅を無駄にしません。
- **🎉&nbsp; 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 arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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 arm64Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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」を見つけ、インストールをクリックし、ドメイン名などの基本情報を設定してインストールを完了します。
### ソフトウェアスクリーンショット
![ホームページ](https://static.ziying.site/images/home.png)
### 重要な更新
- Web端のdockerデプロイをサポート
- デスクトップ端のUIを更新
### 更新ログ
- デスクトップ端のUIを更新
- Web端のdockerデプロイをサポート
- 新たにビデオ再生機能を追加、デスクトップとモバイル端両方で再生可能
- Macでの画面表示ができない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windowsのポータブル版(インストール不要)を追加
- ダウンロードリストの最適化、ページ内の複数のビデオリソースを嗅ぎ取る機能を追加
- 手動でお気に入りリストのインポートとエクスポートをサポート
- ホームページのダウンロードリストエクスポートをサポート
- 「新規ダウンロード」フォームのインタラクションロジックを最適化
- UrlSchemeでアプリを開き、ダウンロードタスクを追加する機能をサポート
- バグの修正とユーザー体験の向上
## ソフトウェアスクリーンショット
![ホームページ](https://static.ziying.site/images/home.png)
![ホームページ(ダークモード)](https://static.ziying.site/images/home-dark.png)
![設定ページ](https://static.ziying.site/images/settings.png)
![リソース抽出](https://static.ziying.site/images/browser.png)
## 技術スタック
- 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> から来ています
+140 -140
View File
@@ -1,140 +1,140 @@
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</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>&nbsp;&nbsp;•&nbsp;&nbsp;</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>
## Intro
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
- **✅&nbsp; 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
- **📱&nbsp; 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
- **⚡️&nbsp; 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
- **🎉&nbsp; 支持 docker 部署** 支持 docker 部署 web 端,方便快捷。
## Quickstart
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
## 运行代码
```shell
# 代码下载
git clone https://github.com/caorushizi/mediago.git
# 安装依赖
pnpm install
# 首次安装需要 rebuild 一下
pnpm rebuild:workspace
# electron 开发环境
pnpm dev:electron
# electron 打包运行
pnpm release:electron
# server 开发环境
pnpm dev:server
# server 打包运行
pnpm release:server
```
## 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 arm64apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) 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`
### docker 宝塔面板一键部署(推荐)
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/new/download.html?r=dk_mediago) 官网,选择正式版的脚本下载安装
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
3. 安装完成后在应用商店中找到`MediaGo`,点击安装,配置域名等基本信息即可完成安装
### 软件截图
![首页](./images/home.png)
### 重要更新
- 支持 docker 部署 web 端
- 更新桌面端 UI
### 更新日志
- 更新桌面端 UI
- 支持 docker 部署 web 端
- 新增视频播放,支持桌面端和移动端播放
- 修复 mac 打开无法展示界面的问题
- 优化了批量下载的交互
- 添加了 windows 的便携版(免安装哦)
- 优化了下载列表,支持页面中多个视频的嗅探
- 支持收藏列表手动导入导出
- 支持首页下载列表导出
- 优化了【新建下载】表单的交互逻辑
- 支持 UrlScheme 打开应用,并添加下载任务
- 修复了一些 bug 并提升用户体验
## 软件截图
![首页](./images/home.png)
![首页-dark](./images/home-dark.png)
![设置页面](./images/settings.png)
![资源提取](./images/browser.png)
## 技术栈
- 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>
<div align="center">
<h1>MediaGo</h1>
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</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>&nbsp;&nbsp;•&nbsp;&nbsp;</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>
## Intro
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
- **✅&nbsp; 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
- **📱&nbsp; 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
- **⚡️&nbsp; 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
- **🎉&nbsp; 支持 docker 部署** 支持 docker 部署 web 端,方便快捷。
## Quickstart
运行代码需要 node 和 pnpm,node 需要在官网下载安装,pnpm 可以通过`npm i -g pnpm`安装。
## 运行代码
```shell
# 代码下载
git clone https://github.com/caorushizi/mediago.git
# 安装依赖
pnpm install
# 首次安装需要 rebuild 一下
pnpm rebuild:workspace
# electron 开发环境
pnpm dev:electron
# electron 打包运行
pnpm release:electron
# server 开发环境
pnpm dev:server
# server 打包运行
pnpm release:server
```
## 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 arm64apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) 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`
### docker 宝塔面板一键部署(推荐)
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/new/download.html?r=dk_mediago) 官网,选择正式版的脚本下载安装
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
3. 安装完成后在应用商店中找到`MediaGo`,点击安装,配置域名等基本信息即可完成安装
### 软件截图
![首页](./images/home.png)
### 重要更新
- 支持 docker 部署 web 端
- 更新桌面端 UI
### 更新日志
- 更新桌面端 UI
- 支持 docker 部署 web 端
- 新增视频播放,支持桌面端和移动端播放
- 修复 mac 打开无法展示界面的问题
- 优化了批量下载的交互
- 添加了 windows 的便携版(免安装哦)
- 优化了下载列表,支持页面中多个视频的嗅探
- 支持收藏列表手动导入导出
- 支持首页下载列表导出
- 优化了【新建下载】表单的交互逻辑
- 支持 UrlScheme 打开应用,并添加下载任务
- 修复了一些 bug 并提升用户体验
## 软件截图
![首页](./images/home.png)
![首页-dark](./images/home-dark.png)
![设置页面](./images/settings.png)
![资源提取](./images/browser.png)
## 技术栈
- 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>
+9 -9
View File
@@ -1,9 +1,9 @@
.bin
node_modules
build
dist
bin/Logs
release
./types
.DS_Store
logs
.bin
node_modules
build
dist
bin/Logs
release
./types
.DS_Store
logs
+48 -48
View File
@@ -1,48 +1,48 @@
# Electron Main Process
The Electron main process that handles the desktop application lifecycle, window management, and system integration for MediaGo.
## Overview
This package contains the Electron main process code that:
- Creates and manages application windows
- Handles system tray integration
- Manages application menus and shortcuts
- Provides native file system access
- Handles auto-updates and app packaging
- Communicates with the frontend renderer process
## Key Features
- **Window Management**: Creates and manages the main application window
- **System Integration**: System tray, notifications, and OS-specific features
- **Security**: Implements secure IPC communication with the renderer
- **Auto-Updates**: Built-in update mechanism for the desktop app
- **Cross-Platform**: Supports Windows, macOS, and Linux
## Technologies
- **Electron**: Desktop application framework
- **Node.js**: Backend runtime for main process
- **IPC**: Inter-process communication with frontend
- **Native APIs**: File system, OS integration
## Development
```bash
# Start development mode
pnpm dev
# Build electron app
pnpm build:electron
# Package for release
pnpm release
# Create beta build
pnpm beta
```
## Architecture
The electron app loads the frontend React application in a BrowserWindow and provides native desktop functionality through Electron's main process APIs.
# Electron Main Process
The Electron main process that handles the desktop application lifecycle, window management, and system integration for MediaGo.
## Overview
This package contains the Electron main process code that:
- Creates and manages application windows
- Handles system tray integration
- Manages application menus and shortcuts
- Provides native file system access
- Handles auto-updates and app packaging
- Communicates with the frontend renderer process
## Key Features
- **Window Management**: Creates and manages the main application window
- **System Integration**: System tray, notifications, and OS-specific features
- **Security**: Implements secure IPC communication with the renderer
- **Auto-Updates**: Built-in update mechanism for the desktop app
- **Cross-Platform**: Supports Windows, macOS, and Linux
## Technologies
- **Electron**: Desktop application framework
- **Node.js**: Backend runtime for main process
- **IPC**: Inter-process communication with frontend
- **Native APIs**: File system, OS integration
## Development
```bash
# Start development mode
pnpm dev
# Build electron app
pnpm build:electron
# Package for release
pnpm release
# Create beta build
pnpm beta
```
## Architecture
The electron app loads the frontend React application in a BrowserWindow and provides native desktop functionality through Electron's main process APIs.
+7 -7
View File
@@ -1,7 +1,7 @@
node_modules
output
bin
mobile
plugin
build
dist
node_modules
output
bin
mobile
plugin
build
dist
+5 -2
View File
@@ -9,11 +9,14 @@
"license": "MIT",
"dependencies": {
"@ghostery/adblocker-electron": "2.12.4",
"@mediago/browser-extension": "workspace:*",
"@mediago/core": "0.0.11",
"@mediago/deps": "0.0.3",
"@mediago/player": "0.0.9",
"@mediago/electron-preload": "workspace:*",
"@mediago/browser-extension": "workspace:*"
"@mediago/player": "0.0.9",
"better-sqlite3": "12.4.1",
"reflect-metadata": "^0.2.2",
"typeorm": "0.3.27"
},
"devDependencies": {
"electron": "30.0.8"
+3 -3
View File
@@ -1,3 +1,3 @@
provider: generic
updaterCacheDirName: mediago-updater
url: "http://127.0.0.1:9000/release"
provider: generic
updaterCacheDirName: mediago-updater
url: "http://127.0.0.1:9000/release"
+1
View File
@@ -29,6 +29,7 @@
"glob": "^11.0.3",
"prebuild-install": "^7.1.3",
"rimraf": "^6.0.1",
"rollup-plugin-copy": "^3.5.0",
"tsdown": "^0.15.9",
"tsx": "^4.20.6"
},
+160 -154
View File
@@ -1,154 +1,160 @@
import fs from "node:fs/promises";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import dotenvFlow from "dotenv-flow";
import type { Configuration } from "electron-builder";
import * as builder from "electron-builder";
const args = process.argv.slice(2);
const isDir = args.includes("--dir");
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = path.resolve(__dirname, "../../..");
const appRoot = path.resolve(__dirname, "..");
dotenvFlow.config({
path: projectRoot,
});
const pkg = JSON.parse(await fs.readFile("./app/package.json", "utf-8"));
function getReleaseConfig(): Configuration {
return {
asar: true,
productName: process.env.APP_NAME,
buildVersion: pkg.version,
appId: process.env.APP_ID,
copyright: process.env.APP_COPYRIGHT,
artifactName: "${productName}-setup-${platform}-${arch}-${buildVersion}.${ext}",
npmRebuild: true,
directories: {
output: "./release",
},
files: [
{
from: "./build/main",
to: "./main",
},
{
from: "./build/renderer",
to: "./renderer",
},
{
from: "./build/preload",
to: "./preload",
},
"./package.json",
],
extraResources: [
{
from: "./app/build/plugin",
to: "plugin",
},
],
win: {
icon: "../assets/icon.ico",
target: [
{
target: "nsis",
arch: ["x64"],
},
"portable",
],
},
portable: {
artifactName: "${productName}-portable-${platform}-${arch}-${buildVersion}.${ext}",
},
dmg: {
contents: [
{
x: 410,
y: 150,
type: "link",
path: "/Applications",
},
{
x: 130,
y: 150,
type: "file",
},
],
},
mac: {
icon: "../assets/icon.icns",
target: [
{
target: "dmg",
arch: [process.arch === "arm64" ? "arm64" : "x64"],
},
],
extendInfo: {
CFBundleURLTypes: [
{
CFBundleURLName: "Mediago URL Scheme",
CFBundleURLSchemes: ["mediago"],
},
],
},
},
linux: {
category: "Utility",
icon: "../assets/icon.icns",
maintainer: "caorushizi <84996057@qq.com>",
target: {
target: "deb",
arch: ["x64"],
},
},
nsis: {
oneClick: false,
allowElevation: true,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
};
}
const electronTask = [
{
src: "apps/electron/build",
dest: "app/build/main",
},
{
src: "apps/ui/build/electron",
dest: "app/build/renderer",
},
{
src: "packages/electron-preload/build",
dest: "app/build/preload",
},
{
src: "packages/browser-extension/build",
dest: "app/build/plugin",
},
];
for (const task of electronTask) {
await fs.cp(path.resolve(projectRoot, task.src), path.resolve(appRoot, task.dest), {
recursive: true,
force: true,
});
}
const config = getReleaseConfig();
if (process.env.GH_TOKEN) {
config.publish = {
provider: "github",
repo: "mediago",
owner: "caorushizi",
releaseType: "draft",
};
}
await builder.build({ config, dir: isDir });
import fs from "node:fs/promises";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import dotenvFlow from "dotenv-flow";
import type { Configuration } from "electron-builder";
import * as builder from "electron-builder";
const args = process.argv.slice(2);
const isDir = args.includes("--dir");
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = path.resolve(__dirname, "../../..");
const appRoot = path.resolve(__dirname, "..");
dotenvFlow.config({
path: projectRoot,
});
const pkg = JSON.parse(await fs.readFile("./app/package.json", "utf-8"));
function getReleaseConfig(): Configuration {
return {
asar: true,
productName: process.env.APP_NAME,
buildVersion: pkg.version,
appId: process.env.APP_ID,
copyright: process.env.APP_COPYRIGHT,
artifactName:
"${productName}-setup-${platform}-${arch}-${buildVersion}.${ext}",
npmRebuild: true,
directories: {
output: "./release",
},
files: [
{
from: "./build/main",
to: "./main",
},
{
from: "./build/renderer",
to: "./renderer",
},
{
from: "./build/preload",
to: "./preload",
},
"./package.json",
],
extraResources: [
{
from: "./app/build/plugin",
to: "plugin",
},
],
win: {
icon: "../assets/icon.ico",
target: [
{
target: "nsis",
arch: ["x64"],
},
"portable",
],
},
portable: {
artifactName:
"${productName}-portable-${platform}-${arch}-${buildVersion}.${ext}",
},
dmg: {
contents: [
{
x: 410,
y: 150,
type: "link",
path: "/Applications",
},
{
x: 130,
y: 150,
type: "file",
},
],
},
mac: {
icon: "../assets/icon.icns",
target: [
{
target: "dmg",
arch: [process.arch === "arm64" ? "arm64" : "x64"],
},
],
extendInfo: {
CFBundleURLTypes: [
{
CFBundleURLName: "Mediago URL Scheme",
CFBundleURLSchemes: ["mediago"],
},
],
},
},
linux: {
category: "Utility",
icon: "../assets/icon.icns",
maintainer: "caorushizi <84996057@qq.com>",
target: {
target: "deb",
arch: ["x64"],
},
},
nsis: {
oneClick: false,
allowElevation: true,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
};
}
const electronTask = [
{
src: "apps/electron/build",
dest: "app/build/main",
},
{
src: "apps/ui/build/electron",
dest: "app/build/renderer",
},
{
src: "packages/electron-preload/build",
dest: "app/build/preload",
},
{
src: "packages/browser-extension/build",
dest: "app/build/plugin",
},
];
for (const task of electronTask) {
await fs.cp(
path.resolve(projectRoot, task.src),
path.resolve(appRoot, task.dest),
{
recursive: true,
force: true,
},
);
}
const config = getReleaseConfig();
if (process.env.GH_TOKEN) {
config.publish = {
provider: "github",
repo: "mediago",
owner: "caorushizi",
releaseType: "draft",
};
}
await builder.build({ config, dir: isDir });
+146 -146
View File
@@ -1,146 +1,146 @@
import path from "node:path";
import { provide } from "@inversifyjs/binding-decorators";
import { DownloadStatus } from "@mediago/shared-common";
import { DownloaderServer, i18n, TypeORM, DownloadTaskService, VideoServer } from "@mediago/shared-node";
import { app, BrowserWindow, type Event, Menu, nativeImage, nativeTheme, Tray } from "electron";
import { inject, injectable } from "inversify";
import TrayIcon from "../assets/tray-icon.png";
import TrayIconLight from "../assets/tray-icon-light.png";
import ProtocolService from "./core/protocol";
import ElectronRouter from "./core/router";
import { db, isMac, logDir } from "./helper/variables";
import ElectronDevtools from "./vendor/ElectronDevtools";
import ElectronStore from "./vendor/ElectronStore";
import ElectronUpdater from "./vendor/ElectronUpdater";
import MainWindow from "./windows/main.window";
import "./controller";
@injectable()
@provide()
export default class ElectronApp {
constructor(
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(ProtocolService)
private readonly protocol: ProtocolService,
@inject(ElectronUpdater)
private readonly updater: ElectronUpdater,
@inject(ElectronRouter)
private readonly router: ElectronRouter,
@inject(TypeORM)
private readonly db: TypeORM,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(ElectronDevtools)
private readonly devTools: ElectronDevtools,
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(VideoServer)
private readonly videoServer: VideoServer,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
) { }
private async serviceInit(): Promise<void> {
this.mainWindow.init();
}
private async vendorInit() {
await this.db.init({ dbPath: db });
this.updater.init();
this.devTools.init();
}
async init(): Promise<void> {
this.protocol.create();
this.router.init();
// vendor
await this.vendorInit();
// service
await this.serviceInit();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
this.mainWindow.init();
}
});
this.initAppTheme();
this.initLanguage();
this.resetDownloadStatus();
this.initTray();
const local = this.store.get("local");
this.videoServer.start({ local });
// Start the download service
this.downloaderServer.start({
logDir: logDir,
localDir: this.store.get("local"),
deleteSegments: this.store.get("deleteSegments"),
proxy: this.store.get("proxy"),
useProxy: this.store.get("useProxy"),
maxRunner: this.store.get("maxRunner"),
});
this.store.onDidChange("maxRunner", (maxRunner) => {
this.downloaderServer.changeConfig({ maxRunner: maxRunner || 1 });
});
this.store.onDidChange("proxy", (proxy) => {
this.downloaderServer.changeConfig({ proxy: proxy || "" });
});
}
initAppTheme(): void {
const theme = this.store.get("theme");
nativeTheme.themeSource = theme;
}
initLanguage(): void {
const language = this.store.get("language");
i18n.changeLanguage(language);
}
initTray() {
const iconPath = path.resolve(__dirname, isMac ? TrayIconLight : TrayIcon);
const icon = nativeImage.createFromPath(iconPath);
const tray = new Tray(icon);
tray.setToolTip("Media Go");
tray.addListener("click", () => {
this.mainWindow.init();
});
const contextMenu = Menu.buildFromTemplate([
{
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> {
// If data in the downloading state still fails after the restart, all downloads fail
const videos = await this.downloadTaskService.findActiveTasks();
const videoIds = videos.map((video) => video.id);
await this.downloadTaskService.setStatus(videoIds, DownloadStatus.Failed);
}
secondInstance = (event: Event, commandLine: string[]) => {
const url = commandLine.pop() || "";
this.mainWindow.showWindow(url);
};
handleOpenUrl(url: string): void {
this.mainWindow.handleUrl(url);
}
send(url: string): void {
this.mainWindow.send("url-params", url);
}
}
import path from "node:path";
import { provide } from "@inversifyjs/binding-decorators";
import { DownloadStatus } from "@mediago/shared-common";
import { DownloaderServer, i18n, TypeORM, DownloadTaskService, VideoServer } from "@mediago/shared-node";
import { app, BrowserWindow, type Event, Menu, nativeImage, nativeTheme, Tray } from "electron";
import { inject, injectable } from "inversify";
import TrayIcon from "../assets/tray-icon.png";
import TrayIconLight from "../assets/tray-icon-light.png";
import ProtocolService from "./core/protocol";
import ElectronRouter from "./core/router";
import { db, isMac, logDir } from "./helper/variables";
import ElectronDevtools from "./vendor/ElectronDevtools";
import ElectronStore from "./vendor/ElectronStore";
import ElectronUpdater from "./vendor/ElectronUpdater";
import MainWindow from "./windows/main.window";
import "./controller";
@injectable()
@provide()
export default class ElectronApp {
constructor(
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(ProtocolService)
private readonly protocol: ProtocolService,
@inject(ElectronUpdater)
private readonly updater: ElectronUpdater,
@inject(ElectronRouter)
private readonly router: ElectronRouter,
@inject(TypeORM)
private readonly db: TypeORM,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(ElectronDevtools)
private readonly devTools: ElectronDevtools,
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(VideoServer)
private readonly videoServer: VideoServer,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
) { }
private async serviceInit(): Promise<void> {
this.mainWindow.init();
}
private async vendorInit() {
await this.db.init({ dbPath: db });
this.updater.init();
this.devTools.init();
}
async init(): Promise<void> {
this.protocol.create();
this.router.init();
// vendor
await this.vendorInit();
// service
await this.serviceInit();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
this.mainWindow.init();
}
});
this.initAppTheme();
this.initLanguage();
this.resetDownloadStatus();
this.initTray();
const local = this.store.get("local");
this.videoServer.start({ local });
// Start the download service
this.downloaderServer.start({
logDir: logDir,
localDir: this.store.get("local"),
deleteSegments: this.store.get("deleteSegments"),
proxy: this.store.get("proxy"),
useProxy: this.store.get("useProxy"),
maxRunner: this.store.get("maxRunner"),
});
this.store.onDidChange("maxRunner", (maxRunner) => {
this.downloaderServer.changeConfig({ maxRunner: maxRunner || 1 });
});
this.store.onDidChange("proxy", (proxy) => {
this.downloaderServer.changeConfig({ proxy: proxy || "" });
});
}
initAppTheme(): void {
const theme = this.store.get("theme");
nativeTheme.themeSource = theme;
}
initLanguage(): void {
const language = this.store.get("language");
i18n.changeLanguage(language);
}
initTray() {
const iconPath = path.resolve(__dirname, isMac ? TrayIconLight : TrayIcon);
const icon = nativeImage.createFromPath(iconPath);
const tray = new Tray(icon);
tray.setToolTip("Media Go");
tray.addListener("click", () => {
this.mainWindow.init();
});
const contextMenu = Menu.buildFromTemplate([
{
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> {
// If data in the downloading state still fails after the restart, all downloads fail
const videos = await this.downloadTaskService.findActiveTasks();
const videoIds = videos.map((video) => video.id);
await this.downloadTaskService.setStatus(videoIds, DownloadStatus.Failed);
}
secondInstance = (event: Event, commandLine: string[]) => {
const url = commandLine.pop() || "";
this.mainWindow.showWindow(url);
};
handleOpenUrl(url: string): void {
this.mainWindow.handleUrl(url);
}
send(url: string): void {
this.mainWindow.send("url-params", url);
}
}
@@ -1,35 +1,35 @@
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_CONVERSION,
type Controller,
type ConversionPagination,
DELETE_CONVERSION,
GET_CONVERSIONS,
} from "@mediago/shared-common";
import { type ConversionService, handle, TYPES, type Conversion } from "@mediago/shared-node";
import type { IpcMainEvent } from "electron/main";
import { inject, injectable } from "inversify";
@injectable()
@provide(TYPES.Controller)
export default class ConversionController implements Controller {
constructor(
@inject(TYPES.ConversionService)
private readonly conversionService: ConversionService,
) {}
@handle(GET_CONVERSIONS)
async getConversions(e: IpcMainEvent, pagination: ConversionPagination) {
return await this.conversionService.getConversions(pagination);
}
@handle(ADD_CONVERSION)
async addConversion(e: IpcMainEvent, conversion: Conversion) {
return await this.conversionService.addConversion(conversion);
}
@handle(DELETE_CONVERSION)
async deleteConversion(e: IpcMainEvent, id: number) {
return await this.conversionService.deleteConversion(id);
}
}
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_CONVERSION,
type Controller,
type ConversionPagination,
DELETE_CONVERSION,
GET_CONVERSIONS,
} from "@mediago/shared-common";
import { type ConversionService, handle, TYPES, type Conversion } from "@mediago/shared-node";
import type { IpcMainEvent } from "electron/main";
import { inject, injectable } from "inversify";
@injectable()
@provide(TYPES.Controller)
export default class ConversionController implements Controller {
constructor(
@inject(TYPES.ConversionService)
private readonly conversionService: ConversionService,
) {}
@handle(GET_CONVERSIONS)
async getConversions(e: IpcMainEvent, pagination: ConversionPagination) {
return await this.conversionService.getConversions(pagination);
}
@handle(ADD_CONVERSION)
async addConversion(e: IpcMainEvent, conversion: Conversion) {
return await this.conversionService.addConversion(conversion);
}
@handle(DELETE_CONVERSION)
async deleteConversion(e: IpcMainEvent, id: number) {
return await this.conversionService.deleteConversion(id);
}
}
@@ -1,111 +1,111 @@
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_DOWNLOAD_ITEMS,
type Controller,
DELETE_DOWNLOAD_ITEM,
type DownloadTask,
type DownloadTaskPagination,
EDIT_DOWNLOAD_ITEM,
GET_DOWNLOAD_ITEMS,
type ListPagination,
SHOW_DOWNLOAD_DIALOG,
START_DOWNLOAD,
STOP_DOWNLOAD,
} from "@mediago/shared-common";
import { DownloadTaskService, handle, TYPES } from "@mediago/shared-node";
import type { IpcMainEvent } from "electron/main";
import { inject, injectable } from "inversify";
import WebviewService from "../services/webview.service";
import ElectronStore from "../vendor/ElectronStore";
import MainWindow from "../windows/main.window";
@injectable()
@provide(TYPES.Controller)
export default class DownloadController implements Controller {
constructor(
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(WebviewService)
private readonly webviewService: WebviewService,
) {}
@handle(SHOW_DOWNLOAD_DIALOG)
async showDownloadDialog(e: IpcMainEvent, data: DownloadTask) {
const image = await this.webviewService.captureView();
this.webviewService.sendToWindow(
SHOW_DOWNLOAD_DIALOG,
data,
image?.toDataURL(),
);
}
@handle(ADD_DOWNLOAD_ITEMS)
async createDownloadTasks(
e: IpcMainEvent,
tasks: Omit<DownloadTask, "id">[],
startDownload?: boolean,
) {
const items = await this.downloadTaskService.createMany(tasks);
// This sends a message to the page notifying it of the update
this.mainWindow.send("download-item-notifier", items);
// Start downloading immediately if requested
if (startDownload) {
const local = this.store.get("local");
const deleteSegments = this.store.get("deleteSegments");
items.forEach((task) => {
this.downloadTaskService.start(task.id, local, deleteSegments);
});
}
return items;
}
@handle(EDIT_DOWNLOAD_ITEM)
async updateDownloadTask(
e: IpcMainEvent,
task: DownloadTask,
startDownload?: boolean,
) {
const item = await this.downloadTaskService.update(task);
// Start downloading immediately if requested
if (startDownload) {
const local = this.store.get("local");
const deleteSegments = this.store.get("deleteSegments");
await this.downloadTaskService.start(item.id, local, deleteSegments);
}
return item;
}
@handle(GET_DOWNLOAD_ITEMS)
async getDownloadTasks(
e: IpcMainEvent,
pagination: DownloadTaskPagination,
): Promise<ListPagination> {
const local = this.store.get("local");
return await this.downloadTaskService.list(pagination, local);
}
@handle(START_DOWNLOAD)
async startDownloadTask(e: IpcMainEvent, vid: number) {
const local = this.store.get("local");
const deleteSegments = this.store.get("deleteSegments");
await this.downloadTaskService.start(vid, local, deleteSegments);
}
@handle(STOP_DOWNLOAD)
async stopDownloadTask(e: IpcMainEvent, id: number) {
return this.downloadTaskService.stop(id);
}
@handle(DELETE_DOWNLOAD_ITEM)
async deleteDownloadTask(e: IpcMainEvent, id: number) {
return await this.downloadTaskService.remove(id);
}
}
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_DOWNLOAD_ITEMS,
type Controller,
DELETE_DOWNLOAD_ITEM,
type DownloadTask,
type DownloadTaskPagination,
EDIT_DOWNLOAD_ITEM,
GET_DOWNLOAD_ITEMS,
type ListPagination,
SHOW_DOWNLOAD_DIALOG,
START_DOWNLOAD,
STOP_DOWNLOAD,
} from "@mediago/shared-common";
import { DownloadTaskService, handle, TYPES } from "@mediago/shared-node";
import type { IpcMainEvent } from "electron/main";
import { inject, injectable } from "inversify";
import WebviewService from "../services/webview.service";
import ElectronStore from "../vendor/ElectronStore";
import MainWindow from "../windows/main.window";
@injectable()
@provide(TYPES.Controller)
export default class DownloadController implements Controller {
constructor(
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(WebviewService)
private readonly webviewService: WebviewService,
) {}
@handle(SHOW_DOWNLOAD_DIALOG)
async showDownloadDialog(e: IpcMainEvent, data: DownloadTask) {
const image = await this.webviewService.captureView();
this.webviewService.sendToWindow(
SHOW_DOWNLOAD_DIALOG,
data,
image?.toDataURL(),
);
}
@handle(ADD_DOWNLOAD_ITEMS)
async createDownloadTasks(
e: IpcMainEvent,
tasks: Omit<DownloadTask, "id">[],
startDownload?: boolean,
) {
const items = await this.downloadTaskService.createMany(tasks);
// This sends a message to the page notifying it of the update
this.mainWindow.send("download-item-notifier", items);
// Start downloading immediately if requested
if (startDownload) {
const local = this.store.get("local");
const deleteSegments = this.store.get("deleteSegments");
items.forEach((task) => {
this.downloadTaskService.start(task.id, local, deleteSegments);
});
}
return items;
}
@handle(EDIT_DOWNLOAD_ITEM)
async updateDownloadTask(
e: IpcMainEvent,
task: DownloadTask,
startDownload?: boolean,
) {
const item = await this.downloadTaskService.update(task);
// Start downloading immediately if requested
if (startDownload) {
const local = this.store.get("local");
const deleteSegments = this.store.get("deleteSegments");
await this.downloadTaskService.start(item.id, local, deleteSegments);
}
return item;
}
@handle(GET_DOWNLOAD_ITEMS)
async getDownloadTasks(
e: IpcMainEvent,
pagination: DownloadTaskPagination,
): Promise<ListPagination> {
const local = this.store.get("local");
return await this.downloadTaskService.list(pagination, local);
}
@handle(START_DOWNLOAD)
async startDownloadTask(e: IpcMainEvent, vid: number) {
const local = this.store.get("local");
const deleteSegments = this.store.get("deleteSegments");
await this.downloadTaskService.start(vid, local, deleteSegments);
}
@handle(STOP_DOWNLOAD)
async stopDownloadTask(e: IpcMainEvent, id: number) {
return this.downloadTaskService.stop(id);
}
@handle(DELETE_DOWNLOAD_ITEM)
async deleteDownloadTask(e: IpcMainEvent, id: number) {
return await this.downloadTaskService.remove(id);
}
}
+498 -498
View File
@@ -1,498 +1,498 @@
import path from "node:path";
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_FAVORITE,
CHECK_UPDATE,
COMBINE_TO_HOME_PAGE,
CONVERT_TO_AUDIO,
type Controller,
DownloadStatus,
EXPORT_DOWNLOAD_LIST,
EXPORT_FAVORITES,
GET_APP_STORE,
GET_DOWNLOAD_LOG,
GET_ENV_PATH,
GET_FAVORITES,
GET_LOCAL_IP,
GET_MACHINE_ID,
GET_PAGE_TITLE,
GET_SHARED_STATE,
GET_VIDEO_FOLDERS,
IMPORT_FAVORITES,
INSTALL_UPDATE,
ON_DOWNLOAD_LIST_CONTEXT_MENU,
ON_FAVORITE_ITEM_CONTEXT_MENU,
OPEN_DIR,
OPEN_URL,
REMOVE_FAVORITE,
SELECT_DOWNLOAD_DIR,
SELECT_FILE,
SET_APP_STORE,
SET_SHARED_STATE,
SHOW_BROWSER_WINDOW,
START_UPDATE,
} from "@mediago/shared-common";
import {
type AppStore,
type ConversionService,
DownloadTaskService,
type EnvPath,
type Favorite,
type FavoriteManagementService,
getLocalIP,
handle,
i18n,
TYPES,
} from "@mediago/shared-node";
import axios from "axios";
import {
clipboard,
dialog,
type IpcMainEvent,
Menu,
type MenuItem,
type MenuItemConstructorOptions,
nativeTheme,
shell,
} from "electron";
import fs from "node:fs/promises";
import { inject, injectable } from "inversify";
import { nanoid } from "nanoid";
import MachineId from "node-machine-id";
import { convertToAudio, db, exePath, workspace } from "../helper/index";
import WebviewService from "../services/webview.service";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
import ElectronUpdater from "../vendor/ElectronUpdater";
import BrowserWindow from "../windows/browser.window";
import MainWindow from "../windows/main.window";
const { machineId } = MachineId;
@injectable()
@provide(TYPES.Controller)
export default class HomeController implements Controller {
private sharedState: Record<string, unknown> = {};
private readonly appStoreHandlers: Partial<{
[K in keyof AppStore]: (value: AppStore[K]) => void | Promise<void>;
}> = {
useProxy: (value) => {
const proxy = this.store.get("proxy");
this.webviewService.setProxy(value, proxy);
},
proxy: (value) => {
const useProxy = this.store.get("useProxy");
if (useProxy) {
this.webviewService.setProxy(true, value);
}
},
blockAds: (value) => {
this.webviewService.setBlocking(value);
},
theme: (value) => {
nativeTheme.themeSource = value;
},
isMobile: (value) => {
this.webviewService.setUserAgent(value);
},
privacy: (value) => {
this.webviewService.setDefaultSession(value);
},
language: async (value) => {
await i18n.changeLanguage(value);
},
allowBeta: (value) => {
this.updater.changeAllowBeta(value);
},
audioMuted: (value) => {
this.webviewService.setAudioMuted(value);
},
};
constructor(
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(TYPES.FavoriteManagementService)
private readonly favoriteService: FavoriteManagementService,
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(BrowserWindow)
private readonly browserWindow: BrowserWindow,
@inject(WebviewService)
private readonly webviewService: WebviewService,
@inject(TYPES.ConversionService)
private readonly conversionService: ConversionService,
@inject(ElectronLogger)
private readonly logger: ElectronLogger,
@inject(ElectronUpdater)
private readonly updater: ElectronUpdater,
) {}
@handle(GET_ENV_PATH)
async getEnvPath(): Promise<EnvPath> {
return {
binPath: exePath,
dbPath: db,
workspace: workspace,
platform: process.platform,
local: this.store.get("local"),
};
}
@handle(GET_FAVORITES)
getFavorites() {
return this.favoriteService.getFavorites();
}
@handle(ADD_FAVORITE)
addFavorite(e: IpcMainEvent, favorite: Favorite) {
return this.favoriteService.addFavorite(favorite);
}
@handle(REMOVE_FAVORITE)
removeFavorite(e: IpcMainEvent, id: number): Promise<void> {
return this.favoriteService.removeFavorite(id);
}
@handle(GET_APP_STORE)
getAppStore() {
return this.store.store;
}
@handle(ON_FAVORITE_ITEM_CONTEXT_MENU)
async onFavoriteItemContextMenu(e: IpcMainEvent, id: number) {
const send = (action: string) => {
this.mainWindow.send("favorite-item-event", {
action,
payload: id,
});
};
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: i18n.t("open"),
click: () => {
send("open");
},
},
{ type: "separator" },
{
label: i18n.t("delete"),
click: () => {
send("delete");
},
},
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
}
@handle(SELECT_DOWNLOAD_DIR)
async selectDownloadDir(): Promise<string> {
const window = this.mainWindow.window;
if (!window) return Promise.reject(i18n.t("noMainWindow"));
const result = await dialog.showOpenDialog(window, {
properties: ["openDirectory"],
});
if (!result.canceled) {
const dir = result.filePaths[0];
this.store.set("local", dir);
return dir;
}
return "";
}
@handle(SET_APP_STORE)
async setAppStore<K extends keyof AppStore>(
_e: IpcMainEvent,
key: K,
val: AppStore[K],
) {
const handler = this.appStoreHandlers[key];
if (handler) {
await handler(val);
}
this.store.set(key, val);
}
@handle(OPEN_DIR)
async openDir(_e: IpcMainEvent, dir: string) {
await shell.openPath(dir);
}
@handle(OPEN_URL)
async openUrl(e: IpcMainEvent, url: string) {
await shell.openExternal(url);
}
@handle(ON_DOWNLOAD_LIST_CONTEXT_MENU)
async downloadListContextMenu(e: IpcMainEvent, id: number) {
const send = (action: string) => {
this.mainWindow.send("download-item-event", {
action,
payload: id,
});
};
const item = await this.downloadTaskService.list(
{ current: 1, pageSize: 1 },
this.store.get("local"),
);
const task = item.list.find((t: any) => t.id === id);
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: i18n.t("copyLinkAddress"),
click: () => {
clipboard.writeText(task?.url || "");
},
},
{
label: i18n.t("select"),
click: () => {
send("select");
},
},
{
label: i18n.t("download"),
visible: task?.status !== DownloadStatus.Success,
click: () => {
send("download");
},
},
{
label: i18n.t("refresh"),
click: () => {
send("refresh");
},
},
{ type: "separator" },
{
label: i18n.t("delete"),
click: () => {
send("delete");
},
},
];
if (task?.status === DownloadStatus.Success && task?.exists && task?.file) {
template.unshift(
{
label: i18n.t("openFolder"),
click: () => {
shell.showItemInFolder(task.file!);
},
},
{
label: i18n.t("openFile"),
click: () => {
shell.openPath(task.file!);
},
},
{ type: "separator" },
);
}
const menu = Menu.buildFromTemplate(template);
menu.popup();
}
@handle(CONVERT_TO_AUDIO)
async convertToAudio(e: IpcMainEvent, id: number) {
const conversion = await this.conversionService.findByIdOrFail(id);
const local = this.store.get("local");
const input = conversion.path;
const outName = `${path.basename(input, path.extname(input))}.mp3`;
const output = path.join(local, outName);
const exist = await fs.stat(input);
if (exist) {
return await convertToAudio(input, output);
} else {
return Promise.reject(i18n.t("noFileFound"));
}
}
@handle(SHOW_BROWSER_WINDOW)
async showBrowserWindow() {
this.browserWindow.showWindow();
}
@handle(COMBINE_TO_HOME_PAGE)
async combineToHomePage() {
// Close browser window
this.browserWindow.hideWindow();
// Modify the properties in the Settings
this.store.set("openInNewWindow", false);
}
@handle(GET_LOCAL_IP)
async getLocalIp() {
return getLocalIP();
}
@handle(GET_SHARED_STATE)
async getSharedState() {
return this.sharedState;
}
@handle(SET_SHARED_STATE)
async setSharedState(event: IpcMainEvent, state: any) {
this.sharedState = state;
}
@handle(GET_DOWNLOAD_LOG)
async getDownloadLog(event: IpcMainEvent, id: number) {
try {
return await this.downloadTaskService.getLog(id);
} catch {
return "";
}
}
@handle(SELECT_FILE)
async selectFile() {
const window = this.mainWindow.window;
if (!window) return Promise.reject(i18n.t("noMainWindow"));
const result = await dialog.showOpenDialog(window, {
properties: ["openFile"],
});
if (!result.canceled) {
return result.filePaths[0];
}
return "";
}
@handle(GET_MACHINE_ID)
async getMachineId() {
try {
const id = this.store.get("machineId");
if (id) return id;
const newId = await machineId();
this.store.set("machineId", newId);
return newId;
} catch {
const id = this.store.get("machineId");
if (id) return id;
const newId = nanoid();
this.store.set("machineId", newId);
return newId;
}
}
@handle(EXPORT_FAVORITES)
async exportFavorites() {
const json = await this.favoriteService.exportFavorites();
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.readFile(filePath, "utf-8");
await this.favoriteService.importFavorites(json as any);
}
}
@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 txt = await this.downloadTaskService.exportList();
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.downloadTaskService.listFolders();
}
@handle(GET_PAGE_TITLE)
async getPageTitle(
event: IpcMainEvent,
url: string,
): Promise<{ data: string }> {
try {
const response = await axios.get(url, {
timeout: 10000,
maxRedirects: 5,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
},
});
const html = response.data;
let title = "无标题";
const patterns = [
/<meta\s+property="og:title"\s+content="([^"]*)"/i,
/<meta\s+name="title"\s+content="([^"]*)"/i,
/<title[^>]*>([^<]+)<\/title>/i,
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match && match[1]) {
title = match[1].trim();
break;
}
}
return { data: title };
} catch (error) {
console.error("Error fetching page title:", error);
return { data: "无标题" };
}
}
}
import path from "node:path";
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_FAVORITE,
CHECK_UPDATE,
COMBINE_TO_HOME_PAGE,
CONVERT_TO_AUDIO,
type Controller,
DownloadStatus,
EXPORT_DOWNLOAD_LIST,
EXPORT_FAVORITES,
GET_APP_STORE,
GET_DOWNLOAD_LOG,
GET_ENV_PATH,
GET_FAVORITES,
GET_LOCAL_IP,
GET_MACHINE_ID,
GET_PAGE_TITLE,
GET_SHARED_STATE,
GET_VIDEO_FOLDERS,
IMPORT_FAVORITES,
INSTALL_UPDATE,
ON_DOWNLOAD_LIST_CONTEXT_MENU,
ON_FAVORITE_ITEM_CONTEXT_MENU,
OPEN_DIR,
OPEN_URL,
REMOVE_FAVORITE,
SELECT_DOWNLOAD_DIR,
SELECT_FILE,
SET_APP_STORE,
SET_SHARED_STATE,
SHOW_BROWSER_WINDOW,
START_UPDATE,
} from "@mediago/shared-common";
import {
type AppStore,
type ConversionService,
DownloadTaskService,
type EnvPath,
type Favorite,
type FavoriteManagementService,
getLocalIP,
handle,
i18n,
TYPES,
} from "@mediago/shared-node";
import axios from "axios";
import {
clipboard,
dialog,
type IpcMainEvent,
Menu,
type MenuItem,
type MenuItemConstructorOptions,
nativeTheme,
shell,
} from "electron";
import fs from "node:fs/promises";
import { inject, injectable } from "inversify";
import { nanoid } from "nanoid";
import MachineId from "node-machine-id";
import { convertToAudio, db, exePath, workspace } from "../helper/index";
import WebviewService from "../services/webview.service";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
import ElectronUpdater from "../vendor/ElectronUpdater";
import BrowserWindow from "../windows/browser.window";
import MainWindow from "../windows/main.window";
const { machineId } = MachineId;
@injectable()
@provide(TYPES.Controller)
export default class HomeController implements Controller {
private sharedState: Record<string, unknown> = {};
private readonly appStoreHandlers: Partial<{
[K in keyof AppStore]: (value: AppStore[K]) => void | Promise<void>;
}> = {
useProxy: (value) => {
const proxy = this.store.get("proxy");
this.webviewService.setProxy(value, proxy);
},
proxy: (value) => {
const useProxy = this.store.get("useProxy");
if (useProxy) {
this.webviewService.setProxy(true, value);
}
},
blockAds: (value) => {
this.webviewService.setBlocking(value);
},
theme: (value) => {
nativeTheme.themeSource = value;
},
isMobile: (value) => {
this.webviewService.setUserAgent(value);
},
privacy: (value) => {
this.webviewService.setDefaultSession(value);
},
language: async (value) => {
await i18n.changeLanguage(value);
},
allowBeta: (value) => {
this.updater.changeAllowBeta(value);
},
audioMuted: (value) => {
this.webviewService.setAudioMuted(value);
},
};
constructor(
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(TYPES.FavoriteManagementService)
private readonly favoriteService: FavoriteManagementService,
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(BrowserWindow)
private readonly browserWindow: BrowserWindow,
@inject(WebviewService)
private readonly webviewService: WebviewService,
@inject(TYPES.ConversionService)
private readonly conversionService: ConversionService,
@inject(ElectronLogger)
private readonly logger: ElectronLogger,
@inject(ElectronUpdater)
private readonly updater: ElectronUpdater,
) {}
@handle(GET_ENV_PATH)
async getEnvPath(): Promise<EnvPath> {
return {
binPath: exePath,
dbPath: db,
workspace: workspace,
platform: process.platform,
local: this.store.get("local"),
};
}
@handle(GET_FAVORITES)
getFavorites() {
return this.favoriteService.getFavorites();
}
@handle(ADD_FAVORITE)
addFavorite(e: IpcMainEvent, favorite: Favorite) {
return this.favoriteService.addFavorite(favorite);
}
@handle(REMOVE_FAVORITE)
removeFavorite(e: IpcMainEvent, id: number): Promise<void> {
return this.favoriteService.removeFavorite(id);
}
@handle(GET_APP_STORE)
getAppStore() {
return this.store.store;
}
@handle(ON_FAVORITE_ITEM_CONTEXT_MENU)
async onFavoriteItemContextMenu(e: IpcMainEvent, id: number) {
const send = (action: string) => {
this.mainWindow.send("favorite-item-event", {
action,
payload: id,
});
};
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: i18n.t("open"),
click: () => {
send("open");
},
},
{ type: "separator" },
{
label: i18n.t("delete"),
click: () => {
send("delete");
},
},
];
const menu = Menu.buildFromTemplate(template);
menu.popup();
}
@handle(SELECT_DOWNLOAD_DIR)
async selectDownloadDir(): Promise<string> {
const window = this.mainWindow.window;
if (!window) return Promise.reject(i18n.t("noMainWindow"));
const result = await dialog.showOpenDialog(window, {
properties: ["openDirectory"],
});
if (!result.canceled) {
const dir = result.filePaths[0];
this.store.set("local", dir);
return dir;
}
return "";
}
@handle(SET_APP_STORE)
async setAppStore<K extends keyof AppStore>(
_e: IpcMainEvent,
key: K,
val: AppStore[K],
) {
const handler = this.appStoreHandlers[key];
if (handler) {
await handler(val);
}
this.store.set(key, val);
}
@handle(OPEN_DIR)
async openDir(_e: IpcMainEvent, dir: string) {
await shell.openPath(dir);
}
@handle(OPEN_URL)
async openUrl(e: IpcMainEvent, url: string) {
await shell.openExternal(url);
}
@handle(ON_DOWNLOAD_LIST_CONTEXT_MENU)
async downloadListContextMenu(e: IpcMainEvent, id: number) {
const send = (action: string) => {
this.mainWindow.send("download-item-event", {
action,
payload: id,
});
};
const item = await this.downloadTaskService.list(
{ current: 1, pageSize: 1 },
this.store.get("local"),
);
const task = item.list.find((t: any) => t.id === id);
const template: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: i18n.t("copyLinkAddress"),
click: () => {
clipboard.writeText(task?.url || "");
},
},
{
label: i18n.t("select"),
click: () => {
send("select");
},
},
{
label: i18n.t("download"),
visible: task?.status !== DownloadStatus.Success,
click: () => {
send("download");
},
},
{
label: i18n.t("refresh"),
click: () => {
send("refresh");
},
},
{ type: "separator" },
{
label: i18n.t("delete"),
click: () => {
send("delete");
},
},
];
if (task?.status === DownloadStatus.Success && task?.exists && task?.file) {
template.unshift(
{
label: i18n.t("openFolder"),
click: () => {
shell.showItemInFolder(task.file!);
},
},
{
label: i18n.t("openFile"),
click: () => {
shell.openPath(task.file!);
},
},
{ type: "separator" },
);
}
const menu = Menu.buildFromTemplate(template);
menu.popup();
}
@handle(CONVERT_TO_AUDIO)
async convertToAudio(e: IpcMainEvent, id: number) {
const conversion = await this.conversionService.findByIdOrFail(id);
const local = this.store.get("local");
const input = conversion.path;
const outName = `${path.basename(input, path.extname(input))}.mp3`;
const output = path.join(local, outName);
const exist = await fs.stat(input);
if (exist) {
return await convertToAudio(input, output);
} else {
return Promise.reject(i18n.t("noFileFound"));
}
}
@handle(SHOW_BROWSER_WINDOW)
async showBrowserWindow() {
this.browserWindow.showWindow();
}
@handle(COMBINE_TO_HOME_PAGE)
async combineToHomePage() {
// Close browser window
this.browserWindow.hideWindow();
// Modify the properties in the Settings
this.store.set("openInNewWindow", false);
}
@handle(GET_LOCAL_IP)
async getLocalIp() {
return getLocalIP();
}
@handle(GET_SHARED_STATE)
async getSharedState() {
return this.sharedState;
}
@handle(SET_SHARED_STATE)
async setSharedState(event: IpcMainEvent, state: any) {
this.sharedState = state;
}
@handle(GET_DOWNLOAD_LOG)
async getDownloadLog(event: IpcMainEvent, id: number) {
try {
return await this.downloadTaskService.getLog(id);
} catch {
return "";
}
}
@handle(SELECT_FILE)
async selectFile() {
const window = this.mainWindow.window;
if (!window) return Promise.reject(i18n.t("noMainWindow"));
const result = await dialog.showOpenDialog(window, {
properties: ["openFile"],
});
if (!result.canceled) {
return result.filePaths[0];
}
return "";
}
@handle(GET_MACHINE_ID)
async getMachineId() {
try {
const id = this.store.get("machineId");
if (id) return id;
const newId = await machineId();
this.store.set("machineId", newId);
return newId;
} catch {
const id = this.store.get("machineId");
if (id) return id;
const newId = nanoid();
this.store.set("machineId", newId);
return newId;
}
}
@handle(EXPORT_FAVORITES)
async exportFavorites() {
const json = await this.favoriteService.exportFavorites();
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.readFile(filePath, "utf-8");
await this.favoriteService.importFavorites(json as any);
}
}
@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 txt = await this.downloadTaskService.exportList();
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.downloadTaskService.listFolders();
}
@handle(GET_PAGE_TITLE)
async getPageTitle(
event: IpcMainEvent,
url: string,
): Promise<{ data: string }> {
try {
const response = await axios.get(url, {
timeout: 10000,
maxRedirects: 5,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
},
});
const html = response.data;
let title = "无标题";
const patterns = [
/<meta\s+property="og:title"\s+content="([^"]*)"/i,
/<meta\s+name="title"\s+content="([^"]*)"/i,
/<title[^>]*>([^<]+)<\/title>/i,
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match && match[1]) {
title = match[1].trim();
break;
}
}
return { data: title };
} catch (error) {
console.error("Error fetching page title:", error);
return { data: "无标题" };
}
}
}
+42 -40
View File
@@ -1,40 +1,42 @@
import path, { join } from "node:path";
import { URL } from "node:url";
import { provide } from "@inversifyjs/binding-decorators";
import { app, protocol } from "electron";
import isDev from "electron-is-dev";
import { stat, readFile } from "node:fs/promises";
import { injectable } from "inversify";
import mime from "mime-types";
import { defaultScheme } from "../helper/index";
@injectable()
@provide()
export default class ProtocolService {
create(): void {
if (isDev) return;
protocol.handle(defaultScheme, async (req) => {
const pathName = new URL(req.url).pathname;
let filePath = join(__dirname, "../renderer", pathName);
const stats = await stat(filePath);
if (!stats.isFile()) {
// If the file is not found, return index.html directly, react history mode
filePath = join(__dirname, "../renderer/index.html");
}
const mimeType = mime.lookup(filePath);
const data = await readFile(filePath);
return new Response(data, {
headers: { "Content-Type": mimeType || "text/html" },
});
});
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(defaultScheme, process.execPath, [path.resolve(process.argv[1])]);
}
} else {
app.setAsDefaultProtocolClient(defaultScheme);
}
}
}
import path, { join } from "node:path";
import { URL } from "node:url";
import { provide } from "@inversifyjs/binding-decorators";
import { app, protocol } from "electron";
import isDev from "electron-is-dev";
import { access, readFile } from "node:fs/promises";
import { injectable } from "inversify";
import mime from "mime-types";
import { defaultScheme } from "../helper/index";
@injectable()
@provide()
export default class ProtocolService {
create(): void {
if (isDev) return;
protocol.handle(defaultScheme, async (req) => {
const pathName = new URL(req.url).pathname;
let filePath = join(__dirname, "../renderer", pathName);
try {
await access(filePath);
} catch {
filePath = join(__dirname, "../renderer/index.html");
}
const mimeType = mime.lookup(filePath);
const data = await readFile(filePath);
return new Response(data, {
headers: { "Content-Type": mimeType || "text/html" },
});
});
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(defaultScheme, process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient(defaultScheme);
}
}
}
+128 -128
View File
@@ -1,128 +1,128 @@
import { spawn } from "node:child_process";
import EventEmitter from "node:events";
import fs from "node:fs/promises";
import https from "node:https";
import { createRequire } from "node:module";
import { LRUCache } from "lru-cache";
import fetch from "node-fetch";
import { loadModule } from "@mediago/shared-node";
import path from "node:path";
const ffmpegModule = loadModule("@mediago/deps");
export const ffmpegPath = path.resolve(
path.dirname(ffmpegModule),
"bin",
"ffmpeg",
);
export * from "./variables";
export { fetchWrapper as fetch };
export { error, type IpcResponse, success } from "./ipcResponse";
function fetchWrapper(url: string) {
const options = {
agent: new https.Agent({
rejectUnauthorized: false,
}),
};
return fetch(url, options);
}
export function noop() {}
const options: LRUCache.OptionsMaxLimit<string, boolean, unknown> = {
max: 5000,
// how long to live in ms
ttl: 1000 * 60 * 5,
};
export const urlCache = new LRUCache(options);
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();
// Determine whether it is a function of deeplink
export function isDeeplink(url: string): boolean {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:";
} catch {
return false;
}
}
/**
* from url get file extension
* @param url URL string
* @returns string file extension (without dot), if no extension returns empty string
*/
export function getFileExtension(url: string): string {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const extension = pathname.split(".").pop() || "";
return extension.toLowerCase();
} catch {
return "";
}
}
export function fileExists(path: string): Promise<boolean> {
return fs
.access(path, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
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));
}
});
});
};
const require = createRequire(import.meta.url);
export const preloadUrl = require.resolve("@mediago/electron-preload");
export const pluginUrl = require.resolve("@mediago/browser-extension");
import { spawn } from "node:child_process";
import EventEmitter from "node:events";
import fs from "node:fs/promises";
import https from "node:https";
import { createRequire } from "node:module";
import { LRUCache } from "lru-cache";
import fetch from "node-fetch";
import { loadModule } from "@mediago/shared-node";
import path from "node:path";
const ffmpegModule = loadModule("@mediago/deps");
export const ffmpegPath = path.resolve(
path.dirname(ffmpegModule),
"bin",
"ffmpeg",
);
export * from "./variables";
export { fetchWrapper as fetch };
export { error, type IpcResponse, success } from "./ipcResponse";
function fetchWrapper(url: string) {
const options = {
agent: new https.Agent({
rejectUnauthorized: false,
}),
};
return fetch(url, options);
}
export function noop() {}
const options: LRUCache.OptionsMaxLimit<string, boolean, unknown> = {
max: 5000,
// how long to live in ms
ttl: 1000 * 60 * 5,
};
export const urlCache = new LRUCache(options);
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();
// Determine whether it is a function of deeplink
export function isDeeplink(url: string): boolean {
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:";
} catch {
return false;
}
}
/**
* from url get file extension
* @param url URL string
* @returns string file extension (without dot), if no extension returns empty string
*/
export function getFileExtension(url: string): string {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const extension = pathname.split(".").pop() || "";
return extension.toLowerCase();
} catch {
return "";
}
}
export function fileExists(path: string): Promise<boolean> {
return fs
.access(path, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
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));
}
});
});
};
const require = createRequire(import.meta.url);
export const preloadUrl = require.resolve("@mediago/electron-preload");
export const pluginUrl = require.resolve("@mediago/browser-extension");
+30 -30
View File
@@ -1,30 +1,30 @@
import { resolve } from "node:path";
import { app } from "electron";
export const appData = app.getPath("appData");
export const download = app.getPath("downloads");
export const exePath = app.getPath("userData");
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 const appName = process.env.APP_NAME || "mediago";
export const workspace = resolve(appData, appName);
export const defaultScheme = "mediago";
export const PERSIST_MEDIAGO = "persist:mediago";
export const PERSIST_WEBVIEW = "persist:webview";
export const PRIVACY_WEBVIEW = "webview";
export const db = resolve(workspace, "app.db");
export const logDir = resolve(workspace, "logs");
// 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";
import { resolve } from "node:path";
import { app } from "electron";
export const appData = app.getPath("appData");
export const download = app.getPath("downloads");
export const exePath = app.getPath("userData");
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 const appName = process.env.APP_NAME || "mediago";
export const workspace = resolve(appData, appName);
export const defaultScheme = "mediago";
export const PERSIST_MEDIAGO = "persist:mediago";
export const PERSIST_WEBVIEW = "persist:webview";
export const PRIVACY_WEBVIEW = "webview";
export const db = resolve(workspace, "app.db");
export const logDir = resolve(workspace, "logs");
// 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";
+389 -389
View File
@@ -1,389 +1,389 @@
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { ElectronBlocker } from "@ghostery/adblocker-electron";
import { provide } from "@inversifyjs/binding-decorators";
import { DownloadTaskService, i18n } from "@mediago/shared-node";
import {
type Event,
type HandlerDetails,
session,
WebContentsView,
} from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import {
fetch,
isDeeplink,
mobileUA,
PERSIST_WEBVIEW,
PRIVACY_WEBVIEW,
pcUA,
pluginUrl,
} from "../helper/index";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
import BrowserWindow from "../windows/browser.window";
import MainWindow from "../windows/main.window";
import { SniffingHelper, type SourceParams } from "./sniffing-helper.service";
const require = createRequire(import.meta.url);
const preload = require.resolve("@mediago/electron-preload");
@injectable()
@provide()
export default class WebviewService {
private view: WebContentsView | null = null;
private blocker?: ElectronBlocker;
private defaultSession: string;
private viewShow = false;
constructor(
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(ElectronLogger)
private readonly logger: ElectronLogger,
@inject(BrowserWindow)
private readonly browserWindow: BrowserWindow,
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(SniffingHelper)
private readonly sniffingHelper: SniffingHelper,
) {
// Initialize the blocker
this.initBlocker();
const { useProxy, proxy, privacy } = this.store.store;
this.sniffingHelper.start(privacy);
this.sniffingHelper.on("source", this.onSource);
this.setDefaultSession(privacy, true);
this.setProxy(useProxy, proxy);
}
async init(): Promise<void> {
this.view = new WebContentsView({
webPreferences: {
partition: this.defaultSession,
preload: preload,
},
});
if (isDev) {
this.view.webContents.openDevTools();
}
this.view.setBackgroundColor("#fff");
const { isMobile, audioMuted } = this.store.store;
this.setAudioMuted(audioMuted);
this.setUserAgent(isMobile);
this.view.webContents.on("dom-ready", this.onDomReady);
this.view.webContents.on("did-navigate", this.onDidNavigate);
this.view.webContents.on("did-fail-load", this.onDidFailLoad);
this.view.webContents.on("did-navigate-in-page", this.onDidNavigateInPage);
this.view.webContents.on("will-navigate", this.onWillNavigate);
this.view.webContents.setWindowOpenHandler(this.onOpenNewWindow);
}
onWillNavigate = (e: Event, url: string) => {
if (isDeeplink(url)) {
e.preventDefault();
}
};
onDomReady = () => {
if (!this.view) return;
const pageInfo = this.getPageInfo();
this.sniffingHelper.update(pageInfo);
this.window.webContents.send("webview-dom-ready", pageInfo);
};
onDidNavigate = async () => {
if (!this.view) return;
const pageInfo = this.getPageInfo();
this.sniffingHelper.update(pageInfo);
this.window.webContents.send("webview-did-navigate", pageInfo);
try {
// if (isDev && import.meta.env.APP_DEBUG_PLUGINS === "true") {
// const content =
// 'function addScript(src){const script=document.createElement("script");script.src=src;script.type="module";document.body.appendChild(script)}addScript("http://localhost:8080/src/main.ts");';
// await this.view.webContents.executeJavaScript(content);
// } else {
const content = await readFile(pluginUrl, "utf-8");
await this.view.webContents.executeJavaScript(content);
// }
} catch {
// empty
}
this.sniffingHelper.checkPageInfo();
};
onDidFailLoad = (e: Event, code: number, desc: string) => {
// this.window.webContents.send("webview-fail-load", { code, desc });
this.logger.error(`[Webview] fail load: ${code} ${desc}`);
};
onDidNavigateInPage = () => {
if (!this.view) return;
const pageInfo = this.getPageInfo();
this.sniffingHelper.update(pageInfo);
this.window.webContents.send("webview-did-navigate-in-page", pageInfo);
};
onOpenNewWindow = ({ url }: HandlerDetails) => {
this.loadURL(url);
return { action: "deny" } as { action: "deny" };
};
onSource = async (item: SourceParams) => {
if (!this.view) return;
// Here you need to determine whether to use a browser plug-in
const useExtension = this.store.get("useExtension");
if (useExtension) {
// this.view.webContents.send("webview-link-message", item);
this.window.webContents.send("webview-link-message", item);
} else {
const video = await this.downloadTaskService.findByName(item.name);
if (video) {
item.name = `${item.name}-${Date.now()}`;
}
const res = await this.downloadTaskService.create(item);
const mainWebContents = this.mainWindow.window?.webContents;
if (!mainWebContents) return;
// This sends a message to the page notifying it of the update
mainWebContents.send("download-item-notifier", res);
}
};
getPageInfo() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
return {
title: this.view.webContents.getTitle(),
url: this.view.webContents.getURL(),
};
}
getBounds(): Electron.Rectangle {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
return this.view.getBounds();
}
setBounds(bounds: Electron.Rectangle): void {
if (!this.view) return;
this.view.setBounds(bounds);
}
setBackgroundColor(color: string): void {
if (!this.view) return;
this.view.setBackgroundColor(color);
}
show() {
if (!this.view) return;
this.window.contentView.addChildView(this.view);
this.viewShow = true;
}
hide() {
if (!this.view) return;
this.window.contentView.removeChildView(this.view);
this.viewShow = false;
}
loadURL(url: string) {
// Start loading url
if (!this.view) {
this.init();
}
if (!this.view) return;
// 1. Stop current navigation
this.view.webContents.stop();
// 2. Load a new url
this.view.webContents.loadURL(url);
}
async goBack() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
const { webContents } = this.view;
if (webContents.canGoBack()) {
webContents.goBack();
return true;
} else {
this.destroyView();
return false;
}
}
async reload() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
this.view.webContents.reload();
}
async goHome() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
this.view.webContents.stop();
this.view.webContents.clearHistory();
this.destroyView();
}
get window() {
if (this.browserWindow.window) return this.browserWindow.window;
if (this.mainWindow.window) return this.mainWindow.window;
throw new Error(i18n.t("currentWindowNotFound"));
}
private get session() {
return session.fromPartition(this.defaultSession);
}
private enableProxy(proxy: string) {
if (!proxy) {
this.logger.error(`[Proxy] proxy address is empty`);
return;
}
// Process the validity of the proxy address
if (!/https?:\/\//.test(proxy)) {
proxy = `http://${proxy}`;
}
this.session.setProxy({ proxyRules: proxy });
this.logger.info(`[Proxy] enable proxy: ${proxy}`);
}
private disableProxy() {
this.session.setProxy({ proxyRules: "" });
this.logger.info(`[Proxy] disable proxy`);
}
setProxy(useProxy: boolean, proxy: string): void {
if (useProxy) {
this.enableProxy(proxy);
} else {
this.disableProxy();
}
}
setBlocking(enableBlocking: boolean): void {
if (enableBlocking) {
this.enableBlocking();
} else {
this.disableBlocking();
}
}
async initBlocker() {
this.blocker = await ElectronBlocker.fromPrebuiltAdsAndTracking(fetch);
const enableBlocking = this.store.get("blockAds");
this.setBlocking(enableBlocking);
}
private enableBlocking() {
if (!this.blocker) {
this.logger.error(`[AdBlocker] enable failed(not initialized)`);
return;
}
if (this.blocker.isBlockingEnabled(this.session)) {
return;
}
this.blocker.enableBlockingInSession(this.session);
this.logger.info(`[AdBlocker] enable`);
}
private disableBlocking() {
if (!this.blocker) {
this.logger.error(`[AdBlocker] disable failed(not initialized)`);
return;
}
if (!this.blocker.isBlockingEnabled(this.session)) {
return;
}
this.blocker.disableBlockingInSession(this.session);
this.logger.info(`[AdBlocker] disable`);
}
setAudioMuted(audioMuted?: boolean) {
if (!this.view) return;
this.view.webContents.setAudioMuted(audioMuted ? true : false);
this.logger.info(`Play audio: ${!audioMuted}`);
}
setUserAgent(isMobile?: boolean) {
if (!this.view) return;
if (isMobile) {
this.view.webContents.setUserAgent(mobileUA);
} else {
this.view.webContents.setUserAgent(pcUA);
}
this.logger.info(`[UA] User-Agent: ${isMobile ? "mobile" : "PC"}`);
}
async captureView(): Promise<Electron.NativeImage | null> {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
if (!this.viewShow) {
return null;
}
return this.view.webContents.capturePage();
}
sendToWindow(channel: string, ...args: unknown[]) {
this.window.webContents.send(channel, ...args);
}
destroyView() {
if (this.view && this.window) {
this.view.webContents.close();
this.window.contentView.removeChildView(this.view);
}
// FIXME: To avoid memory leaks, the view needs to be destroyed here
this.view = null;
}
async clearCache() {
await this.session.clearCache();
await this.session.clearStorageData();
}
setDefaultSession(isPrivacy = false, init = false) {
this.logger.info(`[Session] ${isPrivacy ? "Privacy" : "Persist"} Mode`);
if (isPrivacy) {
this.defaultSession = PRIVACY_WEBVIEW;
} else {
this.defaultSession = PERSIST_WEBVIEW;
}
if (this.view) {
const { useProxy, proxy } = this.store.store;
this.destroyView();
this.init();
this.setProxy(useProxy, proxy);
this.sniffingHelper.start(isPrivacy);
}
if (!init) {
this.window.webContents.send("change-privacy");
}
}
}
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { ElectronBlocker } from "@ghostery/adblocker-electron";
import { provide } from "@inversifyjs/binding-decorators";
import { DownloadTaskService, i18n } from "@mediago/shared-node";
import {
type Event,
type HandlerDetails,
session,
WebContentsView,
} from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import {
fetch,
isDeeplink,
mobileUA,
PERSIST_WEBVIEW,
PRIVACY_WEBVIEW,
pcUA,
pluginUrl,
} from "../helper/index";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
import BrowserWindow from "../windows/browser.window";
import MainWindow from "../windows/main.window";
import { SniffingHelper, type SourceParams } from "./sniffing-helper.service";
const require = createRequire(import.meta.url);
const preload = require.resolve("@mediago/electron-preload");
@injectable()
@provide()
export default class WebviewService {
private view: WebContentsView | null = null;
private blocker?: ElectronBlocker;
private defaultSession: string;
private viewShow = false;
constructor(
@inject(MainWindow)
private readonly mainWindow: MainWindow,
@inject(ElectronLogger)
private readonly logger: ElectronLogger,
@inject(BrowserWindow)
private readonly browserWindow: BrowserWindow,
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(SniffingHelper)
private readonly sniffingHelper: SniffingHelper,
) {
// Initialize the blocker
this.initBlocker();
const { useProxy, proxy, privacy } = this.store.store;
this.sniffingHelper.start(privacy);
this.sniffingHelper.on("source", this.onSource);
this.setDefaultSession(privacy, true);
this.setProxy(useProxy, proxy);
}
async init(): Promise<void> {
this.view = new WebContentsView({
webPreferences: {
partition: this.defaultSession,
preload: preload,
},
});
if (isDev) {
this.view.webContents.openDevTools();
}
this.view.setBackgroundColor("#fff");
const { isMobile, audioMuted } = this.store.store;
this.setAudioMuted(audioMuted);
this.setUserAgent(isMobile);
this.view.webContents.on("dom-ready", this.onDomReady);
this.view.webContents.on("did-navigate", this.onDidNavigate);
this.view.webContents.on("did-fail-load", this.onDidFailLoad);
this.view.webContents.on("did-navigate-in-page", this.onDidNavigateInPage);
this.view.webContents.on("will-navigate", this.onWillNavigate);
this.view.webContents.setWindowOpenHandler(this.onOpenNewWindow);
}
onWillNavigate = (e: Event, url: string) => {
if (isDeeplink(url)) {
e.preventDefault();
}
};
onDomReady = () => {
if (!this.view) return;
const pageInfo = this.getPageInfo();
this.sniffingHelper.update(pageInfo);
this.window.webContents.send("webview-dom-ready", pageInfo);
};
onDidNavigate = async () => {
if (!this.view) return;
const pageInfo = this.getPageInfo();
this.sniffingHelper.update(pageInfo);
this.window.webContents.send("webview-did-navigate", pageInfo);
try {
// if (isDev && import.meta.env.APP_DEBUG_PLUGINS === "true") {
// const content =
// 'function addScript(src){const script=document.createElement("script");script.src=src;script.type="module";document.body.appendChild(script)}addScript("http://localhost:8080/src/main.ts");';
// await this.view.webContents.executeJavaScript(content);
// } else {
const content = await readFile(pluginUrl, "utf-8");
await this.view.webContents.executeJavaScript(content);
// }
} catch {
// empty
}
this.sniffingHelper.checkPageInfo();
};
onDidFailLoad = (e: Event, code: number, desc: string) => {
// this.window.webContents.send("webview-fail-load", { code, desc });
this.logger.error(`[Webview] fail load: ${code} ${desc}`);
};
onDidNavigateInPage = () => {
if (!this.view) return;
const pageInfo = this.getPageInfo();
this.sniffingHelper.update(pageInfo);
this.window.webContents.send("webview-did-navigate-in-page", pageInfo);
};
onOpenNewWindow = ({ url }: HandlerDetails) => {
this.loadURL(url);
return { action: "deny" } as { action: "deny" };
};
onSource = async (item: SourceParams) => {
if (!this.view) return;
// Here you need to determine whether to use a browser plug-in
const useExtension = this.store.get("useExtension");
if (useExtension) {
// this.view.webContents.send("webview-link-message", item);
this.window.webContents.send("webview-link-message", item);
} else {
const video = await this.downloadTaskService.findByName(item.name);
if (video) {
item.name = `${item.name}-${Date.now()}`;
}
const res = await this.downloadTaskService.create(item);
const mainWebContents = this.mainWindow.window?.webContents;
if (!mainWebContents) return;
// This sends a message to the page notifying it of the update
mainWebContents.send("download-item-notifier", res);
}
};
getPageInfo() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
return {
title: this.view.webContents.getTitle(),
url: this.view.webContents.getURL(),
};
}
getBounds(): Electron.Rectangle {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
return this.view.getBounds();
}
setBounds(bounds: Electron.Rectangle): void {
if (!this.view) return;
this.view.setBounds(bounds);
}
setBackgroundColor(color: string): void {
if (!this.view) return;
this.view.setBackgroundColor(color);
}
show() {
if (!this.view) return;
this.window.contentView.addChildView(this.view);
this.viewShow = true;
}
hide() {
if (!this.view) return;
this.window.contentView.removeChildView(this.view);
this.viewShow = false;
}
loadURL(url: string) {
// Start loading url
if (!this.view) {
this.init();
}
if (!this.view) return;
// 1. Stop current navigation
this.view.webContents.stop();
// 2. Load a new url
this.view.webContents.loadURL(url);
}
async goBack() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
const { webContents } = this.view;
if (webContents.canGoBack()) {
webContents.goBack();
return true;
} else {
this.destroyView();
return false;
}
}
async reload() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
this.view.webContents.reload();
}
async goHome() {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
this.view.webContents.stop();
this.view.webContents.clearHistory();
this.destroyView();
}
get window() {
if (this.browserWindow.window) return this.browserWindow.window;
if (this.mainWindow.window) return this.mainWindow.window;
throw new Error(i18n.t("currentWindowNotFound"));
}
private get session() {
return session.fromPartition(this.defaultSession);
}
private enableProxy(proxy: string) {
if (!proxy) {
this.logger.error(`[Proxy] proxy address is empty`);
return;
}
// Process the validity of the proxy address
if (!/https?:\/\//.test(proxy)) {
proxy = `http://${proxy}`;
}
this.session.setProxy({ proxyRules: proxy });
this.logger.info(`[Proxy] enable proxy: ${proxy}`);
}
private disableProxy() {
this.session.setProxy({ proxyRules: "" });
this.logger.info(`[Proxy] disable proxy`);
}
setProxy(useProxy: boolean, proxy: string): void {
if (useProxy) {
this.enableProxy(proxy);
} else {
this.disableProxy();
}
}
setBlocking(enableBlocking: boolean): void {
if (enableBlocking) {
this.enableBlocking();
} else {
this.disableBlocking();
}
}
async initBlocker() {
this.blocker = await ElectronBlocker.fromPrebuiltAdsAndTracking(fetch);
const enableBlocking = this.store.get("blockAds");
this.setBlocking(enableBlocking);
}
private enableBlocking() {
if (!this.blocker) {
this.logger.error(`[AdBlocker] enable failed(not initialized)`);
return;
}
if (this.blocker.isBlockingEnabled(this.session)) {
return;
}
this.blocker.enableBlockingInSession(this.session);
this.logger.info(`[AdBlocker] enable`);
}
private disableBlocking() {
if (!this.blocker) {
this.logger.error(`[AdBlocker] disable failed(not initialized)`);
return;
}
if (!this.blocker.isBlockingEnabled(this.session)) {
return;
}
this.blocker.disableBlockingInSession(this.session);
this.logger.info(`[AdBlocker] disable`);
}
setAudioMuted(audioMuted?: boolean) {
if (!this.view) return;
this.view.webContents.setAudioMuted(audioMuted ? true : false);
this.logger.info(`Play audio: ${!audioMuted}`);
}
setUserAgent(isMobile?: boolean) {
if (!this.view) return;
if (isMobile) {
this.view.webContents.setUserAgent(mobileUA);
} else {
this.view.webContents.setUserAgent(pcUA);
}
this.logger.info(`[UA] User-Agent: ${isMobile ? "mobile" : "PC"}`);
}
async captureView(): Promise<Electron.NativeImage | null> {
if (!this.view) {
throw new Error(i18n.t("browserViewNotFound"));
}
if (!this.viewShow) {
return null;
}
return this.view.webContents.capturePage();
}
sendToWindow(channel: string, ...args: unknown[]) {
this.window.webContents.send(channel, ...args);
}
destroyView() {
if (this.view && this.window) {
this.view.webContents.close();
this.window.contentView.removeChildView(this.view);
}
// FIXME: To avoid memory leaks, the view needs to be destroyed here
this.view = null;
}
async clearCache() {
await this.session.clearCache();
await this.session.clearStorageData();
}
setDefaultSession(isPrivacy = false, init = false) {
this.logger.info(`[Session] ${isPrivacy ? "Privacy" : "Persist"} Mode`);
if (isPrivacy) {
this.defaultSession = PRIVACY_WEBVIEW;
} else {
this.defaultSession = PERSIST_WEBVIEW;
}
if (this.view) {
const { useProxy, proxy } = this.store.store;
this.destroyView();
this.init();
this.setProxy(useProxy, proxy);
this.sniffingHelper.start(isPrivacy);
}
if (!init) {
this.window.webContents.send("change-privacy");
}
}
}
+3 -1
View File
@@ -9,7 +9,9 @@ import ElectronStore from "../vendor/ElectronStore";
@injectable()
@provide()
export default class BrowserWindow extends Window {
url = isDev ? "http://localhost:8555/browser" : "mediago://index.html/browser";
url = isDev
? "http://localhost:8555/browser"
: "mediago://index.html/browser";
constructor(
@inject(ElectronStore)
+235 -235
View File
@@ -1,235 +1,235 @@
import { provide } from "@inversifyjs/binding-decorators";
import {
DOWNLOAD_EVENT_NAME,
type DownloadProgress,
type DownloadProgressEvent,
DownloadStatus,
type DownloadSuccessEvent,
type DownloadTask,
} from "@mediago/shared-common";
import {
DownloaderServer,
DownloadTaskService,
i18n,
} from "@mediago/shared-node";
import { app, Menu, Notification } from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import _ from "lodash";
import Window from "../core/window";
import { preloadUrl } from "../helper";
import { isWin } from "../helper/variables";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
@injectable()
@provide()
export default class MainWindow extends Window {
url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
private initialUrl: string | null = null;
constructor(
@inject(ElectronLogger)
private readonly logger: ElectronLogger,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
) {
super({
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: preloadUrl,
},
});
// this.taskQueue.on("download-ready-start", this.onDownloadReadyStart);
this.downloaderServer.on("download-success", this.onDownloadSuccess);
this.downloaderServer.on("download-failed", this.onDownloadFailed);
this.downloaderServer.on("download-start", this.onDownloadStart);
this.downloaderServer.on("download-progress", this.onDownloadProgress);
this.downloaderServer.on("download-stop", this.onDownloadStop);
// this.taskQueue.on("download-message", this.onDownloadMessage);
this.store.onDidAnyChange(this.storeChange);
}
closeMainWindow = () => {
const { closeMainWindow } = this.store.store;
if (closeMainWindow) {
app.quit();
}
};
onDownloadProgress = async (tasks: DownloadProgress[]) => {
const data: DownloadProgressEvent = {
type: "progress",
data: tasks,
};
this.send(DOWNLOAD_EVENT_NAME, data);
};
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
if (isLive) {
await this.downloadTaskService.setIsLive(id, true);
}
};
init(): void {
if (this.window) {
// If the window already exists, it is displayed directly
this.window.show();
return;
}
Menu.setApplicationMenu(null);
this.window = this.create();
const mainBounds = this.store.get("mainBounds");
if (mainBounds) {
this.window.setBounds(mainBounds);
}
// Handle current window resize
this.window.on("resized", this.handleResize);
this.window.on("close", this.closeMainWindow);
// if (process.defaultApp) {
// // dev
// if (process.argv.length >= 2) {
// const urlArg = process.argv.find((arg) => arg.startsWith("mediago://"));
// if (urlArg) {
// this.initialUrl = urlArg;
// }
// }
// } else {
// // prod
// if (process.argv.length >= 2) {
// const urlArg = process.argv[1];
// if (urlArg.startsWith("mediago://")) {
// this.initialUrl = urlArg;
// }
// }
// }
// this.window.webContents.on("did-finish-load", () => {
// if (this.initialUrl) {
// this.send("url-params", this.initialUrl);
// }
// });
// if (this.initialUrl) {
// this.window.webContents.send("url-params", this.initialUrl);
// }
}
handleResize = () => {
if (!this.window) return;
const bounds = this.window.getBounds();
this.store.set("mainBounds", _.omit(bounds, ["x", "y"]));
};
storeChange = (store: unknown) => {
// Send notifications to all Windows
this.send("store-change", store);
};
onDownloadSuccess = async (id: number) => {
this.logger.info(`taskId: ${id} success`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Success);
const video = await this.downloadTaskService.findByIdOrFail(id);
const promptTone = this.store.get("promptTone");
if (promptTone) {
new Notification({
title: i18n.t("downloadSuccess"),
body: i18n.t("videoDownloadSuccess", {
name: video.name,
}),
}).show();
}
const data: DownloadSuccessEvent = {
type: "success",
// FIXME: Type 'Video' is not assignable to type 'DownloadTask'.
data: video as unknown as DownloadTask,
};
this.send(DOWNLOAD_EVENT_NAME, data);
};
onDownloadFailed = async (id: number, err: unknown) => {
this.logger.info(`taskId: ${id} failed`, err);
await this.downloadTaskService.setStatus(id, DownloadStatus.Failed);
const promptTone = this.store.get("promptTone");
if (promptTone) {
const video = await this.downloadTaskService.findByIdOrFail(id);
new Notification({
title: i18n.t("downloadFailed"),
body: i18n.t("videoDownloadFailed", { name: video.name }),
}).show();
}
};
onDownloadStart = async (id: number) => {
this.logger.info(`taskId: ${id} start`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Downloading);
};
onDownloadStop = async (id: number) => {
this.logger.info(`taskId: ${id} stopped`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Stopped);
};
onDownloadMessage = async (id: number, message: string) => {
await this.downloadTaskService.appendLog(id, message);
// const showTerminal = this.store.get("showTerminal");
};
showWindow(url?: string) {
if (isWin) {
if (this.window) {
if (this.window.isMinimized()) {
this.window.restore();
}
if (this.window.isVisible()) {
this.window.focus();
} else {
this.window.show();
}
} else {
this.init();
}
if (url) {
this.window!.loadURL(url);
}
}
}
// Handle URL in the form of mediago://
handleUrl(url: string) {
if (!this.window) {
this.init();
}
if (this.window) {
if (this.window.isMinimized()) {
this.window.restore();
}
this.window.focus();
}
this.send("url-params", url); // Send the URL to the renderer process
if (url) {
this.window!.loadURL(url);
}
}
}
import { provide } from "@inversifyjs/binding-decorators";
import {
DOWNLOAD_EVENT_NAME,
type DownloadProgress,
type DownloadProgressEvent,
DownloadStatus,
type DownloadSuccessEvent,
type DownloadTask,
} from "@mediago/shared-common";
import {
DownloaderServer,
DownloadTaskService,
i18n,
} from "@mediago/shared-node";
import { app, Menu, Notification } from "electron";
import isDev from "electron-is-dev";
import { inject, injectable } from "inversify";
import _ from "lodash";
import Window from "../core/window";
import { preloadUrl } from "../helper";
import { isWin } from "../helper/variables";
import ElectronLogger from "../vendor/ElectronLogger";
import ElectronStore from "../vendor/ElectronStore";
@injectable()
@provide()
export default class MainWindow extends Window {
url = isDev ? "http://localhost:8555/" : "mediago://index.html/";
private initialUrl: string | null = null;
constructor(
@inject(ElectronLogger)
private readonly logger: ElectronLogger,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(ElectronStore)
private readonly store: ElectronStore,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
) {
super({
width: 1100,
minWidth: 1100,
height: 680,
minHeight: 680,
show: false,
frame: true,
webPreferences: {
preload: preloadUrl,
},
});
// this.taskQueue.on("download-ready-start", this.onDownloadReadyStart);
this.downloaderServer.on("download-success", this.onDownloadSuccess);
this.downloaderServer.on("download-failed", this.onDownloadFailed);
this.downloaderServer.on("download-start", this.onDownloadStart);
this.downloaderServer.on("download-progress", this.onDownloadProgress);
this.downloaderServer.on("download-stop", this.onDownloadStop);
// this.taskQueue.on("download-message", this.onDownloadMessage);
this.store.onDidAnyChange(this.storeChange);
}
closeMainWindow = () => {
const { closeMainWindow } = this.store.store;
if (closeMainWindow) {
app.quit();
}
};
onDownloadProgress = async (tasks: DownloadProgress[]) => {
const data: DownloadProgressEvent = {
type: "progress",
data: tasks,
};
this.send(DOWNLOAD_EVENT_NAME, data);
};
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
if (isLive) {
await this.downloadTaskService.setIsLive(id, true);
}
};
init(): void {
if (this.window) {
// If the window already exists, it is displayed directly
this.window.show();
return;
}
Menu.setApplicationMenu(null);
this.window = this.create();
const mainBounds = this.store.get("mainBounds");
if (mainBounds) {
this.window.setBounds(mainBounds);
}
// Handle current window resize
this.window.on("resized", this.handleResize);
this.window.on("close", this.closeMainWindow);
// if (process.defaultApp) {
// // dev
// if (process.argv.length >= 2) {
// const urlArg = process.argv.find((arg) => arg.startsWith("mediago://"));
// if (urlArg) {
// this.initialUrl = urlArg;
// }
// }
// } else {
// // prod
// if (process.argv.length >= 2) {
// const urlArg = process.argv[1];
// if (urlArg.startsWith("mediago://")) {
// this.initialUrl = urlArg;
// }
// }
// }
// this.window.webContents.on("did-finish-load", () => {
// if (this.initialUrl) {
// this.send("url-params", this.initialUrl);
// }
// });
// if (this.initialUrl) {
// this.window.webContents.send("url-params", this.initialUrl);
// }
}
handleResize = () => {
if (!this.window) return;
const bounds = this.window.getBounds();
this.store.set("mainBounds", _.omit(bounds, ["x", "y"]));
};
storeChange = (store: unknown) => {
// Send notifications to all Windows
this.send("store-change", store);
};
onDownloadSuccess = async (id: number) => {
this.logger.info(`taskId: ${id} success`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Success);
const video = await this.downloadTaskService.findByIdOrFail(id);
const promptTone = this.store.get("promptTone");
if (promptTone) {
new Notification({
title: i18n.t("downloadSuccess"),
body: i18n.t("videoDownloadSuccess", {
name: video.name,
}),
}).show();
}
const data: DownloadSuccessEvent = {
type: "success",
// FIXME: Type 'Video' is not assignable to type 'DownloadTask'.
data: video as unknown as DownloadTask,
};
this.send(DOWNLOAD_EVENT_NAME, data);
};
onDownloadFailed = async (id: number, err: unknown) => {
this.logger.info(`taskId: ${id} failed`, err);
await this.downloadTaskService.setStatus(id, DownloadStatus.Failed);
const promptTone = this.store.get("promptTone");
if (promptTone) {
const video = await this.downloadTaskService.findByIdOrFail(id);
new Notification({
title: i18n.t("downloadFailed"),
body: i18n.t("videoDownloadFailed", { name: video.name }),
}).show();
}
};
onDownloadStart = async (id: number) => {
this.logger.info(`taskId: ${id} start`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Downloading);
};
onDownloadStop = async (id: number) => {
this.logger.info(`taskId: ${id} stopped`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Stopped);
};
onDownloadMessage = async (id: number, message: string) => {
await this.downloadTaskService.appendLog(id, message);
// const showTerminal = this.store.get("showTerminal");
};
showWindow(url?: string) {
if (isWin) {
if (this.window) {
if (this.window.isMinimized()) {
this.window.restore();
}
if (this.window.isVisible()) {
this.window.focus();
} else {
this.window.show();
}
} else {
this.init();
}
if (url) {
this.window!.loadURL(url);
}
}
}
// Handle URL in the form of mediago://
handleUrl(url: string) {
if (!this.window) {
this.init();
}
if (this.window) {
if (this.window.isMinimized()) {
this.window.restore();
}
this.window.focus();
}
this.send("url-params", url); // Send the URL to the renderer process
if (url) {
this.window!.loadURL(url);
}
}
}
+10 -10
View File
@@ -1,10 +1,10 @@
{
"extends": "../../packages/config/src/tsconfig.node.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "./types",
"outDir": "app/output/build/main"
},
"include": ["src"],
"references": []
}
{
"extends": "../../packages/config/src/tsconfig.node.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "./types",
"outDir": "app/output/build/main"
},
"include": ["src"],
"references": []
}
+92 -77
View File
@@ -1,77 +1,92 @@
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import electron from "electron";
import { defineConfig } from "tsdown";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class ElectronApp {
process: ChildProcessWithoutNullStreams | null = null;
start() {
const args = [
"--inspect=5858",
path.resolve(__dirname, "./build/index.js"),
];
this.process = spawn(String(electron), args);
this.process.stdout.on("data", (data) => {
console.log(String(data));
});
this.process.stderr.on("data", (data) => {
console.log(String(data));
});
}
restart() {
this.kill();
this.start();
}
kill() {
if (this.process?.pid) {
if (process.platform === "win32") {
process.kill(this.process.pid);
} else {
spawn("kill", ["-9", String(this.process.pid)]);
}
this.process = null;
}
}
}
const app = new ElectronApp();
export default defineConfig({
outDir: "build",
shims: true,
external: [
"electron",
"typeorm",
"better-sqlite3",
"@mediago/player",
"@mediago/core",
"@mediago/deps",
],
noExternal: () => true,
define: {
"process.env.NODE_ENV": JSON.stringify(
process.env.NODE_ENV || "production",
),
},
loader: {
".jpg": "asset",
".png": "asset",
},
hooks: {
"build:done": () => {
if (process.env.NODE_ENV === "development") {
app.restart();
}
},
},
});
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import electron from "electron";
import { defineConfig } from "tsdown";
import dotenvFlow from "dotenv-flow";
import copy from "rollup-plugin-copy";
const isDev = process.env.NODE_ENV === "development";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = path.resolve(__dirname, "../..");
const env = dotenvFlow.config({
path: projectRoot,
});
export class ElectronApp {
process: ChildProcessWithoutNullStreams | null = null;
start() {
const args = [
"--inspect=5858",
path.resolve(__dirname, "./build/index.js"),
];
this.process = spawn(String(electron), args);
this.process.stdout.on("data", (data) => {
console.log(String(data));
});
this.process.stderr.on("data", (data) => {
console.log(String(data));
});
}
restart() {
this.kill();
this.start();
}
kill() {
if (this.process?.pid) {
if (process.platform === "win32") {
process.kill(this.process.pid);
} else {
spawn("kill", ["-9", String(this.process.pid)]);
}
this.process = null;
}
}
}
const app = new ElectronApp();
export default defineConfig({
outDir: "build",
shims: true,
external: [
"electron",
"typeorm",
"better-sqlite3",
"@mediago/player",
"@mediago/core",
"@mediago/deps",
],
noExternal: () => true,
define: {
"process.env.NODE_ENV": JSON.stringify(
process.env.NODE_ENV || "production",
),
},
loader: {
".jpg": "asset",
".png": "asset",
},
env: { ...env.parsed },
hooks: {
"build:done": () => {
if (isDev) {
app.restart();
}
},
},
plugins: [
isDev &&
copy({
targets: [{ src: "./dev-app-update.yml", dest: "build" }],
}),
],
});
+20 -20
View File
@@ -1,20 +1,20 @@
node_modules
dist
build
.git
.gitignore
README.md
.env
.env.local
.env.development
.env.production
.DS_Store
Thumbs.db
*.log
coverage
.nyc_output
.vscode
.idea
*.swp
*.swo
*~
node_modules
dist
build
.git
.gitignore
README.md
.env
.env.local
.env.development
.env.production
.DS_Store
Thumbs.db
*.log
coverage
.nyc_output
.vscode
.idea
*.swp
*.swo
*~
+12 -12
View File
@@ -1,12 +1,12 @@
.bin
node_modules
build
dist
bin/Logs
release
log
bin
logs
types/*
.mediago-server
.bin
node_modules
build
dist
bin/Logs
release
log
bin
logs
types/*
.mediago-server
+21 -21
View File
@@ -1,21 +1,21 @@
FROM node:20-bookworm-slim AS base
RUN apt-get update && apt-get install -y \
libicu-dev \
ffmpeg \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY .mediago-server .
RUN npm init -y
RUN npm install pm2 -g
RUN npm i better-sqlite3
EXPOSE 8899
CMD ["pm2-runtime", "backend/index.cjs"]
FROM node:20-bookworm-slim AS base
RUN apt-get update && apt-get install -y \
libicu-dev \
ffmpeg \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY .mediago-server .
RUN npm init -y
RUN npm install pm2 -g
RUN npm i better-sqlite3
EXPOSE 8899
CMD ["pm2-runtime", "backend/index.cjs"]
+65 -65
View File
@@ -1,65 +1,65 @@
# Web API
The standalone Koa.js-based web server that provides HTTP APIs and serves the frontend for browser-based access to MediaGo.
## Overview
This package contains the web API server that:
- Provides RESTful APIs for video downloading operations
- Serves the frontend React application for web access
- Handles file uploads and downloads
- Manages video processing and conversion
- Provides real-time updates via Socket.io
- Can run independently for Docker deployments
## Key Features
- **RESTful APIs**: Complete HTTP API for all download operations
- **Static File Serving**: Serves the built frontend application
- **Real-time Communication**: Socket.io server for live updates
- **File Management**: Handles video files, thumbnails, and metadata
- **Docker Support**: Designed for containerized deployments
- **CORS Support**: Configured for cross-origin requests
- **Database Integration**: TypeORM with SQLite for data persistence
## Technologies
- **Koa.js**: Modern Node.js web framework
- **TypeORM**: Database ORM for data management
- **SQLite**: Lightweight database (Better-sqlite3)
- **Socket.io**: Real-time bidirectional communication
- **Inversify**: Dependency injection container
- **TypeScript**: Type-safe server development
- **Node.js**: Server runtime environment
## Development
```bash
# Start development mode (with frontend)
pnpm dev:web
# Build webapi
pnpm build:backend
# Run in Docker
pnpm build:docker
docker run -d --name mediago -p 8899:8899 -v mediago-data:/root/mediago caorushizi/mediago:latest
```
## Architecture
The webapi server:
1. Serves the frontend React application on the root path
2. Provides `/api/*` endpoints for video operations
3. Uses Socket.io for real-time progress updates
4. Stores data in SQLite database via TypeORM
5. Can run standalone or alongside the Electron desktop app
## API Endpoints
The server provides comprehensive REST APIs for:
- Video URL processing and metadata extraction
- Download queue management
- File operations and storage
- Settings and configuration management
- Real-time status updates via WebSocket
# Web API
The standalone Koa.js-based web server that provides HTTP APIs and serves the frontend for browser-based access to MediaGo.
## Overview
This package contains the web API server that:
- Provides RESTful APIs for video downloading operations
- Serves the frontend React application for web access
- Handles file uploads and downloads
- Manages video processing and conversion
- Provides real-time updates via Socket.io
- Can run independently for Docker deployments
## Key Features
- **RESTful APIs**: Complete HTTP API for all download operations
- **Static File Serving**: Serves the built frontend application
- **Real-time Communication**: Socket.io server for live updates
- **File Management**: Handles video files, thumbnails, and metadata
- **Docker Support**: Designed for containerized deployments
- **CORS Support**: Configured for cross-origin requests
- **Database Integration**: TypeORM with SQLite for data persistence
## Technologies
- **Koa.js**: Modern Node.js web framework
- **TypeORM**: Database ORM for data management
- **SQLite**: Lightweight database (Better-sqlite3)
- **Socket.io**: Real-time bidirectional communication
- **Inversify**: Dependency injection container
- **TypeScript**: Type-safe server development
- **Node.js**: Server runtime environment
## Development
```bash
# Start development mode (with frontend)
pnpm dev:web
# Build webapi
pnpm build:backend
# Run in Docker
pnpm build:docker
docker run -d --name mediago -p 8899:8899 -v mediago-data:/root/mediago caorushizi/mediago:latest
```
## Architecture
The webapi server:
1. Serves the frontend React application on the root path
2. Provides `/api/*` endpoints for video operations
3. Uses Socket.io for real-time progress updates
4. Stores data in SQLite database via TypeORM
5. Can run standalone or alongside the Electron desktop app
## API Endpoints
The server provides comprehensive REST APIs for:
- Video URL processing and metadata extraction
- Download queue management
- File operations and storage
- Settings and configuration management
- Real-time status updates via WebSocket
+21 -21
View File
@@ -1,21 +1,21 @@
version: "3.8"
services:
mediago-server:
build:
context: .
ports:
- "8899:8899"
volumes:
- mediago-data:/app/data
environment:
- NODE_ENV=development
networks:
- mediago-network
volumes:
mediago-data:
networks:
mediago-network:
driver: bridge
version: "3.8"
services:
mediago-server:
build:
context: .
ports:
- "8899:8899"
volumes:
- mediago-data:/app/data
environment:
- NODE_ENV=development
networks:
- mediago-network
volumes:
mediago-data:
networks:
mediago-network:
driver: bridge
+52 -52
View File
@@ -1,53 +1,53 @@
{
"name": "@mediago/server",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "cross-env NODE_ENV=development tsdown --watch src",
"build": "tsdown",
"pack": "tsx scripts/build.ts",
"release": "docker build -t mediago-server .",
"clean": "rimraf .mediago-server",
"lint": "oxlint ."
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/koa": "^3.0.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/node": "^24.7.2",
"@typescript/native-preview": "7.0.0-dev.20251023.1",
"cross-env": "^10.1.0",
"dotenv-flow": "^4.1.0",
"rimraf": "^6.0.1",
"tsdown": "^0.15.9",
"tsx": "^4.20.6"
},
"dependencies": {
"@inversifyjs/binding-decorators": "^1.1.3",
"@koa/cors": "^5.0.0",
"@koa/router": "^14.0.0",
"@mediago/shared-common": "workspace:*",
"@mediago/shared-node": "workspace:*",
"axios": "^1.12.2",
"better-sqlite3": "12.4.1",
"conf": "^15.0.2",
"inversify": "^7.9.1",
"koa": "^3.0.1",
"koa-bodyparser": "^4.4.1",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"lodash": "^4.17.21",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.8.0",
"typeorm": "0.3.27",
"winston": "^3.14.2"
},
"keywords": []
{
"name": "@mediago/server",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "cross-env NODE_ENV=development tsdown --watch src",
"build": "tsdown",
"pack": "tsx scripts/build.ts",
"release": "docker build -t mediago-server .",
"clean": "rimraf .mediago-server",
"lint": "oxlint ."
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/koa": "^3.0.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/node": "^24.7.2",
"@typescript/native-preview": "7.0.0-dev.20251023.1",
"cross-env": "^10.1.0",
"dotenv-flow": "^4.1.0",
"rimraf": "^6.0.1",
"tsdown": "^0.15.9",
"tsx": "^4.20.6"
},
"dependencies": {
"@inversifyjs/binding-decorators": "^1.1.3",
"@koa/cors": "^5.0.0",
"@koa/router": "^14.0.0",
"@mediago/shared-common": "workspace:*",
"@mediago/shared-node": "workspace:*",
"axios": "^1.12.2",
"better-sqlite3": "12.4.1",
"conf": "^15.0.2",
"inversify": "^7.9.1",
"koa": "^3.0.1",
"koa-bodyparser": "^4.4.1",
"koa-send": "^5.0.1",
"koa-static": "^5.0.0",
"lodash": "^4.17.21",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.8.0",
"typeorm": "0.3.27",
"winston": "^3.14.2"
},
"keywords": []
}
+112 -112
View File
@@ -1,112 +1,112 @@
import http from "node:http";
import { provide } from "@inversifyjs/binding-decorators";
import cors from "@koa/cors";
import { DownloadStatus } from "@mediago/shared-common";
import {
DownloaderServer,
TypeORM,
DownloadTaskService,
VideoServer,
} from "@mediago/shared-node";
import { inject, injectable } from "inversify";
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import send from "koa-send";
import serve from "koa-static";
import RouterHandlerService from "./core/router";
import { DB_PATH, LOG_DIR, STATIC_DIR } from "./helper/variables";
import Logger from "./vendor/Logger";
import SocketIO from "./vendor/SocketIO";
import StoreService from "./vendor/Store";
import "./controller";
@injectable()
@provide()
export default class ElectronApp extends Koa {
constructor(
@inject(RouterHandlerService)
private readonly router: RouterHandlerService,
@inject(TypeORM)
private readonly db: TypeORM,
@inject(Logger)
private readonly logger: Logger,
@inject(SocketIO)
private readonly socket: SocketIO,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(StoreService)
private readonly store: StoreService,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
@inject(VideoServer)
private readonly videoServer: VideoServer,
) {
super();
}
private async vendorInit() {
await this.db.init({ dbPath: DB_PATH });
}
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);
const local = this.store.get("local");
this.videoServer.start({ local });
// Initialize download service
this.downloaderServer.start({
logDir: LOG_DIR,
localDir: this.store.get("local"),
deleteSegments: this.store.get("deleteSegments"),
proxy: this.store.get("proxy"),
useProxy: this.store.get("useProxy"),
maxRunner: this.store.get("maxRunner"),
});
this.store.onDidChange("maxRunner", (maxRunner) => {
this.downloaderServer.changeConfig({ maxRunner: maxRunner || 1 });
});
this.store.onDidChange("proxy", (proxy) => {
this.downloaderServer.changeConfig({ proxy: proxy || "" });
});
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.downloadTaskService.findActiveTasks();
const videoIds = videos.map((video) => video.id);
await this.downloadTaskService.setStatus(videoIds, DownloadStatus.Failed);
}
}
import http from "node:http";
import { provide } from "@inversifyjs/binding-decorators";
import cors from "@koa/cors";
import { DownloadStatus } from "@mediago/shared-common";
import {
DownloaderServer,
TypeORM,
DownloadTaskService,
VideoServer,
} from "@mediago/shared-node";
import { inject, injectable } from "inversify";
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import send from "koa-send";
import serve from "koa-static";
import RouterHandlerService from "./core/router";
import { DB_PATH, LOG_DIR, STATIC_DIR } from "./helper/variables";
import Logger from "./vendor/Logger";
import SocketIO from "./vendor/SocketIO";
import StoreService from "./vendor/Store";
import "./controller";
@injectable()
@provide()
export default class ElectronApp extends Koa {
constructor(
@inject(RouterHandlerService)
private readonly router: RouterHandlerService,
@inject(TypeORM)
private readonly db: TypeORM,
@inject(Logger)
private readonly logger: Logger,
@inject(SocketIO)
private readonly socket: SocketIO,
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(StoreService)
private readonly store: StoreService,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
@inject(VideoServer)
private readonly videoServer: VideoServer,
) {
super();
}
private async vendorInit() {
await this.db.init({ dbPath: DB_PATH });
}
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);
const local = this.store.get("local");
this.videoServer.start({ local });
// Initialize download service
this.downloaderServer.start({
logDir: LOG_DIR,
localDir: this.store.get("local"),
deleteSegments: this.store.get("deleteSegments"),
proxy: this.store.get("proxy"),
useProxy: this.store.get("useProxy"),
maxRunner: this.store.get("maxRunner"),
});
this.store.onDidChange("maxRunner", (maxRunner) => {
this.downloaderServer.changeConfig({ maxRunner: maxRunner || 1 });
});
this.store.onDidChange("proxy", (proxy) => {
this.downloaderServer.changeConfig({ proxy: proxy || "" });
});
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.downloadTaskService.findActiveTasks();
const videoIds = videos.map((video) => video.id);
await this.downloadTaskService.setStatus(videoIds, DownloadStatus.Failed);
}
}
@@ -1,100 +1,100 @@
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_DOWNLOAD_ITEMS,
type Controller,
DELETE_DOWNLOAD_ITEM,
type DownloadTask,
type DownloadTaskPagination,
EDIT_DOWNLOAD_ITEM,
GET_DOWNLOAD_ITEMS,
GET_VIDEO_FOLDERS,
START_DOWNLOAD,
STOP_DOWNLOAD,
} from "@mediago/shared-common";
import { DownloadTaskService, handle, TYPES } from "@mediago/shared-node";
import { inject, injectable } from "inversify";
import Logger from "../vendor/Logger";
import SocketIO from "../vendor/SocketIO";
import StoreService from "../vendor/Store";
@injectable()
@provide(TYPES.Controller)
export default class DownloadController implements Controller {
constructor(
@inject(DownloadTaskService)
private readonly downloadService: DownloadTaskService,
@inject(Logger)
private readonly logger: Logger,
@inject(SocketIO)
private readonly socket: SocketIO,
@inject(StoreService)
private readonly store: StoreService,
) {}
@handle(ADD_DOWNLOAD_ITEMS)
async createDownloadTasks({
videos,
startDownload,
}: {
videos: Omit<DownloadTask, "id">[];
startDownload?: boolean;
}) {
const items = await this.downloadService.createMany(videos);
this.socket.refreshList();
// Start downloading immediately if requested
if (startDownload) {
const local = await this.store.get("local");
const deleteSegments = await this.store.get("deleteSegments");
items.forEach((item: any) => {
this.downloadService.start(item.id, local, deleteSegments);
});
}
return items;
}
@handle(GET_DOWNLOAD_ITEMS)
async getDownloadTasks(pagination: DownloadTaskPagination) {
const local = await this.store.get("local");
return await this.downloadService.list(pagination, local);
}
@handle(START_DOWNLOAD)
async startDownloadTask({ vid }: { vid: number }) {
const local = await this.store.get("local");
const deleteSegments = await this.store.get("deleteSegments");
await this.downloadService.start(vid, local, deleteSegments);
}
@handle(DELETE_DOWNLOAD_ITEM)
async deleteDownloadTask({ id }: { id: number }) {
await this.downloadService.remove(id);
}
@handle(EDIT_DOWNLOAD_ITEM)
async updateDownloadTask({ video, startDownload }: { video: DownloadTask; startDownload?: boolean }) {
this.logger.info("editDownloadTask", video);
const item = await this.downloadService.update(video);
// Start downloading immediately if requested
if (startDownload) {
const local = await this.store.get("local");
const deleteSegments = await this.store.get("deleteSegments");
await this.downloadService.start(item.id, local, deleteSegments);
}
return item;
}
@handle(STOP_DOWNLOAD)
async stopDownloadTask({ id }: { id: number }) {
this.downloadService.stop(id);
}
@handle(GET_VIDEO_FOLDERS)
async getVideoFolders() {
return this.downloadService.listFolders();
}
}
import { provide } from "@inversifyjs/binding-decorators";
import {
ADD_DOWNLOAD_ITEMS,
type Controller,
DELETE_DOWNLOAD_ITEM,
type DownloadTask,
type DownloadTaskPagination,
EDIT_DOWNLOAD_ITEM,
GET_DOWNLOAD_ITEMS,
GET_VIDEO_FOLDERS,
START_DOWNLOAD,
STOP_DOWNLOAD,
} from "@mediago/shared-common";
import { DownloadTaskService, handle, TYPES } from "@mediago/shared-node";
import { inject, injectable } from "inversify";
import Logger from "../vendor/Logger";
import SocketIO from "../vendor/SocketIO";
import StoreService from "../vendor/Store";
@injectable()
@provide(TYPES.Controller)
export default class DownloadController implements Controller {
constructor(
@inject(DownloadTaskService)
private readonly downloadService: DownloadTaskService,
@inject(Logger)
private readonly logger: Logger,
@inject(SocketIO)
private readonly socket: SocketIO,
@inject(StoreService)
private readonly store: StoreService,
) {}
@handle(ADD_DOWNLOAD_ITEMS)
async createDownloadTasks({
videos,
startDownload,
}: {
videos: Omit<DownloadTask, "id">[];
startDownload?: boolean;
}) {
const items = await this.downloadService.createMany(videos);
this.socket.refreshList();
// Start downloading immediately if requested
if (startDownload) {
const local = await this.store.get("local");
const deleteSegments = await this.store.get("deleteSegments");
items.forEach((item: any) => {
this.downloadService.start(item.id, local, deleteSegments);
});
}
return items;
}
@handle(GET_DOWNLOAD_ITEMS)
async getDownloadTasks(pagination: DownloadTaskPagination) {
const local = await this.store.get("local");
return await this.downloadService.list(pagination, local);
}
@handle(START_DOWNLOAD)
async startDownloadTask({ vid }: { vid: number }) {
const local = await this.store.get("local");
const deleteSegments = await this.store.get("deleteSegments");
await this.downloadService.start(vid, local, deleteSegments);
}
@handle(DELETE_DOWNLOAD_ITEM)
async deleteDownloadTask({ id }: { id: number }) {
await this.downloadService.remove(id);
}
@handle(EDIT_DOWNLOAD_ITEM)
async updateDownloadTask({ video, startDownload }: { video: DownloadTask; startDownload?: boolean }) {
this.logger.info("editDownloadTask", video);
const item = await this.downloadService.update(video);
// Start downloading immediately if requested
if (startDownload) {
const local = await this.store.get("local");
const deleteSegments = await this.store.get("deleteSegments");
await this.downloadService.start(item.id, local, deleteSegments);
}
return item;
}
@handle(STOP_DOWNLOAD)
async stopDownloadTask({ id }: { id: number }) {
this.downloadService.stop(id);
}
@handle(GET_VIDEO_FOLDERS)
async getVideoFolders() {
return this.downloadService.listFolders();
}
}
+110 -110
View File
@@ -1,110 +1,110 @@
import { loadModule } from "@mediago/shared-node";
import { spawn } from "node:child_process";
import EventEmitter from "node:events";
import os from "node:os";
import path from "node:path";
const ffmpegModule = loadModule("@mediago/deps");
export const ffmpegPath = path.resolve(
path.dirname(ffmpegModule),
"bin",
"ffmpeg",
);
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,
};
}
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));
}
});
});
};
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 * from "./variables";
import { loadModule } from "@mediago/shared-node";
import { spawn } from "node:child_process";
import EventEmitter from "node:events";
import os from "node:os";
import path from "node:path";
const ffmpegModule = loadModule("@mediago/deps");
export const ffmpegPath = path.resolve(
path.dirname(ffmpegModule),
"bin",
"ffmpeg",
);
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,
};
}
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));
}
});
});
};
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 * from "./variables";
+36 -36
View File
@@ -1,36 +1,36 @@
import os from "node:os";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node: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 WORKSPACE = `${DOWNLOAD_DIR}/.store`;
export const DB_PATH = `${WORKSPACE}/mediago.db`;
export const LOG_DIR = `${WORKSPACE}/logs`;
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 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";
// 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";
import os from "node:os";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node: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 WORKSPACE = `${DOWNLOAD_DIR}/.store`;
export const DB_PATH = `${WORKSPACE}/mediago.db`;
export const LOG_DIR = `${WORKSPACE}/logs`;
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 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";
// 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";
+104 -104
View File
@@ -1,104 +1,104 @@
import type http from "node:http";
import { provide } from "@inversifyjs/binding-decorators";
import {
DOWNLOAD_EVENT_NAME,
DownloadStatus,
DownloadSuccessEvent,
DownloadTask,
type DownloadProgress,
type DownloadProgressEvent,
} from "@mediago/shared-common";
import { DownloaderServer, DownloadTaskService } from "@mediago/shared-node";
import { inject, injectable } from "inversify";
import _ from "lodash";
import { Server } from "socket.io";
import type { Vendor } from "../core/vendor";
import Logger from "./Logger";
import StoreService from "./Store";
@injectable()
@provide()
export default class SocketIO implements Vendor {
io: Server;
constructor(
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(Logger)
private readonly logger: Logger,
@inject(StoreService)
private readonly store: StoreService,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
) {}
async init() {}
async initSocketIO(server: http.Server) {
this.io = new Server(server, {
/* options */
cors: {
origin: "*",
},
});
// this.taskQueueService.on("download-ready-start", this.onDownloadReadyStart);
this.downloaderServer.on("download-progress", this.onDownloadProgress);
this.downloaderServer.on("download-success", this.onDownloadSuccess);
this.downloaderServer.on("download-failed", this.onDownloadFailed);
this.downloaderServer.on("download-start", this.onDownloadStart);
this.downloaderServer.on("download-stop", this.onDownloadStop);
// this.taskQueueService.on("download-message", this.receiveMessage);
}
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
if (isLive) {
await this.downloadTaskService.setIsLive(id, true);
}
};
onDownloadProgress = (tasks: DownloadProgress[]) => {
const data: DownloadProgressEvent = {
type: "progress",
data: tasks,
};
this.io.emit(DOWNLOAD_EVENT_NAME, data);
};
onDownloadSuccess = async (id: number) => {
this.logger.info(`download task: ${id} success`);
const video = await this.downloadTaskService.findByIdOrFail(id);
const data: DownloadSuccessEvent = {
type: "success",
// FIXME: Type 'Video' is not assignable to type 'DownloadTask'.
data: video as unknown as DownloadTask,
};
this.io.emit(DOWNLOAD_EVENT_NAME, data);
};
onDownloadFailed = async (id: number, err: unknown) => {
this.logger.error(`download task: ${id} failed: ${err}`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Failed);
};
onDownloadStart = async (id: number) => {
this.logger.info(`download task: ${id} start`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Downloading);
};
onDownloadStop = async (id: number) => {
this.logger.info(`download task: ${id} stop`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Stopped);
};
receiveMessage = async (id: number, message: string) => {
await this.downloadTaskService.appendLog(id, message);
};
refreshList = async () => {
// 页面刷新
};
}
import type http from "node:http";
import { provide } from "@inversifyjs/binding-decorators";
import {
DOWNLOAD_EVENT_NAME,
DownloadStatus,
DownloadSuccessEvent,
DownloadTask,
type DownloadProgress,
type DownloadProgressEvent,
} from "@mediago/shared-common";
import { DownloaderServer, DownloadTaskService } from "@mediago/shared-node";
import { inject, injectable } from "inversify";
import _ from "lodash";
import { Server } from "socket.io";
import type { Vendor } from "../core/vendor";
import Logger from "./Logger";
import StoreService from "./Store";
@injectable()
@provide()
export default class SocketIO implements Vendor {
io: Server;
constructor(
@inject(DownloadTaskService)
private readonly downloadTaskService: DownloadTaskService,
@inject(Logger)
private readonly logger: Logger,
@inject(StoreService)
private readonly store: StoreService,
@inject(DownloaderServer)
private readonly downloaderServer: DownloaderServer,
) {}
async init() {}
async initSocketIO(server: http.Server) {
this.io = new Server(server, {
/* options */
cors: {
origin: "*",
},
});
// this.taskQueueService.on("download-ready-start", this.onDownloadReadyStart);
this.downloaderServer.on("download-progress", this.onDownloadProgress);
this.downloaderServer.on("download-success", this.onDownloadSuccess);
this.downloaderServer.on("download-failed", this.onDownloadFailed);
this.downloaderServer.on("download-start", this.onDownloadStart);
this.downloaderServer.on("download-stop", this.onDownloadStop);
// this.taskQueueService.on("download-message", this.receiveMessage);
}
onDownloadReadyStart = async ({ id, isLive }: DownloadProgress) => {
if (isLive) {
await this.downloadTaskService.setIsLive(id, true);
}
};
onDownloadProgress = (tasks: DownloadProgress[]) => {
const data: DownloadProgressEvent = {
type: "progress",
data: tasks,
};
this.io.emit(DOWNLOAD_EVENT_NAME, data);
};
onDownloadSuccess = async (id: number) => {
this.logger.info(`download task: ${id} success`);
const video = await this.downloadTaskService.findByIdOrFail(id);
const data: DownloadSuccessEvent = {
type: "success",
// FIXME: Type 'Video' is not assignable to type 'DownloadTask'.
data: video as unknown as DownloadTask,
};
this.io.emit(DOWNLOAD_EVENT_NAME, data);
};
onDownloadFailed = async (id: number, err: unknown) => {
this.logger.error(`download task: ${id} failed: ${err}`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Failed);
};
onDownloadStart = async (id: number) => {
this.logger.info(`download task: ${id} start`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Downloading);
};
onDownloadStop = async (id: number) => {
this.logger.info(`download task: ${id} stop`);
await this.downloadTaskService.setStatus(id, DownloadStatus.Stopped);
};
receiveMessage = async (id: number, message: string) => {
await this.downloadTaskService.appendLog(id, message);
};
refreshList = async () => {
// 页面刷新
};
}
+10 -10
View File
@@ -1,10 +1,10 @@
{
"extends": "../../packages/config/src/tsconfig.node.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "./types",
"outDir": "app/output/build/main"
},
"include": ["src"],
"references": []
}
{
"extends": "../../packages/config/src/tsconfig.node.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "./types",
"outDir": "app/output/build/main"
},
"include": ["src"],
"references": []
}
+60 -54
View File
@@ -1,54 +1,60 @@
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "tsdown";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class NodeApp {
process: ChildProcessWithoutNullStreams | null = null;
start() {
const args = [path.resolve(__dirname, "./build/index.js")];
this.process = spawn("node", args);
this.process.stdout.on("data", (data) => {
console.log(String(data));
});
this.process.stderr.on("data", (data) => {
console.log(String(data));
});
}
restart() {
this.kill();
this.start();
}
kill() {
if (this.process?.pid) {
if (process.platform === "win32") {
process.kill(this.process.pid);
} else {
spawn("kill", ["-9", String(this.process.pid)]);
}
this.process = null;
}
}
}
const app = new NodeApp();
export default defineConfig({
outDir: "build",
hooks: {
"build:done": () => {
if (process.env.NODE_ENV === "development") {
app.restart();
}
},
},
});
import dotenvFlow from "dotenv-flow";
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "tsdown";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = path.resolve(__dirname, "../..");
const env = dotenvFlow.config({
path: projectRoot,
});
export class NodeApp {
process: ChildProcessWithoutNullStreams | null = null;
start() {
const args = [path.resolve(__dirname, "./build/index.js")];
this.process = spawn("node", args);
this.process.stdout.on("data", (data) => {
console.log(String(data));
});
this.process.stderr.on("data", (data) => {
console.log(String(data));
});
}
restart() {
this.kill();
this.start();
}
kill() {
if (this.process?.pid) {
if (process.platform === "win32") {
process.kill(this.process.pid);
} else {
spawn("kill", ["-9", String(this.process.pid)]);
}
this.process = null;
}
}
}
const app = new NodeApp();
export default defineConfig({
outDir: "build",
env: { ...env.parsed },
hooks: {
"build:done": () => {
if (process.env.NODE_ENV === "development") {
app.restart();
}
},
},
});
+25 -25
View File
@@ -1,25 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
build
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
build
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+58 -58
View File
@@ -1,58 +1,58 @@
# Frontend
The React-based frontend application that provides the user interface for MediaGo, used in both Electron desktop app and standalone web application.
## Overview
This package contains the React frontend that:
- Provides the main user interface for video downloading
- Handles video URL input and processing
- Displays download progress and history
- Manages application settings and preferences
- Communicates with backend services via Socket.io
- Works in both Electron renderer and standalone web contexts
## Key Features
- **Video Download Interface**: Clean UI for adding and managing downloads
- **Real-time Updates**: Live progress tracking via Socket.io
- **Responsive Design**: Works on desktop and mobile devices
- **Theme Support**: Light/dark mode capabilities
- **Multi-platform**: Shared UI for Electron desktop and web versions
- **Settings Management**: User preferences and configuration
## Technologies
- **React 18**: Modern React with hooks and concurrent features
- **Vite**: Fast build tool and dev server
- **Ant Design**: UI component library
- **TailwindCSS**: Utility-first CSS framework
- **Shadcn/ui**: Modern component library
- **Zustand**: Lightweight state management
- **Socket.io Client**: Real-time communication
- **TypeScript**: Type-safe development
## Development
```bash
# Start development mode (with Electron)
pnpm dev
# Start web-only development
pnpm dev:web
# Build frontend
pnpm build:web
pnpm build:renderer
# Type checking
pnpm types:renderer
```
## Architecture
The frontend is designed to work in two contexts:
1. **Electron Renderer**: Loaded in Electron's BrowserWindow
2. **Standalone Web**: Served by the webapi backend for browser access
It communicates with the backend services through Socket.io for real-time updates and HTTP APIs for data operations.
# Frontend
The React-based frontend application that provides the user interface for MediaGo, used in both Electron desktop app and standalone web application.
## Overview
This package contains the React frontend that:
- Provides the main user interface for video downloading
- Handles video URL input and processing
- Displays download progress and history
- Manages application settings and preferences
- Communicates with backend services via Socket.io
- Works in both Electron renderer and standalone web contexts
## Key Features
- **Video Download Interface**: Clean UI for adding and managing downloads
- **Real-time Updates**: Live progress tracking via Socket.io
- **Responsive Design**: Works on desktop and mobile devices
- **Theme Support**: Light/dark mode capabilities
- **Multi-platform**: Shared UI for Electron desktop and web versions
- **Settings Management**: User preferences and configuration
## Technologies
- **React 18**: Modern React with hooks and concurrent features
- **Vite**: Fast build tool and dev server
- **Ant Design**: UI component library
- **TailwindCSS**: Utility-first CSS framework
- **Shadcn/ui**: Modern component library
- **Zustand**: Lightweight state management
- **Socket.io Client**: Real-time communication
- **TypeScript**: Type-safe development
## Development
```bash
# Start development mode (with Electron)
pnpm dev
# Start web-only development
pnpm dev:web
# Build frontend
pnpm build:web
pnpm build:renderer
# Type checking
pnpm types:renderer
```
## Architecture
The frontend is designed to work in two contexts:
1. **Electron Renderer**: Loaded in Electron's BrowserWindow
2. **Standalone Web**: Served by the webapi backend for browser access
It communicates with the backend services through Socket.io for real-time updates and HTTP APIs for data operations.
+22 -22
View File
@@ -1,22 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
+13 -13
View File
@@ -1,13 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<title>MediaGo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<title>MediaGo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4 -4
View File
@@ -1,5 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
+11 -11
View File
@@ -1,12 +1,12 @@
<svg width="42" height="30" viewBox="0 0 42 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 5C0 2.23858 2.23858 0 5 0H29.3967C31.0283 0 32.5572 0.796055 33.4929 2.13269L40.4929 12.1327C41.698 13.8543 41.698 16.1457 40.4929 17.8673L33.4929 27.8673C32.5572 29.2039 31.0283 30 29.3967 30H5C2.23858 30 0 27.7614 0 25V5Z"
fill="url(#paint0_linear_5_668)" />
<defs>
<linearGradient id="paint0_linear_5_668" x1="0" y1="15" x2="45" y2="15"
gradientUnits="userSpaceOnUse">
<stop stop-color="#24C1FF" stop-opacity="0" />
<stop offset="1" stop-color="white" />
</linearGradient>
</defs>
<svg width="42" height="30" viewBox="0 0 42 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 5C0 2.23858 2.23858 0 5 0H29.3967C31.0283 0 32.5572 0.796055 33.4929 2.13269L40.4929 12.1327C41.698 13.8543 41.698 16.1457 40.4929 17.8673L33.4929 27.8673C32.5572 29.2039 31.0283 30 29.3967 30H5C2.23858 30 0 27.7614 0 25V5Z"
fill="url(#paint0_linear_5_668)" />
<defs>
<linearGradient id="paint0_linear_5_668" x1="0" y1="15" x2="45" y2="15"
gradientUnits="userSpaceOnUse">
<stop stop-color="#24C1FF" stop-opacity="0" />
<stop offset="1" stop-color="white" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 649 B

+11 -11
View File
@@ -1,12 +1,12 @@
<svg width="42" height="30" viewBox="0 0 42 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 5C0 2.23858 2.23858 0 5 0H29.3967C31.0283 0 32.5572 0.796055 33.4929 2.13269L40.4929 12.1327C41.698 13.8543 41.698 16.1457 40.4929 17.8673L33.4929 27.8673C32.5572 29.2039 31.0283 30 29.3967 30H5C2.23858 30 0 27.7614 0 25V5Z"
fill="url(#paint0_linear_5_673)" />
<defs>
<linearGradient id="paint0_linear_5_673" x1="0" y1="15" x2="40" y2="15"
gradientUnits="userSpaceOnUse">
<stop stop-color="#24C1FF" stop-opacity="0" />
<stop offset="1" stop-color="white" stop-opacity="0.14" />
</linearGradient>
</defs>
<svg width="42" height="30" viewBox="0 0 42 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 5C0 2.23858 2.23858 0 5 0H29.3967C31.0283 0 32.5572 0.796055 33.4929 2.13269L40.4929 12.1327C41.698 13.8543 41.698 16.1457 40.4929 17.8673L33.4929 27.8673C32.5572 29.2039 31.0283 30 29.3967 30H5C2.23858 30 0 27.7614 0 25V5Z"
fill="url(#paint0_linear_5_673)" />
<defs>
<linearGradient id="paint0_linear_5_673" x1="0" y1="15" x2="40" y2="15"
gradientUnits="userSpaceOnUse">
<stop stop-color="#24C1FF" stop-opacity="0" />
<stop offset="1" stop-color="white" stop-opacity="0.14" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 669 B

+442 -442
View File
@@ -1,442 +1,442 @@
import { DockerOutlined } from "@ant-design/icons";
import { useAsyncEffect, useMemoizedFn } from "ahooks";
import {
App,
AutoComplete,
Button,
Form,
Input,
Modal,
Select,
Switch,
} from "antd";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/react/shallow";
import { ADD_TO_LIST, DOWNLOAD_NOW } from "@/const";
import useAPI from "@/hooks/use-api";
import { useDockerApi } from "@/hooks/use-docker-api";
import { appStoreSelector, useAppStore } from "@/store/app";
import { downloadFormSelector, useConfigStore } from "@/store/config";
import { DownloadType } from "@/types";
import { randomName, tdApp } from "@/utils";
import { DownloadTask } from "@mediago/shared-common";
const { TextArea } = Input;
export interface DownloadFormItem {
batch?: boolean;
batchList?: string;
name?: string;
type?: DownloadType;
headers?: string;
url?: string;
id?: number;
folder?: string;
}
export interface DownloadFormProps {
isEdit?: boolean;
destroyOnClose?: boolean;
onFormVisibleChange?: (open: boolean) => void;
onConfirm?: (values: DownloadFormItem) => void;
id: string;
}
export interface DownloadFormRef {
setFieldsValue: (value: DownloadFormItem) => void;
getFieldsValue: () => DownloadFormItem;
openModal: (value: DownloadFormItem) => void;
}
export interface DownloadTaskForm extends DownloadTask {
batch?: boolean;
batchList?: string;
}
interface Options {
label: string;
value: string;
}
export default forwardRef<DownloadFormRef, DownloadFormProps>(
function DownloadForm(
{ isEdit, destroyOnClose, onFormVisibleChange, id, onConfirm },
ref,
) {
const { enableDocker } = useAppStore(useShallow(appStoreSelector));
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm<DownloadFormItem>();
const { t } = useTranslation();
const { message } = App.useApp();
const { setLastDownloadTypes, setLastIsBatch } = useConfigStore(
useShallow(downloadFormSelector),
);
const [folders, setFolders] = useState<Options[]>([]);
const [videoFolders, setVideoFolders] = useState<string[]>([]);
const { createDownloadTasks, getVideoFolders } = useAPI();
const { addVideosToDocker } = useDockerApi();
useAsyncEffect(async () => {
if (modalOpen) {
const folders = await getVideoFolders();
setVideoFolders(folders);
setFolders(() =>
folders.map((f) => ({
value: f,
label: f,
})),
);
}
}, [modalOpen]);
useImperativeHandle(ref, () => {
return {
openModal: (value) => {
form.setFieldsValue(value);
setModalOpen(true);
},
setFieldsValue: (value) => {
form.setFieldsValue(value);
},
getFieldsValue: () => {
return form.getFieldsValue();
},
};
}, []);
const handleValuesChange = useMemoizedFn((values: any) => {
const { type, batch } = values;
if (type) {
setLastDownloadTypes(type);
}
if (batch != null) {
setLastIsBatch(batch);
}
});
const afterOpenChange = useMemoizedFn((open: boolean) => {
onFormVisibleChange?.(open);
if (!open) {
form.resetFields();
}
});
const handleSave = useMemoizedFn(async () => {
try {
await form.validateFields();
} catch {
return;
}
try {
const tasks = await getFormItems();
await createDownloadTasks(tasks);
setModalOpen(false);
onConfirm?.(form.getFieldsValue());
tdApp.onEvent(ADD_TO_LIST, { id });
} catch (e: any) {
console.error(e);
message.error(e?.message || t("pleaseEnterCorrectFormInfo"));
}
});
const handleAddToDocker = useMemoizedFn(async () => {
try {
await form.validateFields();
} catch {
return;
}
try {
const tasks = await getFormItems();
await addVideosToDocker({ items: tasks });
message.success(t("addToDockerSuccess"));
} catch (e: any) {
message.error(e?.message || t("pleaseEnterCorrectFormInfo"));
}
});
const handleDownloadNow = useMemoizedFn(async () => {
try {
await form.validateFields();
} catch {
return;
}
try {
const tasks = await getFormItems();
await createDownloadTasks(tasks, true);
setModalOpen(false);
onConfirm?.(form.getFieldsValue());
tdApp.onEvent(DOWNLOAD_NOW, { id });
} catch (e: any) {
console.error(e);
message.error(e?.message || t("pleaseEnterCorrectFormInfo"));
}
});
const handleSearchFolder = useMemoizedFn((val: string) => {
return setFolders(() => {
const videoOptions = videoFolders.map((f) => ({ value: f, label: f }));
if (!val) return videoOptions;
return [{ value: val, label: val }, ...videoOptions];
});
});
const getFormItems = useMemoizedFn(async () => {
const { batch } = form.getFieldsValue();
if (batch) {
const {
batchList = "",
headers,
type = DownloadType.m3u8,
} = form.getFieldsValue();
const tasks: Omit<DownloadTask, "id">[] = await Promise.all(
batchList.split("\n").map(async (line: string) => {
const [url, customName, folder] = line.trim().split(" ");
return {
url: url.trim(),
name: customName?.trim() || randomName(),
headers,
type,
folder,
};
}),
);
return tasks;
} else {
const {
name = "",
url = "",
headers,
type = DownloadType.m3u8,
folder,
} = form.getFieldsValue();
const task: Omit<DownloadTask, "id"> = {
name: name || randomName(),
url: url || "",
headers,
type,
folder,
};
return [task];
}
});
return (
<Modal
open={modalOpen}
key={isEdit ? "edit" : "new"}
title={isEdit ? t("editDownload") : t("newDownload")}
width={500}
onCancel={() => setModalOpen(false)}
afterOpenChange={afterOpenChange}
destroyOnHidden={destroyOnClose}
footer={[
enableDocker && (
<Button
key="docker"
onClick={handleAddToDocker}
icon={<DockerOutlined />}
>
{t("addToDocker")}
</Button>
),
<Button key="link" type="primary" onClick={handleDownloadNow}>
{t("downloadNow")}
</Button>,
<Button key="cancel" onClick={() => setModalOpen(false)}>
{t("cancel")}
</Button>,
<Button key="submit" onClick={handleSave}>
{t("save")}
</Button>,
]}
>
<Form
form={form}
autoFocus
labelCol={{ span: 5 }}
layout="horizontal"
colon={false}
onValuesChange={handleValuesChange}
>
<Form.Item name="id" hidden />
<Form.Item hidden={isEdit} label={t("batchDownload")} name={"batch"}>
<Switch />
</Form.Item>
<Form.Item
key="type"
name="type"
label={t("videoType")}
rules={[
{
required: true,
message: t("pleaseEnterVideoName"),
},
]}
>
<Select
disabled={isEdit}
options={[
{
label: t("streamMedia"),
value: "m3u8",
},
{
label: t("bilibiliMedia"),
value: "bilibili",
},
{
label: t("direct"),
value: "direct",
},
]}
placeholder={t("pleaseSelectVideoType")}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
const isBatch = form.getFieldValue("batch");
if (isBatch) {
return null;
}
return (
<Form.Item
shouldUpdate
name="name"
label={t("videoName")}
rules={[
{
required: form.getFieldsValue().type !== "bilibili",
message: t("pleaseEnterCorrectFormInfo"),
},
]}
>
<Input placeholder={t("pleaseEnterVideoName")} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (isEdit || !form.getFieldValue("batch")) {
return null;
}
return (
<Form.Item
label={t("videoLink")}
name="batchList"
required
rules={[
{
required: true,
message: t("pleaseEnterVideoLink"),
},
{
validator: (_, value) => {
const lines = value.split("\n");
for (const line of lines) {
const params = line.trim().split(" ");
if (params.length > 3) {
return Promise.reject(
new Error(t("pleaseEnterCorrectBatchList")),
);
}
const [url] = params;
if (!/^(https?):\/\/.+/.test(url)) {
return Promise.reject(
new Error(t("pleaseEnterCorrectBatchList")),
);
}
}
return Promise.resolve();
},
},
]}
>
<TextArea rows={5} placeholder={t("videoLikeDescription")} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (form.getFieldValue("batch") && !isEdit) {
return null;
}
return (
<Form.Item
name="url"
label={t("videoLink")}
required
rules={[
{
required: true,
message: t("pleaseEnterOnlineVideoUrl"),
},
{
pattern: /^(file|https?):\/\/.+/,
message: t("pleaseEnterCorrectVideoLink"),
},
]}
>
<Input
placeholder={t("pleaseEnterOnlineVideoUrlOrDragM3U8Here")}
onDrop={(e) => {
const file: any = e.dataTransfer.files[0];
form.setFieldValue("url", `file://${file.path}`);
form.validateFields(["url"]);
}}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (form.getFieldValue("batch")) {
return null;
}
return (
<Form.Item name="folder" label={t("folder")}>
<AutoComplete
placeholder={t("pleaseInputVideoFolder")}
optionFilterProp="label"
options={folders}
onSearch={handleSearchFolder}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (
form.getFieldValue("type") !== "m3u8" &&
!form.getFieldValue("batch")
) {
return null;
}
return (
<Form.Item label={t("additionalHeaders")} name="headers">
<TextArea
rows={4}
placeholder={t("additionalHeadersDescription")}
/>
</Form.Item>
);
}}
</Form.Item>
</Form>
</Modal>
);
},
);
import { DockerOutlined } from "@ant-design/icons";
import { useAsyncEffect, useMemoizedFn } from "ahooks";
import {
App,
AutoComplete,
Button,
Form,
Input,
Modal,
Select,
Switch,
} from "antd";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/react/shallow";
import { ADD_TO_LIST, DOWNLOAD_NOW } from "@/const";
import useAPI from "@/hooks/use-api";
import { useDockerApi } from "@/hooks/use-docker-api";
import { appStoreSelector, useAppStore } from "@/store/app";
import { downloadFormSelector, useConfigStore } from "@/store/config";
import { DownloadType } from "@/types";
import { randomName, tdApp } from "@/utils";
import { DownloadTask } from "@mediago/shared-common";
const { TextArea } = Input;
export interface DownloadFormItem {
batch?: boolean;
batchList?: string;
name?: string;
type?: DownloadType;
headers?: string;
url?: string;
id?: number;
folder?: string;
}
export interface DownloadFormProps {
isEdit?: boolean;
destroyOnClose?: boolean;
onFormVisibleChange?: (open: boolean) => void;
onConfirm?: (values: DownloadFormItem) => void;
id: string;
}
export interface DownloadFormRef {
setFieldsValue: (value: DownloadFormItem) => void;
getFieldsValue: () => DownloadFormItem;
openModal: (value: DownloadFormItem) => void;
}
export interface DownloadTaskForm extends DownloadTask {
batch?: boolean;
batchList?: string;
}
interface Options {
label: string;
value: string;
}
export default forwardRef<DownloadFormRef, DownloadFormProps>(
function DownloadForm(
{ isEdit, destroyOnClose, onFormVisibleChange, id, onConfirm },
ref,
) {
const { enableDocker } = useAppStore(useShallow(appStoreSelector));
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm<DownloadFormItem>();
const { t } = useTranslation();
const { message } = App.useApp();
const { setLastDownloadTypes, setLastIsBatch } = useConfigStore(
useShallow(downloadFormSelector),
);
const [folders, setFolders] = useState<Options[]>([]);
const [videoFolders, setVideoFolders] = useState<string[]>([]);
const { createDownloadTasks, getVideoFolders } = useAPI();
const { addVideosToDocker } = useDockerApi();
useAsyncEffect(async () => {
if (modalOpen) {
const folders = await getVideoFolders();
setVideoFolders(folders);
setFolders(() =>
folders.map((f) => ({
value: f,
label: f,
})),
);
}
}, [modalOpen]);
useImperativeHandle(ref, () => {
return {
openModal: (value) => {
form.setFieldsValue(value);
setModalOpen(true);
},
setFieldsValue: (value) => {
form.setFieldsValue(value);
},
getFieldsValue: () => {
return form.getFieldsValue();
},
};
}, []);
const handleValuesChange = useMemoizedFn((values: any) => {
const { type, batch } = values;
if (type) {
setLastDownloadTypes(type);
}
if (batch != null) {
setLastIsBatch(batch);
}
});
const afterOpenChange = useMemoizedFn((open: boolean) => {
onFormVisibleChange?.(open);
if (!open) {
form.resetFields();
}
});
const handleSave = useMemoizedFn(async () => {
try {
await form.validateFields();
} catch {
return;
}
try {
const tasks = await getFormItems();
await createDownloadTasks(tasks);
setModalOpen(false);
onConfirm?.(form.getFieldsValue());
tdApp.onEvent(ADD_TO_LIST, { id });
} catch (e: any) {
console.error(e);
message.error(e?.message || t("pleaseEnterCorrectFormInfo"));
}
});
const handleAddToDocker = useMemoizedFn(async () => {
try {
await form.validateFields();
} catch {
return;
}
try {
const tasks = await getFormItems();
await addVideosToDocker({ items: tasks });
message.success(t("addToDockerSuccess"));
} catch (e: any) {
message.error(e?.message || t("pleaseEnterCorrectFormInfo"));
}
});
const handleDownloadNow = useMemoizedFn(async () => {
try {
await form.validateFields();
} catch {
return;
}
try {
const tasks = await getFormItems();
await createDownloadTasks(tasks, true);
setModalOpen(false);
onConfirm?.(form.getFieldsValue());
tdApp.onEvent(DOWNLOAD_NOW, { id });
} catch (e: any) {
console.error(e);
message.error(e?.message || t("pleaseEnterCorrectFormInfo"));
}
});
const handleSearchFolder = useMemoizedFn((val: string) => {
return setFolders(() => {
const videoOptions = videoFolders.map((f) => ({ value: f, label: f }));
if (!val) return videoOptions;
return [{ value: val, label: val }, ...videoOptions];
});
});
const getFormItems = useMemoizedFn(async () => {
const { batch } = form.getFieldsValue();
if (batch) {
const {
batchList = "",
headers,
type = DownloadType.m3u8,
} = form.getFieldsValue();
const tasks: Omit<DownloadTask, "id">[] = await Promise.all(
batchList.split("\n").map(async (line: string) => {
const [url, customName, folder] = line.trim().split(" ");
return {
url: url.trim(),
name: customName?.trim() || randomName(),
headers,
type,
folder,
};
}),
);
return tasks;
} else {
const {
name = "",
url = "",
headers,
type = DownloadType.m3u8,
folder,
} = form.getFieldsValue();
const task: Omit<DownloadTask, "id"> = {
name: name || randomName(),
url: url || "",
headers,
type,
folder,
};
return [task];
}
});
return (
<Modal
open={modalOpen}
key={isEdit ? "edit" : "new"}
title={isEdit ? t("editDownload") : t("newDownload")}
width={500}
onCancel={() => setModalOpen(false)}
afterOpenChange={afterOpenChange}
destroyOnHidden={destroyOnClose}
footer={[
enableDocker && (
<Button
key="docker"
onClick={handleAddToDocker}
icon={<DockerOutlined />}
>
{t("addToDocker")}
</Button>
),
<Button key="link" type="primary" onClick={handleDownloadNow}>
{t("downloadNow")}
</Button>,
<Button key="cancel" onClick={() => setModalOpen(false)}>
{t("cancel")}
</Button>,
<Button key="submit" onClick={handleSave}>
{t("save")}
</Button>,
]}
>
<Form
form={form}
autoFocus
labelCol={{ span: 5 }}
layout="horizontal"
colon={false}
onValuesChange={handleValuesChange}
>
<Form.Item name="id" hidden />
<Form.Item hidden={isEdit} label={t("batchDownload")} name={"batch"}>
<Switch />
</Form.Item>
<Form.Item
key="type"
name="type"
label={t("videoType")}
rules={[
{
required: true,
message: t("pleaseEnterVideoName"),
},
]}
>
<Select
disabled={isEdit}
options={[
{
label: t("streamMedia"),
value: "m3u8",
},
{
label: t("bilibiliMedia"),
value: "bilibili",
},
{
label: t("direct"),
value: "direct",
},
]}
placeholder={t("pleaseSelectVideoType")}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
const isBatch = form.getFieldValue("batch");
if (isBatch) {
return null;
}
return (
<Form.Item
shouldUpdate
name="name"
label={t("videoName")}
rules={[
{
required: form.getFieldsValue().type !== "bilibili",
message: t("pleaseEnterCorrectFormInfo"),
},
]}
>
<Input placeholder={t("pleaseEnterVideoName")} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (isEdit || !form.getFieldValue("batch")) {
return null;
}
return (
<Form.Item
label={t("videoLink")}
name="batchList"
required
rules={[
{
required: true,
message: t("pleaseEnterVideoLink"),
},
{
validator: (_, value) => {
const lines = value.split("\n");
for (const line of lines) {
const params = line.trim().split(" ");
if (params.length > 3) {
return Promise.reject(
new Error(t("pleaseEnterCorrectBatchList")),
);
}
const [url] = params;
if (!/^(https?):\/\/.+/.test(url)) {
return Promise.reject(
new Error(t("pleaseEnterCorrectBatchList")),
);
}
}
return Promise.resolve();
},
},
]}
>
<TextArea rows={5} placeholder={t("videoLikeDescription")} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (form.getFieldValue("batch") && !isEdit) {
return null;
}
return (
<Form.Item
name="url"
label={t("videoLink")}
required
rules={[
{
required: true,
message: t("pleaseEnterOnlineVideoUrl"),
},
{
pattern: /^(file|https?):\/\/.+/,
message: t("pleaseEnterCorrectVideoLink"),
},
]}
>
<Input
placeholder={t("pleaseEnterOnlineVideoUrlOrDragM3U8Here")}
onDrop={(e) => {
const file: any = e.dataTransfer.files[0];
form.setFieldValue("url", `file://${file.path}`);
form.validateFields(["url"]);
}}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (form.getFieldValue("batch")) {
return null;
}
return (
<Form.Item name="folder" label={t("folder")}>
<AutoComplete
placeholder={t("pleaseInputVideoFolder")}
optionFilterProp="label"
options={folders}
onSearch={handleSearchFolder}
/>
</Form.Item>
);
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form) => {
if (
form.getFieldValue("type") !== "m3u8" &&
!form.getFieldValue("batch")
) {
return null;
}
return (
<Form.Item label={t("additionalHeaders")} name="headers">
<TextArea
rows={4}
placeholder={t("additionalHeadersDescription")}
/>
</Form.Item>
);
}}
</Form.Item>
</Form>
</Modal>
);
},
);
+29 -29
View File
@@ -1,29 +1,29 @@
import type React from "react";
import { cn } from "@/utils";
interface DownloadTagProps {
icon?: React.ReactNode;
text: string;
color: string;
className?: string;
}
export function DownloadTag({
icon,
text,
color,
className,
}: DownloadTagProps) {
return (
<div
className={cn(
"flex shrink-0 cursor-default flex-row items-center gap-0.5 rounded-2xl rounded-bl-lg py-0.5 pl-1.5 pr-1.5",
className,
)}
style={{ background: color }}
>
{icon}
<span className="text-xs text-white">{text}</span>
</div>
);
}
import type React from "react";
import { cn } from "@/utils";
interface DownloadTagProps {
icon?: React.ReactNode;
text: string;
color: string;
className?: string;
}
export function DownloadTag({
icon,
text,
color,
className,
}: DownloadTagProps) {
return (
<div
className={cn(
"flex shrink-0 cursor-default flex-row items-center gap-0.5 rounded-2xl rounded-bl-lg py-0.5 pl-1.5 pr-1.5",
className,
)}
style={{ background: color }}
>
{icon}
<span className="text-xs text-white">{text}</span>
</div>
);
}
+71 -71
View File
@@ -1,71 +1,71 @@
import { type FC, type ReactNode, useEffect, useRef } from "react";
import "@xterm/xterm/css/xterm.css";
import { FitAddon } from "@xterm/addon-fit";
import { Terminal as XTerminal } from "@xterm/xterm";
import { cn } from "@/utils";
import useAPI from "@/hooks/use-api";
interface TerminalProps {
className?: string;
id: number;
log: string;
header?: ReactNode;
}
const Terminal: FC<TerminalProps> = ({ className, id, log, header }) => {
const terminalRef = useRef<HTMLDivElement | null>(null);
const { addIpcListener, removeIpcListener } = useAPI();
useEffect(() => {
if (!terminalRef.current) return;
const terminal = new XTerminal({
fontFamily: "Consolas, 'Courier New', monospace",
disableStdin: true,
cursorBlink: false,
allowProposedApi: true,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(terminalRef.current);
fitAddon.fit();
if (log) {
terminal.write(log);
}
const onDownloadMessage = (
_: unknown,
messageId: number,
message: string,
) => {
if (id === messageId) {
terminal.write(message);
}
};
const resize = () => {
fitAddon.fit();
};
addIpcListener("download-message", onDownloadMessage);
window.addEventListener("resize", resize);
return () => {
removeIpcListener("download-message", onDownloadMessage);
window.removeEventListener("resize", resize);
terminal.dispose();
};
}, [id]);
return (
<div className={cn("flex flex-col", className)}>
{header}
<div className="flex-1">
<div ref={terminalRef} />
</div>
</div>
);
};
export default Terminal;
import { type FC, type ReactNode, useEffect, useRef } from "react";
import "@xterm/xterm/css/xterm.css";
import { FitAddon } from "@xterm/addon-fit";
import { Terminal as XTerminal } from "@xterm/xterm";
import { cn } from "@/utils";
import useAPI from "@/hooks/use-api";
interface TerminalProps {
className?: string;
id: number;
log: string;
header?: ReactNode;
}
const Terminal: FC<TerminalProps> = ({ className, id, log, header }) => {
const terminalRef = useRef<HTMLDivElement | null>(null);
const { addIpcListener, removeIpcListener } = useAPI();
useEffect(() => {
if (!terminalRef.current) return;
const terminal = new XTerminal({
fontFamily: "Consolas, 'Courier New', monospace",
disableStdin: true,
cursorBlink: false,
allowProposedApi: true,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(terminalRef.current);
fitAddon.fit();
if (log) {
terminal.write(log);
}
const onDownloadMessage = (
_: unknown,
messageId: number,
message: string,
) => {
if (id === messageId) {
terminal.write(message);
}
};
const resize = () => {
fitAddon.fit();
};
addIpcListener("download-message", onDownloadMessage);
window.addEventListener("resize", resize);
return () => {
removeIpcListener("download-message", onDownloadMessage);
window.removeEventListener("resize", resize);
terminal.dispose();
};
}, [id]);
return (
<div className={cn("flex flex-col", className)}>
{header}
<div className="flex-1">
<div ref={terminalRef} />
</div>
</div>
);
};
export default Terminal;
+57 -57
View File
@@ -1,57 +1,57 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 gap-2 cursor-pointer",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90 bg-linear-to-r from-[#127AF3] to-[#06D5FB]",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-[30px] px-3 py-1",
sm: "h-[20px] rounded-md px-1.5 text-xs",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 gap-2 cursor-pointer",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90 bg-linear-to-r from-[#127AF3] to-[#06D5FB]",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-[30px] px-3 py-1",
sm: "h-[20px] rounded-md px-1.5 text-xs",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+199 -199
View File
@@ -1,199 +1,199 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
html,
body {
height: 100vh;
width: 100vw;
}
#root {
height: 100vh;
width: 100vw;
}
* {
&::-webkit-scrollbar {
height: 8px;
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0 0 0 / 15%);
}
@media (prefers-color-scheme: dark) {
&::-webkit-scrollbar-thumb {
background: rgba(255 255 255 / 15%);
}
}
}
.ant-modal-content {
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100px;
background-image: linear-gradient(#89d4ff 0%, rgba(255, 255, 255, 0) 100%);
border-radius: 8px 8px 0 0;
z-index: 1;
}
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100px;
background-image: url("./assets/images/modal-bg.png");
background-size: contain;
background-repeat: no-repeat;
background-position: 80% 0;
border-radius: 0 0 8px 8px;
z-index: 1;
}
@media (prefers-color-scheme: dark) {
&:before {
filter: invert(100%) hue-rotate(180deg);
}
}
@media (prefers-color-scheme: dark) {
&:after {
filter: invert(100%) hue-rotate(180deg) contrast(100%);
}
}
.ant-modal-header,
.ant-modal-body,
.ant-modal-footer {
position: relative;
z-index: 2;
background: transparent;
}
}
/* biome-ignore lint/suspicious/noUnknownAtRules: shadcn */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
html,
body {
height: 100vh;
width: 100vw;
}
#root {
height: 100vh;
width: 100vw;
}
* {
&::-webkit-scrollbar {
height: 8px;
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(0 0 0 / 15%);
}
@media (prefers-color-scheme: dark) {
&::-webkit-scrollbar-thumb {
background: rgba(255 255 255 / 15%);
}
}
}
.ant-modal-content {
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100px;
background-image: linear-gradient(#89d4ff 0%, rgba(255, 255, 255, 0) 100%);
border-radius: 8px 8px 0 0;
z-index: 1;
}
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100px;
background-image: url("./assets/images/modal-bg.png");
background-size: contain;
background-repeat: no-repeat;
background-position: 80% 0;
border-radius: 0 0 8px 8px;
z-index: 1;
}
@media (prefers-color-scheme: dark) {
&:before {
filter: invert(100%) hue-rotate(180deg);
}
}
@media (prefers-color-scheme: dark) {
&:after {
filter: invert(100%) hue-rotate(180deg) contrast(100%);
}
}
.ant-modal-header,
.ant-modal-body,
.ant-modal-footer {
position: relative;
z-index: 2;
background: transparent;
}
}
/* biome-ignore lint/suspicious/noUnknownAtRules: shadcn */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+213 -213
View File
@@ -1,213 +1,213 @@
import {
type AppStore,
type DownloadTask,
type DownloadTaskPagination,
type ElectronApi,
ADD_DOWNLOAD_ITEMS,
DELETE_DOWNLOAD_ITEM,
EDIT_DOWNLOAD_ITEM,
GET_APP_STORE,
GET_DOWNLOAD_ITEMS,
GET_PAGE_TITLE,
GET_VIDEO_FOLDERS,
SET_APP_STORE,
START_DOWNLOAD,
STOP_DOWNLOAD,
} from "@mediago/shared-common";
import { api, getSocket } from "@/utils";
import { IpcListener } from "./utils";
const defaultResp = {
code: 0,
msg: "",
data: {},
} as any;
/**
* Web 环境适配器
* 为不支持的 Electron 特定功能提供默认实现
* 对于支持的功能调用后端 HTTP API
*/
export const webAdapter: ElectronApi = {
getEnvPath: async () => {
return defaultResp;
},
getFavorites: async () => {
return defaultResp;
},
addFavorite: async () => {
return defaultResp;
},
removeFavorite: async () => {
return defaultResp;
},
setWebviewBounds: async () => {
return defaultResp;
},
webviewGoBack: async () => {
return defaultResp;
},
webviewReload: async () => {
return defaultResp;
},
webviewLoadURL: async () => {
return defaultResp;
},
webviewGoHome: async () => {
return defaultResp;
},
getAppStore: async () => {
return api.post(GET_APP_STORE);
},
onSelectDownloadDir: async () => {
return defaultResp;
},
setAppStore: async (key: keyof AppStore, val: AppStore[keyof AppStore]) => {
return api.post(SET_APP_STORE, { key, val });
},
openDir: async () => {
return defaultResp;
},
createDownloadTasks: async (
items: Omit<DownloadTask, "id">[],
startDownload?: boolean,
) => {
return api.post(ADD_DOWNLOAD_ITEMS, { videos: items, startDownload });
},
getDownloadTasks: async (p: DownloadTaskPagination) => {
return api.post(GET_DOWNLOAD_ITEMS, p);
},
startDownload: async (vid: number) => {
return api.post(START_DOWNLOAD, { vid });
},
openUrl: async (url: string) => {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.click();
return defaultResp;
},
stopDownload: async (id: number) => {
return api.post(STOP_DOWNLOAD, { id });
},
onDownloadListContextMenu: async () => {
return defaultResp;
},
onFavoriteItemContextMenu: async () => {
return defaultResp;
},
deleteDownloadTask: async (id: number) => {
return api.post(DELETE_DOWNLOAD_ITEM, { id });
},
convertToAudio: async () => {
return defaultResp;
},
rendererEvent: async () => {
return defaultResp;
},
removeEventListener: async () => {
return defaultResp;
},
showBrowserWindow: async () => {
return defaultResp;
},
webviewHide: async () => {
return defaultResp;
},
webviewShow: async () => {
return defaultResp;
},
webviewUrlContextMenu: async () => {
return defaultResp;
},
combineToHomePage: async () => {
return defaultResp;
},
updateDownloadTask: async (video: DownloadTask, startDownload?: boolean) => {
return api.post(EDIT_DOWNLOAD_ITEM, { video, startDownload });
},
getLocalIP: async () => {
return defaultResp;
},
openBrowser: async () => {
return defaultResp;
},
selectFile: async () => {
return defaultResp;
},
getSharedState: async () => {
return defaultResp;
},
setSharedState: async () => {
return defaultResp;
},
setUserAgent: async () => {
return defaultResp;
},
getDownloadLog: async () => {
return defaultResp;
},
showDownloadDialog: async () => {
return defaultResp;
},
pluginReady: async () => {
return defaultResp;
},
getConversions: async () => {
return defaultResp;
},
addConversion: async () => {
return defaultResp;
},
deleteConversion: async () => {
return defaultResp;
},
getMachineId: async () => {
return defaultResp;
},
clearWebviewCache: async () => {
return defaultResp;
},
exportFavorites: async () => {
return defaultResp;
},
importFavorites: async () => {
return defaultResp;
},
checkUpdate: async () => {
return defaultResp;
},
startUpdate: async () => {
return defaultResp;
},
installUpdate: async () => {
return defaultResp;
},
exportDownloadList: async () => {
return defaultResp;
},
getVideoFolders: async () => {
return api.post(GET_VIDEO_FOLDERS);
},
getPageTitle: async (url: string) => {
return api.post(GET_PAGE_TITLE, { url });
},
};
/**
* Web 环境 IPC 适配器
* 使用 Socket.io 进行通信
*/
export const webIpcAdapter: IpcListener = {
addIpcListener: (event: string, func: any) => {
const socket = getSocket();
socket.on(event, (payload) => func(null, payload));
},
removeIpcListener: (event: string, func: any) => {
const socket = getSocket();
socket.off(event, (payload) => func(null, payload));
},
};
export type { IpcListener };
import {
type AppStore,
type DownloadTask,
type DownloadTaskPagination,
type ElectronApi,
ADD_DOWNLOAD_ITEMS,
DELETE_DOWNLOAD_ITEM,
EDIT_DOWNLOAD_ITEM,
GET_APP_STORE,
GET_DOWNLOAD_ITEMS,
GET_PAGE_TITLE,
GET_VIDEO_FOLDERS,
SET_APP_STORE,
START_DOWNLOAD,
STOP_DOWNLOAD,
} from "@mediago/shared-common";
import { api, getSocket } from "@/utils";
import { IpcListener } from "./utils";
const defaultResp = {
code: 0,
msg: "",
data: {},
} as any;
/**
* Web 环境适配器
* 为不支持的 Electron 特定功能提供默认实现
* 对于支持的功能调用后端 HTTP API
*/
export const webAdapter: ElectronApi = {
getEnvPath: async () => {
return defaultResp;
},
getFavorites: async () => {
return defaultResp;
},
addFavorite: async () => {
return defaultResp;
},
removeFavorite: async () => {
return defaultResp;
},
setWebviewBounds: async () => {
return defaultResp;
},
webviewGoBack: async () => {
return defaultResp;
},
webviewReload: async () => {
return defaultResp;
},
webviewLoadURL: async () => {
return defaultResp;
},
webviewGoHome: async () => {
return defaultResp;
},
getAppStore: async () => {
return api.post(GET_APP_STORE);
},
onSelectDownloadDir: async () => {
return defaultResp;
},
setAppStore: async (key: keyof AppStore, val: AppStore[keyof AppStore]) => {
return api.post(SET_APP_STORE, { key, val });
},
openDir: async () => {
return defaultResp;
},
createDownloadTasks: async (
items: Omit<DownloadTask, "id">[],
startDownload?: boolean,
) => {
return api.post(ADD_DOWNLOAD_ITEMS, { videos: items, startDownload });
},
getDownloadTasks: async (p: DownloadTaskPagination) => {
return api.post(GET_DOWNLOAD_ITEMS, p);
},
startDownload: async (vid: number) => {
return api.post(START_DOWNLOAD, { vid });
},
openUrl: async (url: string) => {
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.rel = "noopener noreferrer";
a.click();
return defaultResp;
},
stopDownload: async (id: number) => {
return api.post(STOP_DOWNLOAD, { id });
},
onDownloadListContextMenu: async () => {
return defaultResp;
},
onFavoriteItemContextMenu: async () => {
return defaultResp;
},
deleteDownloadTask: async (id: number) => {
return api.post(DELETE_DOWNLOAD_ITEM, { id });
},
convertToAudio: async () => {
return defaultResp;
},
rendererEvent: async () => {
return defaultResp;
},
removeEventListener: async () => {
return defaultResp;
},
showBrowserWindow: async () => {
return defaultResp;
},
webviewHide: async () => {
return defaultResp;
},
webviewShow: async () => {
return defaultResp;
},
webviewUrlContextMenu: async () => {
return defaultResp;
},
combineToHomePage: async () => {
return defaultResp;
},
updateDownloadTask: async (video: DownloadTask, startDownload?: boolean) => {
return api.post(EDIT_DOWNLOAD_ITEM, { video, startDownload });
},
getLocalIP: async () => {
return defaultResp;
},
openBrowser: async () => {
return defaultResp;
},
selectFile: async () => {
return defaultResp;
},
getSharedState: async () => {
return defaultResp;
},
setSharedState: async () => {
return defaultResp;
},
setUserAgent: async () => {
return defaultResp;
},
getDownloadLog: async () => {
return defaultResp;
},
showDownloadDialog: async () => {
return defaultResp;
},
pluginReady: async () => {
return defaultResp;
},
getConversions: async () => {
return defaultResp;
},
addConversion: async () => {
return defaultResp;
},
deleteConversion: async () => {
return defaultResp;
},
getMachineId: async () => {
return defaultResp;
},
clearWebviewCache: async () => {
return defaultResp;
},
exportFavorites: async () => {
return defaultResp;
},
importFavorites: async () => {
return defaultResp;
},
checkUpdate: async () => {
return defaultResp;
},
startUpdate: async () => {
return defaultResp;
},
installUpdate: async () => {
return defaultResp;
},
exportDownloadList: async () => {
return defaultResp;
},
getVideoFolders: async () => {
return api.post(GET_VIDEO_FOLDERS);
},
getPageTitle: async (url: string) => {
return api.post(GET_PAGE_TITLE, { url });
},
};
/**
* Web 环境 IPC 适配器
* 使用 Socket.io 进行通信
*/
export const webIpcAdapter: IpcListener = {
addIpcListener: (event: string, func: any) => {
const socket = getSocket();
socket.on(event, (payload) => func(null, payload));
},
removeIpcListener: (event: string, func: any) => {
const socket = getSocket();
socket.off(event, (payload) => func(null, payload));
},
};
export type { IpcListener };
+18 -18
View File
@@ -1,18 +1,18 @@
const eventMap = new Map();
import { nanoid } from "nanoid";
export const getIpcId = (func: any) => {
let id = "";
if (eventMap.get(func)) {
id = eventMap.get(func);
} else {
id = nanoid();
eventMap.set(func, id);
}
return id;
};
export interface IpcListener {
addIpcListener: (eventName: string, func: any) => void;
removeIpcListener: (eventName: string, func: any) => void;
}
const eventMap = new Map();
import { nanoid } from "nanoid";
export const getIpcId = (func: any) => {
let id = "";
if (eventMap.get(func)) {
id = eventMap.get(func);
} else {
id = nanoid();
eventMap.set(func, id);
}
return id;
};
export interface IpcListener {
addIpcListener: (eventName: string, func: any) => void;
removeIpcListener: (eventName: string, func: any) => void;
}
+73 -73
View File
@@ -1,73 +1,73 @@
import {
DOWNLOAD_EVENT_NAME,
type DownloadEvent,
DownloadStatus,
type DownloadSuccessEvent,
} from "@mediago/shared-common";
import { produce } from "immer";
import { useEffect } from "react";
import { useShallow } from "zustand/react/shallow";
import { downloadStoreSelector, useDownloadStore } from "@/store/download";
import useAPI from "./use-api";
import { useTasks } from "./use-tasks";
const isSuccessEvent = (obj: DownloadEvent): obj is DownloadSuccessEvent =>
obj.type === "success";
const isProgressEvent = (
obj: DownloadEvent,
): obj is DownloadEvent<DownloadProgress[]> => obj.type === "progress";
export function useDownloadEvent() {
const { setEvents } = useDownloadStore(useShallow(downloadStoreSelector));
const { addIpcListener, removeIpcListener } = useAPI();
const { data, mutate, total } = useTasks();
useEffect(() => {
const handleEvent = (_: unknown, eventData: DownloadEvent) => {
console.log("Received download event:", _, eventData);
if (isSuccessEvent(eventData)) {
const newState = produce({ list: data, total }, (draft) => {
const index = draft.list.findIndex(
(item) => item.id === eventData.data.id,
);
if (index > -1) {
draft.list.splice(index, 1);
draft.total = draft.total - 1;
}
});
mutate(newState);
}
if (isProgressEvent(eventData)) {
const eventDataMap = new Map(
eventData.data.map((item) => [String(item.id), item]),
);
const newState = produce({ list: data, total }, (draft) => {
draft.list.forEach((item) => {
const progressItem = eventDataMap.get(String(item.id));
if (progressItem) {
item.status = DownloadStatus.Downloading;
}
});
});
mutate(newState, {
revalidate: false,
});
setEvents(
eventData.data.map((item) => ({
percent: item.percent,
speed: item.speed,
id: item.id,
})),
);
}
};
addIpcListener(DOWNLOAD_EVENT_NAME, handleEvent);
return () => {
removeIpcListener(DOWNLOAD_EVENT_NAME, handleEvent);
};
}, [data, total, addIpcListener, removeIpcListener, mutate, setEvents]);
}
import {
DOWNLOAD_EVENT_NAME,
type DownloadEvent,
DownloadStatus,
type DownloadSuccessEvent,
} from "@mediago/shared-common";
import { produce } from "immer";
import { useEffect } from "react";
import { useShallow } from "zustand/react/shallow";
import { downloadStoreSelector, useDownloadStore } from "@/store/download";
import useAPI from "./use-api";
import { useTasks } from "./use-tasks";
const isSuccessEvent = (obj: DownloadEvent): obj is DownloadSuccessEvent =>
obj.type === "success";
const isProgressEvent = (
obj: DownloadEvent,
): obj is DownloadEvent<DownloadProgress[]> => obj.type === "progress";
export function useDownloadEvent() {
const { setEvents } = useDownloadStore(useShallow(downloadStoreSelector));
const { addIpcListener, removeIpcListener } = useAPI();
const { data, mutate, total } = useTasks();
useEffect(() => {
const handleEvent = (_: unknown, eventData: DownloadEvent) => {
console.log("Received download event:", _, eventData);
if (isSuccessEvent(eventData)) {
const newState = produce({ list: data, total }, (draft) => {
const index = draft.list.findIndex(
(item) => item.id === eventData.data.id,
);
if (index > -1) {
draft.list.splice(index, 1);
draft.total = draft.total - 1;
}
});
mutate(newState);
}
if (isProgressEvent(eventData)) {
const eventDataMap = new Map(
eventData.data.map((item) => [String(item.id), item]),
);
const newState = produce({ list: data, total }, (draft) => {
draft.list.forEach((item) => {
const progressItem = eventDataMap.get(String(item.id));
if (progressItem) {
item.status = DownloadStatus.Downloading;
}
});
});
mutate(newState, {
revalidate: false,
});
setEvents(
eventData.data.map((item) => ({
percent: item.percent,
speed: item.speed,
id: item.id,
})),
);
}
};
addIpcListener(DOWNLOAD_EVENT_NAME, handleEvent);
return () => {
removeIpcListener(DOWNLOAD_EVENT_NAME, handleEvent);
};
}, [data, total, addIpcListener, removeIpcListener, mutate, setEvents]);
}
+75 -75
View File
@@ -1,75 +1,75 @@
import { DownloadFilter, type DownloadTask } from "@mediago/shared-common";
import { useMemo, useState } from "react";
import useSWR from "swr";
import { useShallow } from "zustand/react/shallow";
import { downloadStoreSelector, useDownloadStore } from "@/store/download";
import useAPI from "./use-api";
/**
* Extended Download Task with real-time details
*/
export interface DownloadTaskDetails extends DownloadTask {
// Current download progress percentage
percent: string;
// Current download speed
speed: string;
// Whether the downloaded file still exists
exists?: boolean;
// Local file path
file?: string;
}
export function useTasks(filter: DownloadFilter = DownloadFilter.list) {
const { eventsMap } = useDownloadStore(useShallow(downloadStoreSelector));
const { getDownloadTasks } = useAPI();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const { data, error, isLoading, mutate } = useSWR(
{
key: "api/tasks",
args: {
current: page,
pageSize,
filter,
},
},
({ args }) => {
return getDownloadTasks(args);
},
);
const detail: DownloadTaskDetails[] = useMemo(() => {
return (data?.list || []).map((item) => {
const evnetItem = eventsMap.get(String(item.id));
if (!evnetItem) {
return {
...item,
percent: "0",
speed: "0 B/s",
};
}
return {
...item,
percent: evnetItem.percent || "0",
speed: evnetItem.speed || "0 B/s",
};
});
}, [data, eventsMap]);
return {
data: detail,
total: data?.total || 0,
isLoading,
error,
mutate,
pagination: {
page,
pageSize,
},
setPage,
setPageSize,
};
}
import { DownloadFilter, type DownloadTask } from "@mediago/shared-common";
import { useMemo, useState } from "react";
import useSWR from "swr";
import { useShallow } from "zustand/react/shallow";
import { downloadStoreSelector, useDownloadStore } from "@/store/download";
import useAPI from "./use-api";
/**
* Extended Download Task with real-time details
*/
export interface DownloadTaskDetails extends DownloadTask {
// Current download progress percentage
percent: string;
// Current download speed
speed: string;
// Whether the downloaded file still exists
exists?: boolean;
// Local file path
file?: string;
}
export function useTasks(filter: DownloadFilter = DownloadFilter.list) {
const { eventsMap } = useDownloadStore(useShallow(downloadStoreSelector));
const { getDownloadTasks } = useAPI();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const { data, error, isLoading, mutate } = useSWR(
{
key: "api/tasks",
args: {
current: page,
pageSize,
filter,
},
},
({ args }) => {
return getDownloadTasks(args);
},
);
const detail: DownloadTaskDetails[] = useMemo(() => {
return (data?.list || []).map((item) => {
const evnetItem = eventsMap.get(String(item.id));
if (!evnetItem) {
return {
...item,
percent: "0",
speed: "0 B/s",
};
}
return {
...item,
percent: evnetItem.percent || "0",
speed: evnetItem.speed || "0 B/s",
};
});
}, [data, eventsMap]);
return {
data: detail,
total: data?.total || 0,
isLoading,
error,
mutate,
pagination: {
page,
pageSize,
},
setPage,
setPageSize,
};
}
+17 -17
View File
@@ -1,17 +1,17 @@
import dayjs from "dayjs";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "dayjs/locale/zh-cn";
import App from "./App";
import { tdApp } from "./utils";
import "./i18n";
import "./globals.css";
dayjs.locale("zh-cn");
tdApp.init();
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<App />
</StrictMode>,
);
import dayjs from "dayjs";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "dayjs/locale/zh-cn";
import App from "./App";
import { tdApp } from "./utils";
import "./i18n";
import "./globals.css";
dayjs.locale("zh-cn");
tdApp.init();
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<App />
</StrictMode>,
);
@@ -1,342 +1,342 @@
import {
FileTextOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
} from "@ant-design/icons";
import type {
DownloadTask,
DownloadTaskWithFile,
} from "@mediago/shared-common";
import { useMemoizedFn } from "ahooks";
import { Progress } from "antd";
import { type ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/react/shallow";
import selectedBg from "@/assets/images/select-item-bg.png";
import {
DownloadIcon,
DownloadListIcon,
EditIcon,
FailedIcon,
PauseIcon,
} from "@/assets/svg";
import { DownloadTag } from "@/components/download-tag";
import { IconButton } from "@/components/icon-button";
import { Checkbox } from "@/components/ui/checkbox";
import {
CONTINUE_DOWNLOAD,
DOWNLOAD_NOW,
PLAY_VIDEO,
RESTART_DOWNLOAD,
STOP_DOWNLOAD,
} from "@/const";
import type { DownloadTaskDetails } from "@/hooks/use-tasks";
import { appStoreSelector, useAppStore } from "@/store/app";
import { DownloadStatus } from "@/types";
import { cn, fromatDateTime, tdApp } from "@/utils";
import { TerminalDialog } from "./terminal-dialog";
import useAPI from "@/hooks/use-api";
interface Props {
task: DownloadTaskDetails;
onSelectChange: (id: number) => void;
selected: boolean;
onStartDownload: (id: number) => void;
onStopDownload: (taskId: number) => void;
onContextMenu: (taskId: number) => void;
progress?: DownloadProgress;
onShowEditForm?: (value: DownloadTask) => void;
downloadStatus?: DownloadStatus;
}
export const DownloadTaskItem = ({
task,
onSelectChange,
selected,
onStartDownload,
onStopDownload,
onContextMenu,
onShowEditForm,
}: Props) => {
const appStore = useAppStore(useShallow(appStoreSelector));
const { t } = useTranslation();
const { openUrl } = useAPI();
// Handlers
const handlePlay = useMemoizedFn(() => {
// FIXME: 播放器接入
tdApp.onEvent(PLAY_VIDEO);
openUrl("http://localhost:9800/");
});
const startWithEvent = useMemoizedFn((eventName: string) => {
onStartDownload(task.id);
tdApp.onEvent(eventName);
});
const handleStop = useMemoizedFn(() => {
onStopDownload(task.id);
tdApp.onEvent(STOP_DOWNLOAD);
});
// Action buttons by status (consolidated for clarity)
const actionButtons = useMemo<ReactNode[]>(() => {
const buttons: ReactNode[] = [];
const terminalBtn = appStore.showTerminal ? (
<TerminalDialog
key="terminal"
trigger={
<IconButton
key="terminal"
title={t("terminal")}
icon={<FileTextOutlined />}
/>
}
title={task.name}
id={task.id}
/>
) : null;
const editBtn = (
<IconButton
key="edit"
title={t("edit")}
icon={<EditIcon />}
onClick={() => onShowEditForm?.(task)}
/>
);
switch (task.status) {
case DownloadStatus.Ready:
terminalBtn && buttons.push(terminalBtn);
buttons.push(editBtn);
buttons.push(
<IconButton
key="download"
icon={<DownloadListIcon />}
title={t("download")}
onClick={() => startWithEvent(DOWNLOAD_NOW)}
/>,
);
break;
case DownloadStatus.Downloading:
terminalBtn && buttons.push(terminalBtn);
buttons.push(
<IconButton
key="stop"
title={t("pause")}
icon={<PauseCircleOutlined />}
onClick={handleStop}
/>,
);
break;
case DownloadStatus.Failed:
terminalBtn && buttons.push(terminalBtn);
buttons.push(editBtn);
buttons.push(
<IconButton
key="redownload"
title={t("redownload")}
icon={<DownloadListIcon />}
onClick={() => startWithEvent(RESTART_DOWNLOAD)}
/>,
);
break;
case DownloadStatus.Watting:
buttons.push(<span key="watting">{t("watting")}</span>);
break;
case DownloadStatus.Stopped:
terminalBtn && buttons.push(terminalBtn);
buttons.push(editBtn);
buttons.push(
<IconButton
key="restart"
icon={<DownloadListIcon />}
title={t("continueDownload")}
onClick={() => startWithEvent(CONTINUE_DOWNLOAD)}
/>,
);
break;
default:
// Success
buttons.push(
<IconButton
key="play"
icon={<PlayCircleOutlined />}
title={t("playVideo")}
disabled={!task.exists}
onClick={handlePlay}
/>,
);
break;
}
return buttons;
}, [
appStore.showTerminal,
handlePlay,
handleStop,
task,
onShowEditForm,
startWithEvent,
t,
]);
const renderTitle = useMemoizedFn((task: DownloadTaskWithFile): ReactNode => {
return (
<div
className={cn("truncate text-sm dark:text-[#B4B4B4]", {
"text-[#127af3]": selected,
})}
title={task.name}
>
{task.folder ? `${task.folder}/` : task.folder}
{task.name}
</div>
);
});
const tags = useMemo<ReactNode[]>(() => {
const list: ReactNode[] = [];
if (task.isLive)
list.push(
<DownloadTag key="live" text={t("liveResource")} color="#9abbe2" />,
);
switch (task.status) {
case DownloadStatus.Downloading:
list.push(
<DownloadTag
key="downloading"
icon={<DownloadIcon fill="#fff" width={14} height={14} />}
text={t("downloading")}
color="#127af3"
/>,
);
break;
case DownloadStatus.Success:
list.push(
<DownloadTag
key="success"
text={t("downloadSuccess")}
color="#09ce87"
/>,
);
if (!task.exists) {
list.push(
<DownloadTag
key="notExists"
text={t("fileNotExist")}
color="#9abbe2"
/>,
);
}
break;
case DownloadStatus.Failed:
list.push(
<TerminalDialog
key="failed"
trigger={
<DownloadTag
icon={<FailedIcon />}
text={t("downloadFailed")}
color="#ff7373"
className="cursor-pointer"
/>
}
title={task.name}
id={task.id}
/>,
);
break;
case DownloadStatus.Stopped:
list.push(
<DownloadTag
key="pause"
icon={<PauseIcon />}
text={t("downloadPause")}
color="#9abbe2"
/>,
);
break;
}
return list;
}, [task, t]);
const renderDescription = useMemoizedFn(
(task: DownloadTaskDetails): ReactNode => {
if (task.percent && task.status === DownloadStatus.Downloading) {
const val = Math.round(Number(task.percent));
return (
<div className="flex flex-row items-center gap-2 text-xs text-[rgba(0,0,0,0.88)] dark:text-[rgba(255,255,255,0.85)]">
<Progress percent={val} strokeLinecap="butt" showInfo={false} />
<div className="min-w-5 shrink-0">{val}%</div>
<div className="min-w-20 shrink-0">{task.speed}</div>
</div>
);
}
return (
<div
className="relative flex flex-col gap-1 text-xs text-[#B3B3B3] dark:text-[#515257]"
title={task.url}
>
<div className="truncate">{task.url}</div>
<div className="truncate">
{t("createdAt")} {fromatDateTime(task.createdDate)}
</div>
{task.status === DownloadStatus.Failed && (
<TerminalDialog
asChild
trigger={
<div className="cursor-pointer truncate text-[#ff7373] dark:text-[rgba(255,115,115,0.6)]">
{t("failReason")}: ...
</div>
}
title={task.name}
id={task.id}
/>
)}
</div>
);
},
);
return (
<div
className={cn(
"relative flex flex-row gap-3 rounded-lg bg-[#FAFCFF] px-3 pb-3.5 pt-2 dark:bg-[#27292F]",
{
"bg-linear-to-r from-[#D0E8FF] to-[#F2F7FF] dark:from-[#27292F] dark:to-[#00244E]":
selected,
"opacity-70": task.status === DownloadStatus.Success && !task.exists,
},
)}
onContextMenu={() => onContextMenu(task.id)}
>
<Checkbox
className="mt-2"
checked={selected}
onCheckedChange={() => onSelectChange(task.id)}
/>
<div className={cn("flex flex-1 flex-col gap-1 overflow-hidden")}>
{selected && (
<img
alt=""
src={selectedBg}
className="absolute bottom-0 right-[126px] top-0 block h-full select-none"
/>
)}
<div className="relative flex flex-row items-center gap-2">
{renderTitle(task)}
<div className="flex shrink-0 grow flex-row gap-2">{tags}</div>
<div className="flex flex-row items-center gap-3 rounded-md bg-[#eff4fa] px-1.5 py-1.5 dark:bg-[#3B3F48]">
{actionButtons}
</div>
</div>
{renderDescription(task)}
</div>
</div>
);
};
import {
FileTextOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
} from "@ant-design/icons";
import type {
DownloadTask,
DownloadTaskWithFile,
} from "@mediago/shared-common";
import { useMemoizedFn } from "ahooks";
import { Progress } from "antd";
import { type ReactNode, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/react/shallow";
import selectedBg from "@/assets/images/select-item-bg.png";
import {
DownloadIcon,
DownloadListIcon,
EditIcon,
FailedIcon,
PauseIcon,
} from "@/assets/svg";
import { DownloadTag } from "@/components/download-tag";
import { IconButton } from "@/components/icon-button";
import { Checkbox } from "@/components/ui/checkbox";
import {
CONTINUE_DOWNLOAD,
DOWNLOAD_NOW,
PLAY_VIDEO,
RESTART_DOWNLOAD,
STOP_DOWNLOAD,
} from "@/const";
import type { DownloadTaskDetails } from "@/hooks/use-tasks";
import { appStoreSelector, useAppStore } from "@/store/app";
import { DownloadStatus } from "@/types";
import { cn, fromatDateTime, tdApp } from "@/utils";
import { TerminalDialog } from "./terminal-dialog";
import useAPI from "@/hooks/use-api";
interface Props {
task: DownloadTaskDetails;
onSelectChange: (id: number) => void;
selected: boolean;
onStartDownload: (id: number) => void;
onStopDownload: (taskId: number) => void;
onContextMenu: (taskId: number) => void;
progress?: DownloadProgress;
onShowEditForm?: (value: DownloadTask) => void;
downloadStatus?: DownloadStatus;
}
export const DownloadTaskItem = ({
task,
onSelectChange,
selected,
onStartDownload,
onStopDownload,
onContextMenu,
onShowEditForm,
}: Props) => {
const appStore = useAppStore(useShallow(appStoreSelector));
const { t } = useTranslation();
const { openUrl } = useAPI();
// Handlers
const handlePlay = useMemoizedFn(() => {
// FIXME: 播放器接入
tdApp.onEvent(PLAY_VIDEO);
openUrl("http://localhost:9800/");
});
const startWithEvent = useMemoizedFn((eventName: string) => {
onStartDownload(task.id);
tdApp.onEvent(eventName);
});
const handleStop = useMemoizedFn(() => {
onStopDownload(task.id);
tdApp.onEvent(STOP_DOWNLOAD);
});
// Action buttons by status (consolidated for clarity)
const actionButtons = useMemo<ReactNode[]>(() => {
const buttons: ReactNode[] = [];
const terminalBtn = appStore.showTerminal ? (
<TerminalDialog
key="terminal"
trigger={
<IconButton
key="terminal"
title={t("terminal")}
icon={<FileTextOutlined />}
/>
}
title={task.name}
id={task.id}
/>
) : null;
const editBtn = (
<IconButton
key="edit"
title={t("edit")}
icon={<EditIcon />}
onClick={() => onShowEditForm?.(task)}
/>
);
switch (task.status) {
case DownloadStatus.Ready:
terminalBtn && buttons.push(terminalBtn);
buttons.push(editBtn);
buttons.push(
<IconButton
key="download"
icon={<DownloadListIcon />}
title={t("download")}
onClick={() => startWithEvent(DOWNLOAD_NOW)}
/>,
);
break;
case DownloadStatus.Downloading:
terminalBtn && buttons.push(terminalBtn);
buttons.push(
<IconButton
key="stop"
title={t("pause")}
icon={<PauseCircleOutlined />}
onClick={handleStop}
/>,
);
break;
case DownloadStatus.Failed:
terminalBtn && buttons.push(terminalBtn);
buttons.push(editBtn);
buttons.push(
<IconButton
key="redownload"
title={t("redownload")}
icon={<DownloadListIcon />}
onClick={() => startWithEvent(RESTART_DOWNLOAD)}
/>,
);
break;
case DownloadStatus.Watting:
buttons.push(<span key="watting">{t("watting")}</span>);
break;
case DownloadStatus.Stopped:
terminalBtn && buttons.push(terminalBtn);
buttons.push(editBtn);
buttons.push(
<IconButton
key="restart"
icon={<DownloadListIcon />}
title={t("continueDownload")}
onClick={() => startWithEvent(CONTINUE_DOWNLOAD)}
/>,
);
break;
default:
// Success
buttons.push(
<IconButton
key="play"
icon={<PlayCircleOutlined />}
title={t("playVideo")}
disabled={!task.exists}
onClick={handlePlay}
/>,
);
break;
}
return buttons;
}, [
appStore.showTerminal,
handlePlay,
handleStop,
task,
onShowEditForm,
startWithEvent,
t,
]);
const renderTitle = useMemoizedFn((task: DownloadTaskWithFile): ReactNode => {
return (
<div
className={cn("truncate text-sm dark:text-[#B4B4B4]", {
"text-[#127af3]": selected,
})}
title={task.name}
>
{task.folder ? `${task.folder}/` : task.folder}
{task.name}
</div>
);
});
const tags = useMemo<ReactNode[]>(() => {
const list: ReactNode[] = [];
if (task.isLive)
list.push(
<DownloadTag key="live" text={t("liveResource")} color="#9abbe2" />,
);
switch (task.status) {
case DownloadStatus.Downloading:
list.push(
<DownloadTag
key="downloading"
icon={<DownloadIcon fill="#fff" width={14} height={14} />}
text={t("downloading")}
color="#127af3"
/>,
);
break;
case DownloadStatus.Success:
list.push(
<DownloadTag
key="success"
text={t("downloadSuccess")}
color="#09ce87"
/>,
);
if (!task.exists) {
list.push(
<DownloadTag
key="notExists"
text={t("fileNotExist")}
color="#9abbe2"
/>,
);
}
break;
case DownloadStatus.Failed:
list.push(
<TerminalDialog
key="failed"
trigger={
<DownloadTag
icon={<FailedIcon />}
text={t("downloadFailed")}
color="#ff7373"
className="cursor-pointer"
/>
}
title={task.name}
id={task.id}
/>,
);
break;
case DownloadStatus.Stopped:
list.push(
<DownloadTag
key="pause"
icon={<PauseIcon />}
text={t("downloadPause")}
color="#9abbe2"
/>,
);
break;
}
return list;
}, [task, t]);
const renderDescription = useMemoizedFn(
(task: DownloadTaskDetails): ReactNode => {
if (task.percent && task.status === DownloadStatus.Downloading) {
const val = Math.round(Number(task.percent));
return (
<div className="flex flex-row items-center gap-2 text-xs text-[rgba(0,0,0,0.88)] dark:text-[rgba(255,255,255,0.85)]">
<Progress percent={val} strokeLinecap="butt" showInfo={false} />
<div className="min-w-5 shrink-0">{val}%</div>
<div className="min-w-20 shrink-0">{task.speed}</div>
</div>
);
}
return (
<div
className="relative flex flex-col gap-1 text-xs text-[#B3B3B3] dark:text-[#515257]"
title={task.url}
>
<div className="truncate">{task.url}</div>
<div className="truncate">
{t("createdAt")} {fromatDateTime(task.createdDate)}
</div>
{task.status === DownloadStatus.Failed && (
<TerminalDialog
asChild
trigger={
<div className="cursor-pointer truncate text-[#ff7373] dark:text-[rgba(255,115,115,0.6)]">
{t("failReason")}: ...
</div>
}
title={task.name}
id={task.id}
/>
)}
</div>
);
},
);
return (
<div
className={cn(
"relative flex flex-row gap-3 rounded-lg bg-[#FAFCFF] px-3 pb-3.5 pt-2 dark:bg-[#27292F]",
{
"bg-linear-to-r from-[#D0E8FF] to-[#F2F7FF] dark:from-[#27292F] dark:to-[#00244E]":
selected,
"opacity-70": task.status === DownloadStatus.Success && !task.exists,
},
)}
onContextMenu={() => onContextMenu(task.id)}
>
<Checkbox
className="mt-2"
checked={selected}
onCheckedChange={() => onSelectChange(task.id)}
/>
<div className={cn("flex flex-1 flex-col gap-1 overflow-hidden")}>
{selected && (
<img
alt=""
src={selectedBg}
className="absolute bottom-0 right-[126px] top-0 block h-full select-none"
/>
)}
<div className="relative flex flex-row items-center gap-2">
{renderTitle(task)}
<div className="flex shrink-0 grow flex-row gap-2">{tags}</div>
<div className="flex flex-row items-center gap-3 rounded-md bg-[#eff4fa] px-1.5 py-1.5 dark:bg-[#3B3F48]">
{actionButtons}
</div>
</div>
{renderDescription(task)}
</div>
</div>
);
};
@@ -1,233 +1,233 @@
import type { DownloadTask } from "@mediago/shared-common";
import { useMemoizedFn } from "ahooks";
import { App, Empty } from "antd";
import { produce } from "immer";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import DownloadForm, { type DownloadFormRef } from "@/components/download-form";
import Loading from "@/components/loading";
import { EDIT_DOWNLOAD } from "@/const";
import useAPI from "@/hooks/use-api";
import { useTasks } from "@/hooks/use-tasks";
import type { DownloadFilter } from "@/types";
import { cn, tdApp } from "@/utils";
import { DownloadTaskItem } from "./download-item";
import { ListHeader } from "./list-header";
interface Props {
filter: DownloadFilter;
}
export function DownloadTaskList({ filter }: Props) {
const [selected, setSelected] = useState<number[]>([]);
const {
startDownload,
addIpcListener,
removeIpcListener,
stopDownload,
onDownloadListContextMenu,
deleteDownloadTask,
} = useAPI();
const { message } = App.useApp();
const { t } = useTranslation();
const editFormRef = useRef<DownloadFormRef>(null);
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [hasInitialLoaded, setHasInitialLoaded] = useState(false); // 追踪是否已完成首次加载
const downloadListId = useId();
const { mutate, isLoading, data } = useTasks(filter);
// 追踪首次加载完成
useEffect(() => {
if (!isLoading && !hasInitialLoaded && data.length >= 0) {
setHasInitialLoaded(true);
}
}, [isLoading, hasInitialLoaded, data.length]);
useEffect(() => {
const onDownloadMenuEvent = async (
_e: unknown,
params: { action: string; payload: number },
) => {
const { action, payload } = params;
if (action === "select") {
setSelected((keys) => [...keys, payload]);
} else if (action === "download") {
onStartDownload(payload);
} else if (action === "refresh") {
mutate();
} else if (action === "delete") {
await deleteDownloadTask(payload);
mutate();
}
};
addIpcListener("download-item-event", onDownloadMenuEvent);
return () => {
removeIpcListener("download-item-event", onDownloadMenuEvent);
// 清理未完成的刷新定时器
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
refreshTimeoutRef.current = null;
}
};
}, [mutate]);
const handleItemSelectChange = useMemoizedFn((id: number) => {
setSelected(
produce((draft) => {
const index = draft.indexOf(id);
if (index !== -1) {
draft.splice(index, 1);
} else {
draft.push(id);
}
}),
);
});
const handleSelectAll = useMemoizedFn(() => {
setSelected(
produce((draft) => {
if (draft.length) {
draft.splice(0, draft.length);
} else {
draft.push(...data.map((task) => task.id));
}
}),
);
});
const listChecked = useMemo(() => {
if (selected.length === 0) {
return false;
}
if (selected.length === data.length) {
return true;
}
return "indeterminate";
}, [selected, data.length]);
useEffect(() => {
setSelected([]);
// 筛选条件变化时也重置首次加载状态,但稍作延迟以避免闪烁
setTimeout(() => {
setHasInitialLoaded(false);
}, 50);
}, [filter]);
const onStartDownload = useMemoizedFn(async (id: number) => {
await startDownload(id);
message.success(t("addTaskSuccess"));
mutate();
});
const onStopDownload = useMemoizedFn(async (id: number) => {
await stopDownload(id);
setTimeout(() => {
mutate();
}, 500);
});
const handleFormConfirm = useMemoizedFn(async () => {
mutate();
});
const handleContext = useMemoizedFn((item: number) => {
onDownloadListContextMenu(item);
});
const onDeleteItems = useMemoizedFn(async (ids: number[]) => {
for (const id of ids) {
await deleteDownloadTask(Number(id));
}
setSelected([]);
mutate();
});
const onDownloadItems = useMemoizedFn(async (ids: number[]) => {
for (const id of ids) {
await startDownload(Number(id));
}
message.success(t("addTaskSuccess"));
mutate();
setSelected([]);
});
const onCancelItems = useMemoizedFn(async () => {
setSelected([]);
});
const handleShowDownloadForm = useMemoizedFn((task: DownloadTask) => {
tdApp.onEvent(EDIT_DOWNLOAD);
const { id, name, url, headers, type, folder } = task;
const values = {
batch: false,
id,
name,
url,
headers,
type,
folder,
};
editFormRef.current?.openModal(values);
});
// 只有在非首次加载或首次加载完成后,且确实没有数据时才显示空状态
if (data.length === 0 && (hasInitialLoaded || !isLoading)) {
return (
<div className="flex h-full flex-1 flex-row items-center justify-center rounded-lg bg-white dark:bg-[#1F2024]">
<Empty />
</div>
);
}
return (
<div className="flex flex-col flex-1 overflow-auto">
<ListHeader
selected={selected}
checked={listChecked}
onSelectAll={handleSelectAll}
onDeleteItems={onDeleteItems}
onDownloadItems={onDownloadItems}
onCancelItems={onCancelItems}
filter={filter}
/>
{isLoading && !hasInitialLoaded && data.length === 0 && <Loading />}
<div
className={cn(
"flex w-full flex-1 shrink-0 flex-col gap-3 overflow-auto",
)}
>
{data.map((task) => {
return (
<DownloadTaskItem
key={task.id}
task={task}
selected={selected.includes(task.id)}
onSelectChange={handleItemSelectChange}
onStartDownload={onStartDownload}
onStopDownload={onStopDownload}
onContextMenu={handleContext}
onShowEditForm={handleShowDownloadForm}
/>
);
})}
</div>
<DownloadForm
id={downloadListId}
ref={editFormRef}
isEdit
onConfirm={handleFormConfirm}
/>
</div>
);
}
// Legacy export for backward compatibility
export const DownloadList = DownloadTaskList;
import type { DownloadTask } from "@mediago/shared-common";
import { useMemoizedFn } from "ahooks";
import { App, Empty } from "antd";
import { produce } from "immer";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import DownloadForm, { type DownloadFormRef } from "@/components/download-form";
import Loading from "@/components/loading";
import { EDIT_DOWNLOAD } from "@/const";
import useAPI from "@/hooks/use-api";
import { useTasks } from "@/hooks/use-tasks";
import type { DownloadFilter } from "@/types";
import { cn, tdApp } from "@/utils";
import { DownloadTaskItem } from "./download-item";
import { ListHeader } from "./list-header";
interface Props {
filter: DownloadFilter;
}
export function DownloadTaskList({ filter }: Props) {
const [selected, setSelected] = useState<number[]>([]);
const {
startDownload,
addIpcListener,
removeIpcListener,
stopDownload,
onDownloadListContextMenu,
deleteDownloadTask,
} = useAPI();
const { message } = App.useApp();
const { t } = useTranslation();
const editFormRef = useRef<DownloadFormRef>(null);
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [hasInitialLoaded, setHasInitialLoaded] = useState(false); // 追踪是否已完成首次加载
const downloadListId = useId();
const { mutate, isLoading, data } = useTasks(filter);
// 追踪首次加载完成
useEffect(() => {
if (!isLoading && !hasInitialLoaded && data.length >= 0) {
setHasInitialLoaded(true);
}
}, [isLoading, hasInitialLoaded, data.length]);
useEffect(() => {
const onDownloadMenuEvent = async (
_e: unknown,
params: { action: string; payload: number },
) => {
const { action, payload } = params;
if (action === "select") {
setSelected((keys) => [...keys, payload]);
} else if (action === "download") {
onStartDownload(payload);
} else if (action === "refresh") {
mutate();
} else if (action === "delete") {
await deleteDownloadTask(payload);
mutate();
}
};
addIpcListener("download-item-event", onDownloadMenuEvent);
return () => {
removeIpcListener("download-item-event", onDownloadMenuEvent);
// 清理未完成的刷新定时器
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
refreshTimeoutRef.current = null;
}
};
}, [mutate]);
const handleItemSelectChange = useMemoizedFn((id: number) => {
setSelected(
produce((draft) => {
const index = draft.indexOf(id);
if (index !== -1) {
draft.splice(index, 1);
} else {
draft.push(id);
}
}),
);
});
const handleSelectAll = useMemoizedFn(() => {
setSelected(
produce((draft) => {
if (draft.length) {
draft.splice(0, draft.length);
} else {
draft.push(...data.map((task) => task.id));
}
}),
);
});
const listChecked = useMemo(() => {
if (selected.length === 0) {
return false;
}
if (selected.length === data.length) {
return true;
}
return "indeterminate";
}, [selected, data.length]);
useEffect(() => {
setSelected([]);
// 筛选条件变化时也重置首次加载状态,但稍作延迟以避免闪烁
setTimeout(() => {
setHasInitialLoaded(false);
}, 50);
}, [filter]);
const onStartDownload = useMemoizedFn(async (id: number) => {
await startDownload(id);
message.success(t("addTaskSuccess"));
mutate();
});
const onStopDownload = useMemoizedFn(async (id: number) => {
await stopDownload(id);
setTimeout(() => {
mutate();
}, 500);
});
const handleFormConfirm = useMemoizedFn(async () => {
mutate();
});
const handleContext = useMemoizedFn((item: number) => {
onDownloadListContextMenu(item);
});
const onDeleteItems = useMemoizedFn(async (ids: number[]) => {
for (const id of ids) {
await deleteDownloadTask(Number(id));
}
setSelected([]);
mutate();
});
const onDownloadItems = useMemoizedFn(async (ids: number[]) => {
for (const id of ids) {
await startDownload(Number(id));
}
message.success(t("addTaskSuccess"));
mutate();
setSelected([]);
});
const onCancelItems = useMemoizedFn(async () => {
setSelected([]);
});
const handleShowDownloadForm = useMemoizedFn((task: DownloadTask) => {
tdApp.onEvent(EDIT_DOWNLOAD);
const { id, name, url, headers, type, folder } = task;
const values = {
batch: false,
id,
name,
url,
headers,
type,
folder,
};
editFormRef.current?.openModal(values);
});
// 只有在非首次加载或首次加载完成后,且确实没有数据时才显示空状态
if (data.length === 0 && (hasInitialLoaded || !isLoading)) {
return (
<div className="flex h-full flex-1 flex-row items-center justify-center rounded-lg bg-white dark:bg-[#1F2024]">
<Empty />
</div>
);
}
return (
<div className="flex flex-col flex-1 overflow-auto">
<ListHeader
selected={selected}
checked={listChecked}
onSelectAll={handleSelectAll}
onDeleteItems={onDeleteItems}
onDownloadItems={onDownloadItems}
onCancelItems={onCancelItems}
filter={filter}
/>
{isLoading && !hasInitialLoaded && data.length === 0 && <Loading />}
<div
className={cn(
"flex w-full flex-1 shrink-0 flex-col gap-3 overflow-auto",
)}
>
{data.map((task) => {
return (
<DownloadTaskItem
key={task.id}
task={task}
selected={selected.includes(task.id)}
onSelectChange={handleItemSelectChange}
onStartDownload={onStartDownload}
onStopDownload={onStopDownload}
onContextMenu={handleContext}
onShowEditForm={handleShowDownloadForm}
/>
);
})}
</div>
<DownloadForm
id={downloadListId}
ref={editFormRef}
isEdit
onConfirm={handleFormConfirm}
/>
</div>
);
}
// Legacy export for backward compatibility
export const DownloadList = DownloadTaskList;
+200 -200
View File
@@ -1,200 +1,200 @@
import { QrcodeOutlined } from "@ant-design/icons";
import { DownloadTask } from "@mediago/shared-common";
import { useMemoizedFn, useMount } from "ahooks";
import { Pagination, Popover, QRCode } from "antd";
import { type FC, useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { useShallow } from "zustand/react/shallow";
import { ExtractIcon, FolderIcon } from "@/assets/svg";
import DownloadForm, {
type DownloadFormItem,
type DownloadFormRef,
} from "@/components/download-form";
import { HomeDownloadButton } from "@/components/home-download-button";
import PageContainer from "@/components/page-container";
import { Button } from "@/components/ui/button";
import { CLICK_DOWNLOAD } from "@/const";
import useAPI from "@/hooks/use-api";
import { useDownloadEvent } from "@/hooks/use-download-event";
import { useTasks } from "@/hooks/use-tasks";
import { appStoreSelector, useAppStore } from "@/store/app";
import { downloadFormSelector, useConfigStore } from "@/store/config";
import { DownloadFilter } from "@/types";
import {
isDownloadType,
isWeb,
randomName,
tdApp,
urlDownloadType,
} from "@/utils";
import { DownloadList } from "./components/download-list";
interface Props {
filter?: DownloadFilter;
}
const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
const {
openDir,
showBrowserWindow,
createDownloadTasks,
getLocalIP,
addIpcListener,
removeIpcListener,
} = useAPI();
const appStore = useAppStore(useShallow(appStoreSelector));
const { t } = useTranslation();
const [localIP, setLocalIP] = useState<string>("");
const newFormRef = useRef<DownloadFormRef>(null);
const homeId = useId();
const { lastIsBatch, lastDownloadTypes } = useConfigStore(
useShallow(downloadFormSelector),
);
const location = useLocation();
const { pagination, total, mutate, setPage } = useTasks(filter);
useDownloadEvent();
useEffect(() => {
const search = new URLSearchParams(location.search);
// new
if (search.has("n")) {
const typeParam = search.get("type");
const silent = !!search.get("silent");
const urlDecode = decodeURIComponent(search.get("encodedURL") || "");
const url = urlDecode || search.get("url") || "";
const name = search.get("name") + randomName() || randomName();
const type = isDownloadType(typeParam) ? typeParam : urlDownloadType(url);
const headers = decodeURIComponent(search.get("headers") || "");
if (silent) {
const item: Omit<DownloadTask, "id"> = {
type,
url,
name,
headers,
folder: "",
};
createDownloadTasks([item], true);
} else {
const item: DownloadFormItem = {
batch: false,
type,
url,
name,
headers,
};
newFormRef.current?.openModal(item);
}
}
}, [location.search]);
// mac ipc event get url params in macos schceme
useEffect(() => {
const handleUrlEvent = (event: unknown, url: string) => {
const searchParams = new URLSearchParams(url.split("?")[1]);
if (searchParams.get("n") === "true") {
const name = searchParams.get("name") || randomName();
const urlParam = searchParams.get("url") || "";
const item: DownloadFormItem = {
batch: false,
type: urlDownloadType(urlParam),
url: urlParam,
name,
headers: "",
};
newFormRef.current?.openModal(item);
}
};
addIpcListener("url-params", handleUrlEvent);
return () => {
removeIpcListener("url-params", handleUrlEvent);
};
}, []);
useMount(async () => {
const ip = await getLocalIP();
setLocalIP(ip);
});
const handleOpenForm = useMemoizedFn(() => {
tdApp.onEvent(CLICK_DOWNLOAD);
const item: DownloadFormItem = {
batch: lastIsBatch,
type: lastDownloadTypes,
};
newFormRef.current?.openModal(item);
});
const handleConfirm = useMemoizedFn(async () => {
mutate();
});
return (
<PageContainer
title={
filter === DownloadFilter.list
? t("downloadList")
: t("downloadComplete")
}
rightExtra={
<div className="flex flex-row gap-2">
{!isWeb && (
<Button onClick={() => openDir(appStore.local)}>
<FolderIcon />
{t("openFolder")}
</Button>
)}
{filter === DownloadFilter.list && appStore.openInNewWindow && (
<Button onClick={() => showBrowserWindow()}>
<ExtractIcon fill="#fff" />
{t("materialExtraction")}
</Button>
)}
{filter === DownloadFilter.done && !isWeb && (
<Popover
content={
<div>
<QRCode value={localIP ? `http://${localIP}:3222/` : ""} />
<div className="text-xs">{t("scanToWatch")}</div>
</div>
}
placement="bottomRight"
>
<Button>
<QrcodeOutlined />
{t("playOnMobile")}
</Button>
</Popover>
)}
{filter === DownloadFilter.list && (
<HomeDownloadButton onClick={handleOpenForm} />
)}
</div>
}
className="bg-white p-3 dark:bg-[#1F2024] flex flex-col flex-1 h-full rounded-lg gap-3"
>
<DownloadList filter={filter} />
<Pagination
className="flex justify-end"
current={pagination.page}
pageSize={pagination.pageSize}
onChange={setPage}
total={total}
showSizeChanger={false}
/>
<DownloadForm
id={homeId}
ref={newFormRef}
destroyOnClose
onConfirm={handleConfirm}
/>
</PageContainer>
);
};
export default HomePage;
import { QrcodeOutlined } from "@ant-design/icons";
import { DownloadTask } from "@mediago/shared-common";
import { useMemoizedFn, useMount } from "ahooks";
import { Pagination, Popover, QRCode } from "antd";
import { type FC, useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { useShallow } from "zustand/react/shallow";
import { ExtractIcon, FolderIcon } from "@/assets/svg";
import DownloadForm, {
type DownloadFormItem,
type DownloadFormRef,
} from "@/components/download-form";
import { HomeDownloadButton } from "@/components/home-download-button";
import PageContainer from "@/components/page-container";
import { Button } from "@/components/ui/button";
import { CLICK_DOWNLOAD } from "@/const";
import useAPI from "@/hooks/use-api";
import { useDownloadEvent } from "@/hooks/use-download-event";
import { useTasks } from "@/hooks/use-tasks";
import { appStoreSelector, useAppStore } from "@/store/app";
import { downloadFormSelector, useConfigStore } from "@/store/config";
import { DownloadFilter } from "@/types";
import {
isDownloadType,
isWeb,
randomName,
tdApp,
urlDownloadType,
} from "@/utils";
import { DownloadList } from "./components/download-list";
interface Props {
filter?: DownloadFilter;
}
const HomePage: FC<Props> = ({ filter = DownloadFilter.list }) => {
const {
openDir,
showBrowserWindow,
createDownloadTasks,
getLocalIP,
addIpcListener,
removeIpcListener,
} = useAPI();
const appStore = useAppStore(useShallow(appStoreSelector));
const { t } = useTranslation();
const [localIP, setLocalIP] = useState<string>("");
const newFormRef = useRef<DownloadFormRef>(null);
const homeId = useId();
const { lastIsBatch, lastDownloadTypes } = useConfigStore(
useShallow(downloadFormSelector),
);
const location = useLocation();
const { pagination, total, mutate, setPage } = useTasks(filter);
useDownloadEvent();
useEffect(() => {
const search = new URLSearchParams(location.search);
// new
if (search.has("n")) {
const typeParam = search.get("type");
const silent = !!search.get("silent");
const urlDecode = decodeURIComponent(search.get("encodedURL") || "");
const url = urlDecode || search.get("url") || "";
const name = search.get("name") + randomName() || randomName();
const type = isDownloadType(typeParam) ? typeParam : urlDownloadType(url);
const headers = decodeURIComponent(search.get("headers") || "");
if (silent) {
const item: Omit<DownloadTask, "id"> = {
type,
url,
name,
headers,
folder: "",
};
createDownloadTasks([item], true);
} else {
const item: DownloadFormItem = {
batch: false,
type,
url,
name,
headers,
};
newFormRef.current?.openModal(item);
}
}
}, [location.search]);
// mac ipc event get url params in macos schceme
useEffect(() => {
const handleUrlEvent = (event: unknown, url: string) => {
const searchParams = new URLSearchParams(url.split("?")[1]);
if (searchParams.get("n") === "true") {
const name = searchParams.get("name") || randomName();
const urlParam = searchParams.get("url") || "";
const item: DownloadFormItem = {
batch: false,
type: urlDownloadType(urlParam),
url: urlParam,
name,
headers: "",
};
newFormRef.current?.openModal(item);
}
};
addIpcListener("url-params", handleUrlEvent);
return () => {
removeIpcListener("url-params", handleUrlEvent);
};
}, []);
useMount(async () => {
const ip = await getLocalIP();
setLocalIP(ip);
});
const handleOpenForm = useMemoizedFn(() => {
tdApp.onEvent(CLICK_DOWNLOAD);
const item: DownloadFormItem = {
batch: lastIsBatch,
type: lastDownloadTypes,
};
newFormRef.current?.openModal(item);
});
const handleConfirm = useMemoizedFn(async () => {
mutate();
});
return (
<PageContainer
title={
filter === DownloadFilter.list
? t("downloadList")
: t("downloadComplete")
}
rightExtra={
<div className="flex flex-row gap-2">
{!isWeb && (
<Button onClick={() => openDir(appStore.local)}>
<FolderIcon />
{t("openFolder")}
</Button>
)}
{filter === DownloadFilter.list && appStore.openInNewWindow && (
<Button onClick={() => showBrowserWindow()}>
<ExtractIcon fill="#fff" />
{t("materialExtraction")}
</Button>
)}
{filter === DownloadFilter.done && !isWeb && (
<Popover
content={
<div>
<QRCode value={localIP ? `http://${localIP}:3222/` : ""} />
<div className="text-xs">{t("scanToWatch")}</div>
</div>
}
placement="bottomRight"
>
<Button>
<QrcodeOutlined />
{t("playOnMobile")}
</Button>
</Popover>
)}
{filter === DownloadFilter.list && (
<HomeDownloadButton onClick={handleOpenForm} />
)}
</div>
}
className="bg-white p-3 dark:bg-[#1F2024] flex flex-col flex-1 h-full rounded-lg gap-3"
>
<DownloadList filter={filter} />
<Pagination
className="flex justify-end"
current={pagination.page}
pageSize={pagination.pageSize}
onChange={setPage}
total={total}
showSizeChanger={false}
/>
<DownloadForm
id={homeId}
ref={newFormRef}
destroyOnClose
onConfirm={handleConfirm}
/>
</PageContainer>
);
};
export default HomePage;
@@ -1,110 +1,110 @@
import {
DeleteOutlined,
DockerOutlined,
EditOutlined,
} from "@ant-design/icons";
import { useMemoizedFn } from "ahooks";
import { Button as AntdButton, App } from "antd";
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/react/shallow";
import { Button } from "@/components/ui/button";
import { appStoreSelector, useAppStore } from "@/store/app";
import {
browserStoreSelector,
type SourceData,
setBrowserSelector,
useBrowserStore,
} from "@/store/browser";
import useAPI from "@/hooks/use-api";
export function BrowserViewPanel() {
const store = useBrowserStore(useShallow(browserStoreSelector));
const { enableDocker } = useAppStore(useShallow(appStoreSelector));
const { deleteSource, clearSources } = useBrowserStore(
useShallow(setBrowserSelector)
);
const { t } = useTranslation();
const { showDownloadDialog, createDownloadTasks } = useAPI();
const { message } = App.useApp();
const handleClear = useMemoizedFn(() => {
clearSources();
});
const handleDownloadNow = useMemoizedFn(async (item: SourceData) => {
try {
const downloadTask: Omit<DownloadTask, "id"> = {
url: item.url,
name: item.name,
headers: item.headers,
type: item.type,
folder: "",
};
await createDownloadTasks([downloadTask], true);
} catch (e) {
message.error((e as any).message);
}
});
return (
<div className="flex h-full flex-col gap-3 overflow-y-auto bg-white p-3 dark:bg-[#1F2024]">
<div>
<AntdButton size="small" danger onClick={handleClear}>
</AntdButton>
</div>
{store.sources.map((item, index) => {
return (
<div
className="flex flex-col gap-2 rounded-lg bg-[#FAFCFF] p-2 dark:bg-[#27292F]"
key={item.url}
>
<span
className="line-clamp-2 cursor-default break-words text-sm text-[#343434] dark:text-[#B4B4B4]"
title={item.name}
>
{item.name}
</span>
<span
className="line-clamp-2 cursor-default break-words text-xs dark:text-[#515257]"
title={item.url}
>
{item.url}
</span>
<div className="flex flex-row items-center justify-between gap-3">
<div className="flex flex-row items-center gap-2">
<AntdButton
icon={<DeleteOutlined />}
type="text"
size="small"
onClick={() => deleteSource(item.url)}
title={t("delete")}
danger
/>
<AntdButton
icon={<EditOutlined />}
type="text"
size="small"
title={t("edit")}
onClick={() => showDownloadDialog([item])}
/>
{enableDocker && (
<AntdButton
icon={<DockerOutlined />}
type="text"
size="small"
title={t("edit")}
onClick={() => showDownloadDialog([item])}
/>
)}
</div>
<Button size="sm" onClick={() => handleDownloadNow(item)}>
{t("downloadNow")}
</Button>
</div>
</div>
);
})}
</div>
);
}
import {
DeleteOutlined,
DockerOutlined,
EditOutlined,
} from "@ant-design/icons";
import { useMemoizedFn } from "ahooks";
import { Button as AntdButton, App } from "antd";
import { useTranslation } from "react-i18next";
import { useShallow } from "zustand/react/shallow";
import { Button } from "@/components/ui/button";
import { appStoreSelector, useAppStore } from "@/store/app";
import {
browserStoreSelector,
type SourceData,
setBrowserSelector,
useBrowserStore,
} from "@/store/browser";
import useAPI from "@/hooks/use-api";
export function BrowserViewPanel() {
const store = useBrowserStore(useShallow(browserStoreSelector));
const { enableDocker } = useAppStore(useShallow(appStoreSelector));
const { deleteSource, clearSources } = useBrowserStore(
useShallow(setBrowserSelector)
);
const { t } = useTranslation();
const { showDownloadDialog, createDownloadTasks } = useAPI();
const { message } = App.useApp();
const handleClear = useMemoizedFn(() => {
clearSources();
});
const handleDownloadNow = useMemoizedFn(async (item: SourceData) => {
try {
const downloadTask: Omit<DownloadTask, "id"> = {
url: item.url,
name: item.name,
headers: item.headers,
type: item.type,
folder: "",
};
await createDownloadTasks([downloadTask], true);
} catch (e) {
message.error((e as any).message);
}
});
return (
<div className="flex h-full flex-col gap-3 overflow-y-auto bg-white p-3 dark:bg-[#1F2024]">
<div>
<AntdButton size="small" danger onClick={handleClear}>
</AntdButton>
</div>
{store.sources.map((item, index) => {
return (
<div
className="flex flex-col gap-2 rounded-lg bg-[#FAFCFF] p-2 dark:bg-[#27292F]"
key={item.url}
>
<span
className="line-clamp-2 cursor-default break-words text-sm text-[#343434] dark:text-[#B4B4B4]"
title={item.name}
>
{item.name}
</span>
<span
className="line-clamp-2 cursor-default break-words text-xs dark:text-[#515257]"
title={item.url}
>
{item.url}
</span>
<div className="flex flex-row items-center justify-between gap-3">
<div className="flex flex-row items-center gap-2">
<AntdButton
icon={<DeleteOutlined />}
type="text"
size="small"
onClick={() => deleteSource(item.url)}
title={t("delete")}
danger
/>
<AntdButton
icon={<EditOutlined />}
type="text"
size="small"
title={t("edit")}
onClick={() => showDownloadDialog([item])}
/>
{enableDocker && (
<AntdButton
icon={<DockerOutlined />}
type="text"
size="small"
title={t("edit")}
onClick={() => showDownloadDialog([item])}
/>
)}
</div>
<Button size="sm" onClick={() => handleDownloadNow(item)}>
{t("downloadNow")}
</Button>
</div>
</div>
);
})}
</div>
);
}
+105 -105
View File
@@ -1,105 +1,105 @@
declare interface EnvPath {
binPath: string;
dbPath: string;
workspace: string;
platform: string;
local: string;
}
declare interface DownloadTaskResponse {
total: number;
list: DownloadTask[];
}
declare interface DownloadTaskPagination {
current?: number;
pageSize?: number;
filter?: string;
}
declare interface Favorite {
id: number;
title: string;
url: string;
icon?: string;
}
declare interface WebSource {
url: string;
name: string;
headers: string;
}
declare interface UrlDetail {
url: string;
title: 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;
// Whether to block ads
blockAds?: boolean;
// theme
theme?: AppTheme;
// Whether to use extensions
useExtension?: boolean;
// The mobile UA is used by default
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;
// Whether to play sounds in the browser. The default value is mute
audioMuted?: boolean;
// Whether to enable Docker
enableDocker?: boolean;
// Docker URL
dockerUrl?: string;
}
declare interface BrowserStore {
mode: PageMode;
url: string;
title: string;
status: BrowserStatus;
errCode?: number;
errMsg?: string;
sources: SourceData[];
}
declare interface DownloadProgress {
id: number;
speed: string;
percent: string;
isLive: boolean;
}
interface ObjectConstructor {
keys<T>(o: T): (keyof T)[];
}
interface ListPagination {
total: number;
list: DownloadTaskWithFile[];
}
declare interface EnvPath {
binPath: string;
dbPath: string;
workspace: string;
platform: string;
local: string;
}
declare interface DownloadTaskResponse {
total: number;
list: DownloadTask[];
}
declare interface DownloadTaskPagination {
current?: number;
pageSize?: number;
filter?: string;
}
declare interface Favorite {
id: number;
title: string;
url: string;
icon?: string;
}
declare interface WebSource {
url: string;
name: string;
headers: string;
}
declare interface UrlDetail {
url: string;
title: 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;
// Whether to block ads
blockAds?: boolean;
// theme
theme?: AppTheme;
// Whether to use extensions
useExtension?: boolean;
// The mobile UA is used by default
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;
// Whether to play sounds in the browser. The default value is mute
audioMuted?: boolean;
// Whether to enable Docker
enableDocker?: boolean;
// Docker URL
dockerUrl?: string;
}
declare interface BrowserStore {
mode: PageMode;
url: string;
title: string;
status: BrowserStatus;
errCode?: number;
errMsg?: string;
sources: SourceData[];
}
declare interface DownloadProgress {
id: number;
speed: string;
percent: string;
isLive: boolean;
}
interface ObjectConstructor {
keys<T>(o: T): (keyof T)[];
}
interface ListPagination {
total: number;
list: DownloadTaskWithFile[];
}
+56 -56
View File
@@ -1,56 +1,56 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
interface DownloadEvent {
percent: string;
speed: string;
id: number;
}
interface DownloadStore {
count: number;
events: DownloadEvent[];
eventsMap: Map<string, DownloadEvent>;
}
const initialState: DownloadStore = {
count: 0,
events: [],
eventsMap: new Map(),
};
type Actions = {
clearCount: () => void;
increase: () => void;
setEvents: (events: DownloadEvent[]) => void;
};
export const useDownloadStore = create<DownloadStore & Actions>()(
immer((set) => ({
...initialState,
clearCount: () =>
set((state) => {
state.count = 0;
}),
increase: () =>
set((state) => {
state.count += 1;
}),
setEvents: (events: DownloadEvent[]) =>
set((state) => {
state.events = events;
state.eventsMap = new Map(events.map((item) => [String(item.id), item]));
}),
})),
);
export const downloadStoreSelector = (state: DownloadStore & Actions) => {
return {
count: state.count,
clearCount: state.clearCount,
increase: state.increase,
events: state.events,
eventsMap: state.eventsMap,
setEvents: state.setEvents,
};
};
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
interface DownloadEvent {
percent: string;
speed: string;
id: number;
}
interface DownloadStore {
count: number;
events: DownloadEvent[];
eventsMap: Map<string, DownloadEvent>;
}
const initialState: DownloadStore = {
count: 0,
events: [],
eventsMap: new Map(),
};
type Actions = {
clearCount: () => void;
increase: () => void;
setEvents: (events: DownloadEvent[]) => void;
};
export const useDownloadStore = create<DownloadStore & Actions>()(
immer((set) => ({
...initialState,
clearCount: () =>
set((state) => {
state.count = 0;
}),
increase: () =>
set((state) => {
state.count += 1;
}),
setEvents: (events: DownloadEvent[]) =>
set((state) => {
state.events = events;
state.eventsMap = new Map(events.map((item) => [String(item.id), item]));
}),
})),
);
export const downloadStoreSelector = (state: DownloadStore & Actions) => {
return {
count: state.count,
clearCount: state.clearCount,
increase: state.increase,
events: state.events,
eventsMap: state.eventsMap,
setEvents: state.setEvents,
};
};
+100 -100
View File
@@ -1,100 +1,100 @@
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
import { customAlphabet } from "nanoid";
import { twMerge } from "tailwind-merge";
import { DownloadType } from "@/types";
import { isUrl } from "./url";
export { api, http } from "./http";
export { getSocket } from "./socket";
export { tdApp } from "./tdapp";
const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 10);
export const requestImage = (url: string, timeout = 1000): Promise<void> => {
return new Promise((resolve, reject) => {
const img = new Image();
const handleError = () => {
img.src = "";
reject();
};
const timer = setTimeout(handleError, timeout);
const handleOnLoad = () => {
clearTimeout(timer);
resolve();
};
img.onerror = handleError;
img.onload = handleOnLoad;
img.src = url;
});
};
export const getFavIcon = async (url: string) => {
let iconUrl = "";
try {
const urlObject = new URL(url);
const fetchUrl = urlObject.origin ? `${urlObject.origin}/favicon.ico` : "";
await requestImage(fetchUrl);
iconUrl = fetchUrl;
} catch {
// empty
}
return iconUrl;
};
export const generateUrl = (url: string) => {
let result = url;
if (!/^https?:\/\//.test(url)) {
result = `http://${url}`;
}
if (!isUrl(result)) {
result = `https://www.baidu.com/s?word=${url}`;
}
return result;
};
export function moment() {
return dayjs().format("YYYY-MM-DDTHH:mm:ssZ");
}
export function fromatDateTime(d: string | number | Date | undefined, tmpStr: string = "YYYY/MM/DD HH:mm:ss") {
if (!d) return "";
return dayjs(d).format(tmpStr);
}
export function getFileName(url: string) {
const urlObject = new URL(url);
const name = urlObject.pathname.split("/").pop() || "";
return decodeURIComponent(name);
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function randomName() {
return dayjs().format("YYYYMMDD") + "-" + nanoid();
}
export const isWeb = import.meta.env.APP_TARGET === "server";
export function isDownloadType(value: string | null): value is DownloadType {
if (!value) return false;
return Object.values(DownloadType).includes(value as DownloadType);
}
export const urlDownloadType = (url: string): DownloadType => {
if (url.includes("bilibili")) {
return DownloadType.bilibili;
}
if (url.includes("m3u8")) {
return DownloadType.m3u8;
}
return DownloadType.direct;
};
export const convertPlainObject = (obj: unknown) => {
return JSON.parse(JSON.stringify(obj));
};
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
import { customAlphabet } from "nanoid";
import { twMerge } from "tailwind-merge";
import { DownloadType } from "@/types";
import { isUrl } from "./url";
export { api, http } from "./http";
export { getSocket } from "./socket";
export { tdApp } from "./tdapp";
const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 10);
export const requestImage = (url: string, timeout = 1000): Promise<void> => {
return new Promise((resolve, reject) => {
const img = new Image();
const handleError = () => {
img.src = "";
reject();
};
const timer = setTimeout(handleError, timeout);
const handleOnLoad = () => {
clearTimeout(timer);
resolve();
};
img.onerror = handleError;
img.onload = handleOnLoad;
img.src = url;
});
};
export const getFavIcon = async (url: string) => {
let iconUrl = "";
try {
const urlObject = new URL(url);
const fetchUrl = urlObject.origin ? `${urlObject.origin}/favicon.ico` : "";
await requestImage(fetchUrl);
iconUrl = fetchUrl;
} catch {
// empty
}
return iconUrl;
};
export const generateUrl = (url: string) => {
let result = url;
if (!/^https?:\/\//.test(url)) {
result = `http://${url}`;
}
if (!isUrl(result)) {
result = `https://www.baidu.com/s?word=${url}`;
}
return result;
};
export function moment() {
return dayjs().format("YYYY-MM-DDTHH:mm:ssZ");
}
export function fromatDateTime(d: string | number | Date | undefined, tmpStr: string = "YYYY/MM/DD HH:mm:ss") {
if (!d) return "";
return dayjs(d).format(tmpStr);
}
export function getFileName(url: string) {
const urlObject = new URL(url);
const name = urlObject.pathname.split("/").pop() || "";
return decodeURIComponent(name);
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function randomName() {
return dayjs().format("YYYYMMDD") + "-" + nanoid();
}
export const isWeb = import.meta.env.APP_TARGET === "server";
export function isDownloadType(value: string | null): value is DownloadType {
if (!value) return false;
return Object.values(DownloadType).includes(value as DownloadType);
}
export const urlDownloadType = (url: string): DownloadType => {
if (url.includes("bilibili")) {
return DownloadType.bilibili;
}
if (url.includes("m3u8")) {
return DownloadType.m3u8;
}
return DownloadType.direct;
};
export const convertPlainObject = (obj: unknown) => {
return JSON.parse(JSON.stringify(obj));
};
+17 -17
View File
@@ -1,17 +1,17 @@
{
"extends": "../../packages/config/src/tsconfig.app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"include": ["src"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
{
"extends": "../../packages/config/src/tsconfig.app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"include": ["src"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
+11 -11
View File
@@ -1,11 +1,11 @@
{
"compilerOptions": {
"strict": true,
"composite": true,
"forceConsistentCasingInFileNames": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
{
"compilerOptions": {
"strict": true,
"composite": true,
"forceConsistentCasingInFileNames": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+4 -4
View File
@@ -1,4 +1,4 @@
node_modules
.vitepress/dist
.vitepress/cache
.temp
node_modules
.vitepress/dist
.vitepress/cache
.temp
+39 -39
View File
@@ -1,39 +1,39 @@
<script setup lang="ts">
import DefaultTheme from "vitepress/theme";
import { useData, inBrowser } from "vitepress";
import { watchEffect } from "vue";
import Comments from "./components/Comments.vue";
import Footer from "./components/Footer.vue";
import QrCode from "./components/QrCode.vue";
import { useI18n } from "vue-i18n";
import AdBanner from "./components/AdBanner.vue";
const { lang } = useData();
const { locale } = useI18n();
watchEffect(() => {
if (inBrowser) {
locale.value = lang.value;
document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2030 00:00:00 UTC; path=/`;
}
});
const { Layout } = DefaultTheme;
</script>
<template>
<Layout>
<!-- <template #sidebar-nav-before>
<AdBanner />
</template> -->
<template #doc-footer-before>
<QrCode />
</template>
<template #doc-after>
<Comments />
</template>
<template #layout-bottom>
<Footer />
</template>
</Layout>
</template>
<script setup lang="ts">
import DefaultTheme from "vitepress/theme";
import { useData, inBrowser } from "vitepress";
import { watchEffect } from "vue";
import Comments from "./components/Comments.vue";
import Footer from "./components/Footer.vue";
import QrCode from "./components/QrCode.vue";
import { useI18n } from "vue-i18n";
import AdBanner from "./components/AdBanner.vue";
const { lang } = useData();
const { locale } = useI18n();
watchEffect(() => {
if (inBrowser) {
locale.value = lang.value;
document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2030 00:00:00 UTC; path=/`;
}
});
const { Layout } = DefaultTheme;
</script>
<template>
<Layout>
<!-- <template #sidebar-nav-before>
<AdBanner />
</template> -->
<template #doc-footer-before>
<QrCode />
</template>
<template #doc-after>
<Comments />
</template>
<template #layout-bottom>
<Footer />
</template>
</Layout>
</template>
+14 -14
View File
@@ -1,14 +1,14 @@
<template>
<div class="ad-container">
<img src="/images/ad.png" alt="ad" />
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.ad-container {
width: 100%;
padding: 0;
}
</style>
<template>
<div class="ad-container">
<img src="/images/ad.png" alt="ad" />
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.ad-container {
width: 100%;
padding: 0;
}
</style>
+49 -49
View File
@@ -1,49 +1,49 @@
<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>
<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>
+84 -84
View File
@@ -1,84 +1,84 @@
<template>
<footer class="relative z-[1]" :class="{ 'lg:pl-[var(--vp-sidebar-width)] lg:m-0': hasSidebar }">
<div class="relative px-6 py-8 border-t border-[var(--vp-c-divider)] bg-[var(--vp-c-bg)] mx-auto max-w-[1200px]">
<!-- 顶部区域 -->
<div class="flex flex-col md:flex-row justify-between -mx-3 pb-6">
<!-- Logo部分 -->
<div class="px-3 flex-shrink-0 md:flex-1 max-w-full">
<img src="/favicon.ico" alt="MediaGo Logo" class="w-10 h-10" />
<div class="text-base font-semibold my-1 text-[var(--vp-c-text-1)]">
MediaGo
</div>
<div class="text-sm text-[var(--vp-c-text-2)]">{{ t("slogan") }}</div>
</div>
<!-- 右侧区域 -->
<div class="flex flex-col md:flex-row gap-8 md:gap-12">
<!-- 二维码部分 -->
<div class="px-3 flex-shrink-0 mt-8 md:mt-0">
<!-- <div class="font-semibold mb-2 text-[var(--vp-c-text-1)]">{{ t('contact') }}</div> -->
<img src="../assets/wx.png" alt="WeChat QR Code" class="w-[280px] object-contain" />
</div>
<!-- 帮助链接部分 -->
<div class="px-3 text-sm flex-shrink-0 min-w-[150px] mt-8 md:mt-0">
<div class="font-semibold mb-2 text-[var(--vp-c-text-1)]">
{{ t("help") }}
</div>
<ul class="list-none m-0 p-0">
<li class="mb-1">
<a target="_blank" href="https://mediago.pro/"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300">
{{ t("search") }}
</a>
</li>
</ul>
</div>
<!-- 友情链接部分 -->
<div class="px-3 text-sm flex-shrink-0 min-w-[150px] mt-8 md:mt-0">
<div class="font-semibold mb-2 text-[var(--vp-c-text-1)]">
{{ t("links") }}
</div>
<ul class="list-none m-0 p-0">
<li class="mb-1">
<a href="https://www.jiexi.im/player.php"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300"
target="_blank">
{{ t("jiexi.im") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<!-- 底部版权信息 -->
<div
class="border-t border-[var(--vp-c-divider)] pt-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-3 text-sm text-[var(--vp-c-text-2)]">
<div class="copyright">
Copyright © {{ new Date().getFullYear() }} MediaGo. All rights
reserved.
</div>
<div class="flex gap-4">
<a href="/privacy"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300">
{{ t("privacy") }}
</a>
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener noreferrer"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300">
豫ICP备20012967号-2
</a>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
import { useLayout } from "vitepress/theme";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { hasSidebar } = useLayout();
</script>
<template>
<footer class="relative z-[1]" :class="{ 'lg:pl-[var(--vp-sidebar-width)] lg:m-0': hasSidebar }">
<div class="relative px-6 py-8 border-t border-[var(--vp-c-divider)] bg-[var(--vp-c-bg)] mx-auto max-w-[1200px]">
<!-- 顶部区域 -->
<div class="flex flex-col md:flex-row justify-between -mx-3 pb-6">
<!-- Logo部分 -->
<div class="px-3 flex-shrink-0 md:flex-1 max-w-full">
<img src="/favicon.ico" alt="MediaGo Logo" class="w-10 h-10" />
<div class="text-base font-semibold my-1 text-[var(--vp-c-text-1)]">
MediaGo
</div>
<div class="text-sm text-[var(--vp-c-text-2)]">{{ t("slogan") }}</div>
</div>
<!-- 右侧区域 -->
<div class="flex flex-col md:flex-row gap-8 md:gap-12">
<!-- 二维码部分 -->
<div class="px-3 flex-shrink-0 mt-8 md:mt-0">
<!-- <div class="font-semibold mb-2 text-[var(--vp-c-text-1)]">{{ t('contact') }}</div> -->
<img src="../assets/wx.png" alt="WeChat QR Code" class="w-[280px] object-contain" />
</div>
<!-- 帮助链接部分 -->
<div class="px-3 text-sm flex-shrink-0 min-w-[150px] mt-8 md:mt-0">
<div class="font-semibold mb-2 text-[var(--vp-c-text-1)]">
{{ t("help") }}
</div>
<ul class="list-none m-0 p-0">
<li class="mb-1">
<a target="_blank" href="https://mediago.pro/"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300">
{{ t("search") }}
</a>
</li>
</ul>
</div>
<!-- 友情链接部分 -->
<div class="px-3 text-sm flex-shrink-0 min-w-[150px] mt-8 md:mt-0">
<div class="font-semibold mb-2 text-[var(--vp-c-text-1)]">
{{ t("links") }}
</div>
<ul class="list-none m-0 p-0">
<li class="mb-1">
<a href="https://www.jiexi.im/player.php"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300"
target="_blank">
{{ t("jiexi.im") }}
</a>
</li>
</ul>
</div>
</div>
</div>
<!-- 底部版权信息 -->
<div
class="border-t border-[var(--vp-c-divider)] pt-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-3 text-sm text-[var(--vp-c-text-2)]">
<div class="copyright">
Copyright © {{ new Date().getFullYear() }} MediaGo. All rights
reserved.
</div>
<div class="flex gap-4">
<a href="/privacy"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300">
{{ t("privacy") }}
</a>
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener noreferrer"
class="text-[var(--vp-c-text-2)] no-underline transition-colors hover:text-[var(--vp-c-text-1)] duration-300">
豫ICP备20012967号-2
</a>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
import { useLayout } from "vitepress/theme";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { hasSidebar } = useLayout();
</script>
+8 -8
View File
@@ -1,8 +1,8 @@
<template>
<div class="mt-12 py-6 text-center border-t border-[var(--vp-c-divider)]">
<img src="../assets/wx.png" alt="WeChat QR Code" class="w-[300px] object-contain mx-auto" />
<p class="mt-3 text-sm text-[var(--vp-c-text-2)]">
扫码关注获取更多精彩内容
</p>
</div>
</template>
<template>
<div class="mt-12 py-6 text-center border-t border-[var(--vp-c-divider)]">
<img src="../assets/wx.png" alt="WeChat QR Code" class="w-[300px] object-contain mx-auto" />
<p class="mt-3 text-sm text-[var(--vp-c-text-2)]">
扫码关注获取更多精彩内容
</p>
</div>
</template>
+1 -1
View File
@@ -1 +1 @@
@import "tailwindcss";
@import "tailwindcss";
+32 -32
View File
@@ -1,32 +1,32 @@
: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;
}
: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;
}
+33 -33
View File
@@ -1,33 +1,33 @@
---
layout: doc
outline: deep
---
# 通过宝塔面板部署
本文介绍使用 `宝塔面板` 来部署 `MediaGo`
## 前提
- 仅适用于宝塔面板 9.2.0 及以上版本
- 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html?r=dk_mediago),选择正式版的脚本下载安装
## 部署
1. 登录宝塔面板,在左侧菜单栏中点击 `Docker`
![Docker](./images/bt-install-step1.png)
2. 首次会提示安装 `Docker``Docker Compose` 服务,点击立即安装,若已安装请忽略。
![Docker](./images/bt-install-step2.png)
3. 安装完成后在`Docker-应用商店`中找到 `MediaGo`,点击`安装`
![Docker](./images/bt-install-step3.png)
4. 提交后面板会自动进行应用初始化,大概需要1-3分钟,初始化完成后即可访问。
- 端口: 8899
- 版本选择: v3.0.0
## 访问 MediaGo
请在浏览器地址栏中输入域名访问 `http://<宝塔面板IP>:8899`,即可访问 `MediaGo` 控制台。
---
layout: doc
outline: deep
---
# 通过宝塔面板部署
本文介绍使用 `宝塔面板` 来部署 `MediaGo`
## 前提
- 仅适用于宝塔面板 9.2.0 及以上版本
- 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html?r=dk_mediago),选择正式版的脚本下载安装
## 部署
1. 登录宝塔面板,在左侧菜单栏中点击 `Docker`
![Docker](./images/bt-install-step1.png)
2. 首次会提示安装 `Docker``Docker Compose` 服务,点击立即安装,若已安装请忽略。
![Docker](./images/bt-install-step2.png)
3. 安装完成后在`Docker-应用商店`中找到 `MediaGo`,点击`安装`
![Docker](./images/bt-install-step3.png)
4. 提交后面板会自动进行应用初始化,大概需要1-3分钟,初始化完成后即可访问。
- 端口: 8899
- 版本选择: v3.0.0
## 访问 MediaGo
请在浏览器地址栏中输入域名访问 `http://<宝塔面板IP>:8899`,即可访问 `MediaGo` 控制台。
+24 -24
View File
@@ -1,24 +1,24 @@
---
layout: doc
outline: deep
---
# 配合猫爪下载视频
::: tip
感谢 `1颗温暖的心` 提供教程:
<https://www.kdocs.cn/l/coioFy5l7vwW>
:::
![cat-catch](./images/catcatch-2.png)
- 安装 [猫爪](https://github.com/xifangczy/cat-catch) 最新版
- 在设置中添加调用程序:
`mediago://index.html/?n=true&name=${title}&url=${url}&headers=${referer|exists:'Referer:*'|to:urlEncode}&type=m3u8&silent=true`
![alt text](./images/catcatch-3.png)
- 设置完成之后可以在猫爪的面板中点击直接调用 MediaGo 下载
![alt text](./images/catcatch-4.png)
---
layout: doc
outline: deep
---
# 配合猫爪下载视频
::: tip
感谢 `1颗温暖的心` 提供教程:
<https://www.kdocs.cn/l/coioFy5l7vwW>
:::
![cat-catch](./images/catcatch-2.png)
- 安装 [猫爪](https://github.com/xifangczy/cat-catch) 最新版
- 在设置中添加调用程序:
`mediago://index.html/?n=true&name=${title}&url=${url}&headers=${referer|exists:'Referer:*'|to:urlEncode}&type=m3u8&silent=true`
![alt text](./images/catcatch-3.png)
- 设置完成之后可以在猫爪的面板中点击直接调用 MediaGo 下载
![alt text](./images/catcatch-4.png)
+128 -128
View File
@@ -1,128 +1,128 @@
---
layout: doc
outline: deep
---
# 更新日志
## 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 arm64apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) 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 arm64apple 芯片) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) 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`
### 软件截图
![首页](./images/changelog4.png)
### 重要更新
- 支持 docker 部署 web 端
- 更新桌面端 UI
### 更新日志
- 更新桌面端 UI
- 支持 docker 部署 web 端
- 新增视频播放,支持桌面端和移动端播放
- 修复 mac 打开无法展示界面的问题
- 优化了批量下载的交互
- 添加了 windows 的便携版(免安装哦)
- 优化了下载列表,支持页面中多个视频的嗅探
- 支持收藏列表手动导入导出
- 支持首页下载列表导出
- 优化了【新建下载】表单的交互逻辑
- 支持 UrlScheme 打开应用,并添加下载任务
- 修复了一些 bug 并提升用户体验
## v2.2.3 (2024.7.06 发布)
### 下载链接
- [windows mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.exe)
- [macos mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.dmg)
- [linux mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-arm64-2.2.3.dmg)
### 更新日志
- 设置中添加【自动更新】开关:仅更新 release 版本,beta 版本不会自动更新
- 下载表单中增加【批量更新】
- 新增 Linux 版本发布
- 载时自动选择清晰度最高的视频
- 新增【清空缓存】&【无痕模式】
- 自定义选择安装位置
- 修复了一些 bug
## 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.12023.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)
### 软件截图
![首页](./images/changelog3.png)
### 更新日志
- 暗黑模式
- 更多下载配置
- 支持请求标头自动带入
- 支持开启广告过滤
- 支持开启沉浸式嗅探
- 支持切换手机和 PC 模式
- 支持修改同时下载数量
- 修复了一些 bug
## v1.1.52022.2.5 发布)
### 下载链接
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
### 软件截图
![首页](./images/changelog2.webp)
### 更新日志
- 支持视频下载
## v1.0.12021.3.1 发布)
### 软件截图
![首页](./images/changelog1.webp)
### 更新日志
- 支持视频下载
---
layout: doc
outline: deep
---
# 更新日志
## 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 arm64apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) 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 arm64apple 芯片) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64intel 芯片) 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`
### 软件截图
![首页](./images/changelog4.png)
### 重要更新
- 支持 docker 部署 web 端
- 更新桌面端 UI
### 更新日志
- 更新桌面端 UI
- 支持 docker 部署 web 端
- 新增视频播放,支持桌面端和移动端播放
- 修复 mac 打开无法展示界面的问题
- 优化了批量下载的交互
- 添加了 windows 的便携版(免安装哦)
- 优化了下载列表,支持页面中多个视频的嗅探
- 支持收藏列表手动导入导出
- 支持首页下载列表导出
- 优化了【新建下载】表单的交互逻辑
- 支持 UrlScheme 打开应用,并添加下载任务
- 修复了一些 bug 并提升用户体验
## v2.2.3 (2024.7.06 发布)
### 下载链接
- [windows mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.exe)
- [macos mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.dmg)
- [linux mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-arm64-2.2.3.dmg)
### 更新日志
- 设置中添加【自动更新】开关:仅更新 release 版本,beta 版本不会自动更新
- 下载表单中增加【批量更新】
- 新增 Linux 版本发布
- 载时自动选择清晰度最高的视频
- 新增【清空缓存】&【无痕模式】
- 自定义选择安装位置
- 修复了一些 bug
## 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.12023.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)
### 软件截图
![首页](./images/changelog3.png)
### 更新日志
- 暗黑模式
- 更多下载配置
- 支持请求标头自动带入
- 支持开启广告过滤
- 支持开启沉浸式嗅探
- 支持切换手机和 PC 模式
- 支持修改同时下载数量
- 修复了一些 bug
## v1.1.52022.2.5 发布)
### 下载链接
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
### 软件截图
![首页](./images/changelog2.webp)
### 更新日志
- 支持视频下载
## v1.0.12021.3.1 发布)
### 软件截图
![首页](./images/changelog1.webp)
### 更新日志
- 支持视频下载
+123 -123
View File
@@ -1,123 +1,123 @@
---
layout: doc
outline: deep
---
# 使用说明
这个页面主要讲解的是设置页面的参数的含义
![step 1](./images/documents-step1.png)
## 基础设置
::: tip
下载器全局设置
:::
### 1. 选择文件夹
视频下载的路径
### 2. 下载器主题
支持浅色和深色模式
### 3. 显示语言、
支持中文和英文
### 4. 下载完成提示
开启之后下载完成会出现系统提示
### 5. 显示控制台
开启之后会显示下载的控制台
### 6. 自动更新
开启之后软件会自动检查更新
### 7. 允许升级测试版本
开启之后软件会自动检查测试版本
### 8. 关闭主窗口
控制关闭主窗口后软件会【隐藏到系统托盘】还是【退出软件】
## 浏览器设置
::: tip
浏览器窗口相关的设置
:::
### 1. 在新窗口中打开浏览器
开启了之后浏览器页面会独立出一个窗口
![step 1](./images/documents-step2.png)
### 2. 代理设置
填写自己的代理地址
### 3. 代理开关
打开开关后 **浏览器** 默认会走填写的代理地址,如果代理地址这个开关是打不开的
### 4. 开启广告过滤
打开开关后会过滤页面中的广告
### 5. 以手机模式进入
打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
![step 2](./images/documents-step3.png)
### 6. 使用沉浸式嗅探
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面中的【添加到列表】按钮
![step 3](./images/documents-step4.png)
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
### 7. 隐私模式: 打开后软件不会保存用户的浏览记录
![step 4](./images/documents-step5.png)
### 8. 更多操作
- 清空缓存: 清空软件的缓存
- 导出收藏【导入收藏】: 导出软件的收藏
## 下载设置
::: tip
和下载相关的设置
:::
### 1. 下载代理开关
打开开关后 **下载器** 默认会走填写的代理地址,【浏览器】和【下载器】的代理是独立的
### 2. 下载完成删除分片
开启后下载完成后会将分片文件删除
### 3. 最大同时下载数量
最多同时可以下载多少视频文件,目前最大是 10,最小是 1
### 4. 更多操作
- 配置文件目录: 软件数据库路径、日志等等路径
- 可执行文件目录: 下载器二进制文件的路径
- 本地存储路径: 下载视频的本地路径
### 5. 当前版本
当前软件版本信息
---
layout: doc
outline: deep
---
# 使用说明
这个页面主要讲解的是设置页面的参数的含义
![step 1](./images/documents-step1.png)
## 基础设置
::: tip
下载器全局设置
:::
### 1. 选择文件夹
视频下载的路径
### 2. 下载器主题
支持浅色和深色模式
### 3. 显示语言、
支持中文和英文
### 4. 下载完成提示
开启之后下载完成会出现系统提示
### 5. 显示控制台
开启之后会显示下载的控制台
### 6. 自动更新
开启之后软件会自动检查更新
### 7. 允许升级测试版本
开启之后软件会自动检查测试版本
### 8. 关闭主窗口
控制关闭主窗口后软件会【隐藏到系统托盘】还是【退出软件】
## 浏览器设置
::: tip
浏览器窗口相关的设置
:::
### 1. 在新窗口中打开浏览器
开启了之后浏览器页面会独立出一个窗口
![step 1](./images/documents-step2.png)
### 2. 代理设置
填写自己的代理地址
### 3. 代理开关
打开开关后 **浏览器** 默认会走填写的代理地址,如果代理地址这个开关是打不开的
### 4. 开启广告过滤
打开开关后会过滤页面中的广告
### 5. 以手机模式进入
打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
![step 2](./images/documents-step3.png)
### 6. 使用沉浸式嗅探
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面中的【添加到列表】按钮
![step 3](./images/documents-step4.png)
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
### 7. 隐私模式: 打开后软件不会保存用户的浏览记录
![step 4](./images/documents-step5.png)
### 8. 更多操作
- 清空缓存: 清空软件的缓存
- 导出收藏【导入收藏】: 导出软件的收藏
## 下载设置
::: tip
和下载相关的设置
:::
### 1. 下载代理开关
打开开关后 **下载器** 默认会走填写的代理地址,【浏览器】和【下载器】的代理是独立的
### 2. 下载完成删除分片
开启后下载完成后会将分片文件删除
### 3. 最大同时下载数量
最多同时可以下载多少视频文件,目前最大是 10,最小是 1
### 4. 更多操作
- 配置文件目录: 软件数据库路径、日志等等路径
- 可执行文件目录: 下载器二进制文件的路径
- 本地存储路径: 下载视频的本地路径
### 5. 当前版本
当前软件版本信息
+33 -33
View File
@@ -1,33 +1,33 @@
---
layout: doc
outline: deep
---
# Deploying with BT Panel
This document introduces how to deploy `MediaGo` using the `BT Panel`.
## Prerequisites
- Only applicable for BT Panel version 9.2.0 and above
- Install BT Panel by visiting the [official BT Panel website](https://www.bt.cn/new/download.html?r=dk_mediago), select the stable version script for installation
## Deployment
1. Log in to the BT Panel and click on `Docker` in the left menu
![Docker](../images/bt-install-step1.png)
2. The first time, it will prompt to install `Docker` and `Docker Compose` services. Click "Install Now", and ignore if already installed.
![Docker](../images/bt-install-step2.png)
3. After installation, go to `Docker - App Store`, find `MediaGo`, and click `Install`
![Docker](../images/bt-install-step3.png)
4. After submission, the panel will automatically initialize the application, which may take 1-3 minutes. Once initialization is complete, you can access it.
- Port: 8899
- Version: v3.0.0
## Access MediaGo
Enter the domain in the browser address bar to access `http://<BT Panel IP>:8899` to open the `MediaGo` console.
---
layout: doc
outline: deep
---
# Deploying with BT Panel
This document introduces how to deploy `MediaGo` using the `BT Panel`.
## Prerequisites
- Only applicable for BT Panel version 9.2.0 and above
- Install BT Panel by visiting the [official BT Panel website](https://www.bt.cn/new/download.html?r=dk_mediago), select the stable version script for installation
## Deployment
1. Log in to the BT Panel and click on `Docker` in the left menu
![Docker](../images/bt-install-step1.png)
2. The first time, it will prompt to install `Docker` and `Docker Compose` services. Click "Install Now", and ignore if already installed.
![Docker](../images/bt-install-step2.png)
3. After installation, go to `Docker - App Store`, find `MediaGo`, and click `Install`
![Docker](../images/bt-install-step3.png)
4. After submission, the panel will automatically initialize the application, which may take 1-3 minutes. Once initialization is complete, you can access it.
- Port: 8899
- Version: v3.0.0
## Access MediaGo
Enter the domain in the browser address bar to access `http://<BT Panel IP>:8899` to open the `MediaGo` console.
+128 -128
View File
@@ -1,128 +1,128 @@
---
layout: doc
outline: deep
---
# Changelog
## 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 Chip) 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 Chip) 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 Chip) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macOS x64 (Intel Chip) 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`
### Software Screenshots
![Home](../images/changelog4.png)
### Key Updates
- Support for Docker deployment of the web version
- Updated desktop UI
### Changelog
- Updated desktop UI
- Support for Docker deployment of the web version
- Added video playback, supporting both desktop and mobile playback
- Fixed issue where Mac version wouldn't display the interface
- Optimized batch download interaction
- Added portable version for Windows (no installation required)
- Optimized download list to support sniffing multiple videos on the same page
- Supported manual import/export of favorite lists
- Supported export of the homepage download list
- Optimized interaction logic for the "New Download" form
- Supported opening the app via UrlScheme and adding download tasks
- Fixed several bugs and improved user experience
## v2.2.3 (Released on 2024.7.06)
### Download Links
- [Windows mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.exe)
- [macOS mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.dmg)
- [Linux mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-arm64-2.2.3.dmg)
### Changelog
- Added an "Auto Update" toggle in settings: only release versions will be updated automatically, beta versions will not
- Added "Bulk Update" in the download form
- New Linux version released
- Automatically selects the highest video quality during download
- Added "Clear Cache" and "Incognito Mode"
- Custom installation location selection
- Fixed several bugs
## v2.2.0 (Released on 2024.5.22)
### Download Links
- [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)
### Changelog
- Support for live stream downloads
- Support for Bilibili video downloads
- Optimized immersive sniffing process
- Support for console output during download
- Fixed several bugs
## v2.0.1 (Released on 2023.7.1)
### Download Links
- [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)
### Software Screenshots
![Home](../images/changelog3.png)
### Changelog
- Dark mode
- More download configurations
- Support for automatic header injection
- Support for enabling ad filtering
- Support for immersive sniffing
- Support for switching between mobile and PC modes
- Support for modifying simultaneous download limits
- Fixed several bugs
## v1.1.5 (Released on 2022.2.5)
### Download Links
- [Windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
### Software Screenshots
![Home](../images/changelog2.webp)
### Changelog
- Support for video downloads
## v1.0.1 (Released on 2021.3.1)
### Software Screenshots
![Home](../images/changelog1.webp)
### Changelog
- Support for video downloads
---
layout: doc
outline: deep
---
# Changelog
## 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 Chip) 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 Chip) 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 Chip) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macOS x64 (Intel Chip) 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`
### Software Screenshots
![Home](../images/changelog4.png)
### Key Updates
- Support for Docker deployment of the web version
- Updated desktop UI
### Changelog
- Updated desktop UI
- Support for Docker deployment of the web version
- Added video playback, supporting both desktop and mobile playback
- Fixed issue where Mac version wouldn't display the interface
- Optimized batch download interaction
- Added portable version for Windows (no installation required)
- Optimized download list to support sniffing multiple videos on the same page
- Supported manual import/export of favorite lists
- Supported export of the homepage download list
- Optimized interaction logic for the "New Download" form
- Supported opening the app via UrlScheme and adding download tasks
- Fixed several bugs and improved user experience
## v2.2.3 (Released on 2024.7.06)
### Download Links
- [Windows mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.exe)
- [macOS mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.dmg)
- [Linux mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-arm64-2.2.3.dmg)
### Changelog
- Added an "Auto Update" toggle in settings: only release versions will be updated automatically, beta versions will not
- Added "Bulk Update" in the download form
- New Linux version released
- Automatically selects the highest video quality during download
- Added "Clear Cache" and "Incognito Mode"
- Custom installation location selection
- Fixed several bugs
## v2.2.0 (Released on 2024.5.22)
### Download Links
- [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)
### Changelog
- Support for live stream downloads
- Support for Bilibili video downloads
- Optimized immersive sniffing process
- Support for console output during download
- Fixed several bugs
## v2.0.1 (Released on 2023.7.1)
### Download Links
- [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)
### Software Screenshots
![Home](../images/changelog3.png)
### Changelog
- Dark mode
- More download configurations
- Support for automatic header injection
- Support for enabling ad filtering
- Support for immersive sniffing
- Support for switching between mobile and PC modes
- Support for modifying simultaneous download limits
- Fixed several bugs
## v1.1.5 (Released on 2022.2.5)
### Download Links
- [Windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
### Software Screenshots
![Home](../images/changelog2.webp)
### Changelog
- Support for video downloads
## v1.0.1 (Released on 2021.3.1)
### Software Screenshots
![Home](../images/changelog1.webp)
### Changelog
- Support for video downloads
+148 -148
View File
@@ -1,148 +1,148 @@
---
layout: doc
outline: deep
---
# User Guide
This page mainly explains the meaning of the parameters in the settings page.
![step 1](../images/documents-step1.png)
## Basic Settings
::: tip
Global settings for the downloader
:::
### 1. Choose Folder
The path for downloading videos.
### 2. Downloader Theme
Supports light and dark modes.
### 3. Display Language
Supports Chinese and English.
### 4. Download Completion Notification
When enabled, the system will show a notification once the download is complete.
### 5. Show Console
When enabled, the download console will be displayed.
### 6. Auto Update
When enabled, the software will automatically check for updates.
### 7. Allow Testing Version Upgrades
When enabled, the software will automatically check for testing versions.
### 8. Close Main Window
Controls whether the software should "hide to the system tray" or "exit the software" when the main window is closed.
## Browser Settings
::: tip
Settings related to the browser window
:::
### 1. Open Browser in a New Window
When enabled, the browser page will open in a separate window.
![step 1](../images/documents-step2.png)
### 2. Proxy Settings
Fill in your own proxy address.
### 3. Proxy Switch
When enabled, the **browser** will use the proxy address you entered. If the proxy switch is disabled, this setting will not be available.
### 4. Enable Ad Blocking
When enabled, the ads on the page will be filtered.
### 5. Use Mobile Mode
When enabled, the browser will mimic a mobile browser and request the mobile version of the site (on the left is the screenshot with mobile mode enabled).
![step 2](../images/documents-step3.png)
### 6. Use Immersive Sniffing
- **Enabled**: Resources sniffed from the browser will not automatically be added to the download list, you need to manually click the "Add to List" button on the page.
![step 3](../images/documents-step4.png)
- **Disabled**: Resources sniffed from the browser will automatically be added to the download list.
### 7. Privacy Mode: When enabled, the software will not save your browsing history
![step 4](../images/documents-step5.png)
### 8. More Operations
- Clear Cache: Clears the software cache.
- Export Favorites [Import Favorites]: Export the software's favorites.
## Download Settings
::: tip
Settings related to downloads
:::
### 1. Download Proxy Switch
When enabled, the **downloader** will use the proxy address you entered. The proxy settings for the **browser** and **downloader** are independent.
### 2. Delete Part Files After Download Completion
When enabled, part files will be deleted after the download is complete.
### 3. Maximum Simultaneous Downloads
Controls how many video files can be downloaded at the same time. The maximum is 10, and the minimum is 1.
### 4. More Operations
- Configuration File Directory: The path to the software's database, logs, etc.
- Executable File Directory: The path to the downloader's binary files.
- Local Storage Path: The local path where downloaded videos are stored.
### 5. Current Version
Displays the current version of the software.
## Other Issues
### Q: About Live Stream Downloads
A: The software supports live stream downloads. Currently, the software doesn't have a reliable method to differentiate live streams, so all download consoles will be enabled. Users need to manually select the data source to download.
### Q: About macOS Version
A: For **Intel chips**, you need to install the x64 version from the release.
After installation, you need to open the app from unidentified developers in macOS Security Settings.
![QA1](../images/documents-qa1.png)
For **Apple chips**, you need to install the arm64 version from the release.
After installation, execute the command `sudo xattr -dr com.apple.quarantine /Applications/mediago.app` in the console.
### Q: About Old Versions
A: The version 1.1.5 was released quite a while ago and has been verified by many users to be stable. If you want to use the old version, please visit [this link](/history.html).
### Q: About Win7 Users
A: Versions after v2.0.0 no longer support Win7. If you need to use the software on Win7, please download version 1.1.5. For 32-bit systems, the software is currently not supported by default.
---
layout: doc
outline: deep
---
# User Guide
This page mainly explains the meaning of the parameters in the settings page.
![step 1](../images/documents-step1.png)
## Basic Settings
::: tip
Global settings for the downloader
:::
### 1. Choose Folder
The path for downloading videos.
### 2. Downloader Theme
Supports light and dark modes.
### 3. Display Language
Supports Chinese and English.
### 4. Download Completion Notification
When enabled, the system will show a notification once the download is complete.
### 5. Show Console
When enabled, the download console will be displayed.
### 6. Auto Update
When enabled, the software will automatically check for updates.
### 7. Allow Testing Version Upgrades
When enabled, the software will automatically check for testing versions.
### 8. Close Main Window
Controls whether the software should "hide to the system tray" or "exit the software" when the main window is closed.
## Browser Settings
::: tip
Settings related to the browser window
:::
### 1. Open Browser in a New Window
When enabled, the browser page will open in a separate window.
![step 1](../images/documents-step2.png)
### 2. Proxy Settings
Fill in your own proxy address.
### 3. Proxy Switch
When enabled, the **browser** will use the proxy address you entered. If the proxy switch is disabled, this setting will not be available.
### 4. Enable Ad Blocking
When enabled, the ads on the page will be filtered.
### 5. Use Mobile Mode
When enabled, the browser will mimic a mobile browser and request the mobile version of the site (on the left is the screenshot with mobile mode enabled).
![step 2](../images/documents-step3.png)
### 6. Use Immersive Sniffing
- **Enabled**: Resources sniffed from the browser will not automatically be added to the download list, you need to manually click the "Add to List" button on the page.
![step 3](../images/documents-step4.png)
- **Disabled**: Resources sniffed from the browser will automatically be added to the download list.
### 7. Privacy Mode: When enabled, the software will not save your browsing history
![step 4](../images/documents-step5.png)
### 8. More Operations
- Clear Cache: Clears the software cache.
- Export Favorites [Import Favorites]: Export the software's favorites.
## Download Settings
::: tip
Settings related to downloads
:::
### 1. Download Proxy Switch
When enabled, the **downloader** will use the proxy address you entered. The proxy settings for the **browser** and **downloader** are independent.
### 2. Delete Part Files After Download Completion
When enabled, part files will be deleted after the download is complete.
### 3. Maximum Simultaneous Downloads
Controls how many video files can be downloaded at the same time. The maximum is 10, and the minimum is 1.
### 4. More Operations
- Configuration File Directory: The path to the software's database, logs, etc.
- Executable File Directory: The path to the downloader's binary files.
- Local Storage Path: The local path where downloaded videos are stored.
### 5. Current Version
Displays the current version of the software.
## Other Issues
### Q: About Live Stream Downloads
A: The software supports live stream downloads. Currently, the software doesn't have a reliable method to differentiate live streams, so all download consoles will be enabled. Users need to manually select the data source to download.
### Q: About macOS Version
A: For **Intel chips**, you need to install the x64 version from the release.
After installation, you need to open the app from unidentified developers in macOS Security Settings.
![QA1](../images/documents-qa1.png)
For **Apple chips**, you need to install the arm64 version from the release.
After installation, execute the command `sudo xattr -dr com.apple.quarantine /Applications/mediago.app` in the console.
### Q: About Old Versions
A: The version 1.1.5 was released quite a while ago and has been verified by many users to be stable. If you want to use the old version, please visit [this link](/history.html).
### Q: About Win7 Users
A: Versions after v2.0.0 no longer support Win7. If you need to use the software on Win7, please download version 1.1.5. For 32-bit systems, the software is currently not supported by default.
+113 -113
View File
@@ -1,113 +1,113 @@
---
layout: doc
outline: deep
---
# Quick Start
This article provides a simple guide to help you get started with using the software.
::: tip
To facilitate communication and feedback, you can join the feedback group:
MediaGo QQ Feedback Group 1: 574209001
:::
::: info
v3.0 is the latest version. Please feel free to provide feedback within version 3.0, and we will address it as soon as possible.
:::
## Download and Installation
### v3.0.0 (Released on October 7, 2024)
#### Software Download
- [【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`
#### Important Updates
- Support for Docker deployment of the web version.
- Updated desktop UI.
#### Changelog
- Support for Docker deployment of the web version.
- Updated desktop UI.
- Added video playback, supporting both desktop and mobile versions.
- Fixed an issue where macOS could not display the interface.
- Optimized the interaction for batch downloads.
- Added a portable version for Windows (no installation required).
- Optimized the download list, supporting sniffing of multiple videos on a page.
- Supports manual import/export of the favorite list.
- Supports export of the homepage download list.
- Optimized the interaction logic for the "New Download" form.
- Supports UrlScheme to open the app and add download tasks.
- Fixed some bugs and improved the user experience.
## Operation Instructions
### Automatic Video Sniffing
1. Select "Resource Extraction"
![step 1](../images/guides-step1.png)
2. Enter the video URL
![step 2](../images/guides-step2.png)
3. Click "Start Download" to begin downloading the video
![step 3](../images/guides-step3.png)
### Manual Download
1. Click the "New Download" button at the top right of the page
![step 1](../images/guides-step4.png)
2. In the new download window, enter the "Video Name" and the "Stream (m3u8)" or "Bilibili" link
![step 2](../images/guides-step5.png)
3. Click to download the video from the list
![step 3](../images/guides-step3.png)
### Batch Download
![step 3](../images/guides-step6.png)
### Additional Features
1. Convert to Audio
![step 1](../images/guides-step7.png)
2. More features will be added in the future. Stay tuned!
### Video Playback
- PC Playback
![step 2](../images/addition-step3.png)
- Mobile Playback
![step 3](../images/addition-step4.png)
## Start Downloading Your Videos
It's so simple! Go ahead and download your videos now.
::: warning
This software is for learning and communication purposes only.
:::
---
layout: doc
outline: deep
---
# Quick Start
This article provides a simple guide to help you get started with using the software.
::: tip
To facilitate communication and feedback, you can join the feedback group:
MediaGo QQ Feedback Group 1: 574209001
:::
::: info
v3.0 is the latest version. Please feel free to provide feedback within version 3.0, and we will address it as soon as possible.
:::
## Download and Installation
### v3.0.0 (Released on October 7, 2024)
#### Software Download
- [【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`
#### Important Updates
- Support for Docker deployment of the web version.
- Updated desktop UI.
#### Changelog
- Support for Docker deployment of the web version.
- Updated desktop UI.
- Added video playback, supporting both desktop and mobile versions.
- Fixed an issue where macOS could not display the interface.
- Optimized the interaction for batch downloads.
- Added a portable version for Windows (no installation required).
- Optimized the download list, supporting sniffing of multiple videos on a page.
- Supports manual import/export of the favorite list.
- Supports export of the homepage download list.
- Optimized the interaction logic for the "New Download" form.
- Supports UrlScheme to open the app and add download tasks.
- Fixed some bugs and improved the user experience.
## Operation Instructions
### Automatic Video Sniffing
1. Select "Resource Extraction"
![step 1](../images/guides-step1.png)
2. Enter the video URL
![step 2](../images/guides-step2.png)
3. Click "Start Download" to begin downloading the video
![step 3](../images/guides-step3.png)
### Manual Download
1. Click the "New Download" button at the top right of the page
![step 1](../images/guides-step4.png)
2. In the new download window, enter the "Video Name" and the "Stream (m3u8)" or "Bilibili" link
![step 2](../images/guides-step5.png)
3. Click to download the video from the list
![step 3](../images/guides-step3.png)
### Batch Download
![step 3](../images/guides-step6.png)
### Additional Features
1. Convert to Audio
![step 1](../images/guides-step7.png)
2. More features will be added in the future. Stay tuned!
### Video Playback
- PC Playback
![step 2](../images/addition-step3.png)
- Mobile Playback
![step 3](../images/addition-step4.png)
## Start Downloading Your Videos
It's so simple! Go ahead and download your videos now.
::: warning
This software is for learning and communication purposes only.
:::
+21 -21
View File
@@ -1,21 +1,21 @@
---
layout: doc
outline: deep
---
# Old Versions
::: info
This is the permanent link for the old versions. If needed, you can download them here.
**Note: These versions will not automatically update, so you can use them with confidence.**
:::
**v1.1.5 (Released on February 5, 2022)**
- [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 unavailable~~
**Changelog**
- Added support for video downloads.
---
layout: doc
outline: deep
---
# Old Versions
::: info
This is the permanent link for the old versions. If needed, you can download them here.
**Note: These versions will not automatically update, so you can use them with confidence.**
:::
**v1.1.5 (Released on February 5, 2022)**
- [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 unavailable~~
**Changelog**
- Added support for video downloads.
+33 -33
View File
@@ -1,33 +1,33 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "Online Video Downloader"
text: "Simple and Easy, Fast Downloading"
tagline: Simple to learn, no need for packet capture, no plugins required
image:
src: /home.png
alt: home
actions:
- theme: brand
text: Quick Start
link: /guides
- theme: alt
text: User Guide
link: /documents
features:
- icon: ⏩
title: No Packet Capture Required
details: Use the built-in browser of the software to easily sniff video resources on web pages. Select the resource you want to download from the sniffed resource list—simple and fast.
- icon: 📱
title: Mobile Playback
details: Seamlessly switch between PC and mobile devices. After the download is complete, you can watch the video on your phone.
- icon: ⚡️
title: Batch Download Supported
details: Supports downloading multiple videos and live streaming resources at the same time, ensuring that your high-speed bandwidth is fully utilized.
- icon: 🎉
title: Docker Deployment Supported
details: Supports Docker deployment for the web version, making it quick and easy.
---
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "Online Video Downloader"
text: "Simple and Easy, Fast Downloading"
tagline: Simple to learn, no need for packet capture, no plugins required
image:
src: /home.png
alt: home
actions:
- theme: brand
text: Quick Start
link: /guides
- theme: alt
text: User Guide
link: /documents
features:
- icon: ⏩
title: No Packet Capture Required
details: Use the built-in browser of the software to easily sniff video resources on web pages. Select the resource you want to download from the sniffed resource list—simple and fast.
- icon: 📱
title: Mobile Playback
details: Seamlessly switch between PC and mobile devices. After the download is complete, you can watch the video on your phone.
- icon: ⚡️
title: Batch Download Supported
details: Supports downloading multiple videos and live streaming resources at the same time, ensuring that your high-speed bandwidth is fully utilized.
- icon: 🎉
title: Docker Deployment Supported
details: Supports Docker deployment for the web version, making it quick and easy.
---
+22 -22
View File
@@ -1,22 +1,22 @@
---
layout: doc
outline: deep
---
# Supported Websites
::: tip
If you have any websites you'd like to be supported, feel free to suggest them here.
:::
**Template**
---
- Website homepage: xxx
- Video (Live) list page: xxx
- Video (Live) page: xxx
---
Once support is completed, I will reply in the comment section.
---
layout: doc
outline: deep
---
# Supported Websites
::: tip
If you have any websites you'd like to be supported, feel free to suggest them here.
:::
**Template**
---
- Website homepage: xxx
- Video (Live) list page: xxx
- Video (Live) page: xxx
---
Once support is completed, I will reply in the comment section.
+10 -10
View File
@@ -1,10 +1,10 @@
---
layout: doc
outline: deep
---
# Privacy Policy
::: tip
This is the software's privacy policy.
:::
---
layout: doc
outline: deep
---
# Privacy Policy
::: tip
This is the software's privacy policy.
:::
+16 -16
View File
@@ -1,16 +1,16 @@
---
layout: doc
outline: deep
---
# Feedback Collection
::: info
This is for collecting issues, suggestions, or feedback from users.
:::
If the software encounters an error, please clearly describe the website and the video you were downloading when the error occurred.
If you have a suggestion, please describe your requirements in detail.
If you have feedback, please feel free to share it. I will do my best to meet everyone's needs.
---
layout: doc
outline: deep
---
# Feedback Collection
::: info
This is for collecting issues, suggestions, or feedback from users.
:::
If the software encounters an error, please clearly describe the website and the video you were downloading when the error occurred.
If you have a suggestion, please describe your requirements in detail.
If you have feedback, please feel free to share it. I will do my best to meet everyone's needs.
+99 -99
View File
@@ -1,99 +1,99 @@
---
layout: doc
outline: deep
---
# 快速开始
这篇文章是软件的简单说明,可以快速上手使用本软件。
::: tip
方便大家一起沟通交流, 可以加入反馈群:
MediaGo QQ反馈群: 574209001
v3.x.x 是最新版本,大家的意见可以尽量在 3.x.x 版本中提出,我们会尽快修复。
:::
## 下载安装
### v3.0.1 (2025.1.14 发布)
::: tip
国内用户可以通过网盘下载
阿里网盘:
<https://www.alipan.com/s/SwCbQBN2nZQ>
百度网盘:
<https://pan.baidu.com/s/1CLqPKZ4_lwewb2L4wre_eA?pwd=rmdn>
:::
- [【mediago】 windows(安装版) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-win32-x64-3.0.1.exe)
- [【mediago】 windows(便携版) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-portable-win32-x64-3.0.1.exe)
- [【mediago】 macos arm64apple 芯片) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-darwin-arm64-3.0.1.dmg)
- [【mediago】 macos x64intel 芯片) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-darwin-x64-3.0.1.dmg)
- [【mediago】 linux v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-linux-amd64-3.0.1.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`
## 操作说明
### 视频自动嗅探
1. 选择【资源提取】
![step 1](./images/guides-step1.png)
2. 进入视频的网址
![step 2](./images/guides-step2.png)
3. 点击【立即下载】,即可下载视频
![step 3](./images/guides-step3.png)
### 手动下载
1. 点击页面右上角的【新建下载】
![step 1](./images/guides-step4.png)
2. 在新建下载弹窗中填入【视频名称】和【流媒体(m3u8)】或者【哔哩哔哩】即可
![step 2](./images/guides-step5.png)
3. 在列表中点击下载,即可下载视频
![step 3](./images/guides-step3.png)
### 批量下载
![step 3](./images/guides-step6.png)
### 附加功能
1. 转换为音频
![step 1](./images/guides-step7.png)
2. 后续会增加更多功能,敬请期待~
### 播放视频
- PC 播放
![step 2](./images/addition-step3.png)
- 移动端播放
![step 3](./images/addition-step4.png)
## 开始你的视频下载吧
是不是很简单,快去下载你的视频吧
::: warning
本软件仅可用于学习交流
:::
---
layout: doc
outline: deep
---
# 快速开始
这篇文章是软件的简单说明,可以快速上手使用本软件。
::: tip
方便大家一起沟通交流, 可以加入反馈群:
MediaGo QQ反馈群: 574209001
v3.x.x 是最新版本,大家的意见可以尽量在 3.x.x 版本中提出,我们会尽快修复。
:::
## 下载安装
### v3.0.1 (2025.1.14 发布)
::: tip
国内用户可以通过网盘下载
阿里网盘:
<https://www.alipan.com/s/SwCbQBN2nZQ>
百度网盘:
<https://pan.baidu.com/s/1CLqPKZ4_lwewb2L4wre_eA?pwd=rmdn>
:::
- [【mediago】 windows(安装版) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-win32-x64-3.0.1.exe)
- [【mediago】 windows(便携版) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-portable-win32-x64-3.0.1.exe)
- [【mediago】 macos arm64apple 芯片) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-darwin-arm64-3.0.1.dmg)
- [【mediago】 macos x64intel 芯片) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-darwin-x64-3.0.1.dmg)
- [【mediago】 linux v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-linux-amd64-3.0.1.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`
## 操作说明
### 视频自动嗅探
1. 选择【资源提取】
![step 1](./images/guides-step1.png)
2. 进入视频的网址
![step 2](./images/guides-step2.png)
3. 点击【立即下载】,即可下载视频
![step 3](./images/guides-step3.png)
### 手动下载
1. 点击页面右上角的【新建下载】
![step 1](./images/guides-step4.png)
2. 在新建下载弹窗中填入【视频名称】和【流媒体(m3u8)】或者【哔哩哔哩】即可
![step 2](./images/guides-step5.png)
3. 在列表中点击下载,即可下载视频
![step 3](./images/guides-step3.png)
### 批量下载
![step 3](./images/guides-step6.png)
### 附加功能
1. 转换为音频
![step 1](./images/guides-step7.png)
2. 后续会增加更多功能,敬请期待~
### 播放视频
- PC 播放
![step 2](./images/addition-step3.png)
- 移动端播放
![step 3](./images/addition-step4.png)
## 开始你的视频下载吧
是不是很简单,快去下载你的视频吧
::: warning
本软件仅可用于学习交流
:::
+21 -21
View File
@@ -1,21 +1,21 @@
---
layout: doc
outline: deep
---
# 旧版本
::: info
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
**这个链接的软件不会自动升级, 可放心使用**
:::
**v1.1.52022.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 无~~
**更新日志**
- 支持视频下载
---
layout: doc
outline: deep
---
# 旧版本
::: info
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
**这个链接的软件不会自动升级, 可放心使用**
:::
**v1.1.52022.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 无~~
**更新日志**
- 支持视频下载
+33 -33
View File
@@ -1,33 +1,33 @@
---
# 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: 支持同时下载多个视频和直播资源,高速带宽不闲置。
- icon: 🎉
title: 支持 docker 部署
details: 支持 docker 部署 web 端,方便快捷。
---
---
# 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: 支持同时下载多个视频和直播资源,高速带宽不闲置。
- icon: 🎉
title: 支持 docker 部署
details: 支持 docker 部署 web 端,方便快捷。
---
+33 -33
View File
@@ -1,33 +1,33 @@
---
layout: doc
outline: deep
---
# 宝塔パネルを使ったデプロイ
この記事では、`宝塔パネル`を使用して`MediaGo`をデプロイする方法を紹介します。
## 前提条件
- 宝塔パネル 9.2.0 以上のバージョンのみ対応
- 宝塔パネルをインストールするには、[宝塔パネル公式サイト](https://www.bt.cn/new/download.html?r=dk_mediago)にアクセスし、正式版のスクリプトを選んでインストールしてください。
## デプロイ
1. 宝塔パネルにログインし、左側のメニューで`Docker`をクリックします。
![Docker](../images/bt-install-step1.png)
2. 初回は`Docker``Docker Compose`サービスのインストールが求められます。`今すぐインストール`をクリックし、すでにインストールされている場合は無視してください。
![Docker](../images/bt-install-step2.png)
3. インストール後、`Docker-アプリストア``MediaGo`を探し、`インストール`をクリックします。
![Docker](../images/bt-install-step3.png)
4. 提出後、パネルは自動でアプリケーションの初期化を行い、1~3分ほどで完了します。初期化が完了するとアクセス可能になります。
- ポート: 8899
- バージョン選択: v3.0.0
## MediaGoへのアクセス
ブラウザのアドレスバーに`http://<宝塔パネルIP>:8899`を入力して、`MediaGo`のコントロールパネルにアクセスします。
---
layout: doc
outline: deep
---
# 宝塔パネルを使ったデプロイ
この記事では、`宝塔パネル`を使用して`MediaGo`をデプロイする方法を紹介します。
## 前提条件
- 宝塔パネル 9.2.0 以上のバージョンのみ対応
- 宝塔パネルをインストールするには、[宝塔パネル公式サイト](https://www.bt.cn/new/download.html?r=dk_mediago)にアクセスし、正式版のスクリプトを選んでインストールしてください。
## デプロイ
1. 宝塔パネルにログインし、左側のメニューで`Docker`をクリックします。
![Docker](../images/bt-install-step1.png)
2. 初回は`Docker``Docker Compose`サービスのインストールが求められます。`今すぐインストール`をクリックし、すでにインストールされている場合は無視してください。
![Docker](../images/bt-install-step2.png)
3. インストール後、`Docker-アプリストア``MediaGo`を探し、`インストール`をクリックします。
![Docker](../images/bt-install-step3.png)
4. 提出後、パネルは自動でアプリケーションの初期化を行い、1~3分ほどで完了します。初期化が完了するとアクセス可能になります。
- ポート: 8899
- バージョン選択: v3.0.0
## MediaGoへのアクセス
ブラウザのアドレスバーに`http://<宝塔パネルIP>:8899`を入力して、`MediaGo`のコントロールパネルにアクセスします。
+129 -129
View File
@@ -1,129 +1,129 @@
---
layout: doc
outline: deep
---
# 更新ログ
## 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 arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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 arm64Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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`
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog4.png)
### 重要な更新
- dockerを使用したWeb端のデプロイをサポート
- デスクトップUIの更新
### 更新ログ
- デスクトップUIの更新
- dockerを使用したWeb端のデプロイをサポート
- 新たに動画再生機能が追加、デスクトップ端とモバイル端で再生可能
- Macで開けない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windows用のポータブル版を追加(インストール不要)
- ダウンロードリストの最適化、ページ内複数の動画を嗅探
- コレクションリストの手動インポート/エクスポートをサポート
- ホームページのダウンロードリストのエクスポートをサポート
- 新規ダウンロードフォームのインタラクションロジックの最適化
- UrlSchemeを使用してアプリを開き、ダウンロードタスクを追加
- 一部のバグを修正し、ユーザーエクスペリエンスを向上
## v2.2.3 (2024.7.06 リリース)
### ダウンロードリンク
- [windows mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.exe)
- [macos mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.dmg)
- [linux mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-arm64-2.2.3.dmg)
### 更新ログ
- 設定に【自動更新】スイッチを追加:release版のみ更新、beta版は自動更新されません
- ダウンロードフォームに【バッチ更新】を追加
- Linux版を新たにリリース
- ダウンロード時に最も高解像度の動画を自動選択
- 【キャッシュクリア】&【シークレットモード】を追加
- インストール場所をカスタマイズ選択可能
- 一部のバグを修正
## 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)
### 更新ログ
- ライブストリームのダウンロードをサポート
- ビリビリ動画ダウンロードをサポート
- 没入型嗅探プロセスを最適化
- ダウンロードコントロールの出力をサポート
- 一部のバグを修正
## v2.0.12023.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)
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog3.png)
### 更新ログ
- ダークモード
- 追加のダウンロード設定
- リクエストヘッダーの自動挿入をサポート
- 広告フィルタリングをサポート
- 没入型嗅探機能をサポート
- モバイルモードとPCモードの切り替えをサポート
- 同時ダウンロード数の変更をサポート
- 一部のバグを修正
## v1.1.52022.2.5 リリース)
### ダウンロードリンク
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog2.webp)
### 更新ログ
- 動画ダウンロードをサポート
## v1.0.12021.3.1 リリース)
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog1.webp)
### 更新ログ
- 動画ダウンロードをサポート
---
layout: doc
outline: deep
---
# 更新ログ
## 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 arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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 arm64Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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`
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog4.png)
### 重要な更新
- dockerを使用したWeb端のデプロイをサポート
- デスクトップUIの更新
### 更新ログ
- デスクトップUIの更新
- dockerを使用したWeb端のデプロイをサポート
- 新たに動画再生機能が追加、デスクトップ端とモバイル端で再生可能
- Macで開けない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windows用のポータブル版を追加(インストール不要)
- ダウンロードリストの最適化、ページ内複数の動画を嗅探
- コレクションリストの手動インポート/エクスポートをサポート
- ホームページのダウンロードリストのエクスポートをサポート
- 新規ダウンロードフォームのインタラクションロジックの最適化
- UrlSchemeを使用してアプリを開き、ダウンロードタスクを追加
- 一部のバグを修正し、ユーザーエクスペリエンスを向上
## v2.2.3 (2024.7.06 リリース)
### ダウンロードリンク
- [windows mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.exe)
- [macos mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-x64-2.2.3.dmg)
- [linux mediago v2.2.3](https://github.com/caorushizi/mediago/releases/download/v2.2.3/mediago-setup-arm64-2.2.3.dmg)
### 更新ログ
- 設定に【自動更新】スイッチを追加:release版のみ更新、beta版は自動更新されません
- ダウンロードフォームに【バッチ更新】を追加
- Linux版を新たにリリース
- ダウンロード時に最も高解像度の動画を自動選択
- 【キャッシュクリア】&【シークレットモード】を追加
- インストール場所をカスタマイズ選択可能
- 一部のバグを修正
## 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)
### 更新ログ
- ライブストリームのダウンロードをサポート
- ビリビリ動画ダウンロードをサポート
- 没入型嗅探プロセスを最適化
- ダウンロードコントロールの出力をサポート
- 一部のバグを修正
## v2.0.12023.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)
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog3.png)
### 更新ログ
- ダークモード
- 追加のダウンロード設定
- リクエストヘッダーの自動挿入をサポート
- 広告フィルタリングをサポート
- 没入型嗅探機能をサポート
- モバイルモードとPCモードの切り替えをサポート
- 同時ダウンロード数の変更をサポート
- 一部のバグを修正
## v1.1.52022.2.5 リリース)
### ダウンロードリンク
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog2.webp)
### 更新ログ
- 動画ダウンロードをサポート
## v1.0.12021.3.1 リリース)
### ソフトウェアスクリーンショット
![ホームページ](../images/changelog1.webp)
### 更新ログ
- 動画ダウンロードをサポート
+144 -144
View File
@@ -1,144 +1,144 @@
---
layout: doc
outline: deep
---
# 使用方法
このページでは、設定ページのパラメーターの意味について説明します。
![step 1](../images/documents-step1.png)
## 基本設定
::: tip
ダウンローダーのグローバル設定
:::
### 1. フォルダの選択
動画のダウンロード先のパスを設定します。
### 2. ダウンローダーのテーマ
ライトモードとダークモードをサポートしています。
### 3. 表示言語
中国語と英語がサポートされています。
### 4. ダウンロード完了通知
これをオンにすると、ダウンロード完了時にシステム通知が表示されます。
### 5. コンソールの表示
これをオンにすると、ダウンロードコンソールが表示されます。
### 6. 自動更新
これをオンにすると、ソフトウェアは自動的に更新をチェックします。
### 7. テストバージョンのアップグレードを許可
これをオンにすると、ソフトウェアは自動的にテストバージョンをチェックします。
### 8. メインウィンドウを閉じる
メインウィンドウを閉じた後、ソフトウェアを【システムトレイに隠す】か【ソフトウェアを終了】するかを設定します。
## ブラウザ設定
::: tip
ブラウザウィンドウに関連する設定
:::
### 1. 新しいウィンドウでブラウザを開く
これをオンにすると、ブラウザページが独立したウィンドウで開きます。
![step 1](../images/documents-step2.png)
### 2. プロキシ設定
自分のプロキシアドレスを入力します。
### 3. プロキシスイッチ
スイッチをオンにすると、**ブラウザ**はデフォルトで指定されたプロキシアドレスを使用します。このスイッチがオフの場合、ブラウザのプロキシは設定できません。
### 4. 広告フィルタリングを有効にする
スイッチをオンにすると、ページ内の広告がフィルタリングされます。
### 5. モバイルモードで入る
これをオンにすると、ブラウザがモバイルブラウザに偽装され、モバイルサイトを要求します(左側はモバイルモードを有効にしたスクリーンショットです)。
![step 2](../images/documents-step3.png)
### 6. 没入型嗅探を使用
- 有効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されず、ページ内の【リストに追加】ボタンをクリックする必要があります。
![step 3](../images/documents-step4.png)
- 無効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されます。
### 7. プライバシーモード: 有効にすると、ソフトウェアはユーザーのブラウジング履歴を保存しません
![step 4](../images/documents-step5.png)
### 8. その他の操作
- キャッシュをクリア: ソフトウェアのキャッシュをクリアします。
- お気に入りのエクスポート【インポート】: ソフトウェアのお気に入りをエクスポートします。
## ダウンロード設定
::: tip
ダウンロードに関連する設定
:::
### 1. ダウンロードプロキシスイッチ
スイッチをオンにすると、**ダウンローダー**はデフォルトで指定されたプロキシアドレスを使用します。**ブラウザ**と**ダウンローダー**のプロキシは独立しています。
### 2. ダウンロード完了後、分割ファイルを削除
これをオンにすると、ダウンロードが完了した後に分割ファイルが削除されます。
### 3. 最大同時ダウンロード数
最大で同時にダウンロードできる動画ファイルの数を設定します。現在の最大値は10、最小値は1です。
### 4. その他の操作
- 設定ファイルのディレクトリ: ソフトウェアのデータベースパス、ログなどのパスを設定します。
- 実行可能ファイルのディレクトリ: ダウンローダーのバイナリファイルのパスを設定します。
- ローカルストレージパス: ダウンロードした動画のローカルパスを設定します。
### 5. 現在のバージョン
現在使用しているソフトウェアのバージョン情報が表示されます。
## その他の質問
### Q: ライブ動画のダウンロードについて
A: ソフトウェアはライブのダウンロードをサポートしていますが、現時点ではライブストリームを区別する方法がないため、すべてのダウンロードコンソールを開くようにしています。ユーザー自身でダウンロードするデータソースを識別してください。
### Q: MacOS版について
A: 【Intelチップ】の場合、リリース版のx64バージョンをインストールする必要があります。インストール後、Macのセキュリティ設定で「不明な開発元からのアプリケーションを開く」設定を有効にしてください。
![QA1](../images/documents-qa1.png)
【Appleチップ】の場合、リリース版のarm64チップ用バージョンをインストールする必要があります。インストール後、コンソールで `sudo xattr -dr com.apple.quarantine /Applications/mediago.app` コマンドを実行してください。
### Q: 古いバージョンについて
A: バージョン1.1.5は長期間にわたりリリースされ、多くのユーザーによって検証されていますので、比較的安定しています。古いバージョンを使用したい場合は、[こちらのリンク](/history.html)からアクセスしてください。
### Q: Win7ユーザーについて
A: v2.0.0以降のバージョンはWin7をサポートしていません。Win7で使用する場合は、バージョン1.1.5をダウンロードする必要があります。32ビットシステムは現在、デフォルトでサポートされていません。
---
layout: doc
outline: deep
---
# 使用方法
このページでは、設定ページのパラメーターの意味について説明します。
![step 1](../images/documents-step1.png)
## 基本設定
::: tip
ダウンローダーのグローバル設定
:::
### 1. フォルダの選択
動画のダウンロード先のパスを設定します。
### 2. ダウンローダーのテーマ
ライトモードとダークモードをサポートしています。
### 3. 表示言語
中国語と英語がサポートされています。
### 4. ダウンロード完了通知
これをオンにすると、ダウンロード完了時にシステム通知が表示されます。
### 5. コンソールの表示
これをオンにすると、ダウンロードコンソールが表示されます。
### 6. 自動更新
これをオンにすると、ソフトウェアは自動的に更新をチェックします。
### 7. テストバージョンのアップグレードを許可
これをオンにすると、ソフトウェアは自動的にテストバージョンをチェックします。
### 8. メインウィンドウを閉じる
メインウィンドウを閉じた後、ソフトウェアを【システムトレイに隠す】か【ソフトウェアを終了】するかを設定します。
## ブラウザ設定
::: tip
ブラウザウィンドウに関連する設定
:::
### 1. 新しいウィンドウでブラウザを開く
これをオンにすると、ブラウザページが独立したウィンドウで開きます。
![step 1](../images/documents-step2.png)
### 2. プロキシ設定
自分のプロキシアドレスを入力します。
### 3. プロキシスイッチ
スイッチをオンにすると、**ブラウザ**はデフォルトで指定されたプロキシアドレスを使用します。このスイッチがオフの場合、ブラウザのプロキシは設定できません。
### 4. 広告フィルタリングを有効にする
スイッチをオンにすると、ページ内の広告がフィルタリングされます。
### 5. モバイルモードで入る
これをオンにすると、ブラウザがモバイルブラウザに偽装され、モバイルサイトを要求します(左側はモバイルモードを有効にしたスクリーンショットです)。
![step 2](../images/documents-step3.png)
### 6. 没入型嗅探を使用
- 有効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されず、ページ内の【リストに追加】ボタンをクリックする必要があります。
![step 3](../images/documents-step4.png)
- 無効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されます。
### 7. プライバシーモード: 有効にすると、ソフトウェアはユーザーのブラウジング履歴を保存しません
![step 4](../images/documents-step5.png)
### 8. その他の操作
- キャッシュをクリア: ソフトウェアのキャッシュをクリアします。
- お気に入りのエクスポート【インポート】: ソフトウェアのお気に入りをエクスポートします。
## ダウンロード設定
::: tip
ダウンロードに関連する設定
:::
### 1. ダウンロードプロキシスイッチ
スイッチをオンにすると、**ダウンローダー**はデフォルトで指定されたプロキシアドレスを使用します。**ブラウザ**と**ダウンローダー**のプロキシは独立しています。
### 2. ダウンロード完了後、分割ファイルを削除
これをオンにすると、ダウンロードが完了した後に分割ファイルが削除されます。
### 3. 最大同時ダウンロード数
最大で同時にダウンロードできる動画ファイルの数を設定します。現在の最大値は10、最小値は1です。
### 4. その他の操作
- 設定ファイルのディレクトリ: ソフトウェアのデータベースパス、ログなどのパスを設定します。
- 実行可能ファイルのディレクトリ: ダウンローダーのバイナリファイルのパスを設定します。
- ローカルストレージパス: ダウンロードした動画のローカルパスを設定します。
### 5. 現在のバージョン
現在使用しているソフトウェアのバージョン情報が表示されます。
## その他の質問
### Q: ライブ動画のダウンロードについて
A: ソフトウェアはライブのダウンロードをサポートしていますが、現時点ではライブストリームを区別する方法がないため、すべてのダウンロードコンソールを開くようにしています。ユーザー自身でダウンロードするデータソースを識別してください。
### Q: MacOS版について
A: 【Intelチップ】の場合、リリース版のx64バージョンをインストールする必要があります。インストール後、Macのセキュリティ設定で「不明な開発元からのアプリケーションを開く」設定を有効にしてください。
![QA1](../images/documents-qa1.png)
【Appleチップ】の場合、リリース版のarm64チップ用バージョンをインストールする必要があります。インストール後、コンソールで `sudo xattr -dr com.apple.quarantine /Applications/mediago.app` コマンドを実行してください。
### Q: 古いバージョンについて
A: バージョン1.1.5は長期間にわたりリリースされ、多くのユーザーによって検証されていますので、比較的安定しています。古いバージョンを使用したい場合は、[こちらのリンク](/history.html)からアクセスしてください。
### Q: Win7ユーザーについて
A: v2.0.0以降のバージョンはWin7をサポートしていません。Win7で使用する場合は、バージョン1.1.5をダウンロードする必要があります。32ビットシステムは現在、デフォルトでサポートされていません。
+112 -112
View File
@@ -1,112 +1,112 @@
---
layout: doc
outline: deep
---
# クイックスタート
この記事では、ソフトウェアの簡単な説明を行い、すぐに使用できるようにします。
::: tip
皆さんがより便利にコミュニケーションできるよう、フィードバックグループに参加できます:
MediaGo QQフィードバックグループ 1 574209001
:::
::: info
v3.0 は最新バージョンです。皆さんの意見はできるだけ3.0バージョンでお寄せください。私たちはできるだけ早く修正します。
:::
## ダウンロードとインストール
### 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 arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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`
#### 重要な更新内容
- DockerでWeb端末の展開をサポート
- デスクトップ端末UIを更新
#### 更新ログ
- DockerでWeb端末の展開をサポート
- デスクトップ端末UIを更新
- 動画再生機能を追加、デスクトップとモバイル端末両方で再生可能
- macで画面が表示されない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windows用ポータブル版(インストール不要)を追加
- ダウンロードリストを最適化し、ページ内の複数の動画の嗅探をサポート
- お気に入りリストの手動インポート/エクスポートをサポート
- ホームページのダウンロードリストエクスポートをサポート
- 【新規ダウンロード】フォームのインタラクションロジックを最適化
- UrlSchemeを使用してアプリを開き、ダウンロードタスクを追加
- バグの修正とユーザーエクスペリエンスの向上
## 操作方法
### 動画の自動嗅探
1. 【リソース抽出】を選択
![step 1](../images/guides-step1.png)
2. 動画のURLにアクセス
![step 2](../images/guides-step2.png)
3. 【今すぐダウンロード】をクリックすると、動画がダウンロードされます
![step 3](../images/guides-step3.png)
### 手動ダウンロード
1. ページ右上の【新規ダウンロード】をクリック
![step 1](../images/guides-step4.png)
2. 新規ダウンロードのポップアップウィンドウに【動画の名前】と【ストリーミング(m3u8)】または【Bilibili】を入力
![step 2](../images/guides-step5.png)
3. リスト内でダウンロードをクリックして、動画をダウンロードします
![step 3](../images/guides-step3.png)
### バッチダウンロード
![step 3](../images/guides-step6.png)
### 追加機能
1. 音声に変換
![step 1](../images/guides-step7.png)
2. さらに多くの機能が追加される予定です。お楽しみに~
### 動画の再生
- PC再生
![step 2](../images/addition-step3.png)
- モバイル端末再生
![step 3](../images/addition-step4.png)
## 動画ダウンロードを始めましょう
簡単ですよね。さあ、動画をダウンロードし始めましょう!
::: warning
このソフトウェアは学習と交流目的でのみ使用できます。
:::
---
layout: doc
outline: deep
---
# クイックスタート
この記事では、ソフトウェアの簡単な説明を行い、すぐに使用できるようにします。
::: tip
皆さんがより便利にコミュニケーションできるよう、フィードバックグループに参加できます:
MediaGo QQフィードバックグループ 1 574209001
:::
::: info
v3.0 は最新バージョンです。皆さんの意見はできるだけ3.0バージョンでお寄せください。私たちはできるだけ早く修正します。
:::
## ダウンロードとインストール
### 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 arm64Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
- [【mediago】 macos x64Intelチップ) 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`
#### 重要な更新内容
- DockerでWeb端末の展開をサポート
- デスクトップ端末UIを更新
#### 更新ログ
- DockerでWeb端末の展開をサポート
- デスクトップ端末UIを更新
- 動画再生機能を追加、デスクトップとモバイル端末両方で再生可能
- macで画面が表示されない問題を修正
- バッチダウンロードのインタラクションを最適化
- Windows用ポータブル版(インストール不要)を追加
- ダウンロードリストを最適化し、ページ内の複数の動画の嗅探をサポート
- お気に入りリストの手動インポート/エクスポートをサポート
- ホームページのダウンロードリストエクスポートをサポート
- 【新規ダウンロード】フォームのインタラクションロジックを最適化
- UrlSchemeを使用してアプリを開き、ダウンロードタスクを追加
- バグの修正とユーザーエクスペリエンスの向上
## 操作方法
### 動画の自動嗅探
1. 【リソース抽出】を選択
![step 1](../images/guides-step1.png)
2. 動画のURLにアクセス
![step 2](../images/guides-step2.png)
3. 【今すぐダウンロード】をクリックすると、動画がダウンロードされます
![step 3](../images/guides-step3.png)
### 手動ダウンロード
1. ページ右上の【新規ダウンロード】をクリック
![step 1](../images/guides-step4.png)
2. 新規ダウンロードのポップアップウィンドウに【動画の名前】と【ストリーミング(m3u8)】または【Bilibili】を入力
![step 2](../images/guides-step5.png)
3. リスト内でダウンロードをクリックして、動画をダウンロードします
![step 3](../images/guides-step3.png)
### バッチダウンロード
![step 3](../images/guides-step6.png)
### 追加機能
1. 音声に変換
![step 1](../images/guides-step7.png)
2. さらに多くの機能が追加される予定です。お楽しみに~
### 動画の再生
- PC再生
![step 2](../images/addition-step3.png)
- モバイル端末再生
![step 3](../images/addition-step4.png)
## 動画ダウンロードを始めましょう
簡単ですよね。さあ、動画をダウンロードし始めましょう!
::: warning
このソフトウェアは学習と交流目的でのみ使用できます。
:::
+21 -21
View File
@@ -1,21 +1,21 @@
---
layout: doc
outline: deep
---
# 旧バージョン
::: info
ここは旧バージョンの永久リンクです。必要があればこちらからダウンロードできます。
**このリンクのソフトウェアは自動アップデートされませんので、安心して使用できます**
:::
**v1.1.52022年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 はなし~~
**更新ログ**
- 動画ダウンロードのサポート
---
layout: doc
outline: deep
---
# 旧バージョン
::: info
ここは旧バージョンの永久リンクです。必要があればこちらからダウンロードできます。
**このリンクのソフトウェアは自動アップデートされませんので、安心して使用できます**
:::
**v1.1.52022年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 はなし~~
**更新ログ**
- 動画ダウンロードのサポート
+32 -32
View File
@@ -1,32 +1,32 @@
---
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: 複数の動画やライブストリーミングリソースを同時にダウンロードでき、高速な帯域幅が無駄に使われることはありません。
- icon: 🎉
title: Dockerデプロイ対応
details: Web端をDockerでデプロイでき、簡単で迅速に設定できます。
---
---
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: 複数の動画やライブストリーミングリソースを同時にダウンロードでき、高速な帯域幅が無駄に使われることはありません。
- icon: 🎉
title: Dockerデプロイ対応
details: Web端をDockerでデプロイでき、簡単で迅速に設定できます。
---

Some files were not shown because too many files have changed in this diff Show More