mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 20:33:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f778596390 | ||
|
|
c2cbc14492 | ||
|
|
62c22bc6b4 | ||
|
|
b89c7bc608 | ||
|
|
f2cd686563 | ||
|
|
ef5ee99f04 | ||
|
|
6d136604db | ||
|
|
38ff711ea9 | ||
|
|
a1a8abc25f | ||
|
|
abccbabc78 | ||
|
|
c26eb26b8f | ||
|
|
74beea6044 | ||
|
|
e548d7e0c9 | ||
|
|
a996d57914 | ||
|
|
f7dc60ad50 | ||
|
|
8425a96ac9 | ||
|
|
99f4e8c895 | ||
|
|
cc0f61b527 | ||
|
|
922ef969a6 | ||
|
|
3a542054fc | ||
|
|
895a8d2ee4 | ||
|
|
29d0f5ebcf | ||
|
|
31228e3edf | ||
|
|
31a32d7e14 | ||
|
|
65fd6fb489 | ||
|
|
953b878a89 | ||
|
|
ae4ca3da58 |
No files matched your search
+60
-149
@@ -7,14 +7,14 @@ on:
|
|||||||
description: 'Build platform'
|
description: 'Build platform'
|
||||||
required: true
|
required: true
|
||||||
default: 'all'
|
default: 'all'
|
||||||
channel:
|
test:
|
||||||
description: 'Snap channel'
|
description: 'Test mode'
|
||||||
required: true
|
required: true
|
||||||
default: 'stable'
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.21"
|
||||||
FLUTTER_VERSION: "3.16.1"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -23,11 +23,11 @@ jobs:
|
|||||||
get-release:
|
get-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
tag_name: ${{ steps.get-release.outputs.tag_name }}
|
version: ${{ steps.get-release.outputs.version }}
|
||||||
upload_url: ${{ steps.get-release.outputs.upload_url }}
|
upload_url: ${{ steps.get-release.outputs.upload_url }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: monkeyWie/get-latest-release@v1
|
- uses: monkeyWie/get-latest-release@v2.1
|
||||||
id: get-release
|
id: get-release
|
||||||
with:
|
with:
|
||||||
myToken: ${{ github.token }}
|
myToken: ${{ github.token }}
|
||||||
@@ -40,19 +40,20 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
|
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\"
|
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
|
cd ui/flutter
|
||||||
|
$TAG = "v$env:VERSION"
|
||||||
flutter build windows
|
flutter build windows
|
||||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||||
$system = "C:\Windows\System32"
|
$system = "C:\Windows\System32"
|
||||||
@@ -65,7 +66,7 @@ jobs:
|
|||||||
cp $system\vcruntime140_1.dll $release
|
cp $system\vcruntime140_1.dll $release
|
||||||
|
|
||||||
New-Item -Path build\windows\Output -ItemType Directory
|
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
|
cd build/windows
|
||||||
echo @"
|
echo @"
|
||||||
@@ -73,7 +74,7 @@ jobs:
|
|||||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
#define MyAppName "Gopeed"
|
#define MyAppName "Gopeed"
|
||||||
#define MyAppVersion "$($env:VERSION.substring(1))"
|
#define MyAppVersion "$env:VERSION"
|
||||||
#define MyAppPublisher "monkeyWie"
|
#define MyAppPublisher "monkeyWie"
|
||||||
#define MyAppURL "https://gopeed.com"
|
#define MyAppURL "https://gopeed.com"
|
||||||
#define MyAppExeName "gopeed.exe"
|
#define MyAppExeName "gopeed.exe"
|
||||||
@@ -89,7 +90,7 @@ jobs:
|
|||||||
AppPublisherURL={#MyAppURL}
|
AppPublisherURL={#MyAppURL}
|
||||||
AppSupportURL={#MyAppURL}
|
AppSupportURL={#MyAppURL}
|
||||||
AppUpdatesURL={#MyAppURL}
|
AppUpdatesURL={#MyAppURL}
|
||||||
DefaultDirName={autopf}\gopeed
|
DefaultDirName={autopf}\gopeedrsion
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
LicenseFile=..\..\..\..\LICENSE
|
LicenseFile=..\..\..\..\LICENSE
|
||||||
; Remove the following line to run in administrative install mode (install for all users.)
|
; 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
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
"@ > setup.iss
|
"@ > setup.iss
|
||||||
iscc.exe setup.iss
|
iscc.exe setup.iss
|
||||||
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
mv "Output\gopeed.exe" "Output\Gopeed-$TAG-windows-amd64.exe"
|
||||||
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
|
Compress-Archive -Path "Output\Gopeed-$TAG-windows-amd64.exe" -DestinationPath "Output\Gopeed-$TAG-windows-amd64.zip"
|
||||||
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
Remove-Item -Path "Output\Gopeed-$TAG-windows-amd64.exe"
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -143,9 +144,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go install src.techknowlogick.com/xgo@latest
|
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
|
mv libgopeed-*.dylib libgopeed.dylib
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -176,12 +179,12 @@ jobs:
|
|||||||
path: ui/flutter/lib-arm64
|
path: ui/flutter/lib-arm64
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PROJECT_DIR=$(pwd)
|
PROJECT_DIR=$(pwd)
|
||||||
|
|
||||||
# amd64 lib
|
# 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
|
# universal binary
|
||||||
mkdir -p ui/flutter/macos/Frameworks
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
||||||
@@ -203,7 +206,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
mkdir dist
|
mkdir dist
|
||||||
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
|
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -223,126 +226,32 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
sudo apt update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev libappindicator3-dev
|
# flutter build dependencies
|
||||||
|
sudo apt install -y ninja-build libgtk-3-dev libappindicator3-dev
|
||||||
|
# rpm build dependencies
|
||||||
|
sudo apt install -y rpm patchelf
|
||||||
|
# appimage build dependencies
|
||||||
|
sudo apt install -y libfuse2 locate
|
||||||
|
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||||
|
chmod +x appimagetool
|
||||||
|
sudo mv appimagetool /usr/local/bin/
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
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
|
cd ui/flutter
|
||||||
flutter build linux
|
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||||
mkdir -p build/dist
|
flutter_distributor package --platform linux --targets appimage,deb
|
||||||
|
cd dist/*
|
||||||
# Build portable
|
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
|
||||||
tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./
|
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
|
||||||
|
|
||||||
# Build deb package
|
|
||||||
mkdir -p debian/gopeed/opt/gopeed
|
|
||||||
mkdir -p debian/gopeed/usr/share/applications
|
|
||||||
mkdir -p debian/gopeed/usr/share/icons/hicolor/scalable/apps
|
|
||||||
mkdir -p debian/gopeed/DEBIAN
|
|
||||||
|
|
||||||
cp -r build/linux/x64/release/bundle/* debian/gopeed/opt/gopeed/
|
|
||||||
cp assets/icon/icon.svg debian/gopeed/usr/share/icons/hicolor/scalable/apps/com.gopeed.Gopeed.svg
|
|
||||||
|
|
||||||
cat>debian/gopeed/DEBIAN/postinst<<EOF
|
|
||||||
#!/bin/bash
|
|
||||||
ln -sf /opt/gopeed/gopeed /usr/bin/gopeed
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat>debian/gopeed/DEBIAN/postrm<<EOF
|
|
||||||
#!/bin/bash
|
|
||||||
rm -rf /usr/bin/gopeed
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 0755 debian/gopeed/DEBIAN/postinst
|
|
||||||
chmod 0755 debian/gopeed/DEBIAN/postrm
|
|
||||||
|
|
||||||
cat>debian/gopeed/DEBIAN/control<<EOF
|
|
||||||
Package: gopeed
|
|
||||||
Version: ${VERSION:1}
|
|
||||||
Architecture: amd64
|
|
||||||
Essential: no
|
|
||||||
Priority: optional
|
|
||||||
Depends:
|
|
||||||
Maintainer: monkeyWie
|
|
||||||
Description: High speed downloader that supports all platforms.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cp linux/assets/com.gopeed.Gopeed.desktop debian/gopeed/usr/share/applications/gopeed.desktop
|
|
||||||
|
|
||||||
dpkg-deb --build debian/gopeed
|
|
||||||
mv debian/gopeed.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
|
|
||||||
|
|
||||||
# Build AppImage
|
|
||||||
sudo apt-get -y install libfuse2
|
|
||||||
wget -O appimage-builder https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
|
||||||
chmod +x appimage-builder
|
|
||||||
mkdir AppDir
|
|
||||||
cp -r build/linux/x64/release/bundle/* AppDir
|
|
||||||
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
|
|
||||||
cp assets/icon/icon.svg AppDir/usr/share/icons/hicolor/scalable/apps/gopeed.svg
|
|
||||||
cat>AppImageBuilder.yml<<EOF
|
|
||||||
version: 1
|
|
||||||
AppDir:
|
|
||||||
path: ${PWD}/AppDir
|
|
||||||
app_info:
|
|
||||||
id: com.gopeed.gopeed
|
|
||||||
name: Gopeed
|
|
||||||
icon: gopeed
|
|
||||||
version: ${VERSION:1}
|
|
||||||
exec: gopeed
|
|
||||||
exec_args: \$@
|
|
||||||
apt:
|
|
||||||
arch: amd64
|
|
||||||
allow_unauthenticated: true
|
|
||||||
sources:
|
|
||||||
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted
|
|
||||||
universe multiverse
|
|
||||||
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main
|
|
||||||
restricted universe multiverse
|
|
||||||
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports
|
|
||||||
main restricted universe multiverse
|
|
||||||
- sourceline: deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted
|
|
||||||
universe multiverse
|
|
||||||
include: []
|
|
||||||
files:
|
|
||||||
include: []
|
|
||||||
exclude:
|
|
||||||
- usr/share/man
|
|
||||||
- usr/share/doc/*/README.*
|
|
||||||
- usr/share/doc/*/changelog.*
|
|
||||||
- usr/share/doc/*/NEWS.*
|
|
||||||
- usr/share/doc/*/TODO.*
|
|
||||||
test:
|
|
||||||
fedora-30:
|
|
||||||
image: appimagecrafters/tests-env:fedora-30
|
|
||||||
command: ./AppRun
|
|
||||||
debian-stable:
|
|
||||||
image: appimagecrafters/tests-env:debian-stable
|
|
||||||
command: ./AppRun
|
|
||||||
archlinux-latest:
|
|
||||||
image: appimagecrafters/tests-env:archlinux-latest
|
|
||||||
command: ./AppRun
|
|
||||||
centos-7:
|
|
||||||
image: appimagecrafters/tests-env:centos-7
|
|
||||||
command: ./AppRun
|
|
||||||
ubuntu-xenial:
|
|
||||||
image: appimagecrafters/tests-env:ubuntu-xenial
|
|
||||||
command: ./AppRun
|
|
||||||
AppImage:
|
|
||||||
arch: x86_64
|
|
||||||
update-information: guess
|
|
||||||
EOF
|
|
||||||
./appimage-builder --skip-tests --recipe AppImageBuilder.yml
|
|
||||||
mv Gopeed-*-x86_64.AppImage build/dist/Gopeed-$VERSION-linux-amd64.AppImage
|
|
||||||
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: ui/flutter/build/dist/*
|
asset_path: ui/flutter/dist/*/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-snap:
|
build-snap:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
||||||
@@ -362,9 +271,9 @@ jobs:
|
|||||||
channel: latest/stable
|
channel: latest/stable
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
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
|
cd ui/flutter
|
||||||
|
|
||||||
sudo snap install snapcraft --classic
|
sudo snap install snapcraft --classic
|
||||||
@@ -373,7 +282,7 @@ jobs:
|
|||||||
|
|
||||||
cat>snap/snapcraft.yaml<<EOF
|
cat>snap/snapcraft.yaml<<EOF
|
||||||
name: gopeed
|
name: gopeed
|
||||||
version: ${VERSION:1}
|
version: $VERSION
|
||||||
summary: A modern download manager
|
summary: A modern download manager
|
||||||
description: High speed downloader that supports all platforms.
|
description: High speed downloader that supports all platforms.
|
||||||
|
|
||||||
@@ -416,7 +325,7 @@ jobs:
|
|||||||
|
|
||||||
# Snapcraft login
|
# Snapcraft login
|
||||||
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_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:
|
build-android:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -435,7 +344,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
APK_KEYSTORE: ${{ secrets.APK_KEYSTORE }}
|
APK_KEYSTORE: ${{ secrets.APK_KEYSTORE }}
|
||||||
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}
|
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}
|
||||||
APK_STORE_PASSWORD: ${{ secrets.APK_STORE_PASSWORD }}
|
APK_STORE_PASSWORD: ${{ secrets.APK_STORE_PASSWORD }}
|
||||||
@@ -443,12 +352,12 @@ jobs:
|
|||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
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
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk
|
flutter build apk
|
||||||
mkdir dist
|
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
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -469,19 +378,19 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
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
|
cd ui/flutter
|
||||||
flutter build ios --no-codesign
|
flutter build ios --no-codesign
|
||||||
mkdir Payload
|
mkdir Payload
|
||||||
cp -r build/ios/iphoneos/Runner.app Payload
|
cp -r build/ios/iphoneos/Runner.app Payload
|
||||||
zip -r -y Payload.zip Payload/Runner.app
|
zip -r -y Payload.zip Payload/Runner.app
|
||||||
mkdir dist
|
mkdir dist
|
||||||
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
|
mv Payload.zip dist/Gopeed-v$VERSION-ios.ipa
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -502,13 +411,14 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --web-renderer html
|
flutter build web --web-renderer html
|
||||||
cd ../../
|
cd ../../
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
mkdir -p dist/zip
|
mkdir -p dist/zip
|
||||||
|
|
||||||
goos_arr=(windows darwin linux)
|
goos_arr=(windows darwin linux)
|
||||||
goarch_arr=(386 amd64 arm64)
|
goarch_arr=(386 amd64 arm64)
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
@@ -518,9 +428,9 @@ jobs:
|
|||||||
if [ $goos = "darwin" ]; then
|
if [ $goos = "darwin" ]; then
|
||||||
goos_name="macos"
|
goos_name="macos"
|
||||||
fi
|
fi
|
||||||
name=gopeed-web-$VERSION-$goos_name-$goarch
|
name=gopeed-web-v$VERSION-$goos_name-$goarch
|
||||||
dir="dist/$name/"
|
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 \
|
&& cd $dir \
|
||||||
&& file=$(ls -AU | head -1) \
|
&& file=$(ls -AU | head -1) \
|
||||||
&& mkdir $name \
|
&& mkdir $name \
|
||||||
@@ -558,15 +468,16 @@ jobs:
|
|||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
with:
|
with:
|
||||||
push: true
|
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
||||||
|
build-args: VERSION=${{ env.VERSION }}
|
||||||
platforms: |
|
platforms: |
|
||||||
linux/386
|
linux/386
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm64
|
linux/arm64
|
||||||
linux/arm/v6
|
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
|
linux/arm64/v8
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
liwei2633/gopeed:latest
|
||||||
liwei2633/gopeed:${{ env.VERSION }}
|
liwei2633/gopeed:v${{ env.VERSION }}
|
||||||
@@ -27,8 +27,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.21"
|
||||||
FLUTTER_VERSION: "3.16.1"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
ui/flutter/dist/
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,17 +1,18 @@
|
|||||||
FROM instrumentisto/flutter:3.16.1 AS flutter
|
FROM instrumentisto/flutter:3.16.8 AS flutter
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
COPY ./ui/flutter ./
|
COPY ./ui/flutter ./
|
||||||
RUN flutter build web --web-renderer html
|
RUN flutter build web --web-renderer html
|
||||||
|
|
||||||
FROM golang:1.19.3 AS go
|
FROM golang:1.21.6 AS go
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=flutter /app/build/web ./cmd/web/dist
|
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
|
FROM alpine:3.14.2
|
||||||
LABEL maintainer="monkeyWie"
|
LABEL maintainer="monkeyWie"
|
||||||
|
|||||||
@@ -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).
|
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)
|
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
|
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||||
- [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
|
|
||||||
|
|
||||||
### Command tool
|
### Command tool
|
||||||
|
|
||||||
@@ -42,26 +92,6 @@ use `go install`:
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
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
|
## Donate
|
||||||
|
|
||||||
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
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
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|
||||||
|
|||||||
+66
-43
@@ -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
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
- [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` からウェブページにアクセスできます。
|
|
||||||
|
|
||||||
## 寄付
|
## 寄付
|
||||||
|
|
||||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||||
|
|
||||||
## ショーケース
|
## ショーケース
|
||||||
|
|
||||||
@@ -77,8 +100,8 @@ docker コンテナが起動しているときは、`http://localhost:9999` か
|
|||||||
|
|
||||||
### 環境
|
### 環境
|
||||||
|
|
||||||
1. Go 言語 1.19+
|
1. Go 言語 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### クローン
|
### クローン
|
||||||
|
|
||||||
|
|||||||
+66
-35
@@ -17,21 +17,72 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
|
|
||||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
访问 ✈ [官方网站](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
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
- [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` 命令
|
|
||||||
|
|
||||||
### 命令行工具
|
### 命令行工具
|
||||||
|
|
||||||
@@ -41,26 +92,6 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
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)以支持这个项目的发展,非常感谢!
|
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||||
@@ -77,8 +108,8 @@ docker-compose up -d
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
|
|
||||||
|
|||||||
+66
-35
@@ -17,21 +17,72 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
|
|
||||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
前往 ✈ [主頁](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
|
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||||
- [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`
|
|
||||||
|
|
||||||
### 使用CLI安裝
|
### 使用CLI安裝
|
||||||
|
|
||||||
@@ -41,26 +92,6 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
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)以支持該項目的持續發展,謝謝!
|
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||||
@@ -77,8 +108,8 @@ docker-compose up -d
|
|||||||
|
|
||||||
### 開發環境
|
### 開發環境
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆項目
|
### 克隆項目
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||||
github.com/anacrolix/torrent v1.52.5
|
github.com/anacrolix/torrent v1.53.3
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
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/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.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/rs/zerolog v1.31.0
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.8
|
||||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // 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/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.3.0 // indirect
|
github.com/anacrolix/chansync v0.4.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
|
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.2.1 // indirect
|
github.com/anacrolix/envpprof v1.3.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
|
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
|
||||||
github.com/anacrolix/go-libutp v1.3.1 // 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 v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.0 // indirect
|
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||||
github.com/anacrolix/stm v0.4.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.4.0 // indirect
|
github.com/anacrolix/sync v0.5.1 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
github.com/anacrolix/upnp v0.1.3 // indirect
|
||||||
github.com/anacrolix/utp v0.1.0 // indirect
|
github.com/anacrolix/utp v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.3.0 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // 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/edsrzf/mmap-go v1.1.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.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/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // 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-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.4.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // 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-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/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.2 // indirect
|
github.com/pion/datachannel v1.5.5 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.4 // indirect
|
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||||
github.com/pion/ice/v2 v2.2.6 // indirect
|
github.com/pion/ice/v2 v2.3.12 // indirect
|
||||||
github.com/pion/interceptor v0.1.11 // indirect
|
github.com/pion/interceptor v0.1.25 // indirect
|
||||||
github.com/pion/logging v0.2.2 // 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/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.9 // indirect
|
github.com/pion/rtcp v1.2.13 // indirect
|
||||||
github.com/pion/rtp v1.7.13 // indirect
|
github.com/pion/rtp v1.8.3 // indirect
|
||||||
github.com/pion/sctp v1.8.2 // indirect
|
github.com/pion/sctp v1.8.9 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.5 // indirect
|
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.9 // indirect
|
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||||
github.com/pion/stun v0.3.5 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/transport v0.13.1 // indirect
|
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||||
github.com/pion/transport/v2 v2.0.0 // indirect
|
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
||||||
github.com/pion/udp v0.1.4 // indirect
|
|
||||||
github.com/pion/webrtc/v3 v3.1.42 // indirect
|
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // 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/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.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
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
go.opentelemetry.io/otel v1.8.0 // indirect
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.8.0 // indirect
|
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.14.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/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.17.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // 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
|
||||||
)
|
)
|
||||||
@@ -21,6 +21,8 @@ type Fetcher interface {
|
|||||||
Pause() error
|
Pause() error
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
|
// Stats refreshes health statistics and returns the latest information
|
||||||
|
Stats() any
|
||||||
// Meta returns the meta information of the download.
|
// Meta returns the meta information of the download.
|
||||||
Meta() *FetcherMeta
|
Meta() *FetcherMeta
|
||||||
// Progress returns the progress of the download.
|
// Progress returns the progress of the download.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package bt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -63,6 +65,8 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := torrent.NewDefaultClientConfig()
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
cfg.ListenPort = f.config.ListenPort
|
cfg.ListenPort = f.config.ListenPort
|
||||||
if f.ctl.ProxyUrl != nil {
|
if f.ctl.ProxyUrl != nil {
|
||||||
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||||
@@ -128,6 +132,7 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
file.Download()
|
file.Download()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +193,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Stats() any {
|
||||||
|
stats := f.torrent.Stats()
|
||||||
|
baseStats := &bt.Stats{
|
||||||
|
TotalPeers: stats.TotalPeers,
|
||||||
|
ActivePeers: stats.ActivePeers,
|
||||||
|
ConnectedSeeders: stats.ConnectedSeeders,
|
||||||
|
}
|
||||||
|
return baseStats
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
if !f.torrentReady.Load() {
|
if !f.torrentReady.Load() {
|
||||||
return f.progress
|
return f.progress
|
||||||
@@ -207,6 +222,7 @@ func (f *Fetcher) updateRes() {
|
|||||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
Hash: f.torrent.InfoHash().String(),
|
Hash: f.torrent.InfoHash().String(),
|
||||||
}
|
}
|
||||||
|
f.torrent.PeerConns()
|
||||||
for i, file := range f.torrent.Files() {
|
for i, file := range f.torrent.Files() {
|
||||||
res.Files[i] = &base.FileInfo{
|
res.Files[i] = &base.FileInfo{
|
||||||
Name: filepath.Base(file.DisplayPath()),
|
Name: filepath.Base(file.DisplayPath()),
|
||||||
@@ -295,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,4 +3,5 @@ package http
|
|||||||
type config struct {
|
type config struct {
|
||||||
UserAgent string `json:"userAgent"`
|
UserAgent string `json:"userAgent"`
|
||||||
Connections int `json:"connections"`
|
Connections int `json:"connections"`
|
||||||
|
UseServerCtime bool `json:"useServerCtime"`
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/xiaoqidun/setft"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,8 +106,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{},
|
Files: []*base.FileInfo{},
|
||||||
}
|
}
|
||||||
if base.HttpCodePartialContent == httpResp.StatusCode {
|
|
||||||
// 返回206响应码表示支持断点下载
|
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
|
res.Range = true
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
@@ -129,8 +132,17 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
} else {
|
} else {
|
||||||
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
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{
|
file := &base.FileInfo{
|
||||||
Size: res.Size,
|
Size: res.Size,
|
||||||
|
Ctime: lastModifiedTime,
|
||||||
}
|
}
|
||||||
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
||||||
if contentDisposition != "" {
|
if contentDisposition != "" {
|
||||||
@@ -217,6 +229,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Stats() any {
|
||||||
|
// todo http download health
|
||||||
|
return &fhttp.Stats{}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
p := make(fetcher.Progress, 0)
|
p := make(fetcher.Progress, 0)
|
||||||
if len(f.chunks) > 0 {
|
if len(f.chunks) > 0 {
|
||||||
@@ -251,6 +268,10 @@ func (f *Fetcher) fetch() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.file.Close()
|
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
|
f.doneCh <- err
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -269,7 +290,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
// retry until all remain chunks failed
|
// retry until all remain chunks failed
|
||||||
for true {
|
for {
|
||||||
// if chunk is completed, return
|
// if chunk is completed, return
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return
|
||||||
@@ -306,26 +327,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
// If canceled, do not retry
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// retry request after 1 second
|
|
||||||
chunk.retryTimes = chunk.retryTimes + 1
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Http request success, reset retry times
|
// Http request success, reset retry times
|
||||||
chunk.retryTimes = 0
|
chunk.retryTimes = 0
|
||||||
err = func() error {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := resp.Body.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
@@ -342,7 +350,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}()
|
}()
|
||||||
|
if err != nil {
|
||||||
|
// If canceled, do not retry
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// retry request after 1 second
|
||||||
|
chunk.retryTimes = chunk.retryTimes + 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
// if chunk is completed, reset other not complete chunks retry times
|
// if chunk is completed, reset other not complete chunks retry times
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, c := range f.chunks {
|
for _, c := range f.chunks {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -69,7 +68,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
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)
|
downloadWithProxy(httpListener, proxyListener, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_ConfigConnections(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
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()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -338,14 +361,11 @@ func buildFetcher() *Fetcher {
|
|||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher() fetcher.Fetcher {
|
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherBuilder).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
|
||||||
Connections: 16,
|
|
||||||
}
|
|
||||||
newController.GetConfig = func(v any) bool {
|
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 false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/armon/go-socks5"
|
"github.com/armon/go-socks5"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -337,3 +339,11 @@ func StartSocks5Server(usr, pwd string) net.Listener {
|
|||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return 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)
|
||||||
|
}
|
||||||
@@ -16,10 +16,13 @@ const (
|
|||||||
HttpCodePartialContent = 206
|
HttpCodePartialContent = 206
|
||||||
|
|
||||||
HttpHeaderRange = "Range"
|
HttpHeaderRange = "Range"
|
||||||
|
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||||
HttpHeaderContentLength = "Content-Length"
|
HttpHeaderContentLength = "Content-Length"
|
||||||
HttpHeaderContentRange = "Content-Range"
|
HttpHeaderContentRange = "Content-Range"
|
||||||
HttpHeaderContentDisposition = "Content-Disposition"
|
HttpHeaderContentDisposition = "Content-Disposition"
|
||||||
HttpHeaderUserAgent = "Usr-Agent"
|
HttpHeaderUserAgent = "User-Agent"
|
||||||
|
HttpHeaderLastModified = "Last-Modified"
|
||||||
|
|
||||||
|
HttpHeaderBytes = "bytes"
|
||||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||||
)
|
)
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request download request
|
// Request download request
|
||||||
@@ -62,6 +63,7 @@ type FileInfo struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
Ctime *time.Time `json:"ctime"`
|
||||||
|
|
||||||
Req *Request `json:"req"`
|
Req *Request `json:"req"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
-1
@@ -6,8 +6,11 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/internal/logger"
|
"github.com/GopeedLab/gopeed/internal/logger"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
"github.com/virtuald/go-paniclog"
|
"github.com/virtuald/go-paniclog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -33,6 +36,7 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTaskNotFound = errors.New("task not found")
|
ErrTaskNotFound = errors.New("task not found")
|
||||||
|
ErrUnSupportedProtocol = errors.New("unsupported protocol")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener func(event *Event)
|
type Listener func(event *Event)
|
||||||
@@ -91,6 +95,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||||
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
||||||
d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
|
d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
|
||||||
if cfg.ProductionMode {
|
if cfg.ProductionMode {
|
||||||
@@ -119,6 +124,11 @@ func (d *Downloader) Setup() error {
|
|||||||
}
|
}
|
||||||
// init default config
|
// init default config
|
||||||
d.cfg.DownloaderStoreConfig.Init()
|
d.cfg.DownloaderStoreConfig.Init()
|
||||||
|
for _, fb := range d.fetcherBuilders {
|
||||||
|
f := fb.Build()
|
||||||
|
d.setupFetcher(f)
|
||||||
|
|
||||||
|
}
|
||||||
// load tasks from storage
|
// load tasks from storage
|
||||||
var tasks []*Task
|
var tasks []*Task
|
||||||
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
||||||
@@ -207,7 +217,7 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
|||||||
if ok {
|
if ok {
|
||||||
return fetchBuilder, nil
|
return fetchBuilder, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("unsupported protocol")
|
return nil, ErrUnSupportedProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
||||||
@@ -444,6 +454,13 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
|
|||||||
for i, t := range d.tasks {
|
for i, t := range d.tasks {
|
||||||
if t.ID == id {
|
if t.ID == id {
|
||||||
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -456,6 +473,56 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
|
|||||||
return
|
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) {
|
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
||||||
err = func() error {
|
err = func() error {
|
||||||
if task.fetcher != nil {
|
if task.fetcher != nil {
|
||||||
@@ -544,6 +611,22 @@ func (d *Downloader) GetTasks() []*Task {
|
|||||||
return d.tasks
|
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) {
|
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
|
||||||
return d.cfg.DownloaderStoreConfig, nil
|
return d.cfg.DownloaderStoreConfig, nil
|
||||||
}
|
}
|
||||||
@@ -591,6 +674,26 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
d.emit(EventKeyDone, task)
|
d.emit(EventKeyDone, task)
|
||||||
d.emit(EventKeyFinally, task, err)
|
d.emit(EventKeyFinally, task, err)
|
||||||
d.notifyRunning()
|
d.notifyRunning()
|
||||||
|
|
||||||
|
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
||||||
|
downloadFilePath := task.Meta.SingleFilepath()
|
||||||
|
if e.AutoTorrent && strings.HasSuffix(downloadFilePath, ".torrent") {
|
||||||
|
go func() {
|
||||||
|
_, err2 := d.CreateDirect(
|
||||||
|
&base.Request{
|
||||||
|
URL: downloadFilePath,
|
||||||
|
},
|
||||||
|
&base.Options{
|
||||||
|
Path: task.Meta.Opts.Path,
|
||||||
|
SelectFiles: make([]int, 0),
|
||||||
|
})
|
||||||
|
if err2 != nil {
|
||||||
|
d.Logger.Error().Err(err2).Msgf("auto create torrent task failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) doOnError(task *Task, err error) {
|
func (d *Downloader) doOnError(task *Task, err error) {
|
||||||
@@ -666,6 +769,7 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
|||||||
task := NewTask()
|
task := NewTask()
|
||||||
task.fetcherBuilder = fb
|
task.fetcherBuilder = fb
|
||||||
task.fetcher = fetcher
|
task.fetcher = fetcher
|
||||||
|
task.Protocol = fetcher.Name()
|
||||||
task.Meta = fetcher.Meta()
|
task.Meta = fetcher.Meta()
|
||||||
task.Progress = &Progress{}
|
task.Progress = &Progress{}
|
||||||
initTask(task)
|
initTask(task)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"reflect"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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)
|
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)
|
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,12 +254,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, er
|
|||||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Res.CalcSize(nil)
|
|
||||||
}
|
|
||||||
ctx.Res.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
|
ctx.Res.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
|
||||||
for _, file := range ctx.Res.Files {
|
for _, file := range ctx.Res.Files {
|
||||||
file.Name = util.ReplaceInvalidFilename(file.Name)
|
file.Name = util.ReplaceInvalidFilename(file.Name)
|
||||||
}
|
}
|
||||||
|
ctx.Res.CalcSize(nil)
|
||||||
|
}
|
||||||
res = ctx.Res
|
res = ctx.Res
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -22,6 +23,7 @@ type Task struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||||
Status base.Status `json:"status"`
|
Status base.Status `json:"status"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
Progress *Progress `json:"progress"`
|
Progress *Progress `json:"progress"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|||||||
@@ -3,3 +3,12 @@ package bt
|
|||||||
type ReqExtra struct {
|
type ReqExtra struct {
|
||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stats for download
|
||||||
|
type Stats struct {
|
||||||
|
// bt stats
|
||||||
|
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
||||||
|
TotalPeers int `json:"totalPeers"`
|
||||||
|
ActivePeers int `json:"activePeers"`
|
||||||
|
ConnectedSeeders int `json:"connectedSeeders"`
|
||||||
|
}
|
||||||
@@ -8,4 +8,12 @@ type ReqExtra struct {
|
|||||||
|
|
||||||
type OptsExtra struct {
|
type OptsExtra struct {
|
||||||
Connections int `json:"connections"`
|
Connections int `json:"connections"`
|
||||||
|
// AutoTorrent when task download complete, and it is a .torrent file, it will be auto create a new task for the torrent file
|
||||||
|
AutoTorrent bool `json:"autoTorrent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats for download
|
||||||
|
type Stats struct {
|
||||||
|
// http stats
|
||||||
|
// health indicators of http
|
||||||
}
|
}
|
||||||
+35
-13
@@ -104,6 +104,17 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteJson(w, model.NewNilResult())
|
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) {
|
func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
taskId := vars["id"]
|
taskId := vars["id"]
|
||||||
@@ -122,20 +133,8 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
status := r.Form["status"]
|
status := r.Form["status"]
|
||||||
tasks := Downloader.GetTasks()
|
tasks := Downloader.GetTasksByStatues(convertStatues(status))
|
||||||
if len(status) == 0 {
|
|
||||||
WriteJson(w, model.NewOkResult(tasks))
|
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -282,6 +281,29 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(buf)
|
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) {
|
func writeError(w http.ResponseWriter, msg string) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte(msg))
|
w.Write([]byte(msg))
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package rest
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -93,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/pause").HandlerFunc(PauseAllTask)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
|
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/{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/{id}").HandlerFunc(GetTask)
|
||||||
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
|
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.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
|
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
|
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
|
||||||
@@ -131,6 +135,20 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recover panic
|
||||||
|
r.Use(func(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
err := errors.WithStack(fmt.Errorf("%v", v))
|
||||||
|
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
srv = &http.Server{Handler: handlers.CORS(
|
srv = &http.Server{Handler: handlers.CORS(
|
||||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
||||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||||
|
|||||||
+53
-1
@@ -66,7 +66,7 @@ var (
|
|||||||
func TestResolve(t *testing.T) {
|
func TestResolve(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
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))
|
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) {
|
func TestGetTasks(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"math"
|
"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 {
|
func ByteFmt(size int64) string {
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
|
|||||||
@@ -64,3 +64,7 @@ windows/flutter/generated_plugin_registrant.h
|
|||||||
windows/flutter/generated_plugins.cmake
|
windows/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
/extensions
|
/extensions
|
||||||
|
|
||||||
|
# Hive database files
|
||||||
|
database.hive
|
||||||
|
database.lock
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
output: dist/
|
||||||
@@ -4,13 +4,16 @@ part 'downloader_config.g.dart';
|
|||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class DownloaderConfig {
|
class DownloaderConfig {
|
||||||
String downloadDir = '';
|
String downloadDir;
|
||||||
int maxRunning = 0;
|
int maxRunning;
|
||||||
ProtocolConfig protocolConfig = ProtocolConfig();
|
ProtocolConfig protocolConfig = ProtocolConfig();
|
||||||
ExtraConfig extra = ExtraConfig();
|
ExtraConfig extra = ExtraConfig();
|
||||||
ProxyConfig proxy = ProxyConfig();
|
ProxyConfig proxy = ProxyConfig();
|
||||||
|
|
||||||
DownloaderConfig();
|
DownloaderConfig({
|
||||||
|
this.downloadDir = '',
|
||||||
|
this.maxRunning = 0,
|
||||||
|
});
|
||||||
|
|
||||||
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
|
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
_$DownloaderConfigFromJson(json);
|
_$DownloaderConfigFromJson(json);
|
||||||
@@ -31,11 +34,16 @@ class ProtocolConfig {
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class HttpConfig {
|
class HttpConfig {
|
||||||
String userAgent =
|
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;
|
||||||
int connections = 0;
|
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) =>
|
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
_$HttpConfigFromJson(json);
|
_$HttpConfigFromJson(json);
|
||||||
@@ -69,13 +77,19 @@ class ExtraConfig {
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ProxyConfig {
|
class ProxyConfig {
|
||||||
bool enable = false;
|
bool enable;
|
||||||
String scheme = '';
|
String scheme;
|
||||||
String host = '';
|
String host;
|
||||||
String usr = '';
|
String usr;
|
||||||
String pwd = '';
|
String pwd;
|
||||||
|
|
||||||
ProxyConfig();
|
ProxyConfig({
|
||||||
|
this.enable = false,
|
||||||
|
this.scheme = '',
|
||||||
|
this.host = '',
|
||||||
|
this.usr = '',
|
||||||
|
this.pwd = '',
|
||||||
|
});
|
||||||
|
|
||||||
factory ProxyConfig.fromJson(Map<String, dynamic> json) =>
|
factory ProxyConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
_$ProxyConfigFromJson(json);
|
_$ProxyConfigFromJson(json);
|
||||||
@@ -85,13 +99,20 @@ class ProxyConfig {
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ExtraConfigBt {
|
class ExtraConfigBt {
|
||||||
List<String> trackerSubscribeUrls = [];
|
List<String> trackerSubscribeUrls;
|
||||||
List<String> subscribeTrackers = [];
|
List<String> subscribeTrackers;
|
||||||
|
bool autoUpdateTrackers;
|
||||||
DateTime? lastTrackerUpdateTime;
|
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) =>
|
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
|
||||||
_$ExtraConfigBtFromJson(json);
|
_$ExtraConfigBtFromJson(json);
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ part of 'downloader_config.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||||
DownloaderConfig()
|
DownloaderConfig(
|
||||||
..downloadDir = json['downloadDir'] as String
|
downloadDir: json['downloadDir'] as String? ?? '',
|
||||||
..maxRunning = json['maxRunning'] as int
|
maxRunning: json['maxRunning'] as int? ?? 0,
|
||||||
|
)
|
||||||
..protocolConfig = ProtocolConfig.fromJson(
|
..protocolConfig = ProtocolConfig.fromJson(
|
||||||
json['protocolConfig'] as Map<String, dynamic>?)
|
json['protocolConfig'] as Map<String, dynamic>?)
|
||||||
..extra = ExtraConfig.fromJson(json['extra'] 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(),
|
'bt': instance.bt.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig()
|
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||||
..userAgent = json['userAgent'] as String
|
userAgent: json['userAgent'] as String? ??
|
||||||
..connections = json['connections'] as int;
|
'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) =>
|
Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'userAgent': instance.userAgent,
|
'userAgent': instance.userAgent,
|
||||||
'connections': instance.connections,
|
'connections': instance.connections,
|
||||||
|
'useServerCtime': instance.useServerCtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
|
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
|
||||||
@@ -67,12 +72,13 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
|||||||
'bt': instance.bt.toJson(),
|
'bt': instance.bt.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig()
|
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
||||||
..enable = json['enable'] as bool
|
enable: json['enable'] as bool? ?? false,
|
||||||
..scheme = json['scheme'] as String
|
scheme: json['scheme'] as String? ?? '',
|
||||||
..host = json['host'] as String
|
host: json['host'] as String? ?? '',
|
||||||
..usr = json['usr'] as String
|
usr: json['usr'] as String? ?? '',
|
||||||
..pwd = json['pwd'] as String;
|
pwd: json['pwd'] as String? ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
@@ -84,24 +90,30 @@ Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
|
ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
|
||||||
ExtraConfigBt()
|
ExtraConfigBt(
|
||||||
..trackerSubscribeUrls = (json['trackerSubscribeUrls'] as List<dynamic>)
|
trackerSubscribeUrls: (json['trackerSubscribeUrls'] as List<dynamic>?)
|
||||||
.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList()
|
.toList() ??
|
||||||
..subscribeTrackers = (json['subscribeTrackers'] as List<dynamic>)
|
const [],
|
||||||
.map((e) => e as String)
|
subscribeTrackers: (json['subscribeTrackers'] as List<dynamic>?)
|
||||||
.toList()
|
?.map((e) => e as String)
|
||||||
..lastTrackerUpdateTime = json['lastTrackerUpdateTime'] == null
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
autoUpdateTrackers: json['autoUpdateTrackers'] as bool? ?? true,
|
||||||
|
lastTrackerUpdateTime: json['lastTrackerUpdateTime'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['lastTrackerUpdateTime'] as String)
|
: DateTime.parse(json['lastTrackerUpdateTime'] as String),
|
||||||
..customTrackers = (json['customTrackers'] as List<dynamic>)
|
customTrackers: (json['customTrackers'] as List<dynamic>?)
|
||||||
.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList();
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
|
Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
'trackerSubscribeUrls': instance.trackerSubscribeUrls,
|
'trackerSubscribeUrls': instance.trackerSubscribeUrls,
|
||||||
'subscribeTrackers': instance.subscribeTrackers,
|
'subscribeTrackers': instance.subscribeTrackers,
|
||||||
|
'autoUpdateTrackers': instance.autoUpdateTrackers,
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Options {
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class OptsExtraHttp {
|
class OptsExtraHttp {
|
||||||
int connections = 0;
|
int connections = 0;
|
||||||
|
bool autoTorrent = false;
|
||||||
|
|
||||||
OptsExtraHttp();
|
OptsExtraHttp();
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,12 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
|
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
|
||||||
OptsExtraHttp()..connections = json['connections'] as int;
|
OptsExtraHttp()
|
||||||
|
..connections = json['connections'] as int
|
||||||
|
..autoTorrent = json['autoTorrent'] as bool;
|
||||||
|
|
||||||
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
|
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'connections': instance.connections,
|
'connections': instance.connections,
|
||||||
|
'autoTorrent': instance.autoTorrent,
|
||||||
};
|
};
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
class ApiProvider extends GetConnect {
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
httpClient.baseUrl = 'YOUR-API-URL';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,9 @@ import 'package:app_links/app_links.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:get/get.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:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:uri_to_file/uri_to_file.dart';
|
import 'package:uri_to_file/uri_to_file.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -16,6 +17,7 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/downloader_config.dart';
|
import '../../../../api/model/downloader_config.dart';
|
||||||
import '../../../../core/common/start_config.dart';
|
import '../../../../core/common/start_config.dart';
|
||||||
|
import '../../../../database/database.dart';
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../../util/log_util.dart';
|
import '../../../../util/log_util.dart';
|
||||||
@@ -23,10 +25,6 @@ import '../../../../util/package_info.dart';
|
|||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
|
|
||||||
const _startConfigNetwork = "start.network";
|
|
||||||
const _startConfigAddress = "start.address";
|
|
||||||
const _startConfigApiToken = "start.apiToken";
|
|
||||||
|
|
||||||
const unixSocketPath = 'gopeed.sock';
|
const unixSocketPath = 'gopeed.sock';
|
||||||
|
|
||||||
const allTrackerSubscribeUrls = [
|
const allTrackerSubscribeUrls = [
|
||||||
@@ -49,7 +47,12 @@ const allTrackerCdns = [
|
|||||||
[
|
[
|
||||||
"https://ghproxy.com/https://raw.githubusercontent.com",
|
"https://ghproxy.com/https://raw.githubusercontent.com",
|
||||||
r".*github.com(/.*)/raw(/.*)"
|
r".*github.com(/.*)/raw(/.*)"
|
||||||
]
|
],
|
||||||
|
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
|
[
|
||||||
|
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
|
||||||
|
r".*github.com(/.*)/raw(/.*)"
|
||||||
|
],
|
||||||
];
|
];
|
||||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||||
key: (v) => v as String,
|
key: (v) => v as String,
|
||||||
@@ -92,6 +95,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
_initForegroundTask().onError((error, stackTrace) =>
|
_initForegroundTask().onError((error, stackTrace) =>
|
||||||
logger.w("initForegroundTask error", error, stackTrace));
|
logger.w("initForegroundTask error", error, stackTrace));
|
||||||
|
|
||||||
|
_initTrackerUpdate().onError((error, stackTrace) =>
|
||||||
|
logger.w("initTrackerUpdate error", error, stackTrace));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -118,6 +124,27 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
trayManager.popUpContextMenu();
|
trayManager.popUpContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMaximize() {
|
||||||
|
Database.instance.saveWindowState(WindowStateEntity(isMaximized: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowUnmaximize() {
|
||||||
|
Database.instance.saveWindowState(WindowStateEntity(isMaximized: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _windowsResizeSave = Util.debounce(() async {
|
||||||
|
final size = await windowManager.getSize();
|
||||||
|
Database.instance.saveWindowState(
|
||||||
|
WindowStateEntity(width: size.width, height: size.height));
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowResize() {
|
||||||
|
_windowsResizeSave();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initDeepLinks() async {
|
Future<void> _initDeepLinks() async {
|
||||||
// currently only support android
|
// currently only support android
|
||||||
if (!Util.isAndroid()) {
|
if (!Util.isAndroid()) {
|
||||||
@@ -280,13 +307,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
Future<void> loadStartConfig() async {
|
Future<void> loadStartConfig() async {
|
||||||
final defaultCfg = await _initDefaultStartConfig();
|
final defaultCfg = await _initDefaultStartConfig();
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final saveCfg = Database.instance.getStartConfig();
|
||||||
startConfig.value.network =
|
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
||||||
prefs.getString(_startConfigNetwork) ?? defaultCfg.network;
|
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
||||||
startConfig.value.address =
|
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
||||||
prefs.getString(_startConfigAddress) ?? defaultCfg.address;
|
|
||||||
startConfig.value.apiToken =
|
|
||||||
prefs.getString(_startConfigApiToken) ?? defaultCfg.apiToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadDownloaderConfig() async {
|
Future<void> loadDownloaderConfig() async {
|
||||||
@@ -336,12 +360,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
btConfig.trackers.toSet().toList();
|
btConfig.trackers.toSet().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> trackerUpdateOnStart() async {
|
Future<void> _initTrackerUpdate() async {
|
||||||
final btExtConfig = downloaderConfig.value.extra.bt;
|
final btExtConfig = downloaderConfig.value.extra.bt;
|
||||||
final lastUpdateTime = btExtConfig.lastTrackerUpdateTime;
|
final lastUpdateTime = btExtConfig.lastTrackerUpdateTime;
|
||||||
// if last update time is null or more than 1 day, update trackers
|
// if last update time is null or more than 1 day, update trackers
|
||||||
if (lastUpdateTime == null ||
|
if (btExtConfig.autoUpdateTrackers &&
|
||||||
lastUpdateTime.difference(DateTime.now()).inDays < 0) {
|
(lastUpdateTime == null ||
|
||||||
|
lastUpdateTime.difference(DateTime.now()).inDays < 0)) {
|
||||||
try {
|
try {
|
||||||
await trackerUpdate();
|
await trackerUpdate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -365,9 +390,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
_initDownloaderConfig() async {
|
_initDownloaderConfig() async {
|
||||||
final config = downloaderConfig.value;
|
final config = downloaderConfig.value;
|
||||||
if (config.protocolConfig.http.connections == 0) {
|
|
||||||
config.protocolConfig.http.connections = 16;
|
|
||||||
}
|
|
||||||
final extra = config.extra;
|
final extra = config.extra;
|
||||||
if (extra.themeMode.isEmpty) {
|
if (extra.themeMode.isEmpty) {
|
||||||
extra.themeMode = ThemeMode.dark.name;
|
extra.themeMode = ThemeMode.dark.name;
|
||||||
@@ -391,9 +413,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
if (Util.isDesktop()) {
|
if (Util.isDesktop()) {
|
||||||
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
||||||
} else if (Util.isAndroid()) {
|
} else if (Util.isAndroid()) {
|
||||||
config.downloadDir = (await getExternalStorageDirectory())?.path ??
|
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
|
||||||
(await getApplicationDocumentsDirectory()).path;
|
if (downloadDir != null) {
|
||||||
return;
|
config.downloadDir = '$downloadDir/Gopeed';
|
||||||
|
} else {
|
||||||
|
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||||
|
}
|
||||||
} else if (Util.isIOS()) {
|
} else if (Util.isIOS()) {
|
||||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||||
} else {
|
} else {
|
||||||
@@ -403,10 +428,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveConfig() async {
|
Future<void> saveConfig() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
Database.instance.saveStartConfig(StartConfigEntity(
|
||||||
await prefs.setString(_startConfigNetwork, startConfig.value.network);
|
network: startConfig.value.network,
|
||||||
await prefs.setString(_startConfigAddress, startConfig.value.address);
|
address: startConfig.value.address,
|
||||||
await prefs.setString(_startConfigApiToken, startConfig.value.apiToken);
|
apiToken: startConfig.value.apiToken));
|
||||||
await putConfig(downloaderConfig.value);
|
await putConfig(downloaderConfig.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
@@ -14,7 +15,8 @@ class AppView extends GetView<AppController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final config = controller.downloaderConfig.value;
|
final config = controller.downloaderConfig.value;
|
||||||
return GetMaterialApp.router(
|
return WithForegroundTask(
|
||||||
|
child: GetMaterialApp.router(
|
||||||
useInheritedMediaQuery: true,
|
useInheritedMediaQuery: true,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: GopeedTheme.light,
|
theme: GopeedTheme.light,
|
||||||
@@ -30,6 +32,7 @@ class AppView extends GetView<AppController> {
|
|||||||
],
|
],
|
||||||
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
||||||
getPages: AppPages.routes,
|
getPages: AppPages.routes,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,16 +4,15 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
|
|
||||||
import 'package:gopeed/app/modules/history/views/history_view.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/create_task.dart';
|
import '../../../../api/model/create_task.dart';
|
||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../api/model/resolve_result.dart';
|
import '../../../../api/model/resolve_result.dart';
|
||||||
import '../../../../api/model/resource.dart';
|
import '../../../../database/database.dart';
|
||||||
import '../../../../util/input_formatter.dart';
|
import '../../../../util/input_formatter.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
@@ -21,6 +20,7 @@ import '../../../routes/app_pages.dart';
|
|||||||
import '../../../views/directory_selector.dart';
|
import '../../../views/directory_selector.dart';
|
||||||
import '../../../views/file_list_view.dart';
|
import '../../../views/file_list_view.dart';
|
||||||
import '../../app/controllers/app_controller.dart';
|
import '../../app/controllers/app_controller.dart';
|
||||||
|
import '../../history/views/history_view.dart';
|
||||||
import '../controllers/create_controller.dart';
|
import '../controllers/create_controller.dart';
|
||||||
|
|
||||||
class CreateView extends GetView<CreateController> {
|
class CreateView extends GetView<CreateController> {
|
||||||
@@ -35,7 +35,6 @@ class CreateView extends GetView<CreateController> {
|
|||||||
final _httpCookieController = TextEditingController();
|
final _httpCookieController = TextEditingController();
|
||||||
final _httpRefererController = TextEditingController();
|
final _httpRefererController = TextEditingController();
|
||||||
final _btTrackerController = TextEditingController();
|
final _btTrackerController = TextEditingController();
|
||||||
final _historyController = HistoryController();
|
|
||||||
|
|
||||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
Row(children: [
|
Row(children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
autofocus: true,
|
autofocus: !Util.isMobile(),
|
||||||
controller: _urlController,
|
controller: _urlController,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
@@ -168,10 +167,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
icon: const Icon(Icons.history_rounded),
|
icon: const Icon(Icons.history_rounded),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
List<String> resultOfHistories =
|
List<String> resultOfHistories =
|
||||||
await _historyController.getAllHistory();
|
Database.instance.getCreateHistory() ?? [];
|
||||||
// reversing: display last entered history first
|
|
||||||
List<String> reverseResultOfHistories =
|
|
||||||
resultOfHistories.reversed.toList();
|
|
||||||
// show dialog box to list history
|
// show dialog box to list history
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
@@ -183,16 +179,14 @@ class CreateView extends GetView<CreateController> {
|
|||||||
opacity: a1.value,
|
opacity: a1.value,
|
||||||
child: HistoryView(
|
child: HistoryView(
|
||||||
isHistoryListEmpty:
|
isHistoryListEmpty:
|
||||||
reverseResultOfHistories.isEmpty,
|
resultOfHistories.isEmpty,
|
||||||
historyList: ListView.builder(
|
historyList: ListView.builder(
|
||||||
itemCount:
|
itemCount: resultOfHistories.length,
|
||||||
reverseResultOfHistories.length,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_urlController.text =
|
_urlController.text =
|
||||||
reverseResultOfHistories[
|
resultOfHistories[index];
|
||||||
index];
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
@@ -217,9 +211,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
10.0),
|
10.0),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
reverseResultOfHistories[
|
resultOfHistories[index],
|
||||||
index]
|
|
||||||
.toString(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -409,15 +401,20 @@ class CreateView extends GetView<CreateController> {
|
|||||||
try {
|
try {
|
||||||
_confirmController.start();
|
_confirmController.start();
|
||||||
if (_confirmFormKey.currentState!.validate()) {
|
if (_confirmFormKey.currentState!.validate()) {
|
||||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
final isWebFileChosen =
|
||||||
|
Util.isWeb() && controller.fileDataUri.isNotEmpty;
|
||||||
|
final submitUrl = isWebFileChosen
|
||||||
? controller.fileDataUri.value
|
? controller.fileDataUri.value
|
||||||
: _urlController.text;
|
: _urlController.text;
|
||||||
|
|
||||||
final urls = Util.textToLines(submitUrl);
|
final urls = Util.textToLines(submitUrl);
|
||||||
// Add url to the history
|
// Add url to the history
|
||||||
|
if (!isWebFileChosen) {
|
||||||
for (final url in urls) {
|
for (final url in urls) {
|
||||||
_historyController.addHistory(url);
|
Database.instance.saveCreateHistory(url);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Check if is direct download, there has two ways to direct download
|
Check if is direct download, there has two ways to direct download
|
||||||
1. Direct download option is checked
|
1. Direct download option is checked
|
||||||
@@ -433,7 +430,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
name: isMultiLine ? "" : _renameController.text,
|
name: isMultiLine ? "" : _renameController.text,
|
||||||
path: _pathController.text,
|
path: _pathController.text,
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: parseReqOpts())));
|
extra: parseReqOptsExtra())));
|
||||||
}));
|
}));
|
||||||
Get.rootDelegate.offNamed(Routes.TASK);
|
Get.rootDelegate.offNamed(Routes.TASK);
|
||||||
} else {
|
} else {
|
||||||
@@ -471,11 +468,10 @@ class CreateView extends GetView<CreateController> {
|
|||||||
return reqExtra;
|
return reqExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
Object? parseReqOpts() {
|
Object? parseReqOptsExtra() {
|
||||||
return _connectionsController.text.isEmpty
|
return OptsExtraHttp()
|
||||||
? null
|
..connections = int.tryParse(_connectionsController.text) ?? 0
|
||||||
: (OptsExtraHttp()
|
..autoTorrent = true;
|
||||||
..connections = int.parse(_connectionsController.text));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _hitText() {
|
String _hitText() {
|
||||||
@@ -541,7 +537,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
showMessage('tip'.tr, 'noFileSelected'.tr);
|
showMessage('tip'.tr, 'noFileSelected'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final optExtra = parseReqOpts();
|
final optExtra = parseReqOptsExtra();
|
||||||
if (createFormKey.currentState!.validate()) {
|
if (createFormKey.currentState!.validate()) {
|
||||||
if (rr.id.isEmpty) {
|
if (rr.id.isEmpty) {
|
||||||
// from extension resolve result
|
// from extension resolve result
|
||||||
|
|||||||
@@ -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:flutter/material.dart';
|
||||||
import 'package:get/get.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 {
|
class HistoryView extends StatefulWidget {
|
||||||
const HistoryView({
|
const HistoryView({
|
||||||
@@ -62,8 +62,7 @@ class _HistoryViewState extends State<HistoryView> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
HistoryController _controller = HistoryController();
|
Database.instance.clearCreateHistory();
|
||||||
_controller.clearHistory();
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
tooltip: "clearHistory".tr,
|
tooltip: "clearHistory".tr,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
@@ -12,7 +11,7 @@ class HomeView extends GetView<HomeController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
|
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
|
||||||
switch (currentRoute?.location) {
|
switch (currentRoute?.uri.path) {
|
||||||
case Routes.EXTENSION:
|
case Routes.EXTENSION:
|
||||||
controller.currentIndex.value = 1;
|
controller.currentIndex.value = 1;
|
||||||
break;
|
break;
|
||||||
@@ -24,8 +23,7 @@ class HomeView extends GetView<HomeController> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return WithForegroundTask(
|
return Scaffold(
|
||||||
child: Scaffold(
|
|
||||||
// extendBody: true,
|
// extendBody: true,
|
||||||
body: Row(
|
body: Row(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -158,7 +156,6 @@ class HomeView extends GetView<HomeController> {
|
|||||||
// },
|
// },
|
||||||
// )
|
// )
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// bt config items start
|
||||||
final btConfig = downloaderCfg.value.protocolConfig.bt;
|
final btConfig = downloaderCfg.value.protocolConfig.bt;
|
||||||
@@ -207,8 +223,13 @@ class SettingView extends GetView<SettingController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
value: true,
|
value: btExtConfig.autoUpdateTrackers,
|
||||||
onChanged: (bool value) {},
|
onChanged: (bool value) {
|
||||||
|
downloaderCfg.update((val) {
|
||||||
|
val!.extra.bt.autoUpdateTrackers = value;
|
||||||
|
});
|
||||||
|
debounceSave();
|
||||||
|
},
|
||||||
title: Text('updateDaily'.tr)),
|
title: Text('updateDaily'.tr)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -365,6 +386,18 @@ class SettingView extends GetView<SettingController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildThanks() {
|
||||||
|
const thankPage =
|
||||||
|
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
||||||
|
return ListTile(
|
||||||
|
title: Text('thanks'.tr),
|
||||||
|
subtitle: Text('thanksDesc'.tr),
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(Uri.parse(thankPage), mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// advanced config proxy items start
|
// advanced config proxy items start
|
||||||
final buildProxy = _buildConfigItem(
|
final buildProxy = _buildConfigItem(
|
||||||
'proxy',
|
'proxy',
|
||||||
@@ -387,9 +420,8 @@ class SettingView extends GetView<SettingController> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final items = <Widget>[
|
final scheme = SizedBox(
|
||||||
SizedBox(
|
width: 100,
|
||||||
width: 150,
|
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: proxy.scheme,
|
value: proxy.scheme,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
@@ -414,18 +446,17 @@ class SettingView extends GetView<SettingController> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
];
|
|
||||||
|
|
||||||
final arr = proxy.host.split(':');
|
final arr = proxy.host.split(':');
|
||||||
var ip = '';
|
var host = '';
|
||||||
var port = '';
|
var port = '';
|
||||||
if (arr.length > 1) {
|
if (arr.length > 1) {
|
||||||
ip = arr[0];
|
host = arr[0];
|
||||||
port = arr[1];
|
port = arr[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
final ipController = TextEditingController(text: ip);
|
final ipController = TextEditingController(text: host);
|
||||||
final portController = TextEditingController(text: port);
|
final portController = TextEditingController(text: port);
|
||||||
updateAddress() async {
|
updateAddress() async {
|
||||||
final newAddress = '${ipController.text}:${portController.text}';
|
final newAddress = '${ipController.text}:${portController.text}';
|
||||||
@@ -438,25 +469,18 @@ class SettingView extends GetView<SettingController> {
|
|||||||
|
|
||||||
ipController.addListener(updateAddress);
|
ipController.addListener(updateAddress);
|
||||||
portController.addListener(updateAddress);
|
portController.addListener(updateAddress);
|
||||||
items.addAll([
|
final server = [
|
||||||
const Padding(padding: EdgeInsets.only(left: 20)),
|
Flexible(
|
||||||
SizedBox(
|
|
||||||
width: 150,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: ipController,
|
controller: ipController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'IP',
|
labelText: 'server'.tr,
|
||||||
contentPadding: EdgeInsets.all(0.0),
|
contentPadding: const EdgeInsets.all(0.0),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||||
SizedBox(
|
Flexible(
|
||||||
width: 150,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: portController,
|
controller: portController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -470,40 +494,29 @@ class SettingView extends GetView<SettingController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
];
|
||||||
|
|
||||||
final usrController = TextEditingController(text: proxy.usr);
|
final usrController = TextEditingController(text: proxy.usr);
|
||||||
final pwdController = TextEditingController(text: proxy.pwd);
|
final pwdController = TextEditingController(text: proxy.pwd);
|
||||||
|
|
||||||
final auth = [
|
final auth = [
|
||||||
SizedBox(
|
Flexible(
|
||||||
width: 150,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: usrController,
|
controller: usrController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'username'.tr,
|
labelText: 'username'.tr,
|
||||||
contentPadding: const EdgeInsets.all(0.0),
|
contentPadding: const EdgeInsets.all(0.0),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||||
SizedBox(
|
Flexible(
|
||||||
width: 150,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: pwdController,
|
controller: pwdController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'password'.tr,
|
labelText: 'password'.tr,
|
||||||
contentPadding: const EdgeInsets.all(0.0),
|
contentPadding: const EdgeInsets.all(0.0),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
|
||||||
NumericalRangeFormatter(min: 0, max: 65535),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -513,8 +526,9 @@ class SettingView extends GetView<SettingController> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _addPadding([
|
children: _addPadding([
|
||||||
switcher,
|
switcher,
|
||||||
|
scheme,
|
||||||
Row(
|
Row(
|
||||||
children: items,
|
children: server,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: auth,
|
children: auth,
|
||||||
@@ -534,7 +548,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
(Key key) {
|
(Key key) {
|
||||||
final items = <Widget>[
|
final items = <Widget>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 80,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: startCfg.value.network,
|
value: startCfg.value.network,
|
||||||
onChanged: Util.isDesktop()
|
onChanged: Util.isDesktop()
|
||||||
@@ -585,8 +599,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
portController.addListener(updateAddress);
|
portController.addListener(updateAddress);
|
||||||
items.addAll([
|
items.addAll([
|
||||||
const Padding(padding: EdgeInsets.only(left: 20)),
|
const Padding(padding: EdgeInsets.only(left: 20)),
|
||||||
SizedBox(
|
Flexible(
|
||||||
width: 150,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: ipController,
|
controller: ipController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
@@ -600,8 +613,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||||
SizedBox(
|
Flexible(
|
||||||
width: 150,
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: portController,
|
controller: portController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -685,6 +697,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
buildHttpUa(),
|
buildHttpUa(),
|
||||||
buildHttpConnections(),
|
buildHttpConnections(),
|
||||||
|
buildHttpUseServerCtime(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
const Text('BitTorrent'),
|
const Text('BitTorrent'),
|
||||||
@@ -710,6 +723,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
buildHomepage(),
|
buildHomepage(),
|
||||||
buildVersion(),
|
buildVersion(),
|
||||||
|
buildThanks(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
|
|||||||
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
!Util.isWeb() && !Util.isIOS()
|
!Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: const Icon(Icons.folder_open),
|
icon: const Icon(Icons.folder_open),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ const enUS = {
|
|||||||
'tip': 'Tip',
|
'tip': 'Tip',
|
||||||
'confirm': 'Confirm',
|
'confirm': 'Confirm',
|
||||||
'cancel': 'Cancel',
|
'cancel': 'Cancel',
|
||||||
|
'on': 'On',
|
||||||
|
'off': 'Off',
|
||||||
'selectAll': 'Select All',
|
'selectAll': 'Select All',
|
||||||
'task': 'Tasks',
|
'task': 'Tasks',
|
||||||
'downloading': 'downloading',
|
'downloading': 'downloading',
|
||||||
@@ -31,6 +33,7 @@ const enUS = {
|
|||||||
'downloadDir': 'Download Directory',
|
'downloadDir': 'Download Directory',
|
||||||
'downloadDirValid': 'Please select the download directory',
|
'downloadDirValid': 'Please select the download directory',
|
||||||
'connections': 'Connections',
|
'connections': 'Connections',
|
||||||
|
'useServerCtime': 'Use server time for file creation',
|
||||||
'maxRunning': 'Max Running Tasks',
|
'maxRunning': 'Max Running Tasks',
|
||||||
'items': '@count items',
|
'items': '@count items',
|
||||||
'subscribeTracker': 'Subscribe Tracker',
|
'subscribeTracker': 'Subscribe Tracker',
|
||||||
@@ -79,7 +82,11 @@ const enUS = {
|
|||||||
'serviceText': 'Running',
|
'serviceText': 'Running',
|
||||||
'network': 'Network',
|
'network': 'Network',
|
||||||
'proxy': 'Proxy',
|
'proxy': 'Proxy',
|
||||||
|
'server': 'Server',
|
||||||
'username': 'Username',
|
'username': 'Username',
|
||||||
'password': 'Password',
|
'password': 'Password',
|
||||||
|
'thanks': 'Thanks',
|
||||||
|
'thanksDesc':
|
||||||
|
'Thanks to all the contributors who have helped build and develop the Gopeed community!',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -57,5 +57,8 @@ const faIR = {
|
|||||||
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
||||||
'newVersionUpdate': 'بروزرسانی',
|
'newVersionUpdate': 'بروزرسانی',
|
||||||
'newVersionLater': 'بعداً',
|
'newVersionLater': 'بعداً',
|
||||||
|
'thanks': 'تشکر',
|
||||||
|
'thanksDesc':
|
||||||
|
'از تمامی افرادی که در ساخت و توسعهی جامعهی Gopeed مشارکت کردهاند، سپاسگزاری میکنیم!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -59,5 +59,7 @@ const jaJP = {
|
|||||||
'newVersionTitle': '新しいバージョン @version を発見する',
|
'newVersionTitle': '新しいバージョン @version を発見する',
|
||||||
'newVersionUpdate': 'アップデート',
|
'newVersionUpdate': 'アップデート',
|
||||||
'newVersionLater': '後で',
|
'newVersionLater': '後で',
|
||||||
|
'thanks': '感謝',
|
||||||
|
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Loaded 50 of 61 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user