chore(repo): convert text files to CRLF line endings
This commit is contained in:
+6
-6
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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**
|
||||
Don’t 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 project’s 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**
|
||||
Don’t 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 project’s 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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-unused-vars": "error",
|
||||
"no-console": "warn"
|
||||
}
|
||||
{
|
||||
"rules": {
|
||||
"no-unused-vars": "error",
|
||||
"no-console": "warn"
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
plugins:
|
||||
plugins:
|
||||
- "@prettier/plugin-oxc"
|
||||
Vendored
+5
-5
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
## Intro
|
||||
|
||||
This project supports m3u8 video extraction tools, streaming media download, and m3u8 download.
|
||||
|
||||
- **✅ No need to capture packets**: You can easily sniff video resources on web pages using the built-in browser. Choose the resource you want to download from the sniffed resource list—simple and fast.
|
||||
- **📱 Mobile playback**: Easily switch between PC and mobile devices seamlessly. Once downloaded, you can watch the video on your phone.
|
||||
- **⚡️ Batch download**: Supports downloading multiple videos and live streams simultaneously, ensuring high bandwidth is fully utilized.
|
||||
- **🎉 Docker deployment supported**: Supports Docker deployment for the web version, making it convenient and quick.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To run the code, you'll need Node.js and pnpm. Node.js can be downloaded and installed from the official website, and pnpm can be installed via `npm i -g pnpm`.
|
||||
|
||||
## Run the code
|
||||
|
||||
```shell
|
||||
# Code download
|
||||
git clone https://github.com/caorushizi/mediago.git
|
||||
|
||||
# Installation dependency
|
||||
pnpm i
|
||||
|
||||
# Development environment
|
||||
pnpm dev
|
||||
|
||||
# Package run
|
||||
pnpm release
|
||||
|
||||
# Build a docker image
|
||||
docker buildx build -t caorushizi/mediago:latest .
|
||||
|
||||
# docker startup
|
||||
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
### v3.0.0 (Released on 2024.10.7)
|
||||
|
||||
#### Software Downloads
|
||||
|
||||
- [【mediago】 Windows (Installer) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 Windows (Portable) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macOS x64 (Intel) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 Linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
#### Domestic Downloads
|
||||
|
||||
- [【mediago】 Windows (Installer) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 Windows (Portable) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macOS x64 (Intel) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 Linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### One-click Docker Panel Deployment (Recommended)
|
||||
|
||||
1. Install the BT Panel by visiting [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago) and downloading the official version script.
|
||||
2. After installation, log into the BT Panel, click on `Docker` in the menu bar. The first time you enter, it will prompt you to install the `Docker` service. Click to install and follow the instructions to complete the installation.
|
||||
3. Once installed, find `MediaGo` in the application store, click install, configure domain names, and other basic information to complete the installation.
|
||||
|
||||
### Software Screenshots
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Tech Stack
|
||||
|
||||
- React <https://react.dev/>
|
||||
- Electron <https://www.electronjs.org>
|
||||
- Koa <https://koajs.com>
|
||||
- Vite <https://cn.vitejs.dev>
|
||||
- Ant Design <https://ant.design>
|
||||
- Tailwind CSS <https://tailwindcss.com>
|
||||
- Shadcn <https://ui.shadcn.com/>
|
||||
- Inversify <https://inversify.io>
|
||||
- TypeORM <https://typeorm.io>
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- N_m2u8DL-CLI from <https://github.com/nilaoda/N_m3u8DL-CLI>
|
||||
- N_m3u8DL-RE from <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
- mediago from <https://github.com/caorushizi/hls-downloader>
|
||||
<div align="center">
|
||||
<h1>MediaGo</h1>
|
||||
<a href="https://downloader.caorushizi.cn/en/guides.html?form=github">Quick start</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/en?form=github">Website</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/en/documents.html?form=github">Docs</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
## Intro
|
||||
|
||||
This project supports m3u8 video extraction tools, streaming media download, and m3u8 download.
|
||||
|
||||
- **✅ No need to capture packets**: You can easily sniff video resources on web pages using the built-in browser. Choose the resource you want to download from the sniffed resource list—simple and fast.
|
||||
- **📱 Mobile playback**: Easily switch between PC and mobile devices seamlessly. Once downloaded, you can watch the video on your phone.
|
||||
- **⚡️ Batch download**: Supports downloading multiple videos and live streams simultaneously, ensuring high bandwidth is fully utilized.
|
||||
- **🎉 Docker deployment supported**: Supports Docker deployment for the web version, making it convenient and quick.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To run the code, you'll need Node.js and pnpm. Node.js can be downloaded and installed from the official website, and pnpm can be installed via `npm i -g pnpm`.
|
||||
|
||||
## Run the code
|
||||
|
||||
```shell
|
||||
# Code download
|
||||
git clone https://github.com/caorushizi/mediago.git
|
||||
|
||||
# Installation dependency
|
||||
pnpm i
|
||||
|
||||
# Development environment
|
||||
pnpm dev
|
||||
|
||||
# Package run
|
||||
pnpm release
|
||||
|
||||
# Build a docker image
|
||||
docker buildx build -t caorushizi/mediago:latest .
|
||||
|
||||
# docker startup
|
||||
docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
### v3.0.0 (Released on 2024.10.7)
|
||||
|
||||
#### Software Downloads
|
||||
|
||||
- [【mediago】 Windows (Installer) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 Windows (Portable) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macOS x64 (Intel) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 Linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
#### Domestic Downloads
|
||||
|
||||
- [【mediago】 Windows (Installer) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 Windows (Portable) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macOS ARM64 (Apple Silicon) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macOS x64 (Intel) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 Linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 Docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### One-click Docker Panel Deployment (Recommended)
|
||||
|
||||
1. Install the BT Panel by visiting [BT Panel](https://www.bt.cn/new/download.html?r=dk_mediago) and downloading the official version script.
|
||||
2. After installation, log into the BT Panel, click on `Docker` in the menu bar. The first time you enter, it will prompt you to install the `Docker` service. Click to install and follow the instructions to complete the installation.
|
||||
3. Once installed, find `MediaGo` in the application store, click install, configure domain names, and other basic information to complete the installation.
|
||||
|
||||
### Software Screenshots
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 技術スタック
|
||||
|
||||
- react <https://react.dev/>
|
||||
- electron <https://www.electronjs.org>
|
||||
- koa <https://koajs.com>
|
||||
- vite <https://cn.vitejs.dev>
|
||||
- antd <https://ant.design>
|
||||
- tailwindcss <https://tailwindcss.com>
|
||||
- shadcn <https://ui.shadcn.com/>
|
||||
- inversify <https://inversify.io>
|
||||
- typeorm <https://typeorm.io>
|
||||
|
||||
## 感謝
|
||||
|
||||
- N_m2u8DL-CLI は <https://github.com/nilaoda/N_m3u8DL-CLI> から来ています
|
||||
- N_m3u8DL-RE は <https://github.com/nilaoda/N_m3u8DL-RE> から来ています
|
||||
- mediago は <https://github.com/caorushizi/hls-downloader> から来ています
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
<div align="center">
|
||||
<h1>MediaGo</h1>
|
||||
<a href="https://downloader.caorushizi.cn/guides.html?form=github">快速开始</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
|
||||
<a href="https://github.com/caorushizi/mediago/blob/master/README.en.md">English</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
## Intro
|
||||
|
||||
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
|
||||
|
||||
- **✅ 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- **📱 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **⚡️ 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
|
||||
- **🎉 支持 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 arm64(apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### docker 宝塔面板一键部署(推荐)
|
||||
|
||||
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/new/download.html?r=dk_mediago) 官网,选择正式版的脚本下载安装
|
||||
|
||||
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
|
||||
|
||||
3. 安装完成后在应用商店中找到`MediaGo`,点击安装,配置域名等基本信息即可完成安装
|
||||
|
||||
### 软件截图
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 技术栈
|
||||
|
||||
- react <https://react.dev/>
|
||||
- electron <https://www.electronjs.org>
|
||||
- koa <https://koajs.com>
|
||||
- vite <https://cn.vitejs.dev>
|
||||
- antd <https://ant.design>
|
||||
- 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> • </span>
|
||||
<a href="https://downloader.caorushizi.cn?form=github">官网</a>
|
||||
<span> • </span>
|
||||
<a href="https://downloader.caorushizi.cn/documents.html?form=github">文档</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/discussions">Discussions</a>
|
||||
<br>
|
||||
|
||||
<a href="https://github.com/caorushizi/mediago/blob/master/README.en.md">English</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/caorushizi/mediago/blob/master/README.jp.md">日本語</a>
|
||||
<br>
|
||||
|
||||
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/caorushizi/mediago/total">
|
||||
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/caorushizi/mediago/latest/total">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/caorushizi/mediago">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/caorushizi/mediago">
|
||||
<br>
|
||||
|
||||
<a href="https://trendshift.io/repositories/11083" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11083" alt="caorushizi%2Fmediago | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
|
||||
</a>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
## Intro
|
||||
|
||||
本项目支持 m3u8 视频在线提取工具 流媒体下载 m3u8 下载。
|
||||
|
||||
- **✅ 无需抓包**: 使用软件自带浏览器可以轻松嗅探网页中的视频资源,通过嗅探到的资源列表选择自己想要下载的资源,简单快速。
|
||||
- **📱 移动播放**: 可以轻松无缝的在 PC 和移动设备之前切换,下载完成后即可使用手机观看视频。
|
||||
- **⚡️ 批量下载**: 支持同时下载多个视频和直播资源,高速带宽不闲置。
|
||||
- **🎉 支持 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 arm64(apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### docker 宝塔面板一键部署(推荐)
|
||||
|
||||
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/new/download.html?r=dk_mediago) 官网,选择正式版的脚本下载安装
|
||||
|
||||
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
|
||||
|
||||
3. 安装完成后在应用商店中找到`MediaGo`,点击安装,配置域名等基本信息即可完成安装
|
||||
|
||||
### 软件截图
|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 技术栈
|
||||
|
||||
- react <https://react.dev/>
|
||||
- electron <https://www.electronjs.org>
|
||||
- koa <https://koajs.com>
|
||||
- vite <https://cn.vitejs.dev>
|
||||
- antd <https://ant.design>
|
||||
- tailwindcss <https://tailwindcss.com>
|
||||
- shadcn <https://ui.shadcn.com/>
|
||||
- inversify <https://inversify.io>
|
||||
- typeorm <https://typeorm.io>
|
||||
|
||||
## 鸣谢
|
||||
|
||||
- N_m2u8DL-CLI 来自于 <https://github.com/nilaoda/N_m3u8DL-CLI>
|
||||
- N_m3u8DL-RE 来自于 <https://github.com/nilaoda/N_m3u8DL-RE>
|
||||
- mediago 来自于 <https://github.com/caorushizi/hls-downloader>
|
||||
|
||||
@@ -1,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
@@ -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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
node_modules
|
||||
output
|
||||
bin
|
||||
mobile
|
||||
plugin
|
||||
build
|
||||
dist
|
||||
node_modules
|
||||
output
|
||||
bin
|
||||
mobile
|
||||
plugin
|
||||
build
|
||||
dist
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: "无标题" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Vendored
+104
-104
@@ -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
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -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 |
@@ -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 |
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+105
-105
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
.vitepress/dist
|
||||
.vitepress/cache
|
||||
.temp
|
||||
node_modules
|
||||
.vitepress/dist
|
||||
.vitepress/cache
|
||||
.temp
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
|
||||
@@ -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
@@ -1,33 +1,33 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 通过宝塔面板部署
|
||||
|
||||
本文介绍使用 `宝塔面板` 来部署 `MediaGo`。
|
||||
|
||||
## 前提
|
||||
|
||||
- 仅适用于宝塔面板 9.2.0 及以上版本
|
||||
- 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html?r=dk_mediago),选择正式版的脚本下载安装
|
||||
|
||||
## 部署
|
||||
|
||||
1. 登录宝塔面板,在左侧菜单栏中点击 `Docker`
|
||||

|
||||
|
||||
2. 首次会提示安装 `Docker` 和 `Docker Compose` 服务,点击立即安装,若已安装请忽略。
|
||||

|
||||
|
||||
3. 安装完成后在`Docker-应用商店`中找到 `MediaGo`,点击`安装`
|
||||

|
||||
|
||||
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`
|
||||

|
||||
|
||||
2. 首次会提示安装 `Docker` 和 `Docker Compose` 服务,点击立即安装,若已安装请忽略。
|
||||

|
||||
|
||||
3. 安装完成后在`Docker-应用商店`中找到 `MediaGo`,点击`安装`
|
||||

|
||||
|
||||
4. 提交后面板会自动进行应用初始化,大概需要1-3分钟,初始化完成后即可访问。
|
||||
|
||||
- 端口: 8899
|
||||
- 版本选择: v3.0.0
|
||||
|
||||
## 访问 MediaGo
|
||||
|
||||
请在浏览器地址栏中输入域名访问 `http://<宝塔面板IP>:8899`,即可访问 `MediaGo` 控制台。
|
||||
|
||||
+24
-24
@@ -1,24 +1,24 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 配合猫爪下载视频
|
||||
|
||||
::: tip
|
||||
感谢 `1颗温暖的心` 提供教程:
|
||||
<https://www.kdocs.cn/l/coioFy5l7vwW>
|
||||
:::
|
||||
|
||||

|
||||
|
||||
- 安装 [猫爪](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`
|
||||
|
||||

|
||||
|
||||
- 设置完成之后可以在猫爪的面板中点击直接调用 MediaGo 下载
|
||||
|
||||

|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 配合猫爪下载视频
|
||||
|
||||
::: tip
|
||||
感谢 `1颗温暖的心` 提供教程:
|
||||
<https://www.kdocs.cn/l/coioFy5l7vwW>
|
||||
:::
|
||||
|
||||

|
||||
|
||||
- 安装 [猫爪](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`
|
||||
|
||||

|
||||
|
||||
- 设置完成之后可以在猫爪的面板中点击直接调用 MediaGo 下载
|
||||
|
||||

|
||||
|
||||
+128
-128
@@ -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 arm64(apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### 国内下载
|
||||
|
||||
- [【mediago】 windows(安装版) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(便携版) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(apple 芯片) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 重要更新
|
||||
|
||||
- 支持 docker 部署 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.1(2023.7.1 发布)
|
||||
|
||||
### 下载链接
|
||||
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 更新日志
|
||||
|
||||
- 暗黑模式
|
||||
- 更多下载配置
|
||||
- 支持请求标头自动带入
|
||||
- 支持开启广告过滤
|
||||
- 支持开启沉浸式嗅探
|
||||
- 支持切换手机和 PC 模式
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
## v1.1.5(2022.2.5 发布)
|
||||
|
||||
### 下载链接
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 更新日志
|
||||
|
||||
- 支持视频下载
|
||||
|
||||
## v1.0.1(2021.3.1 发布)
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 更新日志
|
||||
|
||||
- 支持视频下载
|
||||
---
|
||||
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 arm64(apple 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### 国内下载
|
||||
|
||||
- [【mediago】 windows(安装版) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(便携版) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(apple 芯片) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 重要更新
|
||||
|
||||
- 支持 docker 部署 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.1(2023.7.1 发布)
|
||||
|
||||
### 下载链接
|
||||
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 更新日志
|
||||
|
||||
- 暗黑模式
|
||||
- 更多下载配置
|
||||
- 支持请求标头自动带入
|
||||
- 支持开启广告过滤
|
||||
- 支持开启沉浸式嗅探
|
||||
- 支持切换手机和 PC 模式
|
||||
- 支持修改同时下载数量
|
||||
- 修复了一些 bug
|
||||
|
||||
## v1.1.5(2022.2.5 发布)
|
||||
|
||||
### 下载链接
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 更新日志
|
||||
|
||||
- 支持视频下载
|
||||
|
||||
## v1.0.1(2021.3.1 发布)
|
||||
|
||||
### 软件截图
|
||||
|
||||

|
||||
|
||||
### 更新日志
|
||||
|
||||
- 支持视频下载
|
||||
|
||||
+123
-123
@@ -1,123 +1,123 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 使用说明
|
||||
|
||||
这个页面主要讲解的是设置页面的参数的含义
|
||||
|
||||

|
||||
|
||||
## 基础设置
|
||||
|
||||
::: tip
|
||||
下载器全局设置
|
||||
:::
|
||||
|
||||
### 1. 选择文件夹
|
||||
|
||||
视频下载的路径
|
||||
|
||||
### 2. 下载器主题
|
||||
|
||||
支持浅色和深色模式
|
||||
|
||||
### 3. 显示语言、
|
||||
|
||||
支持中文和英文
|
||||
|
||||
### 4. 下载完成提示
|
||||
|
||||
开启之后下载完成会出现系统提示
|
||||
|
||||
### 5. 显示控制台
|
||||
|
||||
开启之后会显示下载的控制台
|
||||
|
||||
### 6. 自动更新
|
||||
|
||||
开启之后软件会自动检查更新
|
||||
|
||||
### 7. 允许升级测试版本
|
||||
|
||||
开启之后软件会自动检查测试版本
|
||||
|
||||
### 8. 关闭主窗口
|
||||
|
||||
控制关闭主窗口后软件会【隐藏到系统托盘】还是【退出软件】
|
||||
|
||||
## 浏览器设置
|
||||
|
||||
::: tip
|
||||
浏览器窗口相关的设置
|
||||
:::
|
||||
|
||||
### 1. 在新窗口中打开浏览器
|
||||
|
||||
开启了之后浏览器页面会独立出一个窗口
|
||||
|
||||

|
||||
|
||||
### 2. 代理设置
|
||||
|
||||
填写自己的代理地址
|
||||
|
||||
### 3. 代理开关
|
||||
|
||||
打开开关后 **浏览器** 默认会走填写的代理地址,如果代理地址这个开关是打不开的
|
||||
|
||||
### 4. 开启广告过滤
|
||||
|
||||
打开开关后会过滤页面中的广告
|
||||
|
||||
### 5. 以手机模式进入
|
||||
|
||||
打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
|
||||
|
||||

|
||||
|
||||
### 6. 使用沉浸式嗅探
|
||||
|
||||
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面中的【添加到列表】按钮
|
||||
|
||||

|
||||
|
||||
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
|
||||
|
||||
### 7. 隐私模式: 打开后软件不会保存用户的浏览记录
|
||||
|
||||

|
||||
|
||||
### 8. 更多操作
|
||||
|
||||
- 清空缓存: 清空软件的缓存
|
||||
- 导出收藏【导入收藏】: 导出软件的收藏
|
||||
|
||||
## 下载设置
|
||||
|
||||
::: tip
|
||||
和下载相关的设置
|
||||
:::
|
||||
|
||||
### 1. 下载代理开关
|
||||
|
||||
打开开关后 **下载器** 默认会走填写的代理地址,【浏览器】和【下载器】的代理是独立的
|
||||
|
||||
### 2. 下载完成删除分片
|
||||
|
||||
开启后下载完成后会将分片文件删除
|
||||
|
||||
### 3. 最大同时下载数量
|
||||
|
||||
最多同时可以下载多少视频文件,目前最大是 10,最小是 1
|
||||
|
||||
### 4. 更多操作
|
||||
|
||||
- 配置文件目录: 软件数据库路径、日志等等路径
|
||||
- 可执行文件目录: 下载器二进制文件的路径
|
||||
- 本地存储路径: 下载视频的本地路径
|
||||
|
||||
### 5. 当前版本
|
||||
|
||||
当前软件版本信息
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 使用说明
|
||||
|
||||
这个页面主要讲解的是设置页面的参数的含义
|
||||
|
||||

|
||||
|
||||
## 基础设置
|
||||
|
||||
::: tip
|
||||
下载器全局设置
|
||||
:::
|
||||
|
||||
### 1. 选择文件夹
|
||||
|
||||
视频下载的路径
|
||||
|
||||
### 2. 下载器主题
|
||||
|
||||
支持浅色和深色模式
|
||||
|
||||
### 3. 显示语言、
|
||||
|
||||
支持中文和英文
|
||||
|
||||
### 4. 下载完成提示
|
||||
|
||||
开启之后下载完成会出现系统提示
|
||||
|
||||
### 5. 显示控制台
|
||||
|
||||
开启之后会显示下载的控制台
|
||||
|
||||
### 6. 自动更新
|
||||
|
||||
开启之后软件会自动检查更新
|
||||
|
||||
### 7. 允许升级测试版本
|
||||
|
||||
开启之后软件会自动检查测试版本
|
||||
|
||||
### 8. 关闭主窗口
|
||||
|
||||
控制关闭主窗口后软件会【隐藏到系统托盘】还是【退出软件】
|
||||
|
||||
## 浏览器设置
|
||||
|
||||
::: tip
|
||||
浏览器窗口相关的设置
|
||||
:::
|
||||
|
||||
### 1. 在新窗口中打开浏览器
|
||||
|
||||
开启了之后浏览器页面会独立出一个窗口
|
||||
|
||||

|
||||
|
||||
### 2. 代理设置
|
||||
|
||||
填写自己的代理地址
|
||||
|
||||
### 3. 代理开关
|
||||
|
||||
打开开关后 **浏览器** 默认会走填写的代理地址,如果代理地址这个开关是打不开的
|
||||
|
||||
### 4. 开启广告过滤
|
||||
|
||||
打开开关后会过滤页面中的广告
|
||||
|
||||
### 5. 以手机模式进入
|
||||
|
||||
打开后会将浏览器伪装成手机浏览器,请求移动端的站点(左侧是开启手机模式的截图)
|
||||
|
||||

|
||||
|
||||
### 6. 使用沉浸式嗅探
|
||||
|
||||
- 打开状态: 从浏览器中嗅探到的资源不会自动添加到下载列表,需要手动点击页面中的【添加到列表】按钮
|
||||
|
||||

|
||||
|
||||
- 关闭状态: 浏览器中嗅探到的资源会自动添加到下载列表
|
||||
|
||||
### 7. 隐私模式: 打开后软件不会保存用户的浏览记录
|
||||
|
||||

|
||||
|
||||
### 8. 更多操作
|
||||
|
||||
- 清空缓存: 清空软件的缓存
|
||||
- 导出收藏【导入收藏】: 导出软件的收藏
|
||||
|
||||
## 下载设置
|
||||
|
||||
::: tip
|
||||
和下载相关的设置
|
||||
:::
|
||||
|
||||
### 1. 下载代理开关
|
||||
|
||||
打开开关后 **下载器** 默认会走填写的代理地址,【浏览器】和【下载器】的代理是独立的
|
||||
|
||||
### 2. 下载完成删除分片
|
||||
|
||||
开启后下载完成后会将分片文件删除
|
||||
|
||||
### 3. 最大同时下载数量
|
||||
|
||||
最多同时可以下载多少视频文件,目前最大是 10,最小是 1
|
||||
|
||||
### 4. 更多操作
|
||||
|
||||
- 配置文件目录: 软件数据库路径、日志等等路径
|
||||
- 可执行文件目录: 下载器二进制文件的路径
|
||||
- 本地存储路径: 下载视频的本地路径
|
||||
|
||||
### 5. 当前版本
|
||||
|
||||
当前软件版本信息
|
||||
|
||||
+33
-33
@@ -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
|
||||

|
||||
|
||||
2. The first time, it will prompt to install `Docker` and `Docker Compose` services. Click "Install Now", and ignore if already installed.
|
||||

|
||||
|
||||
3. After installation, go to `Docker - App Store`, find `MediaGo`, and click `Install`
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
2. The first time, it will prompt to install `Docker` and `Docker Compose` services. Click "Install Now", and ignore if already installed.
|
||||

|
||||
|
||||
3. After installation, go to `Docker - App Store`, find `MediaGo`, and click `Install`
|
||||

|
||||
|
||||
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
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### Changelog
|
||||
|
||||
- Support for video downloads
|
||||
|
||||
## v1.0.1 (Released on 2021.3.1)
|
||||
|
||||
### Software Screenshots
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
### Changelog
|
||||
|
||||
- Support for video downloads
|
||||
|
||||
## v1.0.1 (Released on 2021.3.1)
|
||||
|
||||
### Software Screenshots
|
||||
|
||||

|
||||
|
||||
### Changelog
|
||||
|
||||
- Support for video downloads
|
||||
|
||||
+148
-148
@@ -1,148 +1,148 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# User Guide
|
||||
|
||||
This page mainly explains the meaning of the parameters in the settings page.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
### 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).
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
- **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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
### 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).
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
- **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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||

|
||||
|
||||
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
@@ -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"
|
||||
|
||||

|
||||
|
||||
2. Enter the video URL
|
||||
|
||||

|
||||
|
||||
3. Click "Start Download" to begin downloading the video
|
||||
|
||||

|
||||
|
||||
### Manual Download
|
||||
|
||||
1. Click the "New Download" button at the top right of the page
|
||||
|
||||

|
||||
|
||||
2. In the new download window, enter the "Video Name" and the "Stream (m3u8)" or "Bilibili" link
|
||||
|
||||

|
||||
|
||||
3. Click to download the video from the list
|
||||
|
||||

|
||||
|
||||
### Batch Download
|
||||
|
||||

|
||||
|
||||
### Additional Features
|
||||
|
||||
1. Convert to Audio
|
||||
|
||||

|
||||
|
||||
2. More features will be added in the future. Stay tuned!
|
||||
|
||||
### Video Playback
|
||||
|
||||
- PC Playback
|
||||
|
||||

|
||||
|
||||
- Mobile Playback
|
||||
|
||||

|
||||
|
||||
## 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"
|
||||
|
||||

|
||||
|
||||
2. Enter the video URL
|
||||
|
||||

|
||||
|
||||
3. Click "Start Download" to begin downloading the video
|
||||
|
||||

|
||||
|
||||
### Manual Download
|
||||
|
||||
1. Click the "New Download" button at the top right of the page
|
||||
|
||||

|
||||
|
||||
2. In the new download window, enter the "Video Name" and the "Stream (m3u8)" or "Bilibili" link
|
||||
|
||||

|
||||
|
||||
3. Click to download the video from the list
|
||||
|
||||

|
||||
|
||||
### Batch Download
|
||||
|
||||

|
||||
|
||||
### Additional Features
|
||||
|
||||
1. Convert to Audio
|
||||
|
||||

|
||||
|
||||
2. More features will be added in the future. Stay tuned!
|
||||
|
||||
### Video Playback
|
||||
|
||||
- PC Playback
|
||||
|
||||

|
||||
|
||||
- Mobile Playback
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 arm64(apple 芯片) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-darwin-arm64-3.0.1.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) 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. 选择【资源提取】
|
||||
|
||||

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

|
||||
|
||||
3. 点击【立即下载】,即可下载视频
|
||||
|
||||

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

|
||||
|
||||
2. 在新建下载弹窗中填入【视频名称】和【流媒体(m3u8)】或者【哔哩哔哩】即可
|
||||
|
||||

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

|
||||
|
||||
### 批量下载
|
||||
|
||||

|
||||
|
||||
### 附加功能
|
||||
|
||||
1. 转换为音频
|
||||
|
||||

|
||||
|
||||
2. 后续会增加更多功能,敬请期待~
|
||||
|
||||
### 播放视频
|
||||
|
||||
- PC 播放
|
||||
|
||||

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

|
||||
|
||||
## 开始你的视频下载吧
|
||||
|
||||
是不是很简单,快去下载你的视频吧
|
||||
|
||||
::: 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 arm64(apple 芯片) v3.0.1](https://github.com/caorushizi/mediago/releases/download/v3.0.1/mediago-setup-darwin-arm64-3.0.1.dmg)
|
||||
- [【mediago】 macos x64(intel 芯片) 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. 选择【资源提取】
|
||||
|
||||

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

|
||||
|
||||
3. 点击【立即下载】,即可下载视频
|
||||
|
||||

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

|
||||
|
||||
2. 在新建下载弹窗中填入【视频名称】和【流媒体(m3u8)】或者【哔哩哔哩】即可
|
||||
|
||||

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

|
||||
|
||||
### 批量下载
|
||||
|
||||

|
||||
|
||||
### 附加功能
|
||||
|
||||
1. 转换为音频
|
||||
|
||||

|
||||
|
||||
2. 后续会增加更多功能,敬请期待~
|
||||
|
||||
### 播放视频
|
||||
|
||||
- PC 播放
|
||||
|
||||

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

|
||||
|
||||
## 开始你的视频下载吧
|
||||
|
||||
是不是很简单,快去下载你的视频吧
|
||||
|
||||
::: warning
|
||||
本软件仅可用于学习交流
|
||||
:::
|
||||
|
||||
+21
-21
@@ -1,21 +1,21 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 旧版本
|
||||
|
||||
::: info
|
||||
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
|
||||
|
||||
**这个链接的软件不会自动升级, 可放心使用**
|
||||
:::
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos mediago v1.1.5 无~~
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持视频下载
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 旧版本
|
||||
|
||||
::: info
|
||||
这里是旧版本的永久链接,如果有需求的话,可以在这里下载
|
||||
|
||||
**这个链接的软件不会自动升级, 可放心使用**
|
||||
:::
|
||||
|
||||
**v1.1.5(2022.2.5 发布)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos mediago v1.1.5 无~~
|
||||
|
||||
**更新日志**
|
||||
|
||||
- 支持视频下载
|
||||
|
||||
+33
-33
@@ -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
@@ -1,33 +1,33 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 宝塔パネルを使ったデプロイ
|
||||
|
||||
この記事では、`宝塔パネル`を使用して`MediaGo`をデプロイする方法を紹介します。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- 宝塔パネル 9.2.0 以上のバージョンのみ対応
|
||||
- 宝塔パネルをインストールするには、[宝塔パネル公式サイト](https://www.bt.cn/new/download.html?r=dk_mediago)にアクセスし、正式版のスクリプトを選んでインストールしてください。
|
||||
|
||||
## デプロイ
|
||||
|
||||
1. 宝塔パネルにログインし、左側のメニューで`Docker`をクリックします。
|
||||

|
||||
|
||||
2. 初回は`Docker`と`Docker Compose`サービスのインストールが求められます。`今すぐインストール`をクリックし、すでにインストールされている場合は無視してください。
|
||||

|
||||
|
||||
3. インストール後、`Docker-アプリストア`で`MediaGo`を探し、`インストール`をクリックします。
|
||||

|
||||
|
||||
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`をクリックします。
|
||||

|
||||
|
||||
2. 初回は`Docker`と`Docker Compose`サービスのインストールが求められます。`今すぐインストール`をクリックし、すでにインストールされている場合は無視してください。
|
||||

|
||||
|
||||
3. インストール後、`Docker-アプリストア`で`MediaGo`を探し、`インストール`をクリックします。
|
||||

|
||||
|
||||
4. 提出後、パネルは自動でアプリケーションの初期化を行い、1~3分ほどで完了します。初期化が完了するとアクセス可能になります。
|
||||
|
||||
- ポート: 8899
|
||||
- バージョン選択: v3.0.0
|
||||
|
||||
## MediaGoへのアクセス
|
||||
|
||||
ブラウザのアドレスバーに`http://<宝塔パネルIP>:8899`を入力して、`MediaGo`のコントロールパネルにアクセスします。
|
||||
|
||||
+129
-129
@@ -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 arm64(Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### 国内ダウンロード
|
||||
|
||||
- [【mediago】 windows(インストーラ版) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(ポータブル版) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 重要な更新
|
||||
|
||||
- dockerを使用した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.1(2023.7.1 リリース)
|
||||
|
||||
### ダウンロードリンク
|
||||
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 更新ログ
|
||||
|
||||
- ダークモード
|
||||
- 追加のダウンロード設定
|
||||
- リクエストヘッダーの自動挿入をサポート
|
||||
- 広告フィルタリングをサポート
|
||||
- 没入型嗅探機能をサポート
|
||||
- モバイルモードとPCモードの切り替えをサポート
|
||||
- 同時ダウンロード数の変更をサポート
|
||||
- 一部のバグを修正
|
||||
|
||||
## v1.1.5(2022.2.5 リリース)
|
||||
|
||||
### ダウンロードリンク
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 更新ログ
|
||||
|
||||
- 動画ダウンロードをサポート
|
||||
|
||||
## v1.0.1(2021.3.1 リリース)
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 更新ログ
|
||||
|
||||
- 動画ダウンロードをサポート
|
||||
---
|
||||
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 arm64(Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### 国内ダウンロード
|
||||
|
||||
- [【mediago】 windows(インストーラ版) v3.0.0](https://static.ziying.site/mediago/mediago-setup-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 windows(ポータブル版) v3.0.0](https://static.ziying.site/mediago/mediago-portable-win32-x64-3.0.0.exe)
|
||||
- [【mediago】 macos arm64(Appleチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://static.ziying.site/mediago/mediago-setup-darwin-x64-3.0.0-beta.5.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://static.ziying.site/mediago/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 重要な更新
|
||||
|
||||
- dockerを使用した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.1(2023.7.1 リリース)
|
||||
|
||||
### ダウンロードリンク
|
||||
|
||||
- [windows mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.exe)
|
||||
- [macos mediago v2.0.1](https://github.com/caorushizi/mediago/releases/download/v2.0.1/media-downloader-setup-2.0.1.dmg)
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 更新ログ
|
||||
|
||||
- ダークモード
|
||||
- 追加のダウンロード設定
|
||||
- リクエストヘッダーの自動挿入をサポート
|
||||
- 広告フィルタリングをサポート
|
||||
- 没入型嗅探機能をサポート
|
||||
- モバイルモードとPCモードの切り替えをサポート
|
||||
- 同時ダウンロード数の変更をサポート
|
||||
- 一部のバグを修正
|
||||
|
||||
## v1.1.5(2022.2.5 リリース)
|
||||
|
||||
### ダウンロードリンク
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 更新ログ
|
||||
|
||||
- 動画ダウンロードをサポート
|
||||
|
||||
## v1.0.1(2021.3.1 リリース)
|
||||
|
||||
### ソフトウェアスクリーンショット
|
||||
|
||||

|
||||
|
||||
### 更新ログ
|
||||
|
||||
- 動画ダウンロードをサポート
|
||||
|
||||
+144
-144
@@ -1,144 +1,144 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 使用方法
|
||||
|
||||
このページでは、設定ページのパラメーターの意味について説明します。
|
||||
|
||||

|
||||
|
||||
## 基本設定
|
||||
|
||||
::: tip
|
||||
ダウンローダーのグローバル設定
|
||||
:::
|
||||
|
||||
### 1. フォルダの選択
|
||||
|
||||
動画のダウンロード先のパスを設定します。
|
||||
|
||||
### 2. ダウンローダーのテーマ
|
||||
|
||||
ライトモードとダークモードをサポートしています。
|
||||
|
||||
### 3. 表示言語
|
||||
|
||||
中国語と英語がサポートされています。
|
||||
|
||||
### 4. ダウンロード完了通知
|
||||
|
||||
これをオンにすると、ダウンロード完了時にシステム通知が表示されます。
|
||||
|
||||
### 5. コンソールの表示
|
||||
|
||||
これをオンにすると、ダウンロードコンソールが表示されます。
|
||||
|
||||
### 6. 自動更新
|
||||
|
||||
これをオンにすると、ソフトウェアは自動的に更新をチェックします。
|
||||
|
||||
### 7. テストバージョンのアップグレードを許可
|
||||
|
||||
これをオンにすると、ソフトウェアは自動的にテストバージョンをチェックします。
|
||||
|
||||
### 8. メインウィンドウを閉じる
|
||||
|
||||
メインウィンドウを閉じた後、ソフトウェアを【システムトレイに隠す】か【ソフトウェアを終了】するかを設定します。
|
||||
|
||||
## ブラウザ設定
|
||||
|
||||
::: tip
|
||||
ブラウザウィンドウに関連する設定
|
||||
:::
|
||||
|
||||
### 1. 新しいウィンドウでブラウザを開く
|
||||
|
||||
これをオンにすると、ブラウザページが独立したウィンドウで開きます。
|
||||
|
||||

|
||||
|
||||
### 2. プロキシ設定
|
||||
|
||||
自分のプロキシアドレスを入力します。
|
||||
|
||||
### 3. プロキシスイッチ
|
||||
|
||||
スイッチをオンにすると、**ブラウザ**はデフォルトで指定されたプロキシアドレスを使用します。このスイッチがオフの場合、ブラウザのプロキシは設定できません。
|
||||
|
||||
### 4. 広告フィルタリングを有効にする
|
||||
|
||||
スイッチをオンにすると、ページ内の広告がフィルタリングされます。
|
||||
|
||||
### 5. モバイルモードで入る
|
||||
|
||||
これをオンにすると、ブラウザがモバイルブラウザに偽装され、モバイルサイトを要求します(左側はモバイルモードを有効にしたスクリーンショットです)。
|
||||
|
||||

|
||||
|
||||
### 6. 没入型嗅探を使用
|
||||
|
||||
- 有効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されず、ページ内の【リストに追加】ボタンをクリックする必要があります。
|
||||
|
||||

|
||||
|
||||
- 無効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されます。
|
||||
|
||||
### 7. プライバシーモード: 有効にすると、ソフトウェアはユーザーのブラウジング履歴を保存しません
|
||||
|
||||

|
||||
|
||||
### 8. その他の操作
|
||||
|
||||
- キャッシュをクリア: ソフトウェアのキャッシュをクリアします。
|
||||
- お気に入りのエクスポート【インポート】: ソフトウェアのお気に入りをエクスポートします。
|
||||
|
||||
## ダウンロード設定
|
||||
|
||||
::: tip
|
||||
ダウンロードに関連する設定
|
||||
:::
|
||||
|
||||
### 1. ダウンロードプロキシスイッチ
|
||||
|
||||
スイッチをオンにすると、**ダウンローダー**はデフォルトで指定されたプロキシアドレスを使用します。**ブラウザ**と**ダウンローダー**のプロキシは独立しています。
|
||||
|
||||
### 2. ダウンロード完了後、分割ファイルを削除
|
||||
|
||||
これをオンにすると、ダウンロードが完了した後に分割ファイルが削除されます。
|
||||
|
||||
### 3. 最大同時ダウンロード数
|
||||
|
||||
最大で同時にダウンロードできる動画ファイルの数を設定します。現在の最大値は10、最小値は1です。
|
||||
|
||||
### 4. その他の操作
|
||||
|
||||
- 設定ファイルのディレクトリ: ソフトウェアのデータベースパス、ログなどのパスを設定します。
|
||||
- 実行可能ファイルのディレクトリ: ダウンローダーのバイナリファイルのパスを設定します。
|
||||
- ローカルストレージパス: ダウンロードした動画のローカルパスを設定します。
|
||||
|
||||
### 5. 現在のバージョン
|
||||
|
||||
現在使用しているソフトウェアのバージョン情報が表示されます。
|
||||
|
||||
## その他の質問
|
||||
|
||||
### Q: ライブ動画のダウンロードについて
|
||||
|
||||
A: ソフトウェアはライブのダウンロードをサポートしていますが、現時点ではライブストリームを区別する方法がないため、すべてのダウンロードコンソールを開くようにしています。ユーザー自身でダウンロードするデータソースを識別してください。
|
||||
|
||||
### Q: MacOS版について
|
||||
|
||||
A: 【Intelチップ】の場合、リリース版のx64バージョンをインストールする必要があります。インストール後、Macのセキュリティ設定で「不明な開発元からのアプリケーションを開く」設定を有効にしてください。
|
||||

|
||||
|
||||
【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
|
||||
---
|
||||
|
||||
# 使用方法
|
||||
|
||||
このページでは、設定ページのパラメーターの意味について説明します。
|
||||
|
||||

|
||||
|
||||
## 基本設定
|
||||
|
||||
::: tip
|
||||
ダウンローダーのグローバル設定
|
||||
:::
|
||||
|
||||
### 1. フォルダの選択
|
||||
|
||||
動画のダウンロード先のパスを設定します。
|
||||
|
||||
### 2. ダウンローダーのテーマ
|
||||
|
||||
ライトモードとダークモードをサポートしています。
|
||||
|
||||
### 3. 表示言語
|
||||
|
||||
中国語と英語がサポートされています。
|
||||
|
||||
### 4. ダウンロード完了通知
|
||||
|
||||
これをオンにすると、ダウンロード完了時にシステム通知が表示されます。
|
||||
|
||||
### 5. コンソールの表示
|
||||
|
||||
これをオンにすると、ダウンロードコンソールが表示されます。
|
||||
|
||||
### 6. 自動更新
|
||||
|
||||
これをオンにすると、ソフトウェアは自動的に更新をチェックします。
|
||||
|
||||
### 7. テストバージョンのアップグレードを許可
|
||||
|
||||
これをオンにすると、ソフトウェアは自動的にテストバージョンをチェックします。
|
||||
|
||||
### 8. メインウィンドウを閉じる
|
||||
|
||||
メインウィンドウを閉じた後、ソフトウェアを【システムトレイに隠す】か【ソフトウェアを終了】するかを設定します。
|
||||
|
||||
## ブラウザ設定
|
||||
|
||||
::: tip
|
||||
ブラウザウィンドウに関連する設定
|
||||
:::
|
||||
|
||||
### 1. 新しいウィンドウでブラウザを開く
|
||||
|
||||
これをオンにすると、ブラウザページが独立したウィンドウで開きます。
|
||||
|
||||

|
||||
|
||||
### 2. プロキシ設定
|
||||
|
||||
自分のプロキシアドレスを入力します。
|
||||
|
||||
### 3. プロキシスイッチ
|
||||
|
||||
スイッチをオンにすると、**ブラウザ**はデフォルトで指定されたプロキシアドレスを使用します。このスイッチがオフの場合、ブラウザのプロキシは設定できません。
|
||||
|
||||
### 4. 広告フィルタリングを有効にする
|
||||
|
||||
スイッチをオンにすると、ページ内の広告がフィルタリングされます。
|
||||
|
||||
### 5. モバイルモードで入る
|
||||
|
||||
これをオンにすると、ブラウザがモバイルブラウザに偽装され、モバイルサイトを要求します(左側はモバイルモードを有効にしたスクリーンショットです)。
|
||||
|
||||

|
||||
|
||||
### 6. 没入型嗅探を使用
|
||||
|
||||
- 有効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されず、ページ内の【リストに追加】ボタンをクリックする必要があります。
|
||||
|
||||

|
||||
|
||||
- 無効状態:ブラウザから嗅探されたリソースは自動的にダウンロードリストに追加されます。
|
||||
|
||||
### 7. プライバシーモード: 有効にすると、ソフトウェアはユーザーのブラウジング履歴を保存しません
|
||||
|
||||

|
||||
|
||||
### 8. その他の操作
|
||||
|
||||
- キャッシュをクリア: ソフトウェアのキャッシュをクリアします。
|
||||
- お気に入りのエクスポート【インポート】: ソフトウェアのお気に入りをエクスポートします。
|
||||
|
||||
## ダウンロード設定
|
||||
|
||||
::: tip
|
||||
ダウンロードに関連する設定
|
||||
:::
|
||||
|
||||
### 1. ダウンロードプロキシスイッチ
|
||||
|
||||
スイッチをオンにすると、**ダウンローダー**はデフォルトで指定されたプロキシアドレスを使用します。**ブラウザ**と**ダウンローダー**のプロキシは独立しています。
|
||||
|
||||
### 2. ダウンロード完了後、分割ファイルを削除
|
||||
|
||||
これをオンにすると、ダウンロードが完了した後に分割ファイルが削除されます。
|
||||
|
||||
### 3. 最大同時ダウンロード数
|
||||
|
||||
最大で同時にダウンロードできる動画ファイルの数を設定します。現在の最大値は10、最小値は1です。
|
||||
|
||||
### 4. その他の操作
|
||||
|
||||
- 設定ファイルのディレクトリ: ソフトウェアのデータベースパス、ログなどのパスを設定します。
|
||||
- 実行可能ファイルのディレクトリ: ダウンローダーのバイナリファイルのパスを設定します。
|
||||
- ローカルストレージパス: ダウンロードした動画のローカルパスを設定します。
|
||||
|
||||
### 5. 現在のバージョン
|
||||
|
||||
現在使用しているソフトウェアのバージョン情報が表示されます。
|
||||
|
||||
## その他の質問
|
||||
|
||||
### Q: ライブ動画のダウンロードについて
|
||||
|
||||
A: ソフトウェアはライブのダウンロードをサポートしていますが、現時点ではライブストリームを区別する方法がないため、すべてのダウンロードコンソールを開くようにしています。ユーザー自身でダウンロードするデータソースを識別してください。
|
||||
|
||||
### Q: MacOS版について
|
||||
|
||||
A: 【Intelチップ】の場合、リリース版のx64バージョンをインストールする必要があります。インストール後、Macのセキュリティ設定で「不明な開発元からのアプリケーションを開く」設定を有効にしてください。
|
||||

|
||||
|
||||
【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
@@ -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 arm64(Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
#### 重要な更新内容
|
||||
|
||||
- DockerでWeb端末の展開をサポート
|
||||
- デスクトップ端末UIを更新
|
||||
|
||||
#### 更新ログ
|
||||
|
||||
- DockerでWeb端末の展開をサポート
|
||||
- デスクトップ端末UIを更新
|
||||
- 動画再生機能を追加、デスクトップとモバイル端末両方で再生可能
|
||||
- macで画面が表示されない問題を修正
|
||||
- バッチダウンロードのインタラクションを最適化
|
||||
- Windows用ポータブル版(インストール不要)を追加
|
||||
- ダウンロードリストを最適化し、ページ内の複数の動画の嗅探をサポート
|
||||
- お気に入りリストの手動インポート/エクスポートをサポート
|
||||
- ホームページのダウンロードリストエクスポートをサポート
|
||||
- 【新規ダウンロード】フォームのインタラクションロジックを最適化
|
||||
- UrlSchemeを使用してアプリを開き、ダウンロードタスクを追加
|
||||
- バグの修正とユーザーエクスペリエンスの向上
|
||||
|
||||
## 操作方法
|
||||
|
||||
### 動画の自動嗅探
|
||||
|
||||
1. 【リソース抽出】を選択
|
||||
|
||||

|
||||
|
||||
2. 動画のURLにアクセス
|
||||
|
||||

|
||||
|
||||
3. 【今すぐダウンロード】をクリックすると、動画がダウンロードされます
|
||||
|
||||

|
||||
|
||||
### 手動ダウンロード
|
||||
|
||||
1. ページ右上の【新規ダウンロード】をクリック
|
||||
|
||||

|
||||
|
||||
2. 新規ダウンロードのポップアップウィンドウに【動画の名前】と【ストリーミング(m3u8)】または【Bilibili】を入力
|
||||
|
||||

|
||||
|
||||
3. リスト内でダウンロードをクリックして、動画をダウンロードします
|
||||
|
||||

|
||||
|
||||
### バッチダウンロード
|
||||
|
||||

|
||||
|
||||
### 追加機能
|
||||
|
||||
1. 音声に変換
|
||||
|
||||

|
||||
|
||||
2. さらに多くの機能が追加される予定です。お楽しみに~
|
||||
|
||||
### 動画の再生
|
||||
|
||||
- PC再生
|
||||
|
||||

|
||||
|
||||
- モバイル端末再生
|
||||
|
||||

|
||||
|
||||
## 動画ダウンロードを始めましょう
|
||||
|
||||
簡単ですよね。さあ、動画をダウンロードし始めましょう!
|
||||
|
||||
::: 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 arm64(Appleチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-arm64-3.0.0.dmg)
|
||||
- [【mediago】 macos x64(Intelチップ) v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-darwin-x64-3.0.0.dmg)
|
||||
- [【mediago】 linux v3.0.0](https://github.com/caorushizi/mediago/releases/download/v3.0.0/mediago-setup-linux-amd64-3.0.0.deb)
|
||||
- 【mediago】 docker v3.0 `docker run -d --name mediago -p 8899:8899 -v /root/mediago:/root/mediago registry.cn-beijing.aliyuncs.com/caorushizi/mediago:v3.0.0`
|
||||
|
||||
#### 重要な更新内容
|
||||
|
||||
- DockerでWeb端末の展開をサポート
|
||||
- デスクトップ端末UIを更新
|
||||
|
||||
#### 更新ログ
|
||||
|
||||
- DockerでWeb端末の展開をサポート
|
||||
- デスクトップ端末UIを更新
|
||||
- 動画再生機能を追加、デスクトップとモバイル端末両方で再生可能
|
||||
- macで画面が表示されない問題を修正
|
||||
- バッチダウンロードのインタラクションを最適化
|
||||
- Windows用ポータブル版(インストール不要)を追加
|
||||
- ダウンロードリストを最適化し、ページ内の複数の動画の嗅探をサポート
|
||||
- お気に入りリストの手動インポート/エクスポートをサポート
|
||||
- ホームページのダウンロードリストエクスポートをサポート
|
||||
- 【新規ダウンロード】フォームのインタラクションロジックを最適化
|
||||
- UrlSchemeを使用してアプリを開き、ダウンロードタスクを追加
|
||||
- バグの修正とユーザーエクスペリエンスの向上
|
||||
|
||||
## 操作方法
|
||||
|
||||
### 動画の自動嗅探
|
||||
|
||||
1. 【リソース抽出】を選択
|
||||
|
||||

|
||||
|
||||
2. 動画のURLにアクセス
|
||||
|
||||

|
||||
|
||||
3. 【今すぐダウンロード】をクリックすると、動画がダウンロードされます
|
||||
|
||||

|
||||
|
||||
### 手動ダウンロード
|
||||
|
||||
1. ページ右上の【新規ダウンロード】をクリック
|
||||
|
||||

|
||||
|
||||
2. 新規ダウンロードのポップアップウィンドウに【動画の名前】と【ストリーミング(m3u8)】または【Bilibili】を入力
|
||||
|
||||

|
||||
|
||||
3. リスト内でダウンロードをクリックして、動画をダウンロードします
|
||||
|
||||

|
||||
|
||||
### バッチダウンロード
|
||||
|
||||

|
||||
|
||||
### 追加機能
|
||||
|
||||
1. 音声に変換
|
||||
|
||||

|
||||
|
||||
2. さらに多くの機能が追加される予定です。お楽しみに~
|
||||
|
||||
### 動画の再生
|
||||
|
||||
- PC再生
|
||||
|
||||

|
||||
|
||||
- モバイル端末再生
|
||||
|
||||

|
||||
|
||||
## 動画ダウンロードを始めましょう
|
||||
|
||||
簡単ですよね。さあ、動画をダウンロードし始めましょう!
|
||||
|
||||
::: warning
|
||||
このソフトウェアは学習と交流目的でのみ使用できます。
|
||||
:::
|
||||
|
||||
+21
-21
@@ -1,21 +1,21 @@
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 旧バージョン
|
||||
|
||||
::: info
|
||||
ここは旧バージョンの永久リンクです。必要があればこちらからダウンロードできます。
|
||||
|
||||
**このリンクのソフトウェアは自動アップデートされませんので、安心して使用できます**
|
||||
:::
|
||||
|
||||
**v1.1.5(2022年2月5日リリース)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos mediago v1.1.5 はなし~~
|
||||
|
||||
**更新ログ**
|
||||
|
||||
- 動画ダウンロードのサポート
|
||||
---
|
||||
layout: doc
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# 旧バージョン
|
||||
|
||||
::: info
|
||||
ここは旧バージョンの永久リンクです。必要があればこちらからダウンロードできます。
|
||||
|
||||
**このリンクのソフトウェアは自動アップデートされませんので、安心して使用できます**
|
||||
:::
|
||||
|
||||
**v1.1.5(2022年2月5日リリース)**
|
||||
|
||||
- [windows mediago v1.1.5](https://github.com/caorushizi/mediago/releases/download/1.1.5/media-downloader-setup-1.1.4.exe)
|
||||
- ~~macos mediago v1.1.5 はなし~~
|
||||
|
||||
**更新ログ**
|
||||
|
||||
- 動画ダウンロードのサポート
|
||||
|
||||
+32
-32
@@ -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
Reference in New Issue
Block a user