Compare commits

...
44 Commits
Author SHA1 Message Date
Levi 1c2e86a991 fix: flutter docker image 2023-07-19 23:35:02 +08:00
Levi c78dc5f18f fix: flutter image 2023-07-19 23:26:53 +08:00
Levi 8ea5abb5c3 fix: Dockerfile flutter version 2023-07-19 23:19:24 +08:00
Levi d1a4067dfa release: v1.3.6 2023-07-19 23:05:01 +08:00
Levi a1928d202f fix: high dpi display (#158) 2023-07-19 23:01:25 +08:00
Levi 7376e85895 feat: create task with advanced options (#157) 2023-07-19 22:52:51 +08:00
Li Wei eb79dd5ac5 chore: update docker command 2023-07-19 06:11:02 +00:00
Li Wei 5b945b16c1 ci: auto translate 2023-07-18 09:17:07 +00:00
Levi 80e60f4190 feat: improve tracker cdn (#155) 2023-07-15 17:01:52 +08:00
Levi 3087796ef5 feat: support flutter 3.10 (#154) 2023-07-15 14:52:59 +08:00
Liam Segura 99de0aaf9b chore: Update README.md (#144) 2023-07-07 20:23:41 +08:00
liwei 6bea61cf0a release: v1.3.5 2023-07-07 16:23:31 +08:00
liwei 6c3553805e fix: include Japanese 2023-07-07 16:16:43 +08:00
liwei adc7c7766d feat: include Japanese 2023-07-07 16:10:50 +08:00
Ikko Eltociear Ashimine 15d0296307 feat: add Japanese support (#139) 2023-07-07 09:55:23 +08:00
Li Wei 32d2e46118 feat: bt fetcher reuse (#142) 2023-07-06 17:21:34 +08:00
Li Wei abe3cec13d fix: client log is not recorded (#141) 2023-07-06 17:15:48 +08:00
liwei 9249613601 release: v1.3.4 2023-07-05 21:56:25 +08:00
Li Wei 5bafb40599 fix: torrent udp tracker panic (#138) 2023-07-05 21:32:50 +08:00
Li Wei 5968d3c14a feat: panic log (#137) 2023-07-05 21:12:55 +08:00
Li Wei fcb54d3acc fix: api token not saved (#135) 2023-07-04 12:18:46 +08:00
Li Wei e2c4567ed0 fix: macos directory open (#127) 2023-07-03 19:02:08 +08:00
Li Wei c3b28c7034 fix: dio timeout don't throw exception (#133) 2023-07-03 18:52:17 +08:00
Li Wei 32061200a5 chore: add download total badge 2023-06-30 04:12:02 +00:00
Li Wei 19aad9bc9c fix: android build 2023-06-28 15:33:50 +08:00
Li Wei 2d0cc1c877 release: v1.3.3 2023-06-28 15:21:06 +08:00
Li Wei dc94a1004c build: fix gomobile 2023-06-28 06:38:34 +00:00
Li Wei 1385f62fb0 ci: fix gomobile android build 2023-06-28 03:09:05 +00:00
Li Wei 34fc886662 fix: http filename resolve (#124) 2023-06-28 10:22:45 +08:00
Li Wei c651263747 chore: update clone git url 2023-06-27 03:41:49 +00:00
Li Wei 96a4526bc8 feat: improve web build (#123) 2023-06-27 10:04:15 +08:00
Shane 97cf8ff0c5 fix: remove cli test args (#122) 2023-06-16 09:09:14 +08:00
liwei 70c16195ef release: v1.3.2 2023-06-09 22:17:27 +08:00
Li Wei eb9d966855 feat: support start and pause all tasks (#121) 2023-06-09 22:15:59 +08:00
liwei 0e3ca912a7 fix: add mac tray template icon 2023-06-08 14:10:55 +08:00
Li Wei b3e372326f feat: add desktop system tray support (#120) 2023-06-07 21:50:40 +08:00
Li Wei 0100f0131d feat: android deeplink and file selector (#118) 2023-06-06 20:39:07 +08:00
Levi e4c3b449a4 chore: update readme 2023-05-23 18:23:51 +08:00
liwei aaa8cd6406 fix: flutter downgrade to v3.7.12 2023-05-11 18:06:25 +08:00
Li Wei 328422b3b8 feat: flutter upgrade to v3.10.0 (#113) 2023-05-11 11:42:55 +08:00
Li Wei 17cce89ef1 release: v1.3.1 2023-05-11 09:19:56 +08:00
Li Wei d9c5ca339f feat: support max running tasks config (#112) 2023-05-10 18:00:58 +08:00
Li Wei 1e08ecd8db fix: file explorer and support select file (#111) 2023-05-04 14:32:37 +08:00
Li Wei 34bcbd9571 fix: gopeed cli negative WaitGroup counter error (#110) 2023-05-04 10:05:32 +08:00
89 changed files with 3512 additions and 1033 deletions

No files matched your search

+28 -20
View File
@@ -3,6 +3,10 @@ name: build
on:
workflow_dispatch:
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.10.6
permissions:
contents: write
@@ -24,11 +28,12 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -123,10 +128,9 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- name: Build
run: |
docker pull techknowlogick/xgo:go-1.19.x
@@ -142,14 +146,15 @@ jobs:
needs: [get-release, build-macos-arm64-lib]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-node@v3
with:
node-version: 16
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg
run: npm install -g appdmg
- uses: actions/download-artifact@v3
@@ -197,14 +202,15 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -256,15 +262,16 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "11"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -292,17 +299,18 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
cd ui/flutter
flutter build web
flutter build web --web-renderer html
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip
+5 -2
View File
@@ -16,6 +16,9 @@ on:
- "go.sum"
- "Dockerfile"
env:
GO_VERSION: 1.19
permissions:
contents: write
@@ -28,9 +31,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
go-version: ${{ env.GO_VERSION }}
- uses: release-drafter/release-drafter@v5
id: create_release
env:
+11 -7
View File
@@ -26,6 +26,10 @@ on:
- "go.mod"
- "go.sum"
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.10.6
jobs:
# lint:
# name: Lint
@@ -45,10 +49,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v3
@@ -61,14 +64,15 @@ jobs:
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Build
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
+30
View File
@@ -0,0 +1,30 @@
name: 'translator'
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
discussion:
types: [created, edited]
discussion_comment:
types: [created, edited]
pull_request_target:
types: [opened, edited]
pull_request_review_comment:
types: [created, edited]
jobs:
translate:
permissions:
issues: write
discussions: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: lizheming/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
IS_MODIFY_TITLE: true
APPEND_TRANSLATION: true
+1
View File
@@ -21,6 +21,7 @@ bin/
*.data
*.db
*.log
.DS_Store
+54
View File
@@ -0,0 +1,54 @@
# Gopeed コントリビューターガイド
まず最初に、Gopeed への貢献に興味を持っていただきありがとうございます。このガイドは、あなたが Gopeed の
開発に参加するための手助けとなるでしょう。
## ブランチの説明
このプロジェクトのメインブランチは `main` ブランチのみです。Gopeed の開発に参加したい場合は、
まずこのプロジェクトをフォークし、フォークしたプロジェクトで開発を行ってください。開発が完了したら、
このプロジェクトに PR を提出し、`main` ブランチにマージしてください。
## ローカル開発
開発およびデバッグはウェブ上で行うことを推奨する。まずバックエンドのサービスを起動し、
コマンドライン `go run cmd/api/main.go` で起動する。サービスのデフォルトポートは `9999` で、
次にフロントエンドの flutter プロジェクトを `debug` モードで起動して実行します。
## 翻訳
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
このディレクトリに対応する言語ファイルを追加するだけでよいです。
ロケール編集後にロケールを生成:
```
get generate locales
```
翻訳については `en_us.dart` を参照してください。
## flutter での開発
コミットする前に `dart format ./ui/flutter` を実行し、コードを標準の dart フォーマットにしておくことを忘れないでください
api/models を編集したい場合は build_runner watcher をオンにします:
```
flutter pub run build_runner watch
```
get-cli コマンドの使用法:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+3 -3
View File
@@ -1,9 +1,9 @@
FROM cirrusci/flutter:3.7.7 AS flutter
FROM instrumentisto/flutter:3.10.6 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
COPY ./ui/flutter ./
RUN flutter build web
RUN flutter build web --web-renderer html
FROM golang:1.19.3 AS go
WORKDIR /app
@@ -18,4 +18,4 @@ LABEL maintainer="monkeyWie"
WORKDIR /app
COPY --from=go /app/dist/gopeed ./
EXPOSE 9999
ENTRYPOINT ["./gopeed"]
ENTRYPOINT ["./gopeed"]
+18 -7
View File
@@ -1,12 +1,13 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg?style=flat-square)](https://github.com/GopeedLab/gopeed/releases)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## Introduction
@@ -43,7 +44,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### Start directly
```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### Using Docker Compose
@@ -56,8 +57,6 @@ docker-compose up -d
When the docker container is running, you can access the web page through `http://localhost:9999`.
> Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
## Donate
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
@@ -80,7 +79,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Clone
```bash
git clone [email protected]:monkeyWie/gopeed.git
git clone [email protected]:GopeedLab/gopeed.git
```
### Contributing
@@ -91,7 +90,7 @@ Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
#### Desktop
First, you need to configure the environment according to the [flutter desktop official website document](https://docs.flutter.dev/development/platform-integration/desktop), and then you need to prepare the `cgo` environment, which can be searched for yourself.
First, you need to configure the environment according to the official [Flutter desktop website documention](https://docs.flutter.dev/development/platform-integration/desktop), then you will need to ensure the cgo environment is set up accordingly. For detailed instructions on setting up the cgo environment, please refer to relevant resources available online.
command:
@@ -153,6 +152,18 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
go run cmd/web/main.go
```
## Thanks
### Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## License
[GPLv3](LICENSE)
+169
View File
@@ -0,0 +1,169 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## はじめに
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
## インストール
**サポートするプラットフォーム**
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] web
- [x] docker
[ダウンロード](https://github.com/GopeedLab/gopeed/releases/latest)
> ヒント: macos を開くのに失敗した場合は、ターミナルで `xattr -d com.apple.quarantine /Applications/Gopeed.app` コマンドを実行してください
### コマンドツール
`go install` を使用:
```bash
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接始める
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### Docker Compose の使用
```bash
docker-compose up -d
```
#### Gopeed へのアクセス
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
## 寄付
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
## ショーケース
![](_docs/img/ui-demo.png)
## 開発
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
> フロントコードは `ui/flutter` ディレクトリにあります。
### 環境
1. Go 言語 1.19+
2. Flutter 3.7+
### クローン
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### コントリビュート
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
### ビルド
#### デスクトップ
まず、[flutter デスクトップ公式サイトドキュメント](https://docs.flutter.dev/development/platform-integration/desktop)に従って環境を設定し、自分で検索できる `cgo` 環境を用意します。
コマンド:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### モバイル
先ほどと同じように、`cgo` 環境を準備し、`gomobile` をインストールする必要があります:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
コマンド:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
#### Web
コマンド:
```bash
cd ui/flutter
flutter build web
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感謝
### コントリビューター
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## ライセンス
[GPLv3](LICENSE)
+17 -5
View File
@@ -1,12 +1,13 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg?style=flat-square)](https://github.com/GopeedLab/gopeed/releases)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## 介绍
@@ -43,7 +44,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接运行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### 使用 Docker Compose
@@ -55,7 +56,6 @@ docker-compose up -d
#### 访问服务
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
## 打赏
@@ -79,7 +79,7 @@ docker-compose up -d
### 克隆项目
```bash
git clone [email protected]:monkeyWie/gopeed.git
git clone [email protected]:GopeedLab/gopeed.git
```
### 贡献代码
@@ -152,6 +152,18 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
go run cmd/web/main.go
```
## 感谢
### 贡献者
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 开源许可
基于 [GPLv3](LICENSE) 协议开源。
+1
View File
@@ -0,0 +1 @@
<svg height="105" viewBox="0 0 105 105" width="105" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="99.03" x2="61.54" y1="59.18" y2="90.91"><stop offset="0" stop-color="#087cfa"/><stop offset=".02" stop-color="#0d7bfa"/><stop offset=".37" stop-color="#5566f9"/><stop offset=".66" stop-color="#8a57f8"/><stop offset=".88" stop-color="#ab4ef7"/><stop offset="1" stop-color="#b74af7"/></linearGradient><linearGradient id="b" x1="37.54" x2="60.33" xlink:href="#a" y1="34.26" y2="8.33"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="17.84" x2="87.09" y1="88.36" y2="43.86"><stop offset="0" stop-color="#087cfa"/><stop offset=".1" stop-color="#1598d3"/><stop offset=".22" stop-color="#23b6aa"/><stop offset=".35" stop-color="#2dcc8b"/><stop offset=".46" stop-color="#35dd74"/><stop offset=".57" stop-color="#39e767"/><stop offset=".67" stop-color="#3bea62"/></linearGradient><path d="m89.06 41.63 11.44 25.37-19.58 33.5-12.06-29.28z" fill="url(#a)"/><path d="m68.86 71.22 12.06 29.28-30.34-10.43z" fill="#b74af7"/><path d="m72.13 31.61-6.22-27.11h-34.49l-26.92 41.18 7.79 18.92-7.79 17.27 55.75-47.43z" fill="url(#b)"/><path d="m100.5 24.88-40.25 9.56-55.75 47.43 35.96 18.63 28.4-29.28z" fill="url(#c)"/><path d="m22.5 22.5h60v60h-60z"/><g fill="#fff"><path d="m28.13 71.25h22.5v3.75h-22.5z"/><path d="m26.44 42.81v-.07a12.7 12.7 0 0 1 12.97-12.74 13.62 13.62 0 0 1 9.73 3.39l-3.43 4.14a9.13 9.13 0 0 0 -6.47-2.53c-4 0-7.11 3.5-7.11 7.7v.07c0 4.53 3.11 7.85 7.5 7.85a8.67 8.67 0 0 0 5.12-1.49v-3.56h-5.48v-4.7h10.73v10.74a16.07 16.07 0 0 1 -10.57 3.93c-7.69 0-12.99-5.41-12.99-12.73z"/><path d="m52.3 42.81v-.07a12.79 12.79 0 0 1 13.19-12.74c7.63 0 13.11 5.69 13.11 12.72v.07a12.79 12.79 0 0 1 -13.18 12.8c-7.64-.05-13.12-5.74-13.12-12.78zm20.61 0v-.07a7.51 7.51 0 0 0 -7.49-7.74 7.4 7.4 0 0 0 -7.42 7.67v.07a7.52 7.52 0 0 0 7.5 7.78 7.4 7.4 0 0 0 7.41-7.71z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+4 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
)
func main() {
@@ -16,7 +17,9 @@ func main() {
}
}).
Create(&base.Options{
Connections: 8,
Extra: http.OptsExtra{
Connections: 8,
},
})
if err != nil {
panic(err)
+1 -1
View File
@@ -16,6 +16,7 @@ func main() {
args := parse()
var wg sync.WaitGroup
wg.Add(1)
_, err := download.Boot().
URL(args.url).
Listener(func(event *download.Event) {
@@ -46,7 +47,6 @@ func main() {
if err != nil {
panic(err)
}
wg.Add(1)
wg.Wait()
}
+4 -6
View File
@@ -3,7 +3,6 @@ package cmd
import (
_ "embed"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"net/http"
@@ -20,18 +19,17 @@ func Start(cfg *model.StartConfig) {
if err != nil {
panic(err)
}
exist, _, err := rest.Downloader.GetConfig()
downloadCfg, err := rest.Downloader.GetConfig()
if err != nil {
panic(err)
}
if !exist {
if downloadCfg.FirstLoad {
// Set default download dir to user download dir
userDir, err := os.UserHomeDir()
if err == nil {
downloadDir := filepath.Join(userDir, "Downloads")
rest.Downloader.PutConfig(&download.DownloaderStoreConfig{
DownloadDir: downloadDir,
})
downloadCfg.DownloadDir = downloadDir
rest.Downloader.PutConfig(downloadCfg)
}
}
+1 -1
View File
@@ -7,5 +7,5 @@ services:
- 9999:9999 # HTTP port (host:container)
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download # replace with your prefer path
- ~/Downloads:/root/Downloads # replace with your prefer path
restart: unless-stopped
+39 -31
View File
@@ -3,71 +3,79 @@ module github.com/GopeedLab/gopeed
go 1.19
require (
github.com/anacrolix/torrent v1.46.0
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.3
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.etcd.io/bbolt v1.3.6
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
)
replace github.com/anacrolix/torrent => github.com/monkeyWie/torrent v1.52.4-alpha
require (
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 // indirect
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 // indirect
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/chansync v0.3.0 // indirect
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 // indirect
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
github.com/anacrolix/envpprof v1.2.1 // indirect
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 // indirect
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
github.com/anacrolix/go-libutp v1.2.0 // indirect
github.com/anacrolix/log v0.13.1 // indirect
github.com/anacrolix/log v0.14.0 // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
github.com/anacrolix/missinggo/v2 v2.7.0 // indirect
github.com/anacrolix/mmsg v1.0.0 // indirect
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc // indirect
github.com/anacrolix/stm v0.3.0 // indirect
github.com/anacrolix/multiless v0.3.0 // indirect
github.com/anacrolix/stm v0.4.0 // indirect
github.com/anacrolix/sync v0.4.0 // indirect
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
github.com/anacrolix/utp v0.1.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/benbjohnson/immutable v0.3.0 // indirect
github.com/bits-and-blooms/bitset v1.2.0 // indirect
github.com/bits-and-blooms/bitset v1.2.2 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/lispad/go-generics-tools v1.0.0 // indirect
github.com/lispad/go-generics-tools v1.1.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.2 // indirect
github.com/pion/ice/v2 v2.1.20 // indirect
github.com/pion/interceptor v0.1.7 // indirect
github.com/pion/dtls/v2 v2.2.4 // indirect
github.com/pion/ice/v2 v2.2.6 // indirect
github.com/pion/interceptor v0.1.11 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.9 // indirect
github.com/pion/rtp v1.7.4 // indirect
github.com/pion/rtp v1.7.13 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/sdp/v3 v3.0.4 // indirect
github.com/pion/srtp/v2 v2.0.5 // indirect
github.com/pion/sdp/v3 v3.0.5 // indirect
github.com/pion/srtp/v2 v2.0.9 // indirect
github.com/pion/stun v0.3.5 // indirect
github.com/pion/transport v0.13.0 // indirect
github.com/pion/turn/v2 v2.0.6 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/transport/v2 v2.0.0 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.4 // indirect
github.com/pion/webrtc/v3 v3.1.42 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
github.com/tidwall/btree v1.3.1 // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/tidwall/btree v1.6.0 // indirect
go.opentelemetry.io/otel v1.8.0 // indirect
go.opentelemetry.io/otel/trace v1.8.0 // indirect
golang.org/x/crypto v0.5.0 // indirect
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
)
+116 -86
View File
@@ -3,43 +3,46 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw=
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 h1:fILCBBFnjnrQ0whVJlGhfv1E/QiaFDNtGFBObEVRnYg=
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs=
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 h1:LxR70Si8X8IlYTeKiPReOBt9pla6wEJDlkawfwDdrB0=
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA=
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 h1:issCwqC43gQ7n0gg9rn0EeVYXnQMI7vlnWub4oidtlU=
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51/go.mod h1:osiyaNrMLG9dw7wUtVMaII/NdCjlXeHjUcYzXnmop68=
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/envpprof v1.2.1 h1:25TJe6t/i0AfzzldiGFKCpD+s+dk8lONBcacJZB2rdE=
github.com/anacrolix/envpprof v1.2.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 h1:k4/h2B1gGF+PJGyGHxs8nmHHt1pzWXZWBj6jn4OBlRc=
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 h1:fyXlBfnlFzZSFckJ8QLb2lfmWfY++4RiUnae7ZMuv0A=
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/go-libutp v1.2.0 h1:sjxoB+/ARiKUR7IK/6wLWyADIBqGmu1fm0xo+8Yy7u0=
github.com/anacrolix/go-libutp v1.2.0/go.mod h1:RrJ3KcaDcf9Jqp33YL5V/5CBEc6xMc7aJL8wXfuWL50=
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.10.0/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI=
github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
github.com/anacrolix/log v0.13.1 h1:BmVwTdxHd5VcNrLylgKwph4P4wf+5VvPgOK4yi91fTY=
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
github.com/anacrolix/log v0.14.0 h1:mYhTSemILe/Z8tIxbGdTIWWpPspI8W/fhZHpoFbDaL0=
github.com/anacrolix/log v0.14.0/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
@@ -53,16 +56,16 @@ github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5ur
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE=
github.com/anacrolix/missinggo/v2 v2.7.0 h1:4fzOAAn/VCvfWGviLmh64MPMttrlYew81JdPO7nSHvI=
github.com/anacrolix/missinggo/v2 v2.7.0/go.mod h1:2IZIvmRTizALNYFYXsPR7ofXPzJgyBpKZ4kMqMEICkI=
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 h1:W/oGeHhYwxueeiDjQfmK9G+X9M2xJgfTtow62v0TWAs=
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc h1:K047jUtd0Xv4SEpv/5DoBgDvj4ZNpT1SOVtMlFpRrh0=
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis=
github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
github.com/anacrolix/stm v0.3.0 h1:peQncJSNJtk1YBrFbW0DLKYqll+sa0kOk8EvXRcO+wA=
github.com/anacrolix/stm v0.3.0/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU=
github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY=
github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8=
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
github.com/anacrolix/sync v0.4.0 h1:T+MdO/u87ir/ijWsTFsPYw5jVm0SMm4kVpg8t4KF38o=
@@ -70,8 +73,6 @@ github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/torrent v1.46.0 h1:m5SGlW4p0dJkqrIh4bCqzcKbKFFmfJorf9LSpSM5dEc=
github.com/anacrolix/torrent v1.46.0/go.mod h1:3DE+VA4AgyfKDPjZcIo70D3VFZRo3bfdEBn70CGjca4=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
@@ -85,8 +86,9 @@ github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylH
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
@@ -109,7 +111,7 @@ github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -122,6 +124,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -145,14 +152,14 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -166,9 +173,10 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
@@ -186,14 +194,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lispad/go-generics-tools v1.0.0 h1:pPtt9QERE5sGQfwvLakKe2sjhcNKKsDSnr3939ofj1E=
github.com/lispad/go-generics-tools v1.0.0/go.mod h1:stn7X24ZIyFvaSyttafq3VlJzGJJJkUtLYdbgi/gopM=
github.com/lispad/go-generics-tools v1.1.0 h1:mbSgcxdFVmpoyso1X/MJHXbSbSL3dD+qhRryyxk+/XY=
github.com/lispad/go-generics-tools v1.1.0/go.mod h1:2csd1EJljo/gy5qG4khXol7ivCPptNjG5Uv2X8MgK84=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
@@ -202,6 +210,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/monkeyWie/torrent v1.52.4-alpha h1:iir8NMyLkv6X/D4skGUx8Htb8q57cPk9A6bYlRfrrqA=
github.com/monkeyWie/torrent v1.52.4-alpha/go.mod h1:yBUcBuIOlttGEgAW/6gamzQGD+k2+wUh65fOPPzxvC4=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
@@ -222,44 +232,47 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.1/go.mod h1:qG3gA7ZPZemBqpEFqRKyURYdKEwFZQCGb7gv9T3ON3Y=
github.com/pion/dtls/v2 v2.1.2 h1:22Q1Jk9L++Yo7BIf9130MonNPfPVb+YgdYLeyQotuAA=
github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/ice/v2 v2.1.20 h1:xpxXyX5b4WjCh/D905gzBeW/hbJxMEPx2ptVfrhVE6M=
github.com/pion/ice/v2 v2.1.20/go.mod h1:hEAldRzBhTtAfvlU1V/2/nLCMvveQWFKPNCop+63/Iw=
github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo=
github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA=
github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/turn/v2 v2.0.6 h1:AsXjSPR6Im15DMTB39NlfdTY9BQfieANPBjdg/aVNwY=
github.com/pion/turn/v2 v2.0.6/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38 h1:+IEql+S+YAj3S5e7Ftl/u4xPcZGG0WwLFsyFj6NRTz4=
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38/go.mod h1:L5S/oAhL0Fzt/rnftVQRrP80/j5jygY7XRZzWwFx6P4=
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA=
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -285,10 +298,10 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 h1:IJ6Df0uxPDtNoByV0KkzVKNseWvZFCNM/S9UoyOMCSI=
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
@@ -300,6 +313,8 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:X
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -307,47 +322,49 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/tidwall/btree v1.3.1 h1:636+tdVDs8Hjcf35Di260W2xCW4KuoXOKyk9QWOvCpA=
github.com/tidwall/btree v1.3.1/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjnw+Ylb2vE27Gtqb7BDmfR+nLcJwvbJh98U=
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835 h1:v6yt1xSu+4xV0fPEmB8s2BHdOaoU0SSZrVQzj6pRvqk=
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835/go.mod h1:M32cGdzp91A8Ex9qQtyZinr19EYxzkFqDjW2oyHzTDQ=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -364,13 +381,19 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -380,7 +403,7 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -405,34 +428,40 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -464,7 +493,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+90 -48
View File
@@ -4,10 +4,10 @@ import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
"path"
"path/filepath"
"sync"
@@ -15,7 +15,12 @@ import (
"time"
)
var client *torrent.Client
var (
client *torrent.Client
lock sync.Mutex
torrentDirMap = make(map[string]string)
ftMap = make(map[string]*fileTorrentImpl)
)
type Fetcher struct {
ctl *controller.Controller
@@ -23,10 +28,9 @@ type Fetcher struct {
torrent *torrent.Torrent
meta *fetcher.FetcherMeta
ready atomic.Bool
progress fetcher.Progress
torrentPaths map[string]string
torrentReady atomic.Bool
create atomic.Bool
progress fetcher.Progress
}
func (f *Fetcher) Name() string {
@@ -38,42 +42,48 @@ func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
var once sync.Once
once.Do(func() {
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
return
}
// Support custom download path for each torrent
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
if err != nil {
pieceCompletion = storage.NewMapPieceCompletion()
}
clientImplCloser := storage.NewFileOpts(storage.NewFileClientOpts{
ClientBaseDir: cfg.DataDir,
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
if dir, ok := f.torrentPaths[infoHash.String()]; ok {
return dir
}
return baseDir
},
PieceCompletion: pieceCompletion,
})
cfg.DefaultStorage = clientImplCloser
client, err = torrent.NewClient(cfg)
if err != nil {
return
}
func (f *Fetcher) initClient() (err error) {
lock.Lock()
defer lock.Unlock()
f.torrentPaths = make(map[string]string)
if client != nil {
return
}
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
if dir, ok := torrentDirMap[infoHash.String()]; ok {
ft.setTorrentDir(dir)
}
ftMap[infoHash.String()] = ft
},
})
client, err = torrent.NewClient(cfg)
return
}
func (f *Fetcher) Resolve(req *base.Request) error {
if err := f.addTorrent(req.URL); err != nil {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
defer f.torrent.Drop()
if err := f.addTorrent(req); err != nil {
return err
}
go func() {
// recycle unused torrent resource
time.Sleep(time.Minute * 3)
if !f.create.Load() {
f.torrentReady.Store(false)
f.safeDrop()
}
}()
res := &base.Resource{
Name: f.torrent.Name(),
Range: true,
@@ -95,6 +105,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
}
func (f *Fetcher) Create(opts *base.Options) (err error) {
f.create.Store(true)
f.meta.Opts = opts
if len(opts.SelectFiles) == 0 {
opts.SelectFiles = make([]int, 0)
@@ -103,17 +114,20 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
}
}
if opts.Path != "" {
f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
torrentDirMap[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
// reuse resolve fetcher
ft.setTorrentDir(torrentDirMap[f.meta.Res.Hash])
}
}
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
f.ready.Store(false)
return nil
}
func (f *Fetcher) Start() (err error) {
if !f.ready.Load() {
if err = f.addTorrent(f.meta.Req.URL); err != nil {
if !f.torrentReady.Load() {
if err = f.addTorrent(f.meta.Req); err != nil {
return
}
}
@@ -130,8 +144,8 @@ func (f *Fetcher) Start() (err error) {
}
func (f *Fetcher) Pause() (err error) {
f.ready.Store(false)
f.torrent.Drop()
f.torrentReady.Store(false)
f.safeDrop()
return
}
@@ -140,13 +154,22 @@ func (f *Fetcher) Continue() (err error) {
}
func (f *Fetcher) Close() (err error) {
f.torrent.Drop()
f.safeDrop()
return nil
}
func (f *Fetcher) safeDrop() {
defer func() {
// ignore panic
_ = recover()
}()
f.torrent.Drop()
}
func (f *Fetcher) Wait() (err error) {
for {
if f.ready.Load() {
if f.torrentReady.Load() {
done := true
for _, selectIndex := range f.meta.Opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
@@ -182,7 +205,7 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
}
func (f *Fetcher) Progress() fetcher.Progress {
if !f.ready.Load() {
if !f.torrentReady.Load() {
return f.progress
}
for i := range f.progress {
@@ -193,12 +216,15 @@ func (f *Fetcher) Progress() fetcher.Progress {
return f.progress
}
func (f *Fetcher) addTorrent(url string) (err error) {
schema := util.ParseSchema(url)
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
if err = f.initClient(); err != nil {
return
}
schema := util.ParseSchema(req.URL)
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(url)
f.torrent, err = client.AddMagnet(req.URL)
} else {
f.torrent, err = client.AddTorrentFromFile(url)
f.torrent, err = client.AddTorrentFromFile(req.URL)
}
if err != nil {
return
@@ -208,15 +234,31 @@ func (f *Fetcher) addTorrent(url string) (err error) {
if err != nil {
return
}
// use map to deduplicate
trackers := make(map[string]bool)
if req.Extra != nil {
extra := req.Extra.(*bt.ReqExtra)
if len(extra.Trackers) > 0 {
for _, tracker := range extra.Trackers {
trackers[tracker] = true
}
}
}
if exist && len(cfg.Trackers) > 0 {
announceList := make([][]string, 0)
for _, tracker := range cfg.Trackers {
trackers[tracker] = true
}
}
if len(trackers) > 0 {
announceList := make([][]string, 0)
for tracker := range trackers {
announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
}
<-f.torrent.GotInfo()
f.ready.Store(true)
f.torrentReady.Store(true)
return
}
+7
View File
@@ -6,6 +6,7 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"reflect"
"testing"
)
@@ -21,6 +22,12 @@ func TestFetcher_Config(t *testing.T) {
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
err := fetcher.Resolve(&base.Request{
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
Extra: bt.ReqExtra{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
},
},
})
if err != nil {
panic(err)
+214
View File
@@ -0,0 +1,214 @@
// modify from github.com/anacrolix/torrent/storage/file.go
package bt
import (
"fmt"
"github.com/anacrolix/torrent/storage"
"io"
"os"
"path/filepath"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/common"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/segments"
)
// File-based storage for torrents, that isn't yet bound to a particular torrent.
type fileClientImpl struct {
opts newFileClientOpts
}
type newFileClientOpts struct {
// The base directory for all downloads.
ClientBaseDir string
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
FilePathMaker storage.FilePathMaker
PieceCompletion storage.PieceCompletion
}
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
if opts.FilePathMaker == nil {
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
var parts []string
if opts.Info.Name != metainfo.NoName {
parts = append(parts, opts.Info.Name)
}
return filepath.Join(append(parts, opts.File.Path...)...)
}
}
if opts.PieceCompletion == nil {
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
if err != nil {
ret = storage.NewMapPieceCompletion()
}
opts.PieceCompletion = ret
}
return fileClientImpl{opts}
}
func (fs fileClientImpl) Close() error {
return fs.opts.PieceCompletion.Close()
}
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
upvertedFiles := info.UpvertedFiles()
files := make([]file, 0, len(upvertedFiles))
for _, fileInfo := range upvertedFiles {
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
Info: info,
File: &fileInfo,
}))
f := file{
rawPath: filePath,
path: filePath,
length: fileInfo.Length,
}
if f.length == 0 {
err = CreateNativeZeroLengthFile(f.path)
if err != nil {
err = fmt.Errorf("creating zero length file: %w", err)
return
}
}
files = append(files, f)
}
t := &fileTorrentImpl{
files,
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
infoHash,
fs.opts.PieceCompletion,
}
fs.opts.HandleFileTorrent(infoHash, t)
return storage.TorrentImpl{
Piece: t.Piece,
Close: t.Close,
}, nil
}
type file struct {
// The safe, OS-local file path.
rawPath string
path string
length int64
}
type fileTorrentImpl struct {
files []file
segmentLocater segments.Index
infoHash metainfo.Hash
completion storage.PieceCompletion
}
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
// Create a view onto the file-based torrent storage.
_io := fileTorrentImplIO{fts}
// Return the appropriate segments of this.
return &filePieceImpl{
fts,
p,
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
io.NewSectionReader(_io, p.Offset(), p.Length()),
}
}
func (fts *fileTorrentImpl) Close() error {
return nil
}
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
for i, f := range fts.files {
fts.files[i].path = filepath.Join(dir, f.rawPath)
}
}
// A helper to create zero-length files which won't appear for file-orientated storage since no
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
// caller should make sure the file name provided is safe/sanitized.
func CreateNativeZeroLengthFile(name string) error {
os.MkdirAll(filepath.Dir(name), 0o777)
var f io.Closer
f, err := os.Create(name)
if err != nil {
return err
}
return f.Close()
}
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
type fileTorrentImplIO struct {
fts *fileTorrentImpl
}
// Returns EOF on short or missing file.
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
f, err := os.Open(file.path)
if os.IsNotExist(err) {
// File missing is treated the same as a short file.
err = io.EOF
return
}
if err != nil {
return
}
defer f.Close()
// Limit the read to within the expected bounds of this file.
if int64(len(b)) > file.length-off {
b = b[:file.length-off]
}
for off < file.length && len(b) != 0 {
n1, err1 := f.ReadAt(b, off)
b = b[n1:]
n += n1
off += int64(n1)
if n1 == 0 {
err = err1
break
}
}
return
}
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
n += n1
b = b[n1:]
err = err1
return err == nil // && int64(n1) == e.Length
})
if len(b) != 0 && err == nil {
err = io.EOF
}
return
}
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
// log.Printf("write at %v: %v bytes", off, len(p))
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
name := fst.fts.files[i].path
os.MkdirAll(filepath.Dir(name), 0o777)
var f *os.File
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return false
}
var n1 int
n1, err = f.WriteAt(p[:e.Length], e.Start)
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
closeErr := f.Close()
n += n1
p = p[n1:]
if err == nil {
err = closeErr
}
if err == nil && int64(n1) != e.Length {
err = io.ErrShortWrite
}
return err == nil
})
return
}
+36
View File
@@ -0,0 +1,36 @@
// modify from github.com/anacrolix/torrent/storage/file_misc.go
package bt
import "github.com/anacrolix/torrent/metainfo"
type requiredLength struct {
fileIndex int
length int64
}
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
if n == 0 {
return
}
for i, fi := range info.UpvertedFiles() {
if off >= fi.Length {
off -= fi.Length
continue
}
n1 := n
if off+n1 > fi.Length {
n1 = fi.Length - off
}
ret = append(ret, requiredLength{
fileIndex: i,
length: off + n1,
})
n -= n1
if n == 0 {
return
}
off = 0
}
return
}
+61
View File
@@ -0,0 +1,61 @@
// modify from github.com/anacrolix/torrent/storage/file_piece.go
package bt
import (
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
"io"
"log"
"os"
)
type filePieceImpl struct {
*fileTorrentImpl
p metainfo.Piece
io.WriterAt
io.ReaderAt
}
var _ storage.PieceImpl = (*filePieceImpl)(nil)
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
}
func (fs *filePieceImpl) Completion() storage.Completion {
c, err := fs.completion.Get(fs.pieceKey())
if err != nil {
log.Printf("error getting piece completion: %s", err)
c.Ok = false
return c
}
verified := true
if c.Complete {
// If it's allegedly complete, check that its constituent files have the necessary length.
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
s, err := os.Stat(fs.files[fi.fileIndex].path)
if err != nil || s.Size() < fi.length {
verified = false
break
}
}
}
if !verified {
// The completion was wrong, fix it.
c.Complete = false
fs.completion.Set(fs.pieceKey(), false)
}
return c
}
func (fs *filePieceImpl) MarkComplete() error {
return fs.completion.Set(fs.pieceKey(), true)
}
func (fs *filePieceImpl) MarkNotComplete() error {
return fs.completion.Set(fs.pieceKey(), false)
}
+1 -1
View File
@@ -128,7 +128,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
if contentDisposition != "" {
_, params, _ := mime.ParseMediaType(contentDisposition)
filename := params["filePath"]
filename := params["filename"]
if filename != "" {
file.Name = filename
}
+16 -5
View File
@@ -15,7 +15,7 @@ import (
)
func TestFetcher_Resolve(t *testing.T) {
testResolve(test.StartTestFileServer, &base.Resource{
testResolve(test.StartTestFileServer, test.BuildName, &base.Resource{
Name: test.BuildName,
Size: test.BuildSize,
Range: true,
@@ -26,7 +26,18 @@ func TestFetcher_Resolve(t *testing.T) {
},
},
}, t)
testResolve(test.StartTestChunkedServer, &base.Resource{
testResolve(test.StartTestCustomServer, "disposition", &base.Resource{
Name: test.BuildName,
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: test.BuildName,
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
Name: test.BuildName,
Size: 0,
Range: false,
@@ -39,12 +50,12 @@ func TestFetcher_Resolve(t *testing.T) {
}, t)
}
func testResolve(startTestServer func() net.Listener, want *base.Resource, t *testing.T) {
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
listener := startTestServer()
defer listener.Close()
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
URL: "http://" + listener.Addr().String() + "/" + path,
})
if err != nil {
t.Fatal(err)
@@ -75,7 +86,7 @@ func TestFetcher_DownloadContinue(t *testing.T) {
}
func TestFetcher_DownloadChunked(t *testing.T) {
listener := test.StartTestChunkedServer()
listener := test.StartTestCustomServer()
defer listener.Close()
// chunked编码下载
downloadNormal(listener, 1, t)
+12 -1
View File
@@ -46,7 +46,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
})
}
func StartTestChunkedServer() net.Listener {
func StartTestCustomServer() net.Listener {
return startTestServer(func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
@@ -57,6 +57,17 @@ func StartTestChunkedServer() net.Listener {
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/disposition", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=\""+BuildName+"\"")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux
})
}
+2 -1
View File
@@ -3,9 +3,10 @@ package base
type Status string
const (
DownloadStatusReady Status = "ready"
DownloadStatusReady Status = "ready" // task create but not start
DownloadStatusRunning Status = "running"
DownloadStatusPause Status = "pause"
DownloadStatusWait Status = "wait" // task is wait for running
DownloadStatusError Status = "error"
DownloadStatusDone Status = "done"
)
+203 -65
View File
@@ -7,8 +7,12 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/virtuald/go-paniclog"
"math"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -36,15 +40,15 @@ type Progress struct {
}
type Downloader struct {
cfg *DownloaderConfig
fetchBuilders map[string]fetcher.FetcherBuilder
fetcherMap map[string]fetcher.Fetcher
storage Storage
tasks []*Task
listener Listener
refreshInterval int
lock *sync.Mutex
closed atomic.Bool
lock *sync.Mutex
closed atomic.Bool
}
func NewDownloader(cfg *DownloaderConfig) *Downloader {
@@ -54,18 +58,21 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
cfg.Init()
d := &Downloader{
cfg: cfg,
fetchBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherMap: make(map[string]fetcher.Fetcher),
refreshInterval: cfg.RefreshInterval,
storage: cfg.Storage,
lock: &sync.Mutex{},
storage: cfg.Storage,
lock: &sync.Mutex{},
}
for _, f := range cfg.FetchBuilders {
for _, p := range f.Schemes() {
d.fetchBuilders[strings.ToUpper(p)] = f
}
}
logPanic(filepath.Join(cfg.StorageDir, "logs"))
return d
}
@@ -74,6 +81,21 @@ func (d *Downloader) Setup() error {
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil {
return err
}
// load config from storage
var cfg DownloaderStoreConfig
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
if err != nil {
return err
}
if exist {
d.cfg.DownloaderStoreConfig = &cfg
} else {
d.cfg.DownloaderStoreConfig = &DownloaderStoreConfig{
FirstLoad: true,
}
}
// init default config
d.cfg.DownloaderStoreConfig.Init()
// load tasks from storage
var tasks []*Task
if err := d.storage.List(bucketTask, &tasks); err != nil {
@@ -94,6 +116,10 @@ func (d *Downloader) Setup() error {
task.Status = base.DownloadStatusPause
}
}
// sort by create time
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].CreatedAt.Before(tasks[j].CreatedAt)
})
}
d.tasks = tasks
@@ -106,9 +132,7 @@ func (d *Downloader) Setup() error {
func() {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusDone ||
task.Status == base.DownloadStatusError ||
task.Status == base.DownloadStatusPause {
if task.Status != base.DownloadStatusRunning {
return
}
// check if task is deleted
@@ -118,7 +142,7 @@ func (d *Downloader) Setup() error {
current := task.fetcher.Progress().TotalDownloaded()
task.Progress.Used = task.timer.Used()
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.refreshInterval)/1000)
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
task.Progress.Downloaded = current
d.emit(EventKeyProgress, task)
@@ -136,7 +160,7 @@ func (d *Downloader) Setup() error {
}()
}
}
time.Sleep(time.Millisecond * time.Duration(d.refreshInterval))
time.Sleep(time.Millisecond * time.Duration(d.cfg.RefreshInterval))
}
}()
return nil
@@ -185,7 +209,7 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return
}
func (d *Downloader) DirectCreate(req *base.Request, opts *base.Options) (taskId string, err error) {
func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) {
rr, err := d.Resolve(req)
if err != nil {
return
@@ -201,6 +225,8 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
if !ok {
return "", errors.New("invalid resource id")
}
defer delete(d.fetcherMap, rrId)
meta := fetcher.Meta()
meta.Opts = opts
res := meta.Res
@@ -211,13 +237,11 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
}
}
if opts.Path == "" {
exist, storeConfig, err := d.GetConfig()
storeConfig, err := d.GetConfig()
if err != nil {
return "", err
}
if exist {
opts.Path = storeConfig.DownloadDir
}
opts.Path = storeConfig.DownloadDir
}
fb, err := d.parseFb(fetcher.Meta().Req.URL)
@@ -253,36 +277,27 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
task.fetcher = fetcher
task.Meta = fetcher.Meta()
task.Progress = &Progress{}
task.Status = base.DownloadStatusRunning
task.Status = base.DownloadStatusReady
// calculate select files size
for _, selectIndex := range opts.SelectFiles {
task.Size += res.Files[selectIndex].Size
}
initTask(task)
task.timer.Start()
err = func() error {
d.lock.Lock()
defer d.lock.Unlock()
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
d.tasks = append(d.tasks, task)
return nil
return d.storage.Put(bucketTask, task.ID, task.clone())
}()
if err != nil {
return
}
d.emit(EventKeyStart, task)
taskId = task.ID
go func() {
err := fetcher.Start()
if err != nil {
d.emit(EventKeyError, task, err)
return
d.tryRunning(func(remain int) {
if remain > 0 {
err = d.start(task)
}
d.watch(task)
}()
delete(d.fetcherMap, rrId)
})
return
}
@@ -291,16 +306,10 @@ func (d *Downloader) Pause(id string) (err error) {
if task == nil {
return
}
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusRunning {
task.Status = base.DownloadStatusPause
task.timer.Pause()
task.fetcher.Pause()
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyPause, task)
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return err
}
return
return d.notifyRunning()
}
func (d *Downloader) Continue(id string) (err error) {
@@ -308,22 +317,57 @@ func (d *Downloader) Continue(id string) (err error) {
if task == nil {
return
}
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError {
task.Status = base.DownloadStatusRunning
task.Progress.Speed = 0
err = d.restoreFetcher(task)
d.tryRunning(func(remain int) {
if remain == 0 {
// no more task can be running, need to doPause a running task
for _, t := range d.tasks {
if t.Status == base.DownloadStatusRunning {
err = d.doPause(t, base.DownloadStatusWait)
break
}
}
}
if err != nil {
return
}
task.timer.Start()
err = task.fetcher.Continue()
if err != nil {
})
if err != nil {
return
}
return d.doContinue(task)
}
func (d *Downloader) PauseAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return
}
}
}
return
}
func (d *Downloader) ContinueAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
runningCount := 0
continueTasks := make([]*Task, 0)
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
runningCount++
} else if task.Status != base.DownloadStatusDone {
continueTasks = append(continueTasks, task)
}
}
// calculate how many tasks can be continued, can't exceed maxRunning
continueCount := int(math.Min(float64(d.cfg.MaxRunning-runningCount), float64(len(continueTasks))))
for i := 0; i < continueCount; i++ {
if err = d.doContinue(continueTasks[i]); err != nil {
return
}
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyContinue, task)
}
return
}
@@ -371,6 +415,11 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
}
}
d.emit(EventKeyDelete, task)
if task.Status == base.DownloadStatusRunning {
if err = d.doNotifyRunning(); err != nil {
return err
}
}
task = nil
return
}
@@ -380,7 +429,7 @@ func (d *Downloader) Close() error {
defer d.lock.Unlock()
d.closed.Store(true)
for _, task := range d.tasks {
d.Pause(task.ID)
d.doPause(task, base.DownloadStatusPause)
}
return d.storage.Close()
}
@@ -426,27 +475,20 @@ func (d *Downloader) GetTasks() []*Task {
return d.tasks
}
func (d *Downloader) GetConfig() (bool, *DownloaderStoreConfig, error) {
var cfg DownloaderStoreConfig
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
if err != nil {
return false, nil, err
}
return exist, &cfg, nil
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
return d.cfg.DownloaderStoreConfig, nil
}
func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
d.cfg.DownloaderStoreConfig = v
return d.storage.Put(bucketConfig, "config", v)
}
func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
exist, cfg, err := d.GetConfig()
cfg, err := d.GetConfig()
if err != nil {
return false, err
}
if !exist {
return false, nil
}
if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil {
return false, nil
}
@@ -479,6 +521,7 @@ func (d *Downloader) watch(task *Task) {
d.emit(EventKeyDone, task)
}
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
}
func (d *Downloader) restoreFetcher(task *Task) error {
@@ -507,13 +550,108 @@ func (d *Downloader) restoreFetcher(task *Task) error {
}
d.setupFetcher(task.fetcher)
task.fetcher.Create(task.Meta.Opts)
go d.watch(task)
}
return nil
}
func (d *Downloader) tryRunning(cb func(remain int)) {
d.lock.Lock()
defer d.lock.Unlock()
runningCount := 0
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
runningCount++
}
}
cb(d.cfg.MaxRunning - runningCount)
}
func (d *Downloader) notifyRunning() error {
d.lock.Lock()
defer d.lock.Unlock()
return d.doNotifyRunning()
}
func (d *Downloader) doNotifyRunning() error {
for _, task := range d.tasks {
if task.Status == base.DownloadStatusReady || task.Status == base.DownloadStatusWait {
return d.doContinue(task)
}
}
return nil
}
func (d *Downloader) start(task *Task) error {
var event EventKey
if task.Status == base.DownloadStatusReady {
event = EventKeyStart
} else {
event = EventKeyContinue
}
task.Status = base.DownloadStatusRunning
task.Progress.Speed = 0
task.timer.Start()
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
var err error
if event == EventKeyStart {
err = task.fetcher.Start()
}
if event == EventKeyContinue {
err = task.fetcher.Continue()
}
if err != nil {
return err
}
go d.watch(task)
d.emit(event, task)
return nil
}
func (d *Downloader) doPause(task *Task, status base.Status) (err error) {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusRunning {
task.Status = status
task.timer.Pause()
task.fetcher.Pause()
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyPause, task)
}
return
}
func (d *Downloader) doContinue(task *Task) (err error) {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
err = d.restoreFetcher(task)
if err != nil {
return
}
err = d.start(task)
}
return
}
// redirect stderr to log file, when panic happened log it
func logPanic(logDir string) {
if err := util.CreateDirIfNotExist(logDir); err != nil {
return
}
f, err := os.Create(filepath.Join(logDir, "crash.log"))
if err != nil {
return
}
paniclog.RedirectStderr(f)
}
func initTask(task *Task) {
task.timer = &util.Timer{}
task.timer = util.NewTimer(task.Progress.Used)
task.lock = &sync.Mutex{}
task.speedArr = make([]int64, 0)
}
+3 -7
View File
@@ -170,8 +170,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
}
storeCfg := &DownloaderStoreConfig{
RefreshInterval: 500,
DownloadDir: "./downloads",
DownloadDir: "./downloads",
ProtocolConfig: map[string]any{
"http": map[string]any{
"connections": 4,
@@ -191,17 +190,14 @@ func TestDownloader_Protocol_Config(t *testing.T) {
},
}
if err := downloader.PutConfig(storeCfg); err != nil {
if err = downloader.PutConfig(storeCfg); err != nil {
t.Fatal(err)
}
exits, newStoreCfg, err := downloader.GetConfig()
newStoreCfg, err := downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
if !exits {
t.Errorf("GetConfig() got = %v, want %v", exits, true)
}
if !test.JsonEqual(storeCfg, newStoreCfg) {
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
+20 -13
View File
@@ -70,7 +70,10 @@ func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
type DownloaderConfig struct {
Controller *controller.Controller
FetchBuilders []fetcher.FetcherBuilder
Storage Storage
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
StorageDir string
*DownloaderStoreConfig
}
@@ -85,24 +88,28 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
new(bt.FetcherBuilder),
}
}
if cfg.Storage == nil {
cfg.Storage = NewMemStorage()
}
if cfg.DownloaderStoreConfig == nil {
cfg.DownloaderStoreConfig = &DownloaderStoreConfig{}
}
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
if cfg.Storage == nil {
cfg.Storage = NewMemStorage()
}
return cfg
}
// DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct {
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
DownloadDir string `json:"downloadDir"` // DownloadDir is the directory to save the downloaded files
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` // Extra is the extra config
FirstLoad bool `json:"-"` // fromNoStore is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` // Extra is the extra config
}
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 {
cfg.MaxRunning = 3
}
return cfg
}
+5
View File
@@ -0,0 +1,5 @@
package bt
type ReqExtra struct {
Trackers []string `json:"trackers"`
}
+18 -2
View File
@@ -33,7 +33,7 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
if req.Rid != "" {
taskId, err = Downloader.Create(req.Rid, req.Opts)
} else if req.Req != nil {
taskId, err = Downloader.DirectCreate(req.Req, req.Opts)
taskId, err = Downloader.CreateDirect(req.Req, req.Opts)
} else {
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
return
@@ -74,6 +74,22 @@ func ContinueTask(w http.ResponseWriter, r *http.Request) {
WriteJson(w, model.NewNilResult())
}
func PauseAllTask(w http.ResponseWriter, r *http.Request) {
if err := Downloader.PauseAll(); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
if err := Downloader.ContinueAll(); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func DeleteTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
@@ -195,6 +211,6 @@ func writeError(w http.ResponseWriter, msg string) {
}
func getServerConfig() *download.DownloaderStoreConfig {
_, cfg, _ := Downloader.GetConfig()
cfg, _ := Downloader.GetConfig()
return cfg
}
+4 -1
View File
@@ -12,9 +12,9 @@ const (
type StartConfig struct {
Network string `json:"network"`
Address string `json:"address"`
RefreshInterval int `json:"refreshInterval"`
Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"`
RefreshInterval int `json:"refreshInterval"`
ApiToken string `json:"apiToken"`
WebEnable bool
@@ -28,6 +28,9 @@ func (cfg *StartConfig) Init() *StartConfig {
if cfg.Address == "" {
cfg.Address = "127.0.0.1:0"
}
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
if cfg.Storage == "" {
cfg.Storage = StorageBolt
}
+6 -2
View File
@@ -57,14 +57,16 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}
startCfg.Init()
downloadCfg := &download.DownloaderConfig{}
downloadCfg := &download.DownloaderConfig{
RefreshInterval: startCfg.RefreshInterval,
}
if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
} else {
downloadCfg.Storage = download.NewMemStorage()
}
downloadCfg.StorageDir = startCfg.StorageDir
downloadCfg.Init()
downloadCfg.RefreshInterval = startCfg.RefreshInterval
Downloader = download.NewDownloader(downloadCfg)
if err := Downloader.Setup(); err != nil {
return nil, nil, err
@@ -84,6 +86,8 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
+43
View File
@@ -144,6 +144,39 @@ func TestPauseAndContinueTask(t *testing.T) {
})
}
func TestPauseAllAndContinueALLTasks(t *testing.T) {
doTest(func() {
cfg, err := Downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
createAndPause := func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
httpRequestCheckOk[*download.Task](http.MethodPut, "/api/v1/tasks/"+taskId+"/pause", nil)
}
total := cfg.MaxRunning + 2
for i := 0; i < total; i++ {
createAndPause()
}
// continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
if len(tasks) != cfg.MaxRunning {
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
}
// pause all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s", base.DownloadStatusReady, base.DownloadStatusPause), nil)
if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
}
})
}
func TestDeleteTask(t *testing.T) {
doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
@@ -275,6 +308,16 @@ func doTest(handler func()) {
Stop()
}()
defer Downloader.Clear()
defer func() {
tasks := Downloader.GetTasks()
taskIds := make([]string, len(tasks))
for i, task := range tasks {
taskIds[i] = task.ID
}
for _, id := range taskIds {
Downloader.Delete(id, true)
}
}()
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
handler()
}
+4 -13
View File
@@ -3,7 +3,6 @@ package util
import (
"errors"
"fmt"
"io"
"os"
syspath "path"
"strings"
@@ -83,17 +82,9 @@ func GetSingleDir(paths []string) string {
return dir
}
// check directory is empty
func isEmpty(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
func CreateDirIfNotExist(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, 0o777)
}
defer f.Close()
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err // Either not empty or error, suits both cases
return nil
}
+6 -1
View File
@@ -2,12 +2,17 @@ package util
import "time"
// 计时器
type Timer struct {
t int64
used int64
}
func NewTimer(used int64) *Timer {
return &Timer{
used: used,
}
}
func (t *Timer) Start() {
t.t = time.Now().UnixNano()
}
@@ -12,9 +12,54 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:host="*"
android:pathPattern=".*torrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:host="*"
android:mimeType="application/x-bittorrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="content"
android:host="*"
android:pathPattern=".*torrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="content"
android:host="*"
android:mimeType="application/x-bittorrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="magnet" />
</intent-filter>
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
+2 -2
View File
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
@@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
+7
View File
@@ -9,7 +9,10 @@
"downloading": "downloading",
"downloaded": "downloaded",
"setting": "Setting",
"donate": "Donate",
"exit": "Exit",
"create": "Create Task",
"advancedOptions": "Advanced Options",
"downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link",
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
@@ -18,12 +21,14 @@
"noFileSelected": "Please select at least one file to continue.",
"noStoragePermission": "Storage permission required",
"selectFile": "Select File",
"rename": "Rename",
"basic": "Basic",
"advanced": "Advanced",
"general": "General",
"downloadDir": "Download Directory",
"downloadDirValid": "Please select the download directory",
"connections": "Connections",
"maxRunning": "Max Running Tasks",
"items": "@count items",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
@@ -47,6 +52,8 @@
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "Start All",
"pauseAll": "Pause All",
"deleteTask": "Delete Task",
"deleteTaskTip": "Keep downloaded files",
"delete": "Delete",
+4
View File
@@ -7,6 +7,8 @@
"selectAll": "انتخاب همه",
"task": "کار",
"setting": "تنظیمات",
"donate": "کمک مالی",
"exit": "خروج",
"create": "ایجاد کار",
"downloading": "دانلود کردن",
"downloaded": "دانلود شدن",
@@ -45,6 +47,8 @@
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "شروع همه",
"pauseAll": "توقف همه",
"deleteTask": "پاک کردن کار",
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
"delete": "پاک کردن",
+61
View File
@@ -0,0 +1,61 @@
{
"label": "日本語",
"error": "エラー",
"tip": "ヒント",
"confirm": "確認",
"cancel": "キャンセル",
"selectAll": "すべてを選択",
"task": "タスク",
"downloading": "ダウンロード中",
"downloaded": "ダウンロード済",
"setting": "セッティング",
"donate": "寄付する",
"exit": "終了",
"create": "タスクを作成",
"downloadLink": "ダウンロードリンク",
"downloadLinkValid": "ダウンロードリンクを入力してください",
"downloadLinkHit": "ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append",
"downloadLinkHitDesktop": "または、torrent ファイルを直接ここにドラッグします",
"download": "ダウンロード",
"noFileSelected": "続行するには少なくとも 1 つのファイルを選択してください。",
"noStoragePermission": "ストレージのパーミッションが必要",
"selectFile": "ファイルを選択",
"basic": "ベーシック",
"advanced": "アドバンスド",
"general": "ジェネラル",
"downloadDir": "ディレクトリのダウンロード",
"downloadDirValid": "ダウンロードディレクトリを選択してください",
"connections": "接続",
"maxRunning": "最大実行タスク",
"items": "@count アイテム",
"subscribeTracker": "トラッカーを購読",
"subscribeFail": "登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください",
"update": "アップデート",
"updateDaily": "毎日アップデート",
"lastUpdate": "最終アップデート: @time",
"addTracker": "トラッカーを追加",
"addTrackerHit": "トラッカーサーバーの URL を 1 行に 1 つずつ入力してください",
"ui": "UI",
"theme": "テーマ",
"themeSystem": "システム",
"themeLight": "ライト",
"themeDark": "ダーク",
"locale": "言語",
"about": "概要",
"homepage": "ホームページ",
"version": "バージョン",
"protocol": "プロトコル",
"port": "ポート",
"apiToken": "API トークン",
"notSet": "未設置",
"set": "設置",
"effectAfterRestart": "再起動後の効果",
"startAll": "すべてを開始",
"pauseAll": "すべてを一時停止",
"deleteTask": "タスクを削除",
"deleteTaskTip": "ダウンロードしたファイルを保持",
"delete": "削除",
"newVersionTitle": "新しいバージョン @version を発見する",
"newVersionUpdate": "今すぐダウンロード",
"newVersionLater": "後で"
}
+5
View File
@@ -9,6 +9,8 @@
"downloading": "Загрузка",
"downloaded": "Загружено",
"setting": "Настройки",
"donate": "Пожертвовать",
"exit": "Выход",
"create": "Создать задачу",
"downloadLink": "Ссылка для скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
@@ -24,6 +26,7 @@
"downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений",
"maxRunning": "Максимальное количество активных задач",
"items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
@@ -47,6 +50,8 @@
"notSet": "Не установлено",
"set": "Установлено",
"effectAfterRestart": "Эффект после перезагрузки",
"startAll": "Запустить все",
"pauseAll": "Приостановить все",
"deleteTask": "Удалить задачу",
"deleteTaskTip": "Сохранить загруженные файлы",
"delete": "Удалить",
+7
View File
@@ -9,7 +9,10 @@
"downloading": "下载中",
"downloaded": "已完成",
"setting": "设置",
"donate": "打赏",
"exit": "退出",
"create": "创建任务",
"advancedOptions": "高级选项",
"downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接",
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
@@ -18,12 +21,14 @@
"noFileSelected": "请至少选择一个文件下载",
"noStoragePermission": "需要开启存储权限",
"selectFile": "选择文件",
"rename": "重命名",
"basic": "基础",
"advanced": "高级",
"general": "通用",
"downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录",
"connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 条",
"subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试",
@@ -47,6 +52,8 @@
"notSet": "未设置",
"set": "已设置",
"effectAfterRestart": "此配置项将在重启应用后生效",
"startAll": "全部开始",
"pauseAll": "全部暂停",
"deleteTask": "删除任务",
"deleteTaskTip": "保留已下载的文件",
"delete": "删除",
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+26 -13
View File
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
@@ -35,6 +34,7 @@ class _Client {
}
dio.options.baseUrl = baseUrl;
dio.options.contentType = Headers.jsonContentType;
dio.options.sendTimeout = const Duration(seconds: 5);
dio.options.connectTimeout = const Duration(seconds: 5);
dio.options.receiveTimeout = const Duration(seconds: 60);
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
@@ -47,7 +47,8 @@ class _Client {
_instance!.dio = dio;
if (isUnixSocket) {
(_instance!.dio.httpClientAdapter as IOHttpClientAdapter)
.onHttpClientCreate = (client) {
.createHttpClient = () {
final client = HttpClient();
client.connectionFactory =
(Uri uri, String? proxyHost, int? proxyPort) {
return Socket.startConnect(
@@ -73,17 +74,19 @@ Future<T> _parse<T>(
) async {
try {
var resp = await fetch();
if (fromJsonT != null) {
final result = Result<T>.fromJson(resp.data, fromJsonT);
if (result.code == 0) {
return result.data as T;
} else {
throw Exception(result);
}
fromJsonT ??= (json) => null as T;
final result = Result<T>.fromJson(resp.data, fromJsonT);
if (result.code == 0) {
return result.data as T;
} else {
return null as T;
throw Exception(result);
}
} on DioException catch (e) {
if (e.type == DioExceptionType.sendTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.connectionTimeout) {
throw Exception(Result(code: 1000, msg: "request timeout"));
}
} on DioError catch (e) {
throw Exception(Result(code: 1000, msg: e.message));
}
}
@@ -115,6 +118,14 @@ Future<void> continueTask(String id) async {
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
}
Future<void> pauseAllTasks() async {
return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null);
}
Future<void> continueAllTasks() async {
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
}
Future<void> deleteTask(String id, bool force) async {
return _parse(
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
@@ -136,6 +147,8 @@ Future<Response<String>> proxyRequest<T>(String uri,
options.headers!["X-Target-Uri"] = uri;
// add timestamp to avoid cache
return _client.dio.request("/api/v1/proxy?t=${DateTime.now()}",
data: data, options: options);
return _client.dio.request(
"/api/v1/proxy?t=${DateTime.now().millisecondsSinceEpoch}",
data: data,
options: options);
}
@@ -5,6 +5,7 @@ part 'downloader_config.g.dart';
@JsonSerializable(explicitToJson: true)
class DownloaderConfig {
String downloadDir = '';
int maxRunning = 0;
ProtocolConfig protocolConfig = ProtocolConfig();
ExtraConfig extra = ExtraConfig();
@@ -9,6 +9,7 @@ part of 'downloader_config.dart';
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
DownloaderConfig()
..downloadDir = json['downloadDir'] as String
..maxRunning = json['maxRunning'] as int
..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>?)
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?);
@@ -16,6 +17,7 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
<String, dynamic>{
'downloadDir': instance.downloadDir,
'maxRunning': instance.maxRunning,
'protocolConfig': instance.protocolConfig.toJson(),
'extra': instance.extra.toJson(),
};
+3 -2
View File
@@ -2,17 +2,18 @@ import 'package:json_annotation/json_annotation.dart';
part 'options.g.dart';
@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
@JsonSerializable(explicitToJson: true)
class Options {
String name;
String path;
List<int> selectFiles;
Map<String, dynamic>? extra;
Object? extra;
Options({
required this.name,
required this.path,
required this.selectFiles,
this.extra,
});
factory Options.fromJson(Map<String, dynamic> json) =>
+2 -1
View File
@@ -11,7 +11,8 @@ Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
path: json['path'] as String,
selectFiles:
(json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(),
)..extra = json['extra'] as Map<String, dynamic>?;
extra: json['extra'],
);
Map<String, dynamic> _$OptionsToJson(Options instance) {
final val = <String, dynamic>{
+15 -2
View File
@@ -5,10 +5,11 @@ part 'request.g.dart';
@JsonSerializable(explicitToJson: true)
class Request {
String url;
Map<String, dynamic>? extra;
Object? extra;
Request({
required this.url,
this.extra,
});
factory Request.fromJson(Map<String, dynamic> json) =>
@@ -20,7 +21,7 @@ class Request {
@JsonSerializable()
class ReqExtraHttp {
String method = 'GET';
Map<String, String> headers = {};
Map<String, String> header = {};
String body = '';
ReqExtraHttp();
@@ -30,3 +31,15 @@ class ReqExtraHttp {
Map<String, dynamic> toJson() => _$ReqExtraHttpToJson(this);
}
@JsonSerializable()
class ReqExtraBt {
List<String> trackers = [];
ReqExtraBt();
factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
_$ReqExtraBtFromJson(json);
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
}
+13 -3
View File
@@ -8,7 +8,8 @@ part of 'request.dart';
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
url: json['url'] as String,
)..extra = json['extra'] as Map<String, dynamic>?;
extra: json['extra'],
);
Map<String, dynamic> _$RequestToJson(Request instance) {
final val = <String, dynamic>{
@@ -27,12 +28,21 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
..method = json['method'] as String
..headers = Map<String, String>.from(json['headers'] as Map)
..header = Map<String, String>.from(json['header'] as Map)
..body = json['body'] as String;
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
<String, dynamic>{
'method': instance.method,
'headers': instance.headers,
'header': instance.header,
'body': instance.body,
};
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt()
..trackers =
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
<String, dynamic>{
'trackers': instance.trackers,
};
+1 -3
View File
@@ -1,12 +1,10 @@
import 'package:json_annotation/json_annotation.dart';
import 'meta.dart';
import 'options.dart';
import 'resource.dart';
part 'task.g.dart';
enum Status { ready, running, pause, error, done }
enum Status { ready, running, pause, wait, error, done }
@JsonSerializable(explicitToJson: true)
class Task {
+1
View File
@@ -28,6 +28,7 @@ const _$StatusEnumMap = {
Status.ready: 'ready',
Status.running: 'running',
Status.pause: 'pause',
Status.wait: 'wait',
Status.error: 'error',
Status.done: 'done',
};
@@ -1,18 +1,27 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:uri_to_file/uri_to_file.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../core/common/start_config.dart';
import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network";
const _startConfigAddress = "start.address";
@@ -34,10 +43,13 @@ const allTrackerSubscribeUrls = [
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
];
const allTrackerCdns = [
// jsdelivr: https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
["https://cdn.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
// nuaa: https://hub.nuaa.cf/ngosang/trackerslist/raw/master/trackers_all.txt
["https://hub.nuaa.cf", r".*github.com(/.*)"]
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
[
"https://ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
]
];
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
key: (v) => v as String,
@@ -55,13 +67,153 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
return ret;
});
class AppController extends GetxController {
class AppController extends GetxController with WindowListener, TrayListener {
static StartConfig? _defaultStartConfig;
final startConfig = StartConfig().obs;
final runningPort = 0.obs;
final downloaderConfig = DownloaderConfig().obs;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
@override
void onReady() {
super.onReady();
try {
_initDeepLinks();
} catch (e) {
logger.w("initDeepLinks error", e);
}
try {
_initWindows();
} catch (e) {
logger.w("initWindows error", e);
}
try {
_initTray();
} catch (e) {
logger.w("initTray error", e);
}
}
@override
void onClose() {
_linkSubscription?.cancel();
trayManager.removeListener(this);
}
@override
void onWindowClose() async {
final isPreventClose = await windowManager.isPreventClose();
if (isPreventClose) {
windowManager.hide();
}
}
@override
void onTrayIconMouseDown() {
windowManager.show();
}
@override
void onTrayIconRightMouseDown() {
trayManager.popUpContextMenu();
}
Future<void> _initDeepLinks() async {
// currently only support android
if (!Util.isAndroid()) {
return;
}
_appLinks = AppLinks();
// Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await _toCreate(uri);
});
// Check initial link if app was in cold state (terminated)
final uri = await _appLinks.getInitialAppLink();
if (uri != null) {
await _toCreate(uri);
}
}
Future<void> _initWindows() async {
if (!Util.isDesktop()) {
return;
}
windowManager.addListener(this);
}
Future<void> _initTray() async {
if (!Util.isDesktop()) {
return;
}
if (Util.isWindows()) {
await trayManager.setIcon('assets/tray_icon/icon.ico');
} else if (Util.isMacos()) {
await trayManager.setIcon('assets/tray_icon/icon_mac.png',
isTemplate: true);
} else {
await trayManager.setIcon('assets/tray_icon/icon.png');
}
final menu = Menu(items: [
MenuItem(
label: "create".tr,
onClick: (menuItem) async => {
await windowManager.show(),
await Get.rootDelegate.offAndToNamed(Routes.CREATE),
},
),
MenuItem.separator(),
MenuItem(
label: "startAll".tr,
onClick: (menuItem) async => {continueAllTasks()},
),
MenuItem(
label: "pauseAll".tr,
onClick: (menuItem) async => {pauseAllTasks()},
),
MenuItem(
label: 'setting'.tr,
onClick: (menuItem) async => {
await windowManager.show(),
await Get.rootDelegate.offAndToNamed(Routes.SETTING),
},
),
MenuItem.separator(),
MenuItem(
label: 'donate'.tr,
onClick: (menuItem) => {
launchUrl(
Uri.parse(
"https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate"),
mode: LaunchMode.externalApplication)
},
),
MenuItem(
label: '${"version".tr}(${packageInfo.version})',
),
MenuItem.separator(),
MenuItem(
label: 'exit'.tr,
onClick: (menuItem) => {windowManager.destroy()},
),
]);
await trayManager.setContextMenu(menu);
trayManager.addListener(this);
}
Future<void> _toCreate(Uri uri) async {
final path = uri.scheme == "magnet"
? uri.toString()
: (await toFile(uri.toString())).path;
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
}
String runningAddress() {
if (startConfig.value.network == 'unix') {
return startConfig.value.address;
@@ -117,18 +269,16 @@ class AppController extends GetxController {
final btExtConfig = downloaderConfig.value.extra.bt;
final result = <String>[];
for (var u in btExtConfig.trackerSubscribeUrls) {
var allFail = true;
for (var cdn in allTrackerSubscribeUrlCdns[u]!) {
try {
result.addAll(await _fetchTrackers(cdn));
allFail = false;
break;
} catch (e) {
logger.w("subscribe trackers fail, url: $cdn", e);
}
final cdns = allTrackerSubscribeUrlCdns[u];
if (cdns == null) {
continue;
}
if (allFail) {
throw Exception('subscribe trackers fail, network error');
try {
final trackers =
await Util.anyOk(cdns.map((cdn) => _fetchTrackers(cdn)));
result.addAll(trackers);
} catch (e) {
logger.w("subscribe trackers fail, url: $u", e);
}
}
btExtConfig.subscribeTrackers.clear();
@@ -188,7 +338,7 @@ class AppController extends GetxController {
extra.themeMode = ThemeMode.system.name;
}
if (extra.locale.isEmpty) {
final systemLocale = getLocaleKey(ui.window.locale);
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
extra.locale = AppTranslation.translations.containsKey(systemLocale)
? systemLocale
: getLocaleKey(fallbackLocale);
@@ -217,6 +367,7 @@ class AppController extends GetxController {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_startConfigNetwork, startConfig.value.network);
await prefs.setString(_startConfigAddress, startConfig.value.address);
await prefs.setString(_startConfigApiToken, startConfig.value.apiToken);
await putConfig(downloaderConfig.value);
}
}
@@ -4,7 +4,7 @@ import 'package:get/get.dart';
import '../../../../generated/locales.g.dart';
import '../../../../theme/theme.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/locale_manager.dart';
import '../../../routes/app_pages.dart';
import '../controllers/app_controller.dart';
@@ -1,8 +1,25 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CreateController extends GetxController {
class CreateController extends GetxController
with GetSingleTickerProviderStateMixin {
// final files = [].obs;
final RxList fileInfos = [].obs;
final RxList selectedIndexes = [].obs;
final RxList openedFolders = [].obs;
final isResolving = false.obs;
final showAdvanced = false.obs;
late TabController advancedTabController;
@override
void onInit() {
super.onInit();
advancedTabController = TabController(length: 2, vsync: this);
}
@override
void onClose() {
advancedTabController.dispose();
super.onClose();
}
}
@@ -1,19 +1,22 @@
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/result.dart';
import 'package:rounded_loading_button/rounded_loading_button.dart';
import '../../../../api/api.dart';
import '../../../../api/model/create_task.dart';
import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
import '../../../views/views/directory_selector.dart';
import '../../../views/views/file_list_view.dart';
import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart';
import '../controllers/create_controller.dart';
@@ -22,11 +25,23 @@ class CreateView extends GetView<CreateController> {
final _urlController = TextEditingController();
final _confirmController = RoundedLoadingButtonController();
final _httpUaController = TextEditingController();
final _httpCookieController = TextEditingController();
final _httpRefererController = TextEditingController();
final _btTrackerController = TextEditingController();
CreateView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final String? filePath = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty && filePath != null) {
_urlController.text = filePath;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _doResolve();
});
}
return Scaffold(
appBar: AppBar(
leading: IconButton(
@@ -40,65 +55,184 @@ class CreateView extends GetView<CreateController> {
_urlController.text = details.files[0].path;
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form(
key: _resolveFormKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
children: [
TextFormField(
autofocus: true,
controller: _urlController,
minLines: 1,
maxLines: 30,
decoration: InputDecoration(
hintText: _hitText(),
hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link)),
validator: (v) {
return v!.trim().isNotEmpty
padding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form(
key: _resolveFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(children: [
Row(
children: [
Expanded(
child: TextFormField(
autofocus: true,
controller: _urlController,
minLines: 1,
maxLines: 5,
decoration: InputDecoration(
hintText: _hitText(),
hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link),
suffixIcon: IconButton(
onPressed: _urlController.clear,
icon: const Icon(Icons.clear),
),
),
validator: (v) {
return v!.trim().isNotEmpty
? null
: 'downloadLinkValid'.tr;
}),
),
Util.isWeb()
? null
: 'downloadLinkValid'.tr;
}),
Center(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(
: IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
var pr = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ["torrent"]);
if (pr != null) {
_urlController.text = pr.files[0].path ?? "";
}
}),
].where((e) => e != null).map((e) => e!).toList(),
),
Obx(() => Visibility(
visible: controller.showAdvanced.value,
child: Padding(
padding: const EdgeInsets.only(left: 40, top: 15),
child: Column(
children: [
TabBar(
controller: controller.advancedTabController,
tabs: const [
Tab(
text: 'HTTP',
),
Tab(
text: 'BitTorrent',
)
],
),
AutoScaleTabBarView(
controller: controller.advancedTabController,
children: [
Column(
children: [
TextFormField(
controller: _httpUaController,
decoration: const InputDecoration(
labelText: 'User-Agent',
)),
TextFormField(
controller: _httpCookieController,
decoration: const InputDecoration(
labelText: 'Cookie',
)),
TextFormField(
controller: _httpRefererController,
decoration: const InputDecoration(
labelText: 'Referer',
)),
],
),
Column(
children: [
TextFormField(
controller: _btTrackerController,
maxLines: 5,
decoration: InputDecoration(
labelText: 'Trakers',
hintText: 'addTrackerHit'.tr,
)),
],
)
],
)
],
),
))),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 15),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: TextButton(
onPressed: () {
controller.showAdvanced.value =
!controller.showAdvanced.value;
},
child:
Row(mainAxisSize: MainAxisSize.min, children: [
Obx(() => Checkbox(
value: controller.showAdvanced.value,
onChanged: (bool? value) {
controller.showAdvanced.value =
value ?? false;
},
)),
Text('advancedOptions'.tr),
]),
),
),
SizedBox(
width: 150,
height: 40,
child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary,
onPressed: _doResolve,
controller: _confirmController,
child: Text('confirm'.tr),
),
),
child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary,
onPressed: () async {
try {
_confirmController.start();
if (_resolveFormKey.currentState!.validate()) {
final rr = await resolve(Request(
url: _urlController.text,
));
await _showResolveDialog(rr);
}
} catch (e) {
showErrorMessage(e);
} finally {
_confirmController.reset();
}
},
controller: _confirmController,
child: Text('confirm'.tr),
),
)),
),
],
),
),
),
],
),
)),
]))),
),
);
}
Future<void> _doResolve() async {
if (controller.isResolving.value) {
return;
}
controller.isResolving.value = true;
try {
_confirmController.start();
if (_resolveFormKey.currentState!.validate()) {
Object? extra;
if (controller.showAdvanced.value) {
final u = Uri.parse(_urlController.text);
if (u.scheme.startsWith("http")) {
extra = ReqExtraHttp()
..header = {
"User-Agent": _httpUaController.text,
"Cookie": _httpCookieController.text,
"Referer": _httpRefererController.text,
};
} else {
extra = ReqExtraBt()
..trackers = Util.textToLines(_btTrackerController.text);
}
}
final rr = await resolve(Request(
url: _urlController.text,
extra: extra,
));
await _showResolveDialog(rr);
}
} catch (e) {
showErrorMessage(e);
} finally {
_confirmController.reset();
controller.isResolving.value = false;
}
}
String _hitText() {
return 'downloadLinkHit'.trParams({
'append': Util.isDesktop() ? 'downloadLinkHitDesktop'.tr : '',
@@ -110,6 +244,8 @@ class CreateView extends GetView<CreateController> {
final appController = Get.find<AppController>();
final createFormKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final connectionsController = TextEditingController();
final pathController = TextEditingController(
text: appController.downloaderConfig.value.downloadDir);
final downloadController = RoundedLoadingButtonController();
@@ -132,6 +268,23 @@ class CreateView extends GetView<CreateController> {
child: Column(
children: [
Expanded(child: FileListView(files: files)),
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'rename'.tr,
),
),
TextFormField(
controller: connectionsController,
decoration: InputDecoration(
labelText: 'connections'.tr,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 1, max: 256),
],
),
DirectorySelector(
controller: pathController,
),
@@ -173,20 +326,18 @@ class CreateView extends GetView<CreateController> {
return;
}
if (createFormKey.currentState!.validate()) {
// if (Util.isAndroid()) {
// if (!await Permission.storage.request().isGranted) {
// Get.snackbar('error'.tr,
// 'noStoragePermission'.tr);
// return;
// }
// }
await createTask(CreateTask(
rid: rr.id,
opts: Options(
name: '',
name: nameController.text,
path: pathController.text,
selectFiles: controller.selectedIndexes
.cast<int>())));
selectFiles:
controller.selectedIndexes.cast<int>(),
extra: connectionsController.text.isEmpty
? null
: (OptsExtraHttp()
..connections = int.parse(
connectionsController.text)))));
Get.back();
Get.rootDelegate.offNamed(Routes.TASK);
}
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../../../views/views/responsive_builder.dart';
import '../../../views/responsive_builder.dart';
import '../controllers/home_controller.dart';
class HomeView extends GetView<HomeController> {
@@ -11,6 +11,15 @@ class HomeView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
switch (currentRoute?.location) {
case Routes.SETTING:
controller.currentIndex.value = 1;
break;
default:
controller.currentIndex.value = 0;
break;
}
return Scaffold(
// extendBody: true,
body: Row(
@@ -28,17 +28,21 @@ class SettingController extends GetxController {
void fetchLatestVersion() async {
String? releaseDataStr;
try {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
} catch (e) {
releaseDataStr = (await proxyRequest(
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
.data;
} catch (e) {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
}
if (releaseDataStr == null) {
return;
}
final releaseData = jsonDecode(releaseDataStr);
final tagName = releaseData["tag_name"];
if (tagName == null) {
return;
}
latestVersion.value = releaseData["tag_name"].substring(1);
}
}
@@ -1,21 +1,22 @@
import 'dart:async';
import 'dart:convert';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../../util/message.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:badges/badges.dart' as badges;
import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/message.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../views/views/check_list_view.dart';
import '../../../views/views/directory_selector.dart';
import '../../../views/views/outlined_button_loading.dart';
import '../../../views/check_list_view.dart';
import '../../../views/directory_selector.dart';
import '../../../views/outlined_button_loading.dart';
import '../../app/controllers/app_controller.dart';
import '../controllers/setting_controller.dart';
@@ -35,7 +36,7 @@ class SettingView extends GetView<SettingController> {
debounceSave({bool needRestart = false}) {
var completer = Completer<void>();
timer?.cancel();
timer = Timer(const Duration(milliseconds: 500), () {
timer = Timer(const Duration(milliseconds: 1000), () {
appController
.saveConfig()
.then(completer.complete)
@@ -67,11 +68,37 @@ class SettingView extends GetView<SettingController> {
showLabel: false,
);
});
final buildMaxRunning = _buildConfigItem(
'maxRunning', () => downloaderCfg.value.maxRunning.toString(),
(Key key) {
final maxRunningController = TextEditingController(
text: downloaderCfg.value.maxRunning.toString());
maxRunningController.addListener(() async {
if (maxRunningController.text.isNotEmpty &&
maxRunningController.text !=
downloaderCfg.value.maxRunning.toString()) {
downloaderCfg.value.maxRunning = int.parse(maxRunningController.text);
await debounceSave();
}
});
return TextField(
key: key,
focusNode: FocusNode(),
controller: maxRunningController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 1, max: 256),
],
);
});
// http config items start
final httpConfig = downloaderCfg.value.protocolConfig.http;
final buildHttpConnections = _buildConfigItem(
'connections'.tr, () => httpConfig.connections.toString(), (Key key) {
'connections', () => httpConfig.connections.toString(), (Key key) {
final connectionsController =
TextEditingController(text: httpConfig.connections.toString());
connectionsController.addListener(() async {
@@ -90,7 +117,7 @@ class SettingView extends GetView<SettingController> {
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
_NumericalRangeFormatter(min: 1, max: 256),
NumericalRangeFormatter(min: 1, max: 256),
],
);
});
@@ -99,7 +126,7 @@ class SettingView extends GetView<SettingController> {
final btExtConfig = downloaderCfg.value.extra.bt;
final buildBtTrackerSubscribeUrls = _buildConfigItem(
'subscribeTracker'.tr,
'subscribeTracker',
() => 'items'.trParams(
{'count': btExtConfig.trackerSubscribeUrls.length.toString()}),
(Key key) {
@@ -155,13 +182,12 @@ class SettingView extends GetView<SettingController> {
);
});
final buildBtTrackers = _buildConfigItem(
'addTracker'.tr,
'addTracker',
() => 'items'
.trParams({'count': btExtConfig.customTrackers.length.toString()}),
(Key key) {
final trackersController = TextEditingController(
text: btExtConfig.customTrackers.join('\r\n').toString());
const ls = LineSplitter();
return TextField(
key: key,
focusNode: FocusNode(),
@@ -172,7 +198,7 @@ class SettingView extends GetView<SettingController> {
hintText: 'addTrackerHit'.tr,
),
onChanged: (value) async {
btExtConfig.customTrackers = ls.convert(value);
btExtConfig.customTrackers = Util.textToLines(value);
appController.refreshTrackers();
await debounceSave();
@@ -298,7 +324,7 @@ class SettingView extends GetView<SettingController> {
// advanced config items start
final buildApiProtocol = _buildConfigItem(
'protocol'.tr,
'protocol',
() => startCfg.value.network == 'tcp'
? 'TCP ${startCfg.value.address}'
: 'Unix',
@@ -382,7 +408,7 @@ class SettingView extends GetView<SettingController> {
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
_NumericalRangeFormatter(min: 0, max: 65535),
NumericalRangeFormatter(min: 0, max: 65535),
],
),
),
@@ -396,8 +422,8 @@ class SettingView extends GetView<SettingController> {
);
},
);
final buildApiToken = _buildConfigItem('apiToken'.tr,
() => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'seted'.tr,
final buildApiToken = _buildConfigItem('apiToken',
() => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'set'.tr,
(Key key) {
final apiTokenController =
TextEditingController(text: startCfg.value.apiToken);
@@ -408,7 +434,6 @@ class SettingView extends GetView<SettingController> {
await debounceSave(needRestart: true);
}
});
apiTokenController.addListener(() async {});
return TextField(
key: key,
obscureText: true,
@@ -426,7 +451,7 @@ class SettingView extends GetView<SettingController> {
length: 2,
child: Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(56),
preferredSize: const Size.fromHeight(56),
child: AppBar(
bottom: TabBar(
tabs: [
@@ -448,9 +473,8 @@ class SettingView extends GetView<SettingController> {
Text('general'.tr),
Card(
child: Column(
children: _addDivider([
buildDownloadDir(),
]),
children: _addDivider(
[buildDownloadDir(), buildMaxRunning()]),
)),
const Text('HTTP'),
Card(
@@ -502,7 +526,7 @@ class SettingView extends GetView<SettingController> {
],
),
],
).paddingOnly(left: 16, right: 16, top: 16, bottom: 64)),
).paddingOnly(left: 16, right: 16, top: 16, bottom: 16)),
),
);
});
@@ -581,39 +605,3 @@ class SettingView extends GetView<SettingController> {
}
}
}
// class _ConfigItem {
// late String label;
// late String Function() text;
// late Widget Function(Key key) inputItem;
//
// _ConfigItem(this.label, this.text, this.inputItem);
// }
class _NumericalRangeFormatter extends TextInputFormatter {
final int min;
final int max;
_NumericalRangeFormatter({required this.min, required this.max});
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) {
return newValue;
}
var intVal = int.tryParse(newValue.text);
if (intVal == null) {
return oldValue;
}
if (intVal < min) {
return newValue.copyWith(text: min.toString());
} else if (intVal > max) {
return oldValue.copyWith(text: max.toString());
} else {
return newValue;
}
}
}
@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
@@ -3,5 +3,11 @@ import 'task_list_controller.dart';
class TaskDownloadingController extends TaskListController {
TaskDownloadingController()
: super([Status.ready, Status.running, Status.pause, Status.error]);
: super([
Status.ready,
Status.running,
Status.pause,
Status.wait,
Status.error
]);
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../views/views/buid_task_list_view.dart';
import '../../../views/buid_task_list_view.dart';
import '../controllers/task_downloaded_controller.dart';
class TaskDownloadedView extends GetView<TaskDownloadedController> {
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../views/views/buid_task_list_view.dart';
import '../../../views/buid_task_list_view.dart';
import '../controllers/task_downloading_controller.dart';
class TaskDownloadingView extends GetView<TaskDownloadingController> {
@@ -1,16 +1,17 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gopeed/api/model/meta.dart';
import 'package:gopeed/api/model/resource.dart';
import 'package:path/path.dart' as path;
import '../../../api/api.dart';
import '../../../api/model/task.dart';
import '../../../util/file_icon.dart';
import '../../../util/icons.dart';
import '../../../util/message.dart';
import '../../../util/util.dart';
import '../../routes/app_pages.dart';
import '../../api/api.dart';
import '../../api/model/task.dart';
import '../../util/file_explorer.dart';
import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/message.dart';
import '../../util/util.dart';
import '../routes/app_pages.dart';
class BuildTaskListView extends GetView {
final List<Task> tasks;
@@ -53,6 +54,18 @@ class BuildTaskListView extends GetView {
return task.status == Status.running;
}
String buildExplorerUrl(Task task) {
if (task.meta.res.rootDir.trim().isEmpty) {
return path.join(
Util.safeDir(task.meta.opts.path),
Util.safeDir(task.meta.res.files[0].path),
task.meta.res.files[0].name);
} else {
return path.join(Util.safeDir(task.meta.opts.path),
Util.safeDir(task.meta.res.rootDir));
}
}
Future<void> showDeleteDialog(String id) {
final keep = true.obs;
@@ -100,9 +113,7 @@ class BuildTaskListView extends GetView {
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
final file = File(Util.buildAbsPath(task.meta.opts.path,
task.meta.res.files[0].path, task.meta.res.files[0].name));
await launchUrl(file.parent.uri);
await FileExplorer.openAndSelectFile(buildExplorerUrl(task));
},
));
}
@@ -111,14 +122,22 @@ class BuildTaskListView extends GetView {
list.add(IconButton(
icon: const Icon(Icons.pause),
onPressed: () async {
await pauseTask(task.id);
try {
await pauseTask(task.id);
} catch (e) {
showErrorMessage(e);
}
},
));
} else {
list.add(IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () async {
await continueTask(task.id);
try {
await continueTask(task.id);
} catch (e) {
showErrorMessage(e);
}
},
));
}
@@ -172,7 +191,7 @@ class BuildTaskListView extends GetView {
: const SizedBox.shrink(),
ListTile(
// isThreeLine: true,
title: Text(task.meta.res.name,
title: Text(fileName(task.meta),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Get.textTheme.titleSmall),
@@ -181,21 +200,19 @@ class BuildTaskListView extends GetView {
style: context.textTheme.bodyLarge
?.copyWith(color: Get.theme.disabledColor),
),
leading: task.meta.res.name.lastIndexOf('.') == -1
? const Icon(FaIcons.file)
: Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]),
leading: (task.meta.res.rootDir.isNotEmpty
? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])),
trailing: SizedBox(
width: task.status == Status.done ? 80 : 180,
width: 180,
child: Row(
// crossAxisAlignment: CrossAxisAlignment.baseline,
// textBaseline: DefaultTextStyle.of(context).style.textBaseline,
mainAxisAlignment: MainAxisAlignment.end,
children: [
task.status == Status.done
? const SizedBox.shrink()
: Text("${Util.fmtByte(task.progress.speed)} / s",
style: context.textTheme.titleSmall),
Text("${Util.fmtByte(task.progress.speed)} / s",
style: context.textTheme.titleSmall),
...buildActions()
],
),
@@ -204,4 +221,11 @@ class BuildTaskListView extends GetView {
])
]));
}
String fileName(Meta meta) {
if (meta.res.files.length > 1) {
return meta.res.name;
}
return meta.opts.name.isEmpty ? meta.res.files[0].name : meta.opts.name;
}
}
@@ -2,8 +2,8 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../util/mac_secure_util.dart';
import '../../../util/util.dart';
import '../../util/mac_secure_util.dart';
import '../../util/util.dart';
class DirectorySelector extends StatefulWidget {
final TextEditingController controller;
@@ -35,19 +35,17 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
},
)),
!Util.isDesktop()
? null
: IconButton(
Util.isDesktop() || Util.isAndroid()
? IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
if (GetPlatform.isDesktop) {
var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
}
var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
}
})
: null
].where((e) => e != null).map((e) => e!).toList(),
);
}
@@ -3,14 +3,14 @@ import 'dart:ui' as ui;
import 'package:fluent_ui/fluent_ui.dart' as fluent;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../api/model/resource.dart';
import '../../api/model/resource.dart';
import 'package:path/path.dart' as path;
import '../../../util/file_icon.dart';
import '../../../util/icons.dart';
import '../../../util/util.dart';
import '../../modules/app/controllers/app_controller.dart';
import '../../modules/create/controllers/create_controller.dart';
import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/util.dart';
import '../modules/app/controllers/app_controller.dart';
import '../modules/create/controllers/create_controller.dart';
class FileListView extends GetView {
final List<FileInfo> files;
@@ -17,7 +17,6 @@ LibgopeedBoot create() => LibgopeedBootNative();
class LibgopeedBootNative implements LibgopeedBoot {
late LibgopeedInterface _libgopeed;
late SendPort _childSendPort;
LibgopeedBootNative() {
if (!Util.isDesktop()) {
@@ -44,11 +43,7 @@ class LibgopeedBootNative implements LibgopeedBoot {
@override
Future<void> stop() async {
if (Util.isDesktop()) {
await _ffiStop();
} else {
await _libgopeed.stop();
}
await _libgopeed.stop();
}
_ffiInit() {
@@ -67,90 +62,9 @@ class LibgopeedBootNative implements LibgopeedBoot {
// FFI run in isolate
Future<int> _ffiStart(StartConfig cfg) async {
var startCompleter = Completer<int>();
var mainReceive = ReceivePort();
var isolate = await Isolate.spawn((SendPort sendPort) {
return await Isolate.run(() async {
_ffiInit();
var childReceive = ReceivePort();
sendPort.send(childReceive.sendPort);
childReceive.listen((message) async {
late _Result result;
try {
if (message is _StartParam) {
result = _StartResult();
(result as _StartResult).port = await _libgopeed.start(message.cfg);
}
if (message is _StopParam) {
result = _StopResult();
await _libgopeed.stop();
childReceive.close();
Isolate.exit(sendPort);
}
} on Exception catch (e) {
result.exception = e;
}
sendPort.send(result);
});
}, mainReceive.sendPort);
var childCompleter = Completer<void>();
mainReceive.listen((message) {
if (message is SendPort) {
_childSendPort = message;
childCompleter.complete();
}
if (message is _StartResult) {
if (message.exception != null) {
startCompleter.completeError(message.exception!);
} else {
startCompleter.complete(message.port!);
}
}
if (message is _StopResult) {
isolate.kill();
if (message.exception != null) {
_stopCompleter.completeError(message.exception!);
} else {
_stopCompleter.complete();
}
}
return await _libgopeed.start(cfg);
});
await childCompleter.future;
_childSendPort.send(_StartParam(cfg: cfg));
return startCompleter.future;
}
late Completer<void> _stopCompleter;
Future<void> _ffiStop() async {
_stopCompleter = Completer<void>();
_childSendPort.send(_StopParam());
return _stopCompleter.future;
}
}
class _Result {
Exception? exception;
_Result();
}
class _StartParam {
final StartConfig cfg;
_StartParam({required this.cfg});
}
class _StartResult extends _Result {
int? port;
_StartResult();
}
class _StopParam {}
class _StopResult extends _Result {
_StopResult();
}
File diff suppressed because it is too large. Load diff
+92
View File
@@ -6,6 +6,7 @@ class AppTranslation {
static Map<String, Map<String, String>> translations = {
'en_US': Locales.en_US,
'fa_IR': Locales.fa_IR,
'ja_JP': Locales.ja_JP,
'ru_RU': Locales.ru_RU,
'zh_CN': Locales.zh_CN,
};
@@ -23,7 +24,10 @@ class LocaleKeys {
static const downloading = 'downloading';
static const downloaded = 'downloaded';
static const setting = 'setting';
static const donate = 'donate';
static const exit = 'exit';
static const create = 'create';
static const advancedOptions = 'advancedOptions';
static const downloadLink = 'downloadLink';
static const downloadLinkValid = 'downloadLinkValid';
static const downloadLinkHit = 'downloadLinkHit';
@@ -32,12 +36,14 @@ class LocaleKeys {
static const noFileSelected = 'noFileSelected';
static const noStoragePermission = 'noStoragePermission';
static const selectFile = 'selectFile';
static const rename = 'rename';
static const basic = 'basic';
static const advanced = 'advanced';
static const general = 'general';
static const downloadDir = 'downloadDir';
static const downloadDirValid = 'downloadDirValid';
static const connections = 'connections';
static const maxRunning = 'maxRunning';
static const items = 'items';
static const subscribeTracker = 'subscribeTracker';
static const subscribeFail = 'subscribeFail';
@@ -61,6 +67,8 @@ class LocaleKeys {
static const notSet = 'notSet';
static const set = 'set';
static const effectAfterRestart = 'effectAfterRestart';
static const startAll = 'startAll';
static const pauseAll = 'pauseAll';
static const deleteTask = 'deleteTask';
static const deleteTaskTip = 'deleteTaskTip';
static const delete = 'delete';
@@ -81,7 +89,10 @@ class Locales {
'downloading': 'downloading',
'downloaded': 'downloaded',
'setting': 'Setting',
'donate': 'Donate',
'exit': 'Exit',
'create': 'Create Task',
'advancedOptions': 'Advanced Options',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link',
'downloadLinkHit':
@@ -91,12 +102,14 @@ class Locales {
'noFileSelected': 'Please select at least one file to continue.',
'noStoragePermission': 'Storage permission required',
'selectFile': 'Select File',
'rename': 'Rename',
'basic': 'Basic',
'advanced': 'Advanced',
'general': 'General',
'downloadDir': 'Download Directory',
'downloadDirValid': 'Please select the download directory',
'connections': 'Connections',
'maxRunning': 'Max Running Tasks',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
@@ -121,6 +134,8 @@ class Locales {
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'Start All',
'pauseAll': 'Pause All',
'deleteTask': 'Delete Task',
'deleteTaskTip': 'Keep downloaded files',
'delete': 'Delete',
@@ -137,6 +152,8 @@ class Locales {
'selectAll': 'انتخاب همه',
'task': 'کار',
'setting': 'تنظیمات',
'donate': 'کمک مالی',
'exit': 'خروج',
'create': 'ایجاد کار',
'downloading': 'دانلود کردن',
'downloaded': 'دانلود شدن',
@@ -176,6 +193,8 @@ class Locales {
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'شروع همه',
'pauseAll': 'توقف همه',
'deleteTask': 'پاک کردن کار',
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
'delete': 'پاک کردن',
@@ -183,6 +202,67 @@ class Locales {
'newVersionUpdate': 'دریافت اکنون',
'newVersionLater': 'بعداً',
};
static const ja_JP = {
'label': '日本語',
'error': 'エラー',
'tip': 'ヒント',
'confirm': '確認',
'cancel': 'キャンセル',
'selectAll': 'すべてを選択',
'task': 'タスク',
'downloading': 'ダウンロード中',
'downloaded': 'ダウンロード済',
'setting': 'セッティング',
'donate': '寄付する',
'exit': '終了',
'create': 'タスクを作成',
'downloadLink': 'ダウンロードリンク',
'downloadLinkValid': 'ダウンロードリンクを入力してください',
'downloadLinkHit': 'ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append',
'downloadLinkHitDesktop': 'または、torrent ファイルを直接ここにドラッグします',
'download': 'ダウンロード',
'noFileSelected': '続行するには少なくとも 1 つのファイルを選択してください。',
'noStoragePermission': 'ストレージのパーミッションが必要',
'selectFile': 'ファイルを選択',
'basic': 'ベーシック',
'advanced': 'アドバンスド',
'general': 'ジェネラル',
'downloadDir': 'ディレクトリのダウンロード',
'downloadDirValid': 'ダウンロードディレクトリを選択してください',
'connections': '接続',
'maxRunning': '最大実行タスク',
'items': '@count アイテム',
'subscribeTracker': 'トラッカーを購読',
'subscribeFail': '登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください',
'update': 'アップデート',
'updateDaily': '毎日アップデート',
'lastUpdate': '最終アップデート: @time',
'addTracker': 'トラッカーを追加',
'addTrackerHit': 'トラッカーサーバーの URL を 1 行に 1 つずつ入力してください',
'ui': 'UI',
'theme': 'テーマ',
'themeSystem': 'システム',
'themeLight': 'ライト',
'themeDark': 'ダーク',
'locale': '言語',
'about': '概要',
'homepage': 'ホームページ',
'version': 'バージョン',
'protocol': 'プロトコル',
'port': 'ポート',
'apiToken': 'API トークン',
'notSet': '未設置',
'set': '設置',
'effectAfterRestart': '再起動後の効果',
'startAll': 'すべてを開始',
'pauseAll': 'すべてを一時停止',
'deleteTask': 'タスクを削除',
'deleteTaskTip': 'ダウンロードしたファイルを保持',
'delete': '削除',
'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': '今すぐダウンロード',
'newVersionLater': '後で',
};
static const ru_RU = {
'label': 'Русский(Россия)',
'error': 'Ошибка',
@@ -194,6 +274,8 @@ class Locales {
'downloading': 'Загрузка',
'downloaded': 'Загружено',
'setting': 'Настройки',
'donate': 'Пожертвовать',
'exit': 'Выход',
'create': 'Создать задачу',
'downloadLink': 'Ссылка для скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
@@ -210,6 +292,7 @@ class Locales {
'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений',
'maxRunning': 'Максимальное количество активных задач',
'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера',
'subscribeFail':
@@ -234,6 +317,8 @@ class Locales {
'notSet': 'Не установлено',
'set': 'Установлено',
'effectAfterRestart': 'Эффект после перезагрузки',
'startAll': 'Запустить все',
'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить задачу',
'deleteTaskTip': 'Сохранить загруженные файлы',
'delete': 'Удалить',
@@ -252,7 +337,10 @@ class Locales {
'downloading': '下载中',
'downloaded': '已完成',
'setting': '设置',
'donate': '打赏',
'exit': '退出',
'create': '创建任务',
'advancedOptions': '高级选项',
'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接',
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
@@ -261,12 +349,14 @@ class Locales {
'noFileSelected': '请至少选择一个文件下载',
'noStoragePermission': '需要开启存储权限',
'selectFile': '选择文件',
'rename': '重命名',
'basic': '基础',
'advanced': '高级',
'general': '通用',
'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录',
'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 条',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
@@ -290,6 +380,8 @@ class Locales {
'notSet': '未设置',
'set': '已设置',
'effectAfterRestart': '此配置项将在重启应用后生效',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务',
'deleteTaskTip': '保留已下载的文件',
'delete': '删除',
+16 -1
View File
@@ -1,16 +1,18 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'api/api.dart' as api;
import 'app/modules/app/controllers/app_controller.dart';
import 'app/modules/app/views/app_view.dart';
import 'core/libgopeed_boot.dart';
import 'generated/locales.g.dart';
import 'util/localeManager.dart';
import 'util/locale_manager.dart';
import 'util/log_util.dart';
import 'util/mac_secure_util.dart';
import 'util/package_info.dart';
import 'util/util.dart';
void main() async {
await init();
@@ -21,6 +23,19 @@ void main() async {
Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop()) {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(800, 600),
center: true,
skipTaskbar: false,
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
await windowManager.setPreventClose(true);
});
}
final controller = Get.put(AppController());
try {
+57
View File
@@ -0,0 +1,57 @@
import 'dart:io';
class FileExplorer {
static Future<void> openAndSelectFile(String filePath) async {
if (await FileSystemEntity.isFile(filePath)) {
_openFile(filePath);
} else if (await FileSystemEntity.isDirectory(filePath)) {
_openDirectory(filePath);
}
}
static Future<void> _openDirectory(String directoryPath) async {
if (Platform.isWindows) {
Process.run('explorer.exe', [directoryPath]);
} else if (Platform.isMacOS) {
Process.run('open', [directoryPath]);
} else if (Platform.isLinux) {
if (await _isUbuntuOrDebian()) {
Process.run('xdg-open', [directoryPath]);
} else if (await _isCentOS()) {
Process.run('nautilus', [directoryPath]);
}
}
}
static Future<void> _openFile(String filePath) async {
if (Platform.isWindows) {
Process.run('explorer.exe', ['/select,', filePath]);
} else if (Platform.isMacOS) {
Process.run('open', ['-R', filePath]);
} else if (Platform.isLinux) {
if (await _isUbuntuOrDebian()) {
Process.run('xdg-open', [filePath]);
} else if (await _isCentOS()) {
Process.run('nautilus', ['--select', filePath]);
}
}
}
static Future<bool> _isUbuntuOrDebian() async {
final result = await Process.run('lsb_release', ['-i']);
if (result.exitCode != 0) {
return false;
}
final output = result.stdout.toString().toLowerCase();
return output.contains('ubuntu') || output.contains('debian');
}
static Future<bool> _isCentOS() async {
final result = await Process.run('cat', ['/etc/os-release']);
if (result.exitCode != 0) {
return false;
}
final output = result.stdout.toString().toLowerCase();
return output.contains('centos');
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
findIcon(String filename) {
String res = 'doc';
String res = 'file';
String ext = filename.substring(filename.lastIndexOf('.') + 1);
for (var iconMap in iconMaps) {
if (iconMap['extensions'].contains(ext)) {
+1
View File
@@ -28,6 +28,7 @@ class FaIcons {
static const _kFontFam = 'FontAwesome';
static const Map<String, IconData> allIcons = {
'file': file,
'doc': doc,
'file_image': file_image,
'file_video': file_video,
+30
View File
@@ -0,0 +1,30 @@
import 'package:flutter/services.dart';
/// A [TextInputFormatter] that restricts input to a numerical range between [min] and [max].
class NumericalRangeFormatter extends TextInputFormatter {
final int min;
final int max;
NumericalRangeFormatter({required this.min, required this.max});
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (newValue.text.isEmpty) {
return newValue;
}
var intVal = int.tryParse(newValue.text);
if (intVal == null) {
return oldValue;
}
if (intVal < min) {
return newValue.copyWith(text: min.toString());
} else if (intVal > max) {
return oldValue.copyWith(text: max.toString());
} else {
return newValue;
}
}
}
+9 -5
View File
@@ -1,19 +1,23 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'util.dart';
final logger = Logger(
printer: PrettyPrinter(),
filter: ProductionFilter(),
printer: LogfmtPrinter(),
output: buildOutput(),
);
buildOutput() {
// if is debug mode, output to console
// if is debug mode, don't log to file
if (!kDebugMode && Util.isDesktop()) {
return FileOutput(file: File('log.txt'));
const logDirPath = 'logs';
var logDir = Directory(logDirPath);
if (!logDir.existsSync()) {
logDir.createSync();
}
return FileOutput(file: File('$logDirPath/client.log'));
}
return null;
}
+1 -1
View File
@@ -12,7 +12,7 @@ void showErrorMessage(msg) {
Get.snackbar(title, ((msg as dynamic).message as Result).msg!);
return;
}
Get.snackbar(title, msg);
Get.snackbar(title, msg.toString());
}
void showMessage(title, msg) {
+43 -7
View File
@@ -1,18 +1,21 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
class Util {
static String buildAbsPath(String dir, String path, String name) {
return (dir == "" ? "" : "$dir/") + buildPath(path, name);
}
static String buildPath(String path, String name) {
return (path == "" ? "" : "$path/") + name;
static String safeDir(String path) {
if (path == "." || path == "./" || path == ".\\") {
return "";
}
return path;
}
static String fmtByte(int byte) {
if (byte < 1024) {
if (byte < 0) {
return "0 B";
} else if (byte < 1024) {
return "$byte B";
} else if (byte < 1024 * 1024) {
return "${(byte / 1024).toStringAsFixed(2)} KB";
@@ -42,6 +45,10 @@ class Util {
return Platform.isWindows || Platform.isLinux || Platform.isMacOS;
}
static isWindows() {
return !kIsWeb && Platform.isWindows;
}
static isMacos() {
return !kIsWeb && Platform.isMacOS;
}
@@ -56,4 +63,33 @@ class Util {
static isWeb() {
return kIsWeb;
}
static List<String> textToLines(String text) {
if (text.isEmpty) {
return [];
}
const ls = LineSplitter();
return ls.convert(text);
}
// if one future complete, return the result, only all future error, return the last error
static anyOk<T>(Iterable<Future<T>> futures) {
final completer = Completer<T>();
Object lastError;
var count = futures.length;
for (var future in futures) {
future.then((value) {
if (!completer.isCompleted) {
completer.complete(value);
}
}).catchError((e) {
lastError = e;
count--;
if (count == 0) {
completer.completeError(lastError);
}
});
}
return completer.future;
}
}
+13
View File
@@ -4,6 +4,19 @@ import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false
}
override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
for window in NSApp.windows {
if !window.isVisible {
window.setIsVisible(true)
}
window.makeKeyAndOrderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
}
return true
}
}
+294 -215
View File
File diff suppressed because it is too large. Load diff
+17 -15
View File
@@ -15,10 +15,10 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.3.0+1
version: 1.3.6+1
environment:
sdk: ">=2.17.6 <3.0.0"
sdk: '>=3.0.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -40,20 +40,24 @@ dependencies:
styled_widget: ^0.4.0+3
context_menus: ^1.0.2
json_annotation: ^4.6.0
dio: ^5.1.1
dio: ^5.2.1
path_provider: ^2.0.11
file_picker: ^5.2.5
macos_secure_bookmarks: ^0.2.1
rounded_loading_button: ^2.1.0
url_launcher: ^6.1.6
shared_preferences: ^2.0.15
shared_preferences: ^2.2.0
logger: ^1.3.0
desktop_drop: ^0.3.3
intl: ^0.17.0
package_info_plus: ^3.0.2
package_info_plus: ^4.0.0
fluent_ui: ^4.4.0
path: any
badges: ^3.0.3
app_links: ^3.4.2
uri_to_file: ^0.2.0
window_manager: ^0.3.4
tray_manager: ^0.2.0
autoscale_tabbarview: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
@@ -64,13 +68,11 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
ffigen: ^6.1.2
ffigen: ^8.0.2
# build json model: flutter pub run build_runner build --delete-conflicting-outputs
build_runner: ^2.2.1
json_serializable: ^6.3.2
flutter_launcher_icons:
git:
url: https://github.com/fluttercommunity/flutter_launcher_icons.git
ref: v0.11.0
flutter_launcher_icons: ^0.13.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -83,9 +85,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - macos/Frameworks/libgopeed.dylib
# - macos/lib/libgopeed.dylib
assets:
- assets/tray_icon/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
@@ -125,10 +126,11 @@ flutter_icons:
generate: true
image_path: "assets/icon/icon_macos_1024.png"
# flutter pub run ffigen
ffigen:
name: LibgopeedBind
description: Bindings to gopeed library.
output: 'lib/core/ffi/libgopeed_bind.dart'
headers:
entry-points:
- 'include/libgopeed.h'
- 'include/libgopeed.h'
-2
View File
@@ -5,10 +5,8 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {});
+6
View File
@@ -7,6 +7,12 @@
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"gopeed");
if (hwnd != NULL) {
::ShowWindow(hwnd, SW_NORMAL);
::SetForegroundWindow(hwnd);
return EXIT_FAILURE;
}
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {