Compare commits

...
9 Commits
45 changed files with 1191 additions and 614 deletions

No files matched your search

+46 -41
View File
@@ -7,14 +7,14 @@ on:
description: 'Build platform'
required: true
default: 'all'
channel:
description: 'Snap channel'
test:
description: 'Test mode'
required: true
default: 'stable'
default: "false"
env:
GO_VERSION: "1.19"
FLUTTER_VERSION: "3.16.1"
GO_VERSION: "1.21"
FLUTTER_VERSION: "3.16.8"
permissions:
contents: write
@@ -23,11 +23,11 @@ jobs:
get-release:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.get-release.outputs.tag_name }}
version: ${{ steps.get-release.outputs.version }}
upload_url: ${{ steps.get-release.outputs.upload_url }}
steps:
- uses: monkeyWie/get-latest-release@v1
- uses: monkeyWie/get-latest-release@v2.1
id: get-release
with:
myToken: ${{ github.token }}
@@ -40,19 +40,20 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: "1.20"
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 }}
VERSION: ${{ needs.get-release.outputs.version }}
run: |
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
$TAG = "v$env:VERSION"
flutter build windows
$mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32"
@@ -65,7 +66,7 @@ jobs:
cp $system\vcruntime140_1.dll $release
New-Item -Path build\windows\Output -ItemType Directory
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$env:VERSION-windows-amd64-portable.zip"
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
cd build/windows
echo @"
@@ -73,7 +74,7 @@ jobs:
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Gopeed"
#define MyAppVersion "$($env:VERSION.substring(1))"
#define MyAppVersion "$env:VERSION"
#define MyAppPublisher "monkeyWie"
#define MyAppURL "https://gopeed.com"
#define MyAppExeName "gopeed.exe"
@@ -89,7 +90,7 @@ jobs:
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\gopeed
DefaultDirName={autopf}\gopeedrsion
DisableProgramGroupPage=yes
LicenseFile=..\..\..\..\LICENSE
; Remove the following line to run in administrative install mode (install for all users.)
@@ -124,9 +125,9 @@ jobs:
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
"@ > setup.iss
iscc.exe setup.iss
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
mv "Output\gopeed.exe" "Output\Gopeed-$TAG-windows-amd64.exe"
Compress-Archive -Path "Output\Gopeed-$TAG-windows-amd64.exe" -DestinationPath "Output\Gopeed-$TAG-windows-amd64.zip"
Remove-Item -Path "Output\Gopeed-$TAG-windows-amd64.exe"
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -143,9 +144,11 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go install src.techknowlogick.com/xgo@latest
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
mv libgopeed-*.dylib libgopeed.dylib
- uses: actions/upload-artifact@v3
with:
@@ -176,12 +179,12 @@ jobs:
path: ui/flutter/lib-arm64
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
run: |
PROJECT_DIR=$(pwd)
# amd64 lib
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
# universal binary
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
@@ -203,7 +206,7 @@ jobs:
}
EOF
mkdir dist
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -235,15 +238,15 @@ jobs:
sudo mv appimagetool /usr/local/bin/
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
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
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
flutter_distributor package --platform linux --targets appimage,deb
cd dist/*
mv gopeed-*-linux.AppImage Gopeed-${VERSION}-linux-x86_64.AppImage
mv gopeed-*-linux.deb Gopeed-${VERSION}-linux-amd64.deb
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -268,9 +271,9 @@ jobs:
channel: latest/stable
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
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
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
sudo snap install snapcraft --classic
@@ -279,7 +282,7 @@ jobs:
cat>snap/snapcraft.yaml<<EOF
name: gopeed
version: ${VERSION:1}
version: $VERSION
summary: A modern download manager
description: High speed downloader that supports all platforms.
@@ -322,7 +325,7 @@ jobs:
# Snapcraft login
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
snapcraft upload --release=${{ github.event.inputs.channel }} gopeed_${VERSION:1}_amd64.snap
snapcraft upload --release=${{ github.event.inputs.test == 'true' && 'edge' || 'stable' }} gopeed_${VERSION}_amd64.snap
build-android:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
runs-on: ubuntu-latest
@@ -341,7 +344,7 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
APK_KEYSTORE: ${{ secrets.APK_KEYSTORE }}
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}
APK_STORE_PASSWORD: ${{ secrets.APK_STORE_PASSWORD }}
@@ -349,12 +352,12 @@ jobs:
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
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
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
flutter build apk
mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -375,19 +378,19 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
gomobile bind -tags nosqlite,netgo -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
mkdir Payload
cp -r build/ios/iphoneos/Runner.app Payload
zip -r -y Payload.zip Payload/Runner.app
mkdir dist
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
mv Payload.zip dist/Gopeed-v$VERSION-ios.ipa
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -408,13 +411,14 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
run: |
cd ui/flutter
flutter build web --web-renderer html
cd ../../
cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip
goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm64)
export CGO_ENABLED=0
@@ -424,9 +428,9 @@ jobs:
if [ $goos = "darwin" ]; then
goos_name="macos"
fi
name=gopeed-web-$VERSION-$goos_name-$goarch
name=gopeed-web-v$VERSION-$goos_name-$goarch
dir="dist/$name/"
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/GopeedLab/gopeed/cmd/web \
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o $dir github.com/GopeedLab/gopeed/cmd/web \
&& cd $dir \
&& file=$(ls -AU | head -1) \
&& mkdir $name \
@@ -464,15 +468,16 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v2
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
VERSION: ${{ needs.get-release.outputs.version }}
with:
push: true
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
build-args: VERSION=${{ env.VERSION }}
platforms: |
linux/386
linux/amd64
linux/arm64
linux/arm/v6
linux/arm/v7
linux/arm64/v8
tags: |
liwei2633/gopeed:latest
liwei2633/gopeed:${{ env.VERSION }}
liwei2633/gopeed:v${{ env.VERSION }}
+2 -2
View File
@@ -27,8 +27,8 @@ on:
- "go.sum"
env:
GO_VERSION: "1.19"
FLUTTER_VERSION: "3.16.1"
GO_VERSION: "1.21"
FLUTTER_VERSION: "3.16.8"
jobs:
# lint:
+4 -3
View File
@@ -1,17 +1,18 @@
FROM instrumentisto/flutter:3.16.1 AS flutter
FROM instrumentisto/flutter:3.16.8 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
COPY ./ui/flutter ./
RUN flutter build web --web-renderer html
FROM golang:1.19.3 AS go
FROM golang:1.21.6 AS go
WORKDIR /app
COPY ./go.mod ./go.sum ./
RUN go mod download
COPY . .
COPY --from=flutter /app/build/web ./cmd/web/dist
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
ARG VERSION=dev
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=${VERSION}" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
FROM alpine:3.14.2
LABEL maintainer="monkeyWie"
+66 -36
View File
@@ -15,24 +15,74 @@
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
## Install
## Download
**Supported platforms**
<table>
<thead>
<tr>
<th>Platform</th>
<th>Package Type</th>
<th>Download Link</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Link</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
</tr>
<tr>
<td><code>Portable TAR</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">Link</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
</tr>
</tbody>
</table>
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [x] ios
- [x] web
- [x] docker
[Download](https://github.com/GopeedLab/gopeed/releases/latest)
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
### Command tool
@@ -42,26 +92,6 @@ use `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### Start directly
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[More details for docker](https://docs.gopeed.com/install.html#docker-installation)
#### Using Docker Compose
```bash
docker-compose up -d
```
#### Access Gopeed
When the docker container is running, you can access the web page through `http://localhost:9999`.
## Donate
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
@@ -78,8 +108,8 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Environment
1. Golang 1.19+
2. Flutter 3.10+
1. Golang 1.21+
2. Flutter 3.16+
### Clone
+66 -43
View File
@@ -19,51 +19,74 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
## インストール
**サポートするプラットフォーム**
<table>
<thead>
<tr>
<th>Platform</th>
<th>Package Type</th>
<th>Download Link</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Link</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
</tr>
<tr>
<td><code>Portable TAR</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">Link</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
</tr>
</tbody>
</table>
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [x] 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 -v /path/to/storage:/app/storage liwei2633/gopeed
```
[Dockerの使用については](https://docs.gopeed.com/install.html#docker-installation)
#### Docker Compose の使用
```bash
docker-compose up -d
```
#### Gopeed へのアクセス
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
## 寄付
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
## ショーケース
@@ -77,8 +100,8 @@ docker コンテナが起動しているときは、`http://localhost:9999` か
### 環境
1. Go 言語 1.19+
2. Flutter 3.10+
1. Go 言語 1.21+
2. Flutter 3.16+
### クローン
+66 -35
View File
@@ -17,21 +17,72 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## 安装
## 下载
**已支持平台**
<table>
<thead>
<tr>
<th>运行平台</th>
<th>安装包类型</th>
<th>下载链接</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">前往</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
</tr>
<tr>
<td><code>Portable TAR</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">前往</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
</tr>
</tbody>
</table>
- [x] windows
- [x] macos
- [x] linux
- [x] android
- [x] ios
- [x] web
- [x] docker
[前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
### 命令行工具
@@ -41,26 +92,6 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接运行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[更多关于 Docker 的使用](https://docs.gopeed.com/zh/install.html#docker-%E5%AE%89%E8%A3%85)
#### 使用 Docker Compose
```bash
docker-compose up -d
```
#### 访问服务
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
## 赞助
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
@@ -77,8 +108,8 @@ docker-compose up -d
### 环境要求
1. Golang 1.19+
2. Flutter 3.10+
1. Golang 1.21+
2. Flutter 3.16+
### 克隆项目
+66 -35
View File
@@ -17,21 +17,72 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
## 安裝
## 下載
**已支援的平台**
<table>
<thead>
<tr>
<th>運行平台</th>
<th>安裝包類型</th>
<th>下載鏈接</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">前往</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
</tr>
<tr>
<td><code>Portable TAR</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">前往</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
</tr>
</tbody>
</table>
- [x] Windows
- [x] MacOS
- [x] Linux
- [x] Android
- [x] iOS
- [x] Web
- [x] Docker
[下載](https://github.com/GopeedLab/gopeed/releases/latest)
> 註:MacOS 版運行時若提示損壞,請在終端中執行 `xattr -d com.apple.quarantine /Applications/Gopeed.app`
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
### 使用CLI安裝
@@ -41,26 +92,6 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接執行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[更多關於 Docker 的使用](https://docs.gopeed.com/zh/install.html#docker-%E5%AE%89%E8%A3%85)
#### 使用 Docker Compose
```bash
docker-compose up -d
```
#### 訪問服務
當 docker 容器運作時,可以通過 `http://localhost:9999` 訪問 web 頁面。
## 贊助
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
@@ -77,8 +108,8 @@ docker-compose up -d
### 開發環境
1. Golang 1.19+
2. Flutter 3.10+
1. Golang 1.21+
2. Flutter 3.16+
### 克隆項目
+62 -53
View File
@@ -1,10 +1,10 @@
module github.com/GopeedLab/gopeed
go 1.19
go 1.21
require (
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.5
github.com/anacrolix/missinggo/v2 v2.7.3
github.com/anacrolix/torrent v1.53.3
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
@@ -12,97 +12,106 @@ require (
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/pkg/errors v0.9.1
github.com/rs/zerolog v1.31.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.etcd.io/bbolt v1.3.8
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/sync v0.3.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/sync v0.6.0
)
require (
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
github.com/RoaringBitmap/roaring v1.7.0 // indirect
github.com/acomagu/bufpipe v1.0.4 // 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.19.2-0.20221121215055-066ad8494444 // indirect
github.com/anacrolix/envpprof v1.2.1 // indirect
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
github.com/anacrolix/chansync v0.4.0 // indirect
github.com/anacrolix/dht/v2 v2.21.0 // indirect
github.com/anacrolix/envpprof v1.3.0 // indirect
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect
github.com/anacrolix/log v0.14.0 // indirect
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
github.com/anacrolix/mmsg v1.0.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/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
github.com/anacrolix/stm v0.5.0 // indirect
github.com/anacrolix/sync v0.5.1 // indirect
github.com/anacrolix/upnp v0.1.3 // indirect
github.com/anacrolix/utp v0.2.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.2 // indirect
github.com/benbjohnson/immutable v0.4.3 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-llsqlite/adapter v0.1.0 // indirect
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/lispad/go-generics-tools v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/pion/datachannel v1.5.2 // 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/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.9 // indirect
github.com/pion/ice/v2 v2.3.12 // indirect
github.com/pion/interceptor v0.1.25 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.9 // indirect
github.com/pion/rtp v1.7.13 // indirect
github.com/pion/sctp v1.8.2 // 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.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/pion/rtcp v1.2.13 // indirect
github.com/pion/rtp v1.8.3 // indirect
github.com/pion/sctp v1.8.9 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect
github.com/pion/srtp/v2 v2.0.18 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v2 v2.1.4 // indirect
github.com/pion/webrtc/v3 v3.2.24 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opentelemetry.io/otel v1.8.0 // indirect
go.opentelemetry.io/otel/trace v1.8.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.14.0 // indirect
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.6.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.40.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
zombiezen.com/go/sqlite v1.1.0 // indirect
)
+179 -130
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -2,6 +2,7 @@ package bt
import (
"bytes"
"fmt"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
@@ -11,6 +12,7 @@ import (
"github.com/anacrolix/torrent/metainfo"
"net/http"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
@@ -63,6 +65,8 @@ func (f *Fetcher) initClient() (err error) {
}
cfg := torrent.NewDefaultClientConfig()
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
cfg.ListenPort = f.config.ListenPort
if f.ctl.ProxyUrl != nil {
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
@@ -218,6 +222,7 @@ func (f *Fetcher) updateRes() {
Files: make([]*base.FileInfo, len(f.torrent.Files())),
Hash: f.torrent.InfoHash().String(),
}
f.torrent.PeerConns()
for i, file := range f.torrent.Files() {
res.Files[i] = &base.FileInfo{
Name: filepath.Base(file.DisplayPath()),
@@ -306,3 +311,12 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
}
}
}
// parse version to bep20 format, fixed length 4, if not enough, fill 0
func parseBep20() string {
s := strings.ReplaceAll(base.Version, ".", "")
if len(s) < 4 {
s += strings.Repeat("0", 4-len(s))
}
return s
}
+3 -2
View File
@@ -1,6 +1,7 @@
package http
type config struct {
UserAgent string `json:"userAgent"`
Connections int `json:"connections"`
UserAgent string `json:"userAgent"`
Connections int `json:"connections"`
UseServerCtime bool `json:"useServerCtime"`
}
+17 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/xiaoqidun/setft"
"golang.org/x/sync/errgroup"
"io"
"mime"
@@ -105,6 +106,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
Range: false,
Files: []*base.FileInfo{},
}
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
res.Range = true
@@ -130,8 +132,17 @@ func (f *Fetcher) Resolve(req *base.Request) error {
} else {
return NewRequestError(httpResp.StatusCode, httpResp.Status)
}
// Parse last modified time
var lastModifiedTime *time.Time
lastModified := httpResp.Header.Get(base.HttpHeaderLastModified)
if lastModified != "" {
// ignore parse error
t, _ := time.Parse(time.RFC1123, lastModified)
lastModifiedTime = &t
}
file := &base.FileInfo{
Size: res.Size,
Size: res.Size,
Ctime: lastModifiedTime,
}
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
if contentDisposition != "" {
@@ -257,6 +268,10 @@ func (f *Fetcher) fetch() {
return
}
f.file.Close()
// Update file last modified time
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
setft.SetFileTime(f.file.Name(), time.Now(), *f.meta.Res.Files[0].Ctime, *f.meta.Res.Files[0].Ctime)
}
f.doneCh <- err
}()
}
@@ -275,7 +290,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
maxRetries = 3
)
// retry until all remain chunks failed
for true {
for {
// if chunk is completed, return
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
return
+29 -9
View File
@@ -10,7 +10,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"net"
"reflect"
"testing"
"time"
)
@@ -69,7 +68,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(want, fetcher.meta.Res) {
if !test.AssertResourceEqual(want, fetcher.meta.Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
}
}
@@ -151,10 +150,34 @@ func TestFetcher_DownloadWithProxy(t *testing.T) {
downloadWithProxy(httpListener, proxyListener, t)
}
func TestFetcher_Config(t *testing.T) {
func TestFetcher_ConfigConnections(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
fetcher := doDownloadReady(buildConfigFetcher(), listener, 0, t)
fetcher := doDownloadReady(buildConfigFetcher(config{
Connections: 16,
}), listener, 0, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
}
func TestFetcher_ConfigUseServerCtime(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
fetcher := doDownloadReady(buildConfigFetcher(config{
Connections: 16,
UseServerCtime: true,
}), listener, 0, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
@@ -338,14 +361,11 @@ func buildFetcher() *Fetcher {
return fetcher.(*Fetcher)
}
func buildConfigFetcher() fetcher.Fetcher {
func buildConfigFetcher(cfg config) fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build()
newController := controller.NewController()
mockCfg := config{
Connections: 16,
}
newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
return false
}
return true
+10
View File
@@ -4,12 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/armon/go-socks5"
"io"
"math/rand"
"net"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"sync/atomic"
@@ -337,3 +339,11 @@ func StartSocks5Server(usr, pwd string) net.Listener {
go server.Serve(listener)
return listener
}
func AssertResourceEqual(want, got *base.Resource) bool {
// Ignore ctime
if got != nil && len(got.Files) > 0 {
got.Files[0].Ctime = nil
}
return reflect.DeepEqual(want, got)
}
+2 -1
View File
@@ -20,7 +20,8 @@ const (
HttpHeaderContentLength = "Content-Length"
HttpHeaderContentRange = "Content-Range"
HttpHeaderContentDisposition = "Content-Disposition"
HttpHeaderUserAgent = "Usr-Agent"
HttpHeaderUserAgent = "User-Agent"
HttpHeaderLastModified = "Last-Modified"
HttpHeaderBytes = "bytes"
HttpHeaderRangeFormat = "bytes=%d-%d"
+5 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/GopeedLab/gopeed/pkg/util"
"golang.org/x/exp/slices"
"time"
)
// Request download request
@@ -59,9 +60,10 @@ func (r *Resource) CalcSize(selectFiles []int) {
}
type FileInfo struct {
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
Name string `json:"name"`
Path string `json:"path"`
Size int64 `json:"size"`
Ctime *time.Time `json:"ctime"`
Req *Request `json:"req"`
}
+10
View File
@@ -0,0 +1,10 @@
package base
// Version is the build version, set at build time, using `go build -ldflags "-X github.com/GopeedLab/gopeed/pkg/base.Version=1.0.0"`.
var Version string
func init() {
if Version == "" {
Version = "dev"
}
}
+82 -2
View File
@@ -35,7 +35,8 @@ const (
)
var (
ErrTaskNotFound = errors.New("task not found")
ErrTaskNotFound = errors.New("task not found")
ErrUnSupportedProtocol = errors.New("unsupported protocol")
)
type Listener func(event *Event)
@@ -123,6 +124,11 @@ func (d *Downloader) Setup() error {
}
// init default config
d.cfg.DownloaderStoreConfig.Init()
for _, fb := range d.fetcherBuilders {
f := fb.Build()
d.setupFetcher(f)
}
// load tasks from storage
var tasks []*Task
if err = d.storage.List(bucketTask, &tasks); err != nil {
@@ -211,7 +217,7 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
if ok {
return fetchBuilder, nil
}
return nil, errors.New("unsupported protocol")
return nil, ErrUnSupportedProtocol
}
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
@@ -448,6 +454,13 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
for i, t := range d.tasks {
if t.ID == id {
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
break
}
}
for i, t := range d.waitTasks {
if t.ID == id {
d.waitTasks = append(d.waitTasks[:i], d.waitTasks[i+1:]...)
break
}
}
}()
@@ -460,6 +473,56 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
return
}
func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err error) {
deleteTasks := d.GetTasksByStatues(statues)
if len(deleteTasks) == 0 {
return
}
deleteIds := make([]string, 0)
for _, task := range deleteTasks {
deleteIds = append(deleteIds, task.ID)
}
func() {
d.lock.Lock()
defer d.lock.Unlock()
for _, id := range deleteIds {
for i, t := range d.tasks {
if t.ID == id {
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
break
}
}
for i, t := range d.waitTasks {
if t.ID == id {
d.waitTasks = append(d.waitTasks[:i], d.waitTasks[i+1:]...)
break
}
}
}
}()
for _, task := range deleteTasks {
err = d.doDelete(task, force)
if err != nil {
return
}
}
d.notifyRunning()
return
}
func (d *Downloader) Stats(id string) (sr any, err error) {
task := d.GetTask(id)
if task == nil {
return sr, ErrTaskNotFound
}
sr = task.fetcher.Stats()
return
}
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
err = func() error {
if task.fetcher != nil {
@@ -548,6 +611,22 @@ func (d *Downloader) GetTasks() []*Task {
return d.tasks
}
func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
if len(statues) == 0 {
return d.tasks
}
tasks := make([]*Task, 0)
for _, task := range d.tasks {
for _, status := range statues {
if task.Status == status {
tasks = append(tasks, task)
break
}
}
}
return tasks
}
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
return d.cfg.DownloaderStoreConfig, nil
}
@@ -690,6 +769,7 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
task := NewTask()
task.fetcherBuilder = fb
task.fetcher = fetcher
task.Protocol = fetcher.Name()
task.Meta = fetcher.Meta()
task.Progress = &Progress{}
initTask(task)
+2 -3
View File
@@ -4,7 +4,6 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"reflect"
"sync"
"testing"
"time"
@@ -37,7 +36,7 @@ func TestDownloader_Resolve(t *testing.T) {
},
},
}
if !reflect.DeepEqual(want, rr.Res) {
if !test.AssertResourceEqual(want, rr.Res) {
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
}
}
@@ -155,7 +154,7 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
},
},
}
if !reflect.DeepEqual(want, rr.Res) {
if !test.AssertResourceEqual(want, rr.Res) {
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
}
}
+2
View File
@@ -7,6 +7,7 @@ import (
"github.com/GopeedLab/gopeed/internal/protocol/http"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"net/url"
"sync"
@@ -22,6 +23,7 @@ type Task struct {
ID string `json:"id"`
Meta *fetcher.FetcherMeta `json:"meta"`
Status base.Status `json:"status"`
Protocol string `json:"protocol"`
Progress *Progress `json:"progress"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
+36 -14
View File
@@ -104,6 +104,17 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
WriteJson(w, model.NewNilResult())
}
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
status := r.Form["status"]
force := r.FormValue("force")
if err := Downloader.DeleteByStatues(convertStatues(status), force == "true"); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func GetTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
@@ -122,20 +133,8 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
func GetTasks(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
status := r.Form["status"]
tasks := Downloader.GetTasks()
if len(status) == 0 {
WriteJson(w, model.NewOkResult(tasks))
return
}
result := make([]*download.Task, 0)
for _, task := range tasks {
for _, s := range status {
if task.Status == base.Status(s) {
result = append(result, task)
}
}
}
WriteJson(w, model.NewOkResult(result))
tasks := Downloader.GetTasksByStatues(convertStatues(status))
WriteJson(w, model.NewOkResult(tasks))
}
func GetConfig(w http.ResponseWriter, r *http.Request) {
@@ -282,6 +281,29 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
w.Write(buf)
}
func GetStats(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
statsResult, err := Downloader.Stats(taskId)
if err != nil {
writeError(w, err.Error())
return
}
WriteJson(w, model.NewOkResult(statsResult))
}
func convertStatues(statues []string) []base.Status {
result := make([]base.Status, 0)
for _, status := range statues {
result = append(result, base.Status(status))
}
return result
}
func writeError(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(msg))
+2
View File
@@ -95,8 +95,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
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.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}/stats").HandlerFunc(GetStats)
r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
+53 -1
View File
@@ -66,7 +66,7 @@ var (
func TestResolve(t *testing.T) {
doTest(func() {
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
if !test.JsonEqual(taskRes, resp.Res) {
if !test.AssertResourceEqual(taskRes, resp.Res) {
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
}
})
@@ -242,6 +242,58 @@ func TestDeleteTaskForce(t *testing.T) {
})
}
func TestDeleteAllTasks(t *testing.T) {
doTest(func() {
taskCount := 3
var wg sync.WaitGroup
wg.Add(taskCount)
Downloader.Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally {
wg.Done()
}
})
for i := 0; i < taskCount; i++ {
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
}
wg.Wait()
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks?force=true", nil)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
if len(tasks) != 0 {
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
}
})
}
func TestDeleteTasksByStatues(t *testing.T) {
doTest(func() {
taskCount := 3
var wg sync.WaitGroup
wg.Add(taskCount)
Downloader.Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally {
wg.Done()
}
})
for i := 0; i < taskCount; i++ {
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
}
wg.Wait()
httpRequestCheckOk[any](http.MethodDelete, fmt.Sprintf("/api/v1/tasks?status=%s&force=true", base.DownloadStatusDone), nil)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
if len(tasks) != 0 {
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
}
})
}
func TestGetTasks(t *testing.T) {
doTest(func() {
var wg sync.WaitGroup
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"math"
)
var unitArr = []string{"B", "KB", "MB", "GB", "TB", "EB"}
var unitArr = []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
func ByteFmt(size int64) string {
if size == 0 {
+5 -1
View File
@@ -63,4 +63,8 @@ windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake
/extensions
/extensions
# Hive database files
database.hive
database.lock
+38 -17
View File
@@ -4,13 +4,16 @@ part 'downloader_config.g.dart';
@JsonSerializable(explicitToJson: true)
class DownloaderConfig {
String downloadDir = '';
int maxRunning = 0;
String downloadDir;
int maxRunning;
ProtocolConfig protocolConfig = ProtocolConfig();
ExtraConfig extra = ExtraConfig();
ProxyConfig proxy = ProxyConfig();
DownloaderConfig();
DownloaderConfig({
this.downloadDir = '',
this.maxRunning = 0,
});
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
_$DownloaderConfigFromJson(json);
@@ -31,11 +34,16 @@ class ProtocolConfig {
@JsonSerializable()
class HttpConfig {
String userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
int connections = 0;
String userAgent;
int connections;
bool useServerCtime;
HttpConfig();
HttpConfig({
this.userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
this.connections = 16,
this.useServerCtime = false,
});
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
_$HttpConfigFromJson(json);
@@ -69,13 +77,19 @@ class ExtraConfig {
@JsonSerializable()
class ProxyConfig {
bool enable = false;
String scheme = '';
String host = '';
String usr = '';
String pwd = '';
bool enable;
String scheme;
String host;
String usr;
String pwd;
ProxyConfig();
ProxyConfig({
this.enable = false,
this.scheme = '',
this.host = '',
this.usr = '',
this.pwd = '',
});
factory ProxyConfig.fromJson(Map<String, dynamic> json) =>
_$ProxyConfigFromJson(json);
@@ -85,13 +99,20 @@ class ProxyConfig {
@JsonSerializable()
class ExtraConfigBt {
List<String> trackerSubscribeUrls = [];
List<String> subscribeTrackers = [];
List<String> trackerSubscribeUrls;
List<String> subscribeTrackers;
bool autoUpdateTrackers;
DateTime? lastTrackerUpdateTime;
List<String> customTrackers = [];
List<String> customTrackers;
ExtraConfigBt();
ExtraConfigBt({
this.trackerSubscribeUrls = const [],
this.subscribeTrackers = const [],
this.autoUpdateTrackers = true,
this.lastTrackerUpdateTime,
this.customTrackers = const [],
});
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
_$ExtraConfigBtFromJson(json);
@@ -7,9 +7,10 @@ part of 'downloader_config.dart';
// **************************************************************************
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
DownloaderConfig()
..downloadDir = json['downloadDir'] as String
..maxRunning = json['maxRunning'] as int
DownloaderConfig(
downloadDir: json['downloadDir'] as String? ?? '',
maxRunning: json['maxRunning'] as int? ?? 0,
)
..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>?)
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?)
@@ -35,14 +36,18 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
'bt': instance.bt.toJson(),
};
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig()
..userAgent = json['userAgent'] as String
..connections = json['connections'] as int;
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
userAgent: json['userAgent'] as String? ??
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
connections: json['connections'] as int? ?? 16,
useServerCtime: json['useServerCtime'] as bool? ?? false,
);
Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
<String, dynamic>{
'userAgent': instance.userAgent,
'connections': instance.connections,
'useServerCtime': instance.useServerCtime,
};
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
@@ -67,12 +72,13 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'bt': instance.bt.toJson(),
};
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig()
..enable = json['enable'] as bool
..scheme = json['scheme'] as String
..host = json['host'] as String
..usr = json['usr'] as String
..pwd = json['pwd'] as String;
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
enable: json['enable'] as bool? ?? false,
scheme: json['scheme'] as String? ?? '',
host: json['host'] as String? ?? '',
usr: json['usr'] as String? ?? '',
pwd: json['pwd'] as String? ?? '',
);
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
<String, dynamic>{
@@ -84,24 +90,30 @@ Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
};
ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
ExtraConfigBt()
..trackerSubscribeUrls = (json['trackerSubscribeUrls'] as List<dynamic>)
.map((e) => e as String)
.toList()
..subscribeTrackers = (json['subscribeTrackers'] as List<dynamic>)
.map((e) => e as String)
.toList()
..lastTrackerUpdateTime = json['lastTrackerUpdateTime'] == null
ExtraConfigBt(
trackerSubscribeUrls: (json['trackerSubscribeUrls'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
subscribeTrackers: (json['subscribeTrackers'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
autoUpdateTrackers: json['autoUpdateTrackers'] as bool? ?? true,
lastTrackerUpdateTime: json['lastTrackerUpdateTime'] == null
? null
: DateTime.parse(json['lastTrackerUpdateTime'] as String)
..customTrackers = (json['customTrackers'] as List<dynamic>)
.map((e) => e as String)
.toList();
: DateTime.parse(json['lastTrackerUpdateTime'] as String),
customTrackers: (json['customTrackers'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
final val = <String, dynamic>{
'trackerSubscribeUrls': instance.trackerSubscribeUrls,
'subscribeTrackers': instance.subscribeTrackers,
'autoUpdateTrackers': instance.autoUpdateTrackers,
};
void writeNotNull(String key, dynamic value) {
@@ -1,8 +0,0 @@
import 'package:get/get.dart';
class ApiProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = 'YOUR-API-URL';
}
}
@@ -6,9 +6,9 @@ import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:gopeed/database/entity.dart';
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.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';
@@ -17,18 +17,14 @@ import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../core/common/start_config.dart';
import '../../../../database/database.dart';
import '../../../../i18n/message.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../../util/window_util.dart' as windows_util;
import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network";
const _startConfigAddress = "start.address";
const _startConfigApiToken = "start.apiToken";
const unixSocketPath = 'gopeed.sock';
const allTrackerSubscribeUrls = [
@@ -99,6 +95,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
_initForegroundTask().onError((error, stackTrace) =>
logger.w("initForegroundTask error", error, stackTrace));
_initTrackerUpdate().onError((error, stackTrace) =>
logger.w("initTrackerUpdate error", error, stackTrace));
}
@override
@@ -127,17 +126,18 @@ class AppController extends GetxController with WindowListener, TrayListener {
@override
void onWindowMaximize() {
windows_util.saveState(isMaximized: true);
Database.instance.saveWindowState(WindowStateEntity(isMaximized: true));
}
@override
void onWindowUnmaximize() {
windows_util.saveState(isMaximized: false);
Database.instance.saveWindowState(WindowStateEntity(isMaximized: false));
}
final _windowsResizeSave = Util.debounce(() async {
final size = await windowManager.getSize();
windows_util.saveState(width: size.width, height: size.height);
Database.instance.saveWindowState(
WindowStateEntity(width: size.width, height: size.height));
}, 500);
@override
@@ -307,13 +307,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
Future<void> loadStartConfig() async {
final defaultCfg = await _initDefaultStartConfig();
final prefs = await SharedPreferences.getInstance();
startConfig.value.network =
prefs.getString(_startConfigNetwork) ?? defaultCfg.network;
startConfig.value.address =
prefs.getString(_startConfigAddress) ?? defaultCfg.address;
startConfig.value.apiToken =
prefs.getString(_startConfigApiToken) ?? defaultCfg.apiToken;
final saveCfg = Database.instance.getStartConfig();
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
}
Future<void> loadDownloaderConfig() async {
@@ -363,12 +360,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
btConfig.trackers.toSet().toList();
}
Future<void> trackerUpdateOnStart() async {
Future<void> _initTrackerUpdate() async {
final btExtConfig = downloaderConfig.value.extra.bt;
final lastUpdateTime = btExtConfig.lastTrackerUpdateTime;
// if last update time is null or more than 1 day, update trackers
if (lastUpdateTime == null ||
lastUpdateTime.difference(DateTime.now()).inDays < 0) {
if (btExtConfig.autoUpdateTrackers &&
(lastUpdateTime == null ||
lastUpdateTime.difference(DateTime.now()).inDays < 0)) {
try {
await trackerUpdate();
} catch (e) {
@@ -392,9 +390,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
_initDownloaderConfig() async {
final config = downloaderConfig.value;
if (config.protocolConfig.http.connections == 0) {
config.protocolConfig.http.connections = 16;
}
final extra = config.extra;
if (extra.themeMode.isEmpty) {
extra.themeMode = ThemeMode.dark.name;
@@ -417,13 +412,15 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (config.downloadDir.isEmpty) {
if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isMobile()) {
} else if (Util.isAndroid()) {
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
if (downloadDir != null) {
config.downloadDir = '$downloadDir/Gopeed';
} else {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
}
} else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else {
config.downloadDir = './';
}
@@ -431,10 +428,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
Future<void> saveConfig() async {
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);
Database.instance.saveStartConfig(StartConfigEntity(
network: startConfig.value.network,
address: startConfig.value.address,
apiToken: startConfig.value.apiToken));
await putConfig(downloaderConfig.value);
}
}
@@ -12,6 +12,7 @@ import '../../../../api/model/create_task.dart';
import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart';
import '../../../../api/model/resolve_result.dart';
import '../../../../database/database.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
@@ -19,7 +20,6 @@ import '../../../routes/app_pages.dart';
import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart';
import '../../history/controller/history_controller.dart';
import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart';
@@ -35,7 +35,6 @@ class CreateView extends GetView<CreateController> {
final _httpCookieController = TextEditingController();
final _httpRefererController = TextEditingController();
final _btTrackerController = TextEditingController();
final _historyController = HistoryController();
final _availableSchemes = ["http:", "https:", "magnet:"];
@@ -117,7 +116,7 @@ class CreateView extends GetView<CreateController> {
Row(children: [
Expanded(
child: TextFormField(
autofocus: true,
autofocus: !Util.isMobile(),
controller: _urlController,
minLines: 1,
maxLines: 5,
@@ -168,10 +167,7 @@ class CreateView extends GetView<CreateController> {
icon: const Icon(Icons.history_rounded),
onPressed: () async {
List<String> resultOfHistories =
await _historyController.getAllHistory();
// reversing: display last entered history first
List<String> reverseResultOfHistories =
resultOfHistories.reversed.toList();
Database.instance.getCreateHistory() ?? [];
// show dialog box to list history
if (context.mounted) {
showGeneralDialog(
@@ -183,16 +179,14 @@ class CreateView extends GetView<CreateController> {
opacity: a1.value,
child: HistoryView(
isHistoryListEmpty:
reverseResultOfHistories.isEmpty,
resultOfHistories.isEmpty,
historyList: ListView.builder(
itemCount:
reverseResultOfHistories.length,
itemCount: resultOfHistories.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_urlController.text =
reverseResultOfHistories[
index];
resultOfHistories[index];
Navigator.pop(context);
},
child: MouseRegion(
@@ -217,9 +211,7 @@ class CreateView extends GetView<CreateController> {
10.0),
),
child: Text(
reverseResultOfHistories[
index]
.toString(),
resultOfHistories[index],
),
),
),
@@ -419,7 +411,7 @@ class CreateView extends GetView<CreateController> {
// Add url to the history
if (!isWebFileChosen) {
for (final url in urls) {
_historyController.addHistory(url);
Database.instance.saveCreateHistory(url);
}
}
@@ -1,26 +0,0 @@
import 'package:shared_preferences/shared_preferences.dart';
class HistoryController {
// Get History in List<String>
Future<List<String>> getAllHistory() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
List<String>? historyList = preferences.getStringList('history');
if(historyList == null) {
return [];
}
return historyList;
}
// Get Existing History & Add New History
void addHistory(String historyText) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
List<String> existingHistoryData = await getAllHistory();
existingHistoryData.add(historyText);
preferences.setStringList('history', existingHistoryData);
}
void clearHistory() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.remove('history');
}
}
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
import 'package:gopeed/database/database.dart';
class HistoryView extends StatefulWidget {
const HistoryView({
@@ -62,8 +62,7 @@ class _HistoryViewState extends State<HistoryView> {
children: <Widget>[
IconButton(
onPressed: () {
HistoryController _controller = HistoryController();
_controller.clearHistory();
Database.instance.clearCreateHistory();
Navigator.pop(context);
},
tooltip: "clearHistory".tr,
@@ -138,6 +138,22 @@ class SettingView extends GetView<SettingController> {
],
);
});
final buildHttpUseServerCtime = _buildConfigItem(
'useServerCtime'.tr, () => httpConfig.useServerCtime.toString(),
(Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value: httpConfig.useServerCtime,
onChanged: (bool value) {
downloaderCfg.update((val) {
val!.protocolConfig.http.useServerCtime = value;
});
debounceSave();
},
),
);
});
// bt config items start
final btConfig = downloaderCfg.value.protocolConfig.bt;
@@ -207,8 +223,13 @@ class SettingView extends GetView<SettingController> {
Expanded(
child: SwitchListTile(
controlAffinity: ListTileControlAffinity.leading,
value: true,
onChanged: (bool value) {},
value: btExtConfig.autoUpdateTrackers,
onChanged: (bool value) {
downloaderCfg.update((val) {
val!.extra.bt.autoUpdateTrackers = value;
});
debounceSave();
},
title: Text('updateDaily'.tr)),
),
],
@@ -676,6 +697,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([
buildHttpUa(),
buildHttpConnections(),
buildHttpUseServerCtime(),
]),
)),
const Text('BitTorrent'),
+103
View File
@@ -0,0 +1,103 @@
import 'dart:convert';
import 'package:gopeed/util/util.dart';
import 'package:hive/hive.dart';
import 'entity.dart';
const String _startConfig = 'startConfig';
const String _windowState = 'windowState';
const String _bookmark = 'bookmark';
const String _createHistory = 'createHistory';
class Database {
static final Database _instance = Database._internal();
static Database get instance => _instance;
factory Database() {
return _instance;
}
late Box box;
Database._internal();
Future<void> init() async {
Hive.init(Util.getStorageDir());
box = await Hive.openBox('database');
}
void save<T>(String key, T entity) {
box.put(key, jsonEncode(entity));
}
T? get<T>(String key, T Function(dynamic json) fromJsonT) {
final json = box.get(key);
if (json == null) {
return null;
}
return fromJsonT(jsonDecode(json));
}
void clear(String key) {
box.delete(key);
}
void saveStartConfig(StartConfigEntity entity) {
save<StartConfigEntity>(_startConfig, entity);
}
StartConfigEntity? getStartConfig() {
return get<StartConfigEntity>(
_startConfig, (json) => StartConfigEntity.fromJson(json));
}
/// Patch non-null fields with the original value
void saveWindowState(WindowStateEntity entity) {
final state = getWindowState();
entity.isMaximized ??= state?.isMaximized;
entity.width ??= state?.width;
entity.height ??= state?.height;
save<WindowStateEntity>(_windowState, entity);
}
WindowStateEntity? getWindowState() {
return get<WindowStateEntity>(
_windowState, (json) => WindowStateEntity.fromJson(json));
}
/// Use map to ensure that the same directory only saves the latest bookmark
void saveBookmark(MapEntry<String, String> entry) {
final map = getBookmark() ?? {};
map[entry.key] = entry.value;
save<Map<String, String>>(_bookmark, map);
}
Map<String, String>? getBookmark() {
return get<Map<String, String>>(_bookmark, (json) {
return (json as Map<String, dynamic>)
.map((key, value) => MapEntry(key, value.toString()));
});
}
void saveCreateHistory(String url) {
var list = getCreateHistory() ?? [];
list.remove(url);
list.insert(0, url);
if (list.length > 64) {
list.removeLast();
}
save<List<String>>(_createHistory, list);
}
List<String>? getCreateHistory() {
return get<List<String>>(_createHistory, (json) {
return (json as List<dynamic>).map((e) => e.toString()).toList();
});
}
void clearCreateHistory() {
clear(_createHistory);
}
}
+37
View File
@@ -0,0 +1,37 @@
import 'package:json_annotation/json_annotation.dart';
part 'entity.g.dart';
@JsonSerializable()
class StartConfigEntity {
String network;
String address;
String apiToken;
StartConfigEntity({
required this.network,
required this.address,
required this.apiToken,
});
factory StartConfigEntity.fromJson(Map<String, dynamic> json) =>
_$StartConfigEntityFromJson(json);
Map<String, dynamic> toJson() => _$StartConfigEntityToJson(this);
}
@JsonSerializable()
class WindowStateEntity {
bool? isMaximized;
double? width;
double? height;
WindowStateEntity({
this.isMaximized,
this.width,
this.height,
});
factory WindowStateEntity.fromJson(Map<String, dynamic> json) =>
_$WindowStateEntityFromJson(json);
Map<String, dynamic> toJson() => _$WindowStateEntityToJson(this);
}
+43
View File
@@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'entity.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StartConfigEntity _$StartConfigEntityFromJson(Map<String, dynamic> json) =>
StartConfigEntity(
network: json['network'] as String,
address: json['address'] as String,
apiToken: json['apiToken'] as String,
);
Map<String, dynamic> _$StartConfigEntityToJson(StartConfigEntity instance) =>
<String, dynamic>{
'network': instance.network,
'address': instance.address,
'apiToken': instance.apiToken,
};
WindowStateEntity _$WindowStateEntityFromJson(Map<String, dynamic> json) =>
WindowStateEntity(
isMaximized: json['isMaximized'] as bool?,
width: (json['width'] as num?)?.toDouble(),
height: (json['height'] as num?)?.toDouble(),
);
Map<String, dynamic> _$WindowStateEntityToJson(WindowStateEntity instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('isMaximized', instance.isMaximized);
writeNotNull('width', instance.width);
writeNotNull('height', instance.height);
return val;
}
+3
View File
@@ -5,6 +5,8 @@ const enUS = {
'tip': 'Tip',
'confirm': 'Confirm',
'cancel': 'Cancel',
'on': 'On',
'off': 'Off',
'selectAll': 'Select All',
'task': 'Tasks',
'downloading': 'downloading',
@@ -31,6 +33,7 @@ const enUS = {
'downloadDir': 'Download Directory',
'downloadDirValid': 'Please select the download directory',
'connections': 'Connections',
'useServerCtime': 'Use server time for file creation',
'maxRunning': 'Max Running Tasks',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
+3
View File
@@ -5,6 +5,8 @@ const zhCN = {
'tip': '提示',
'confirm': '确认',
'cancel': '取消',
'on': '开启',
'off': '关闭',
'selectAll': '全选',
'task': '任务',
'downloading': '下载中',
@@ -30,6 +32,7 @@ const zhCN = {
'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录',
'connections': '连接数',
'useServerCtime': '创建文件使用服务器时间',
'maxRunning': '最大下载数',
'items': '@count 项',
'subscribeTracker': '订阅 Tracker',
+3
View File
@@ -5,6 +5,8 @@ const zhTW = {
'tip': '提示',
'confirm': '確定',
'cancel': '取消',
'on': '開啟',
'off': '關閉',
'selectAll': '全選',
'task': '任務',
'downloading': '下載中',
@@ -30,6 +32,7 @@ const zhTW = {
'downloadDir': '下載目録',
'downloadDirValid': '請選擇下載目録',
'connections': '同時連接數',
'useServerCtime': '使用伺服器時間作為檔案建立時間',
'maxRunning': '最大同時下載數',
'items': '@count 項',
'subscribeTracker': '已訂閲的追蹤者列表',
+5 -9
View File
@@ -7,13 +7,13 @@ 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 'database/database.dart';
import 'i18n/message.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';
import 'util/window_util.dart' as windows_util;
void main() async {
await init();
@@ -24,11 +24,13 @@ void main() async {
Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
await Util.initStorageDir();
await Database.instance.init();
if (Util.isDesktop()) {
await windowManager.ensureInitialized();
final windowState = await windows_util.loadState();
final windowState = Database.instance.getWindowState();
final windowOptions = WindowOptions(
size: Size(windowState.width, windowState.height),
size: Size(windowState?.width ?? 800, windowState?.height ?? 600),
center: true,
skipTaskbar: false,
);
@@ -44,9 +46,6 @@ Future<void> init() async {
});
}
if (!Util.isWeb()) {
await Util.initStorageDir();
}
initLogger();
final controller = Get.put(AppController());
@@ -74,9 +73,6 @@ Future<void> init() async {
}
Future<void> onStart() async {
final appController = Get.find<AppController>();
await appController.trackerUpdateOnStart();
// if is debug mode, check language message is complete,change debug locale to your comfortable language if you want
if (kDebugMode) {
final debugLang = getLocaleKey(debugLocale);
+6 -10
View File
@@ -1,33 +1,29 @@
import 'dart:io';
import 'package:macos_secure_bookmarks/macos_secure_bookmarks.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../database/database.dart';
import 'util.dart';
class MacSecureUtil {
static const _bookmarkKey = "bookmark:";
static Future<void> saveBookmark(String dir) async {
if (!Util.isMacos()) {
return;
}
final secureBookmarks = SecureBookmarks();
final bookmark = await secureBookmarks.bookmark(Directory(dir));
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_bookmarkKey + dir, bookmark);
Database.instance.saveBookmark(MapEntry(dir, bookmark));
}
static Future<void> loadBookmark() async {
if (!Util.isMacos()) {
return;
}
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys();
final secureBookmarks = SecureBookmarks();
keys.where((k) => k.startsWith(_bookmarkKey)).forEach((k) async {
final resolvedFile = await secureBookmarks
.resolveBookmark(prefs.getString(k)!, isDirectory: true);
final bookmark = Database.instance.getBookmark() ?? {};
bookmark.forEach((_, v) async {
final resolvedFile =
await secureBookmarks.resolveBookmark(v, isDirectory: true);
await secureBookmarks.startAccessingSecurityScopedResource(resolvedFile);
});
}
+15 -13
View File
@@ -50,19 +50,21 @@ class Util {
}
static Future<void> initStorageDir() async {
var storageDir = "./";
if (Platform.isAndroid) {
storageDir = (await getExternalStorageDirectory())?.path ?? storageDir;
} else if (Platform.isIOS) {
storageDir = (await getLibraryDirectory()).path;
} else if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
// check has write permission, if not, fallback to application support dir
try {
final testFile = File(path.join(storageDir, ".test"));
await testFile.writeAsString("test");
await testFile.delete();
} catch (e) {
var storageDir = "";
if (Util.isWindows()) {
storageDir = "./";
} else if (!Util.isWeb()) {
if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
// check has write permission, if not, fallback to application support dir
try {
final testFile = File(path.join(storageDir, ".test"));
await testFile.writeAsString("test");
await testFile.delete();
} catch (e) {
storageDir = (await getApplicationSupportDirectory()).path;
}
} else {
storageDir = (await getApplicationSupportDirectory()).path;
}
}
-39
View File
@@ -1,39 +0,0 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
class WindowState {
late bool isMaximized;
late double width;
late double height;
WindowState(this.isMaximized, this.width, this.height);
}
const String _windowStateIsMaximized = "window.isMaximized";
const String _windowStateWidth = "window.width";
const String _windowStateHeight = "window.height";
/// Save window state to storage.
void saveState({bool? isMaximized, double? width, double? height}) async {
final prefs = await SharedPreferences.getInstance();
if (isMaximized != null) {
await prefs.setBool(_windowStateIsMaximized, isMaximized);
}
if (width != null) {
await prefs.setDouble(_windowStateWidth, width);
}
if (height != null) {
await prefs.setDouble(_windowStateHeight, height);
}
}
/// Load window state from storage.
Future<WindowState> loadState() async {
final prefs = await SharedPreferences.getInstance();
return WindowState(
prefs.getBool(_windowStateIsMaximized) ?? false,
prefs.getDouble(_windowStateWidth) ?? 800,
prefs.getDouble(_windowStateHeight) ?? 600,
);
}
+9 -1
View File
@@ -405,6 +405,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.1"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.3"
http:
dependency: transitive
description:
@@ -798,7 +806,7 @@ packages:
source: hosted
version: "3.3.1"
shared_preferences:
dependency: "direct main"
dependency: transitive
description:
name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
+2 -2
View File
@@ -15,7 +15,7 @@ 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.5.3+1
version: 1.5.4+1
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -46,7 +46,6 @@ dependencies:
macos_secure_bookmarks: ^0.2.1
rounded_loading_button: ^2.1.0
url_launcher: ^6.1.6
shared_preferences: ^2.2.0
logger: ^1.3.0
desktop_drop: ^0.4.3
package_info_plus: ^4.0.0
@@ -64,6 +63,7 @@ dependencies:
open_filex: ^4.3.4
tray_manager: ^0.2.1
lecle_downloads_path_provider: ^0.0.2+8
hive: ^2.2.3
dev_dependencies:
flutter_test:
sdk: flutter