Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andmonkeyWie 0bd306773d Replace redundant chunked tests with targeted non-range pause/resume test
Remove TestFetcher_DownloadChunkedContinue (redundant with existing tests).
Replace TestFetcher_DownloadChunkedNotExceedSize with
TestFetcher_DownloadNoRangePauseResume which specifically targets the bug:
- Documents the root cause (non-range resume writes at accumulated offset
  instead of byte 0)
- Asserts downloaded file size does not exceed source file size (the exact
  bug symptom from the issue)
- Verifies file integrity via MD5

Co-authored-by: monkeyWie <[email protected]>
2026-03-12 01:22:06 +00:00
copilot-swe-agent[bot]andmonkeyWie 8b02844ec3 Fix tests: use StartTestCustomServer (no Content-Length) to match real bug scenario
Remove StartTestNoRangeServer and its tests since the real bug is about
servers that don't return Content-Length at all (like GitHub's zip downloads),
not servers with Content-Length but no Range support.

The chunked tests (TestFetcher_DownloadChunkedContinue and
TestFetcher_DownloadChunkedNotExceedSize) correctly simulate the real-world
bug using StartTestCustomServer which serves files without Content-Length.

Co-authored-by: monkeyWie <[email protected]>
2026-03-11 10:53:52 +00:00
copilot-swe-agent[bot]andmonkeyWie b8e517b35b Fix data race in expandConnections: remove orphaned waitForCompletion goroutine
The go f.waitForCompletion() goroutine in expandConnections was redundant
since the downloadLoop's slow start loop already handles stateSteady by
calling waitForCompletion directly. The orphaned goroutine could race with
doStart() when writing f.ctx during pause/resume.

Co-authored-by: monkeyWie <[email protected]>
2026-03-11 02:59:41 +00:00
copilot-swe-agent[bot]andmonkeyWie 2daa9c45df Fix non-range download bug: downloaded bytes exceeding total file size
When HTTP server doesn't return Content-Length or doesn't support Range
requests, the download can accumulate extra bytes on pause/resume because:
1. Each retry/resume re-downloads from the beginning but writes at the
   accumulated offset
2. No size limit enforcement for non-range downloads
3. onDownloadComplete incorrectly uses chunk.remain() for non-range
   downloads, which gives negative values for chunk(0,0)

Fixes:
- Reset chunk state in downloadChunkOnce and runConnectionFallback for
  non-range downloads since server will send from beginning
- Add size limit checks in download loops when Size is known
- Fix onDownloadComplete to use conn.Completed for non-range downloads
  instead of unreliable chunk.remain()
- Add test cases for non-range download, pause/resume, and size limit

Co-authored-by: monkeyWie <[email protected]>
2026-03-11 02:50:48 +00:00
copilot-swe-agent[bot] 27c0f81a1e Initial plan 2026-03-11 02:30:03 +00:00
Jaime Muñoz Martín 1907f404ac feat(lang): Catalan language added (#1303) 2026-03-11 10:26:55 +08:00
Tuna Dinçer 72c5ba7d20 feat(lang): update Turkish translation (#1304) 2026-03-10 13:38:02 +08:00
Levi 5b0254f7c0 release: v1.9.2 2026-03-08 19:52:32 +08:00
Levi d36b345f92 feat: extension store (#1301) 2026-03-06 21:53:19 +08:00
Locon213 bc5c818eae feat(desktop): implement native desktop notifications for task completion and errors (#1293) 2026-03-01 11:12:40 +08:00
Levi 98bd3305b7 feat: update Flutter version to 3.41.2 (#1290) 2026-02-26 20:42:34 +08:00
Tuna Dinçer 3f38848051 feat: Turkish localization improvements 2026-02-25 09:54:15 +08:00
Levi 82d7f8ec81 feat: update debug extension IDs retrieval to use environment (#1281) 2026-02-14 17:25:46 +08:00
Levi a6981d58e9 release: v1.9.1 2026-02-08 18:40:59 +08:00
Copilot 1b1631b41a feat: support post-task script hooks (#1265) 2026-02-08 18:40:06 +08:00
Levi 3eb6df55df feat: implement task patching functionality (#1267) 2026-02-06 23:42:57 +08:00
Levi 6de63c3f63 fix: low FPS on Windows (#1279) 2026-02-06 22:38:31 +08:00
Inori 969615f353 ci: docker image optimization (#1274) 2026-02-05 16:25:59 +08:00
Levi 5a88b98314 fix: add missing lib to Windows ARM64 bundle (#1273) 2026-02-04 18:41:32 +08:00
Levi 2674b1c39e ci: upgrade flutter version on windows arm (#1269) 2026-02-03 21:13:25 +08:00
Copilot 4c90b38247 fix: gbk filename encoding in ZIP extraction (#1260) 2026-01-29 22:37:12 +08:00
Levi 93088b22f1 fix: improve retry handling for expired HTTP redirect url (#1263) 2026-01-29 22:13:42 +08:00
Levi fe90c1d261 fix: deep link hidden handling on MacOS (#1262) 2026-01-28 18:57:09 +08:00
Levi 640e798d70 release: v1.9.0 2026-01-23 22:38:11 +08:00
Levi ba2df0ecff docs: update windows arm download link 2026-01-23 22:33:00 +08:00
Levi d61da67456 feat: add anonymous analytics (#1254) 2026-01-23 22:29:15 +08:00
Levi 7c696ee6e0 feat: upgrade Flutter version to 3.38 (#1256) 2026-01-23 21:28:13 +08:00
Copilot 7b840b4261 feat: add config for auto-torrent (#1219) 2026-01-22 22:18:34 +08:00
Copilot a3c1f84c2c feat: register as android download handler for browser integration (#1251) 2026-01-22 18:33:23 +08:00
Locon213 8d33e1a5b3 feat: add automatic cleanup options for completed torrents and missing files (#1243) 2026-01-22 09:32:14 +08:00
Copilot bad7f1d06c fix: filesystem error when creating long filename (#1240) 2026-01-21 23:12:38 +08:00
Levi af1c8b2719 feat: enhance deep link handling for install extensions (#1249) 2026-01-21 20:23:04 +08:00
Levi d90af0b9d1 feat: support calling Gopeed API via host RPC (#1247) 2026-01-21 13:21:20 +08:00
Levi 6016ee1d24 feat: localize google fonts in flutter web (#1241) 2026-01-15 23:59:41 +08:00
Levi e275fd58bd fix: update goja to avoid some special cases stack overflow (#1242) 2026-01-15 18:49:19 +08:00
Levi c9626cb389 perf: slow down DHT announcing on errors (#1186) 2026-01-14 20:04:21 +08:00
Locon213 f86a379f93 feat(ui): add download percentage and ETA display (#1238) 2026-01-14 09:47:45 +08:00
Levi a9eee26be7 refactor: fethcer interface & download flow (#1229) 2026-01-08 21:50:47 +08:00
Copilot abd16d5326 fix: filename truncation for HTML-encoded ampersands in Content-Disposition (#1215) 2026-01-05 22:56:04 +08:00
Copilot 6756d82ffa fix: use PathUnescape for filename URL decoding (#1214) 2026-01-05 21:46:50 +08:00
Copilot 71c9e6947a feat: add auto-start incomplete tasks on application startup (#1203) 2026-01-05 21:11:42 +08:00
Matteo Pietro Dazzi 24db9bb58b feat: queue extractions (#1227) 2026-01-05 15:53:19 +08:00
Matteo Pietro Dazzi de0afb1481 feat: multipart-archives (#1222) 2026-01-01 19:09:22 +08:00
awe123343 cb6331c20d feat(ui): support running as menubar app (#1216) 2025-12-31 09:57:16 +08:00
Matteo Pietro Dazzi b0e133b554 feat: show extraction progress (#1217) 2025-12-31 09:25:21 +08:00
r6SHsRLF dbe37befa3 ci(windows): keep installer exe in release assets (#1211) 2025-12-29 17:36:27 +08:00
Matteo Pietro Dazzi 0d5c4e8e7f feat: auto extract archives (#1212) 2025-12-29 17:27:49 +08:00
Copilot 5515a1c48c fix: ByteFmt panic on negative int64 values (#1200) 2025-12-28 21:57:40 +08:00
Mine 3abf6ed933 feat: add support for Windows ARM architecture in build and release configurations (#1206) 2025-12-28 21:45:47 +08:00
Levi af40e8aa63 release: v1.8.3 2025-11-30 21:11:04 +08:00
Levi 64c5ece02c docs: add WeChat Official Account qr (#1192) 2025-11-29 13:19:37 +08:00
Levi 4ccbc3ebc5 feat: support custom GitHub mirror config (#1191) 2025-11-29 12:39:24 +08:00
BerryMC 14d9642d3c feat: add RPM package build (#1190) 2025-11-29 11:35:32 +08:00
Copilot 2f5fbfb17c fix: HTTP filename parsing for mixed encodings and GBK-encoded (#1189) 2025-11-29 11:34:50 +08:00
Copilot 49b4d99b5e feat: add download directory placeholders, category support (#1188) 2025-11-27 18:39:08 +08:00
Copilot 9d9f1caaaa feat: support webhook (#1187) 2025-11-25 23:34:45 +08:00
Copilot 5bb8541385 fix: proxy username and password not persisting in settings (#1183) 2025-11-24 19:29:49 +08:00
Levi f7189668fd ci: pin windows runner image to windows-2022 (#1184) 2025-11-24 18:32:56 +08:00
Little-King 1b36ed1807 fix: defect in extension process during rename duplicate file (#1158) 2025-11-14 18:49:21 +08:00
Levi 07dc196c3c feat: add support for Waterfox browser detection (#1146) 2025-10-15 20:44:41 +08:00
Levi 76bec67ab2 ci: fix windows runner 2025-10-12 22:45:43 +08:00
Levi a2a9e3dc2a release: v1.8.2 2025-10-12 21:17:40 +08:00
Levi 9a5680fb45 feat: refactor browser extension communication to use IPC (#1130) 2025-10-12 21:17:07 +08:00
Levi 75a8a3a9ac ci: downgrade to Ubuntu 22.04 (#1128) 2025-10-09 21:01:06 +08:00
Levi 3e8c85b945 feat: update GitHub mirrors (#1137) 2025-10-09 20:59:56 +08:00
Levi 90f70d1bf6 feat: isolate TLS fingerprint for each extension lifecycle (#1135) 2025-10-09 20:37:21 +08:00
Levi 20a70d7f91 fix: certain special single-file torrent downloads (#1136) 2025-09-29 22:52:06 +08:00
Levi d33546a4b5 fix: update deleteAll method to accept force parameter (#1127) 2025-09-29 19:41:48 +08:00
Levi 93614d46a8 release: v1.8.1 2025-09-14 10:10:30 +08:00
Levi 04b0e691eb fix: add timeout to server shutdown (#1116) 2025-09-13 21:19:28 +08:00
铺盖崽 7f08fa0b23 ci: update linux runner 24.04 (#1117) 2025-09-13 18:44:00 +08:00
Levi df6a51d23c feat: add notify when new version option (#1115) 2025-09-12 18:32:45 +08:00
Levi 46c379d556 fix: resolve incorrect download path for single file torrent (#1111) 2025-09-10 07:59:10 +08:00
Levi 46cd9f7a3e fix: windows tray disappearing after sleep/wake cycles (#1108) 2025-09-09 22:44:09 +08:00
Levi 5c2f66f4b6 fix: web auth config (#1094) 2025-09-09 18:35:04 +08:00
Krau cfdcb586ee fix: decode URL-encoded filename from contentDisposition header (#1099) 2025-09-04 18:22:30 +08:00
Levi 55e6bdc4f7 release: v1.8.0 2025-08-28 18:43:44 +08:00
Levi b31b42301e feat: update storage directory path for Windows (#1087) 2025-08-28 08:44:18 +08:00
Levi 7ce0d876b0 feat: add environment variables configuration support (#1088) 2025-08-28 08:21:02 +08:00
Levi 2fd2596aa0 ci: update macOS version to macos-14 (#1086) 2025-08-26 10:07:33 +08:00
Local-Gay-Communist e0973e4a34 feat: add german language support (#1084) 2025-08-22 09:26:37 +08:00
Levi 4f7b8a05ad feat: web login page (#1058) 2025-07-16 01:02:25 +08:00
Levi 2927d1ef2b feat(extension): TLS fingerprinting support in xhr (#1054) 2025-07-15 23:54:51 +08:00
Levi 3451f577ab fix: remove invalid characters in HTTP headers (#1053) 2025-07-15 22:34:10 +08:00
Levi 06671be0d8 release: v1.7.1 2025-07-08 09:13:39 +08:00
Thiago Sabaia 8ae9363f0f feat: Translation into Portuguese (Brazil) (#1047) 2025-06-17 09:51:51 +08:00
unifan 5d84445f8f feat: Open in Folder with support for 3rd file manager (#1045) 2025-06-07 11:14:58 +08:00
Levi 7c541e4047 feat: web support relative base path (#1001) 2025-05-15 22:07:26 +08:00
Charles Huang 3262e5a11b fix: do not add external tracker to a private torrent (#1011) 2025-04-23 22:53:15 +08:00
Charles Huang ff43a373cd fix: json syntax error in bt config (#1014) 2025-04-23 13:48:46 +08:00
Levi 7394d948e4 ci: workaround for app_indicator_new is deprecated (#986) 2025-04-09 22:20:16 +08:00
Levi 5afb3aa43b ci: fix go 1.24 code coverage (#988) 2025-04-09 22:19:50 +08:00
Levi 7705af6e68 release: v1.7.0 2025-04-08 22:22:09 +08:00
Levi 684e060ef3 chore: upgrade go to 1.24 (#984) 2025-04-08 22:18:18 +08:00
Levi 0c67792441 ci: build separate macos packages (#967) 2025-04-08 19:43:30 +08:00
Levi b0ef2c2a87 fix: bt seed cause crash (#985) 2025-04-08 18:38:17 +08:00
mak7im01 1977c2a0fc feat: update ru_ru.dart (#982) 2025-04-08 09:21:29 +08:00
Levi d289708f9e feat: use batch download api for create multitasks (#978) 2025-04-05 22:57:17 +08:00
Levi cd5fd45667 fix: resolve http non encode attachment filename (#975) 2025-04-05 22:37:19 +08:00
Levi 50bc9ab129 feat: improve http download strategy to prevent speed drops near completion (#969) 2025-04-05 22:20:35 +08:00
Levi bfad91dcab release: v1.6.11 2025-03-14 23:03:48 +08:00
Levi 61a959adda ci: apk build split abi (#962) 2025-03-14 22:58:28 +08:00
Levi f4b839fd11 fix: http download size exceeds file size (#959) 2025-03-14 20:07:34 +08:00
Levi 969aeb5e0a fix: : web auth priority when api token and basic auth coexist (#960) 2025-03-14 19:22:31 +08:00
Levi 68d42eb397 feat: web http file server caching and gzip support (#961) 2025-03-14 18:31:05 +08:00
Levi 08603f9510 feat: auto updater (#933) 2025-03-13 18:31:38 +08:00
Internetezoo b450bf67bd feat: Add Hungarian languages (#940) 2025-03-10 11:59:44 +08:00
Levi 03e26dbea8 feat: improve http download retry strategy (#951) 2025-03-07 12:01:52 +08:00
Levi 046a3ac9f9 fix: can not open url encode torrent file path (#945) 2025-02-27 00:00:25 +08:00
Levi 9c5aebaf36 fix: path url encode filename parse (#939) 2025-02-18 19:37:38 +08:00
Xiangxin a094715d69 fix: windows title bar theme switch (#936) 2025-02-17 19:32:09 +08:00
铺盖崽 69bb875172 ci: fix flathub build error (#937) 2025-02-17 09:15:42 +08:00
铺盖崽 589f10c7ef ci: add linux arm64 build (#935) 2025-02-16 10:54:25 +08:00
Levi 654e57e946 release: v1.6.10 2025-02-13 21:56:27 +08:00
Levi b94b99ae09 fix: gopeed scheme safe url params (#932) 2025-02-13 21:55:37 +08:00
Levi fba990a056 fix: http MIME encoded word filename parse (#931) 2025-02-13 21:54:13 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣 cb07ce58bc feat: improve Turkish Locales (#917) 2025-02-07 08:05:38 +08:00
Levi 50beef7b08 docs: improve download links (#913) 2025-02-06 12:19:27 +08:00
Levi bd39e4110f feat: add ukraine language (#909) 2025-02-05 10:51:58 +08:00
Levi 86efcb98c5 fix: web white screen (#908) 2025-02-05 10:51:33 +08:00
Levi f3d9873395 release: v1.6.9 2025-02-03 18:50:25 +08:00
Levi ef228c1722 feat: update android target sdk to 34 (#902) 2025-02-03 16:15:13 +08:00
Levi 6863317e9d feat: support tcp protocol on android (#903) 2025-02-03 13:49:46 +08:00
Levi 2b83a735cb fix: advanced options used by default when creating task (#901) 2025-02-03 13:42:50 +08:00
Levi 447f599783 fix: firefox native host integration (#895) 2025-02-01 18:14:59 +08:00
liwei f986f6253d ci: fix download-artifact version 2025-01-31 20:00:45 +08:00
liwei 2e1b621250 ci: fix 2025-01-31 20:00:14 +08:00
Levi d50947b2e6 release: v1.6.8 2025-01-31 19:15:01 +08:00
Levi 520a72415c feat: browser extensions native messaging host (#890) 2025-01-31 17:57:34 +08:00
187 changed files with 27565 additions and 3493 deletions

No files matched your search

+133 -67
View File
@@ -38,78 +38,144 @@ version-resolver:
default: patch default: patch
template: | template: |
<table> <table>
<thead>
<tr>
<th colspan="2">Windows</th>
<th>MacOS</th>
<th colspan="4">Linux</th>
<th>Android</th>
<th>iOS</th>
<th>Docker</th>
<th colspan="2">Qnap</th>
<th colspan="8">Web</th>
</tr>
<tr>
<td><code>EXE</code></td>
<td><code>Portable</code></td>
<td><code>DMG</code></td>
<td><code>Flathub</code></td>
<td><code>SNAP</code></td>
<td><code>DEB</code></td>
<td><code>AppImage</code></td>
<td><code>APK</code></td>
<td><code>IPA</code></td>
<td>-</td>
<td colspan="2"><code>QPKG</code></td>
<td colspan="3"><code>Windows</code></td>
<td colspan="2"><code>MacOS</code></td>
<td colspan="3"><code>Linux</code></td>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td rowspan="4">🪟 Windows</td>
<td rowspan="2"><code>EXE</code></td>
<td>amd64</td> <td>amd64</td>
<td>amd64</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
<td>-</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>amd64</td>
<td>arm64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
</tr> </tr>
<tr> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td> </tr>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td> <tr>
<td><a href="https://snapcraft.io/gopeed">Link</a></td> <td rowspan="2"><code>Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <tr>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-arm64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Link</a></td> <td rowspan="3">🍎 MacOS</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Link</a></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Link</a></td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Link</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
+219 -61
View File
@@ -13,8 +13,10 @@ on:
default: "false" default: "false"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.24.5" FLUTTER_VERSION: "3.41.2"
GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }}
GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }}
permissions: permissions:
contents: write contents: write
@@ -34,41 +36,95 @@ jobs:
build-windows: build-windows:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
runs-on: windows-latest
needs: [get-release] needs: [get-release]
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
os: windows-11-arm
llvm_ver: "20251202" # Only ARM64
flutter_channel: "main"
flutter_version: "7e1c8868"
- arch: amd64
os: windows-2022
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Enable long paths for flutter main branch checks
run: |
git config --global core.longpaths true
# Install LLVM environment only on ARM64
- name: Install llvm-mingw-ucrt-aarch64 (ARM64 only)
if: matrix.arch == 'arm64'
run: |
$ver = "${{ matrix.llvm_ver }}"
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$ver/llvm-mingw-$ver-ucrt-aarch64.zip"
$zip = "$env:RUNNER_TEMP\\llvm.zip"
$extract = "$env:RUNNER_TEMP\\extract"
$target = "C:\\clangarm64"
curl -L $url -o $zip
rm -r -fo $extract,$target -ea Ignore
mkdir $extract | Out-Null
tar -xf $zip -C $extract
mv (Get-ChildItem $extract)[0].FullName $target
$b = "$target\\bin"
"CC=$b\\clang.exe" >> $env:GITHUB_ENV
"CXX=$b\\clang++.exe" >> $env:GITHUB_ENV
"CLANGARM64_BIN=$b" >> $env:GITHUB_ENV
"CGO_ENABLED=1" >> $env:GITHUB_ENV
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
$b >> $env:GITHUB_PATH
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} channel: ${{ matrix.flutter_channel || 'stable' }}
flutter-version: ${{ matrix.flutter_version || env.FLUTTER_VERSION }}
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
GOARCH: ${{ matrix.arch }}
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 -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -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=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/host/host.exe github.com/GopeedLab/gopeed/cmd/host go build -ldflags="-w -s" -o ui/flutter/assets/exec/host.exe github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter cd ui/flutter
$TAG = "v$env:VERSION" $TAG = "v$env:VERSION"
flutter build windows flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
$mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32" $system = "C:\Windows\System32"
$release = "build\windows\x64\runner\Release\" if ("${{ matrix.arch }}" -eq "amd64") {
cp $mingw\libstdc++-6.dll $release # for amd64
cp $mingw\libgcc_s_seh-1.dll $release $release = "build\windows\x64\runner\Release\"
$mingw = "C:\Program Files\Git\mingw64\bin"
cp $mingw\libstdc++-6.dll $release
cp $mingw\libgcc_s_seh-1.dll $release
} else {
# for ARM64
$release = "build\windows\arm64\runner\Release\"
$mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin"
cp $mingw\libc++.dll $release
cp $mingw\libunwind.dll $release
}
cp $mingw\libwinpthread-1.dll $release cp $mingw\libwinpthread-1.dll $release
cp $system\msvcp140.dll $release cp $system\msvcp140.dll $release
cp $system\vcruntime140.dll $release cp $system\vcruntime140.dll $release
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-$TAG-windows-amd64-portable.zip" Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-${{ matrix.arch }}-portable.zip"
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
cd build/windows cd build/windows
echo @" echo @"
; Script generated by the Inno Setup Script Wizard. ; Script generated by the Inno Setup Script Wizard.
@@ -114,7 +170,7 @@ jobs:
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files] [Files]
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs; Source: ".\${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }}\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs;
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[UninstallDelete] [UninstallDelete]
@@ -128,18 +184,17 @@ 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-$TAG-windows-amd64.exe" mv "Output\gopeed.exe" "Output\Gopeed-$TAG-windows-${{ matrix.arch }}.exe"
Compress-Archive -Path "Output\Gopeed-$TAG-windows-amd64.exe" -DestinationPath "Output\Gopeed-$TAG-windows-amd64.zip" Compress-Archive -Path "Output\Gopeed-$TAG-windows-${{ matrix.arch }}.exe" -DestinationPath "Output\Gopeed-$TAG-windows-${{ matrix.arch }}.zip"
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:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/* asset_path: ui/flutter/build/windows/Output/*
overwrite: true overwrite: true
build-macos-arm64-lib: build-macos-amd64-lib:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-latest runs-on: macos-15-intel
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -152,16 +207,21 @@ jobs:
run: | run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o 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 libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: macos-arm64-lib name: macos-amd64-lib
path: | path: |
libgopeed.dylib libgopeed.dylib
host host
updater
build-macos: build-macos:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-13 runs-on: macos-latest
needs: [get-release, build-macos-arm64-lib] strategy:
matrix:
channel: [arm64, amd64, universal]
needs: [get-release, build-macos-amd64-lib]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -177,33 +237,49 @@ jobs:
run: | run: |
python3 -m pip install setuptools --break-system-packages python3 -m pip install setuptools --break-system-packages
npm install -g appdmg npm install -g appdmg
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: macos-arm64-lib name: macos-amd64-lib
path: ui/flutter/lib-arm64 path: ui/flutter/lib-amd64
- if: ${{ matrix.channel == 'arm64' }}
name: Build Arm64 Libraries
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
- if: ${{ matrix.channel == 'amd64' }}
name: Build Amd64 Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
- if: ${{ matrix.channel == 'universal' }}
name: Build Universal Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
# arm64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
# amd64 lib
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
# universal lib
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
PROJECT_DIR=$(pwd) cd ui/flutter
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
# amd64 lib
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
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/host github.com/GopeedLab/gopeed/cmd/host
# universal binary
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
cp ui/flutter/lib-amd64/host ui/flutter/macos/Frameworks/amd64-host
cp ui/flutter/lib-arm64/host ui/flutter/macos/Frameworks/arm64-host
cd ui/flutter/macos/Frameworks
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
lipo -create -output host amd64-host arm64-host
rm -rf amd64-lib arm64-lib amd64-host arm64-host
mv host ../../assets/host/host
cd $PROJECT_DIR/ui/flutter
flutter build macos
cd build/macos/Build/Products/Release cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF cat>appdmg.json<<EOF
{ {
@@ -217,6 +293,13 @@ jobs:
EOF EOF
mkdir dist mkdir dist
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
fi
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
fi
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -225,7 +308,10 @@ jobs:
overwrite: true overwrite: true
build-linux: build-linux:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
runs-on: ubuntu-22.04 runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-22.04-arm]
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -235,13 +321,12 @@ jobs:
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
channel: master
- run: | - run: |
sudo apt update -y sudo apt update -y
# flutter build dependencies sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev rpm patchelf libfuse2 locate imagemagick potrace libkeybinder-3.0-dev
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev arch=$(uname -m)
# appimage build dependencies wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
sudo apt install -y libfuse2 locate
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/ sudo mv appimagetool /usr/local/bin/
- name: Build - name: Build
@@ -249,13 +334,78 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
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 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
go build -ldflags="-w -s" -o ui/flutter/assets/host/host github.com/GopeedLab/gopeed/cmd/host go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter cd ui/flutter
ICON_PATH=$(find assets -name "logo.png" -o -name "icon.png" | head -n 1)
[ -z "$ICON_PATH" ] && ICON_PATH="assets/icon.png" && touch assets/icon.png
convert "$ICON_PATH" assets/icon.svg
ICON_PATH="assets/icon.svg"
RPM_ARCH=$(uname -m)
mkdir -p linux/packaging/rpm
cat << EOF > linux/packaging/rpm/make_config.yaml
display_name: Gopeed
icon: $ICON_PATH
summary: High speed downloader.
description: A high speed downloader that supports all platforms.
group: Applications/Internet
license: GPL-3.0
url: https://github.com/GopeedLab/gopeed
vendor: GopeedLab
maintainer: GopeedLab <[email protected]>
build_arch: $RPM_ARCH
EOF
cat << EOF > distribute_options.yaml
output: dist/
releases:
- name: linux
jobs:
- name: appimage
package:
platform: linux
target: appimage
build_args:
dart-define:
UPDATE_CHANNEL: linuxAppImage
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
- name: deb
package:
platform: linux
target: deb
build_args:
dart-define:
UPDATE_CHANNEL: linuxDeb
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
- name: rpm
package:
platform: linux
target: rpm
build_args:
dart-define:
UPDATE_CHANNEL: linuxRpm
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
EOF
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
flutter_distributor package --platform linux --targets appimage,deb flutter_distributor release --name linux
cd dist/* cd dist/*
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb ARCH="amd64"
if [[ "${{ matrix.os }}" == *-arm ]]; then
ARCH="arm64"
fi
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
mv gopeed-*-linux.rpm Gopeed-v${VERSION}-linux-${ARCH}.rpm
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -280,7 +430,8 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
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 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
go build -ldflags="-w -s" -o ui/flutter/assets/host/host github.com/GopeedLab/gopeed/cmd/host go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter cd ui/flutter
sudo snap install snapcraft --classic sudo snap install snapcraft --classic
@@ -336,6 +487,7 @@ jobs:
- cmake - cmake
- curl - curl
- libgtk-3-dev - libgtk-3-dev
- libkeybinder-3.0-dev
- ninja-build - ninja-build
- unzip - unzip
- xz-utils - xz-utils
@@ -351,13 +503,14 @@ jobs:
plugin: nil plugin: nil
stage-packages: stage-packages:
- libgtk-3-dev - libgtk-3-dev
- libkeybinder-3.0-dev
- libappindicator3-dev - libappindicator3-dev
override-build: | override-build: |
# work around pub get stack overflow # https://github.com/dart-lang/sdk/issues/51068#issuecomment-1396588253 # work around pub get stack overflow # https://github.com/dart-lang/sdk/issues/51068#issuecomment-1396588253
set +e set +e
dart pub get dart pub get
set -eux set -eux
flutter build linux --release -v flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap" --dart-define="GA4_MEASUREMENT_ID=$GA4_MEASUREMENT_ID" --dart-define="GA4_API_SECRET=$GA4_API_SECRET"
mkdir -p \$CRAFT_PART_INSTALL/bin/ mkdir -p \$CRAFT_PART_INSTALL/bin/
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/ cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
gpu-2404: gpu-2404:
@@ -422,9 +575,13 @@ jobs:
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -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 --dart-define="UPDATE_CHANNEL=androidApk" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
mkdir dist mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/Gopeed-v$VERSION-android-armeabi-v7a.apk
cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk dist/Gopeed-v$VERSION-android-arm64-v8a.apk
cp build/app/outputs/flutter-apk/app-x86_64-release.apk dist/Gopeed-v$VERSION-android-x86_64.apk
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -433,7 +590,7 @@ jobs:
overwrite: true overwrite: true
build-ios: build-ios:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
runs-on: macos-latest runs-on: macos-14
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -452,7 +609,7 @@ jobs:
gomobile init gomobile init
gomobile bind -tags nosqlite -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 gomobile bind -tags nosqlite -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 --dart-define="UPDATE_CHANNEL=iosIpa" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
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
@@ -480,9 +637,9 @@ jobs:
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
cd ui/flutter cd ui/flutter
flutter build web --no-web-resources-cdn flutter build web --no-web-resources-cdn --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
dart ../../.github/workflows/scripts/flutter_local_font.dart
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
@@ -526,7 +683,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.8.18" python-version: "3.8.18"
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: web-dist name: web-dist
path: dist/zip/ path: dist/zip/
@@ -598,7 +755,8 @@ jobs:
- name: Build flutter web - name: Build flutter web
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --no-web-resources-cdn flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
dart ../../.github/workflows/scripts/flutter_local_font.dart
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
permissions: permissions:
contents: write contents: write
@@ -0,0 +1,412 @@
// Localize external Google Fonts (fonts.gstatic.com) references in Flutter Web build output.
//
// Usage (post-build):
// # Run AFTER: flutter build web --no-web-resources-cdn
// dart ../../.github/workflows/scripts/flutter_local_font.dart
//
// What it does:
// In main.dart.js:
// - Find any referenced resources under: https://fonts.gstatic.com/s/
// (including the common pattern where Flutter concatenates the base URL with a relative path)
// - Download them into: build/web/assets/gstatic/<path>
// - Rewrite "https://fonts.gstatic.com/s/" -> "assets/gstatic/" so runtime loads locally
// - Only download font subsets needed by supported locales (parsed from message.dart)
//
// Notes:
// - This script DOES NOT call `flutter build web`. It only patches the output.
// - This is a best-effort post-process. Always verify in browser devtools.
import 'dart:async';
import 'dart:io';
/// Get font families required for a specific locale based on language/script.
/// Uses language code prefix to automatically detect the script system.
/// Returns empty set if base fonts (Latin/Cyrillic/Greek) are sufficient.
Set<String> _getFontFamiliesForLocale(String locale) {
final lang = locale.toLowerCase().split('_').first;
// CJK languages - need specific large font files
if (lang == 'zh') {
// Simplified vs Traditional Chinese
if (locale.contains('cn') || locale.contains('sg')) {
return {'notosanssc'}; // Simplified Chinese
}
return {'notosanstc', 'notosanshk'}; // Traditional Chinese (TW, HK, MO)
}
if (lang == 'ja') return {'notosansjp'}; // Japanese
if (lang == 'ko') return {'notosanskr'}; // Korean
// Arabic script languages
if (lang == 'ar') return {'notosansarabic'}; // Arabic
if (lang == 'fa') return {'notosansarabic'}; // Persian/Farsi
if (lang == 'ur') return {'notosansarabic'}; // Urdu
if (lang == 'ps') return {'notosansarabic'}; // Pashto
if (lang == 'ku') return {'notosansarabic'}; // Kurdish (Arabic script)
// Hebrew script
if (lang == 'he' || lang == 'yi') return {'notosanshebrew'};
// South Asian scripts
if (lang == 'hi' || lang == 'mr' || lang == 'ne' || lang == 'sa') {
return {'notosansdevanagari'}; // Hindi, Marathi, Nepali, Sanskrit
}
if (lang == 'bn' || lang == 'as')
return {'notosansbengali'}; // Bengali, Assamese
if (lang == 'ta')
return {'notosanstamil', 'notosanstamilsupplement'}; // Tamil
if (lang == 'te') return {'notosanstelugu'}; // Telugu
if (lang == 'kn') return {'notosanskannada'}; // Kannada
if (lang == 'ml') return {'notosansmalayalam'}; // Malayalam
if (lang == 'gu') return {'notosansgujarati'}; // Gujarati
if (lang == 'pa') return {'notosansgurmukhi'}; // Punjabi (Gurmukhi)
if (lang == 'or') return {'notosansoriya'}; // Odia/Oriya
if (lang == 'si') return {'notosanssinhala'}; // Sinhala
// Southeast Asian scripts
if (lang == 'th') return {'notosansthai'}; // Thai
if (lang == 'lo') return {'notosanslao'}; // Lao
if (lang == 'my') return {'notosansmyanmar'}; // Myanmar/Burmese
if (lang == 'km') return {'notosanskhmer'}; // Khmer/Cambodian
if (lang == 'jv') return {'notosansjavanese'}; // Javanese
// Other scripts
if (lang == 'ka') return {'notosansgeorgian'}; // Georgian
if (lang == 'hy') return {'notosansarmenian'}; // Armenian
if (lang == 'am' || lang == 'ti')
return {'notosansethiopic'}; // Amharic, Tigrinya
if (lang == 'mn') return {'notosansmongolian'}; // Mongolian
// Latin/Cyrillic/Greek based languages - base notosans is sufficient
// Includes: English, German, French, Spanish, Italian, Portuguese,
// Russian, Ukrainian, Polish, Turkish, Vietnamese, Greek, etc.
return {};
}
/// Base font families that are always included regardless of locale.
/// These provide core functionality and are relatively small.
const _baseFontFamilies = <String>{
'notosans', // Base Latin/Cyrillic/Greek/Vietnamese
'roboto', // Material Design default font
'notosanssymbols', // Common symbols
'notosanssymbols2', // Additional symbols
'notosansmath', // Math symbols
'notomusic', // Music notation symbols
// Note: notocoloremoji (~24MB) and notoemoji (~860KB) are excluded
// to reduce bundle size. Add them back if emoji support is needed.
};
/// Parse supported locales from message.dart file.
/// Reads the import statements and extracts locale codes like 'zh_cn', 'en_us', etc.
Set<String> _parseSupportedLocales(File messageFile) {
final locales = <String>{};
if (!messageFile.existsSync()) {
_fail('Warning: message.dart not found, only base fonts will be used');
}
final content = messageFile.readAsStringSync();
// Match import statements like: import 'langs/zh_cn.dart';
final importRegex = RegExp(r"import\s+'langs/(\w+)\.dart'");
for (final match in importRegex.allMatches(content)) {
final locale = match.group(1);
if (locale != null) {
locales.add(locale);
}
}
if (locales.isEmpty) {
_fail(
'Warning: No locales found in message.dart, only base fonts will be used');
}
return locales;
}
/// Get required font families for the given locales.
/// Returns a set of font family names that should be included.
/// Automatically detects required fonts based on language code prefix.
Set<String> _getRequiredFontFamilies(Set<String> locales) {
final families = <String>{..._baseFontFamilies};
for (final locale in locales) {
final localeFamilies = _getFontFamiliesForLocale(locale);
families.addAll(localeFamilies);
}
return families;
}
/// Check if a font file path should be included based on required font families.
/// Font paths have the format: "fontfamily/version/filename.ext"
/// Example: "notosanssc/v36/xxx.ttf" -> font family is "notosanssc"
bool _fontMatchesRequiredFamilies(
String fontPath, Set<String> requiredFamilies) {
// Extract font family from path (first segment before '/')
final slashIndex = fontPath.indexOf('/');
if (slashIndex == -1) {
// No slash found, include by default (unusual path format)
return true;
}
final fontFamily = fontPath.substring(0, slashIndex).toLowerCase();
return requiredFamilies.contains(fontFamily);
}
Future<void> main(List<String> args) async {
try {
// This script is intentionally argument-free for CI convenience.
// It assumes it is executed from the Flutter project directory (ui/flutter),
// but will also try to locate pubspec.yaml by walking up.
final flutterDir = _findFlutterProjectRoot(Directory.current);
// Parse supported locales from message.dart
final messageFile =
File(_join(flutterDir.path, 'lib', 'i18n', 'message.dart'));
final supportedLocales = _parseSupportedLocales(messageFile);
stdout.writeln('Supported locales: ${supportedLocales.join(', ')}');
// Get required font families based on supported locales
final requiredFamilies = _getRequiredFontFamilies(supportedLocales);
stdout.writeln('Required font families: ${requiredFamilies.join(', ')}');
final webDir = Directory(_join(flutterDir.path, 'build', 'web'));
if (!webDir.existsSync()) {
_fail(
'Web build directory not found: ${webDir.path}\n'
'Did you run: flutter build web --no-web-resources-cdn ?',
);
}
final mainJs = File(_join(webDir.path, 'main.dart.js'));
if (!mainJs.existsSync()) {
_fail('main.dart.js not found at: ${mainJs.path}');
}
final gstaticRoot = Directory(_join(webDir.path, 'assets', 'gstatic'));
if (!gstaticRoot.existsSync()) gstaticRoot.createSync(recursive: true);
const gstaticSPrefix = 'https://fonts.gstatic.com/s/';
final original = mainJs.readAsStringSync();
// Relative font asset paths which Flutter's loader typically concatenates with:
// https://fonts.gstatic.com/s/
// Keep this as a best-effort heuristic to cover the common concatenation pattern.
// Some gstatic assets include extra dot segments like: notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.0.woff2
// Allow dots in the path, while still restricting to font-like extensions.
final relAssetRegex = RegExp(
r"""["']([a-zA-Z0-9/_\.-]+\.(?:woff2|woff|ttf|otf|eot|svg))["']""",
caseSensitive: false,
);
final relAssetsUnderS = <String>{};
for (final m in relAssetRegex.allMatches(original)) {
final p = m.group(1);
if (p == null || p.isEmpty) continue;
if (p.contains('://') || p.startsWith('data:')) continue;
if (p.startsWith('/') ||
p.startsWith('assets/') ||
p.startsWith('packages/')) continue;
if (p.contains('..')) continue;
relAssetsUnderS.add(p);
}
if (relAssetsUnderS.isEmpty) {
_fail('No fonts.gstatic.com assets found in main.dart.js.');
}
// Build a download plan: dest relative path (under assets/gstatic/) -> URL.
// Filter fonts based on required font families to reduce package size.
final downloads = <String, Uri>{};
final skippedFonts = <String, Set<String>>{};
for (final rel in relAssetsUnderS) {
if (_fontMatchesRequiredFamilies(rel, requiredFamilies)) {
downloads[rel] = Uri.parse('$gstaticSPrefix$rel');
} else {
// Track skipped fonts and their paths for fallback generation
final slashIndex = rel.indexOf('/');
final family = slashIndex > 0 ? rel.substring(0, slashIndex) : rel;
skippedFonts.putIfAbsent(family, () => <String>{}).add(rel);
}
}
if (skippedFonts.isNotEmpty) {
stdout.writeln(
'Skipped ${skippedFonts.length} font families not required by supported locales:',
);
stdout.writeln(' ${skippedFonts.keys.join(', ')}');
}
if (downloads.isNotEmpty) {
stdout.writeln(
'Found ${downloads.length} fonts.gstatic.com assets to download...',
);
// Reuse a single HttpClient to avoid creating hundreds of short-lived
// connections (can be flaky on some environments).
final httpClient = HttpClient()
..connectionTimeout = const Duration(seconds: 30)
..idleTimeout = const Duration(seconds: 30)
..maxConnectionsPerHost = 6;
try {
for (final entry in downloads.entries) {
final relPath = entry.key;
final url = entry.value;
final destPath = relPath.replaceAll('/', Platform.pathSeparator);
final dest = File(_join(gstaticRoot.path, destPath));
await _downloadIfMissing(httpClient, url, dest);
}
} finally {
httpClient.close(force: true);
}
}
// Generate empty fallback font files for skipped fonts to prevent infinite loading
// Empty files will cause browsers to fail fast instead of retrying indefinitely
if (skippedFonts.isNotEmpty) {
var fallbackCount = 0;
for (final entry in skippedFonts.entries) {
for (final relPath in entry.value) {
final destPath = relPath.replaceAll('/', Platform.pathSeparator);
final dest = File(_join(gstaticRoot.path, destPath));
if (!dest.existsSync()) {
dest.parent.createSync(recursive: true);
// Create an empty file - browsers will recognize it as invalid and skip
dest.writeAsBytesSync([]);
fallbackCount++;
}
}
}
if (fallbackCount > 0) {
stdout.writeln(
'Generated $fallbackCount empty fallback font files to prevent infinite loading',
);
}
}
// Rewrite remote prefix so requests become:
// <origin>/assets/gstatic/<...>.woff2
final replacedGstatic = original.replaceAll(
gstaticSPrefix,
'assets/gstatic/',
);
if (replacedGstatic == original && downloads.isEmpty) {
stdout.writeln(
'No changes applied (no fonts.gstatic.com references found).',
);
exitCode = 0;
return;
}
mainJs.writeAsStringSync(replacedGstatic);
stdout.writeln('Patched: ${mainJs.path}');
if (downloads.isNotEmpty) {
stdout.writeln(
' - downloaded ${downloads.length} files into: ${gstaticRoot.path}',
);
}
stdout.writeln(' - fonts.gstatic.com rewritten -> assets/gstatic/');
} catch (e, st) {
stderr.writeln('ERROR: $e');
stderr.writeln(st);
exitCode = 1;
}
}
Future<void> _downloadIfMissing(HttpClient client, Uri url, File dest) async {
if (dest.existsSync() && dest.lengthSync() > 0) return;
dest.parent.createSync(recursive: true);
// Network can be flaky in CI. Retry a few times on transient errors.
const maxAttempts = 4;
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
final tmp = File('${dest.path}.tmp');
try {
try {
final req = await client.getUrl(url);
final resp = await req.close().timeout(const Duration(seconds: 60));
if (resp.statusCode != 200) {
// 404 is not transient in practice - fail fast with a clear message.
if (resp.statusCode == 404) {
_fail('Missing gstatic asset (404): $url');
}
throw HttpException('HTTP ${resp.statusCode}', uri: url);
}
// Stream to temp file first, then rename to avoid leaving partial files.
if (tmp.existsSync()) tmp.deleteSync();
final sink = tmp.openWrite();
await resp.pipe(sink).timeout(const Duration(seconds: 120));
if (!tmp.existsSync() || tmp.lengthSync() == 0) {
throw const FormatException('Downloaded asset is empty');
}
if (dest.existsSync()) dest.deleteSync();
tmp.renameSync(dest.path);
return;
} finally {
// no-op: HttpClient lifecycle managed by the caller
}
} on TimeoutException {
if (tmp.existsSync()) tmp.deleteSync();
if (attempt == maxAttempts) {
_fail('Timeout downloading asset: $url');
}
} on SocketException catch (e) {
if (tmp.existsSync()) tmp.deleteSync();
if (attempt == maxAttempts) {
_fail('Socket error downloading asset: $url ($e)');
}
} on HttpException catch (e) {
if (tmp.existsSync()) tmp.deleteSync();
if (attempt == maxAttempts) {
_fail('HTTP error downloading asset: $url ($e)');
}
} on FileSystemException catch (e) {
if (tmp.existsSync()) tmp.deleteSync();
_fail('File write error downloading asset: $url ($e)');
} catch (e) {
if (tmp.existsSync()) tmp.deleteSync();
if (attempt == maxAttempts) {
_fail('Unexpected error downloading asset: $url ($e)');
}
}
// Backoff before retrying.
final backoffSeconds = 1 << (attempt - 1);
stdout.writeln(
'Retrying (${attempt + 1}/$maxAttempts) for: $url (wait ${backoffSeconds}s)',
);
await Future.delayed(Duration(seconds: backoffSeconds));
}
}
Directory _findFlutterProjectRoot(Directory start) {
var dir = start;
for (var i = 0; i < 10; i++) {
final pubspec = File(_join(dir.path, 'pubspec.yaml'));
if (pubspec.existsSync()) return dir;
final parent = dir.parent;
if (parent.path == dir.path) break;
dir = parent;
}
// Fallback: use current directory, error messages will explain what is missing.
return start;
}
Never _fail(String msg) {
throw FormatException(msg);
}
String _join(String a, [String? b, String? c, String? d]) {
final parts = <String>[
a,
if (b != null) b,
if (c != null) c,
if (d != null) d,
];
return parts.join(Platform.pathSeparator);
}
+58 -14
View File
@@ -25,10 +25,11 @@ on:
- ".github/workflows/test.yml" - ".github/workflows/test.yml"
- "go.mod" - "go.mod"
- "go.sum" - "go.sum"
workflow_dispatch:
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.24.5" FLUTTER_VERSION: "3.41.2"
jobs: jobs:
# lint: # lint:
@@ -53,26 +54,68 @@ jobs:
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests. - name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt run: |
# Get packages for testing and coverage (including cmd/web for flags_test.go)
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
# Filter out main.go from coverage report to avoid lowering coverage percentage
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
mv coverage_filtered.txt coverage.txt
- uses: codecov/codecov-action@v4 - uses: codecov/codecov-action@v4
with: with:
files: ./coverage.txt files: ./coverage.txt
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
build-desktop: build-desktop:
name: Build desktop check
runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ windows-latest, macos-latest, ubuntu-22.04 ] include:
needs: [ test ] - os: windows-2022
- os: windows-11-arm
llvm_ver: "20251202" # Only ARM64
flutter_channel: "main"
flutter_version: "7e1c8868"
- os: macos-latest
- os: ubuntu-22.04
- os: ubuntu-22.04-arm
flutter_channel: "main"
name: Build desktop check (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Enable long paths for flutter main branch checks
run: |
git config --global core.longpaths true
- name: Install llvm-mingw-ucrt-aarch64 (ARM64 only)
if: matrix.os == 'windows-11-arm'
run: |
$ver = "${{ matrix.llvm_ver }}"
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$ver/llvm-mingw-$ver-ucrt-aarch64.zip"
$zip = "$env:RUNNER_TEMP\\llvm.zip"
$extract = "$env:RUNNER_TEMP\\extract"
$target = "C:\\clangarm64"
curl -L $url -o $zip
rm -r -fo $extract,$target -ea Ignore
mkdir $extract | Out-Null
tar -xf $zip -C $extract
mv (Get-ChildItem $extract)[0].FullName $target
$b = "$target\\bin"
"CC=$b\\clang.exe" >> $env:GITHUB_ENV
"CXX=$b\\clang++.exe" >> $env:GITHUB_ENV
"CLANGARM64_BIN=$b" >> $env:GITHUB_ENV
"CGO_ENABLED=1" >> $env:GITHUB_ENV
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
$b >> $env:GITHUB_PATH
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} channel: ${{ matrix.flutter_channel || 'stable' }}
flutter-version: ${{ matrix.flutter_version || env.FLUTTER_VERSION }}
- if: runner.os == 'Windows' - if: runner.os == 'Windows'
run: | run: |
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" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
@@ -86,7 +129,7 @@ jobs:
- if: runner.os == 'Linux' - if: runner.os == 'Linux'
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev libkeybinder-3.0-dev
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" -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 flutter build linux
@@ -95,8 +138,8 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ macos-latest, ubuntu-latest ] os: [macos-14, ubuntu-latest]
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -128,7 +171,7 @@ jobs:
build-web: build-web:
name: Build web check name: Build web check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -141,6 +184,7 @@ jobs:
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --no-web-resources-cdn flutter build web --no-web-resources-cdn
dart ../../.github/workflows/scripts/flutter_local_font.dart
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
@@ -148,7 +192,7 @@ jobs:
build-docker: build-docker:
name: Build docker check name: Build docker check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -164,4 +208,4 @@ jobs:
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
docker build -t gopeed . docker build -t gopeed .
+2 -2
View File
@@ -1,4 +1,4 @@
FROM golang:1.23.3 AS go FROM golang:1.24.11-alpine3.23 AS go
WORKDIR /app WORKDIR /app
COPY ./go.mod ./go.sum ./ COPY ./go.mod ./go.sum ./
RUN go mod download RUN go mod download
@@ -8,7 +8,7 @@ RUN CGO_ENABLED=0 go build -tags nosqlite,web \
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \ -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
FROM alpine:3.18 FROM alpine:3.23
LABEL maintainer="monkeyWie" LABEL maintainer="monkeyWie"
WORKDIR /app WORKDIR /app
COPY --from=go /app/dist/gopeed ./ COPY --from=go /app/dist/gopeed ./
+158 -85
View File
@@ -1,10 +1,11 @@
# [![](_docs/img/banner.png)](https://gopeed.com) # [![](_docs/img/banner.svg)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![WeChat](https://img.shields.io/badge/WeChat%20Official%20Account-07C160?logo=wechat&logoColor=white)](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -13,94 +14,160 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## Introduction ## 🚀 Introduction
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)
## Download ## ⬇️ Download
<table> <table>
<thead>
<tr>
<th colspan="2">Windows</th>
<th>MacOS</th>
<th colspan="4">Linux</th>
<th>Android</th>
<th>iOS</th>
<th>Docker</th>
<th colspan="2">Qnap</th>
<th colspan="8">Web</th>
</tr>
<tr>
<td><code>EXE</code></td>
<td><code>Portable</code></td>
<td><code>DMG</code></td>
<td><code>Flathub</code></td>
<td><code>SNAP</code></td>
<td><code>DEB</code></td>
<td><code>AppImage</code></td>
<td><code>APK</code></td>
<td><code>IPA</code></td>
<td>-</td>
<td colspan="2"><code>QPKG</code></td>
<td colspan="3"><code>Windows</code></td>
<td colspan="2"><code>MacOS</code></td>
<td colspan="3"><code>Linux</code></td>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td rowspan="4">🪟 Windows</td>
<td rowspan="2"><code>EXE</code></td>
<td>amd64</td> <td>amd64</td>
<td>amd64</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
<td>-</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>amd64</td>
<td>arm64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
</tr> </tr>
<tr> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-arm64.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td> </tr>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td> <tr>
<td><a href="https://snapcraft.io/gopeed">Link</a></td> <td rowspan="2"><code>Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <tr>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-arm64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Link</a></td> <td rowspan="3">🍎 MacOS</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Link</a></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Link</a></td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Link</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html) More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
### Command tool ### 🛠️ Command tool
use `go install`: use `go install`:
@@ -108,40 +175,46 @@ use `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## Browser Extension ## 🔌 Browser Extension
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension) Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## Donate ## 📱 WeChat Official Account
Follow our WeChat Official Account to get the latest updates and news.
<img src="_docs/img/weixin.png" width="200" />
## 💝 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!
## Showcase ## 🖼️ Showcase
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## Development ## 👨‍💻 Development
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used. This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
> The front code is located in the `ui/flutter` directory. > The front code is located in the `ui/flutter` directory.
### Environment ### 🌍 Environment
1. Golang 1.23+ 1. Golang 1.24+
2. Flutter 3.24+ 2. Flutter 3.38+
### Clone ### 📋 Clone
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### Contributing ### 🤝 Contributing
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
### Build ### 🏗️ Build
#### Desktop #### Desktop
@@ -214,18 +287,18 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## Credits ## ❤️ Credits
### Contributors ### 👥 Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors"> <a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" /> <img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a> </a>
### JetBrains ### 🏢 JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed) [![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## License ## 📄 License
[GPLv3](LICENSE) [GPLv3](LICENSE)
+147 -81
View File
@@ -1,10 +1,11 @@
# [![](_docs/img/banner.png)](https://gopeed.com) # [![](_docs/img/banner.svg)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![WeChat](https://img.shields.io/badge/WeChat%20Official%20Account-07C160?logo=wechat&logoColor=white)](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -13,123 +14,188 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## はじめに ## 🚀 はじめに
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。 Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com) 見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
## インストール ## ⬇️ インストール
<table> <table>
<thead>
<tr>
<th colspan="2">Windows</th>
<th>MacOS</th>
<th colspan="4">Linux</th>
<th>Android</th>
<th>iOS</th>
<th>Docker</th>
<th colspan="2">Qnap</th>
<th colspan="8">Web</th>
</tr>
<tr>
<td><code>EXE</code></td>
<td><code>Portable</code></td>
<td><code>DMG</code></td>
<td><code>Flathub</code></td>
<td><code>SNAP</code></td>
<td><code>DEB</code></td>
<td><code>AppImage</code></td>
<td><code>APK</code></td>
<td><code>IPA</code></td>
<td>-</td>
<td colspan="2"><code>QPKG</code></td>
<td colspan="3"><code>Windows</code></td>
<td colspan="2"><code>MacOS</code></td>
<td colspan="3"><code>Linux</code></td>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td> <td>amd64</td>
<td>amd64</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
<td>-</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>amd64</td>
<td>arm64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
</tr> </tr>
<tr> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td> <td><code>Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td> </tr>
<td><a href="https://snapcraft.io/gopeed">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td> <td rowspan="3">🍎 MacOS</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Link</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Link</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Link</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Link</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Link</a></td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。 インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
## 寄付 ### 🛠️ コマンドツール
## 📱 WeChat 公式アカウント
公式アカウントをフォローして、最新のアップデートやニュースを入手してください。
<img src="_docs/img/weixin.png" width="200" />
## 💝 寄付
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください! もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
## ショーケース ## 🖼️ ショーケース
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## 開発 ## 👨‍💻 開発
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。 このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
> フロントコードは `ui/flutter` ディレクトリにあります。 > フロントコードは `ui/flutter` ディレクトリにあります。
### 環境 ### 🌍 環境
1. Go 言語 1.23+ 1. Go 言語 1.24+
2. Flutter 3.24+ 2. Flutter 3.38+
### クローン ### 📋 クローン
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### コントリビュート ### 🤝 コントリビュート
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください [CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
### ビルド ### 🏗️ ビルド
#### デスクトップ #### デスクトップ
@@ -202,7 +268,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## 感謝 ## ❤️ 感謝
### コントリビューター ### コントリビューター
+146 -82
View File
@@ -1,10 +1,11 @@
# [![](_docs/img/banner.png)](https://gopeed.com) # [![](_docs/img/banner.svg)](https://gopeed.com)
[![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Tải về](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) [![Tải về](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![WeChat](https://img.shields.io/badge/WeChat%20Official%20Account-07C160?logo=wechat&logoColor=white)](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -13,94 +14,151 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## Giới thiệu ## 🚀 Giới thiệu
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html). Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com) Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
## Tải về ## ⬇️ Tải về
<table> <table>
<thead>
<tr>
<th colspan="2">Windows</th>
<th>MacOS</th>
<th colspan="4">Linux</th>
<th>Android</th>
<th>iOS</th>
<th>Docker</th>
<th colspan="2">Qnap</th>
<th colspan="8">Web</th>
</tr>
<tr>
<td><code>EXE</code></td>
<td><code>Portable</code></td>
<td><code>DMG</code></td>
<td><code>Flathub</code></td>
<td><code>SNAP</code></td>
<td><code>DEB</code></td>
<td><code>AppImage</code></td>
<td><code>APK</code></td>
<td><code>IPA</code></td>
<td>-</td>
<td colspan="2"><code>QPKG</code></td>
<td colspan="3"><code>Windows</code></td>
<td colspan="2"><code>MacOS</code></td>
<td colspan="3"><code>Linux</code></td>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td> <td>amd64</td>
<td>amd64</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
<td>-</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>amd64</td>
<td>arm64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
</tr> </tr>
<tr> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td> <td><code>Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td> </tr>
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td> <td rowspan="3">🍎 MacOS</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Liên kết</a></td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Liên kết</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Liên kết</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Liên kết</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Liên kết</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Liên kết</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Liên kết</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Liên kết</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Liên kết</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Liên kết</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Liên kết</a></td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html) Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
### Command tool ### 🛠️ Công cụ lệnh
Sử dụng `go install`: Sử dụng `go install`:
@@ -108,36 +166,42 @@ Sử dụng `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## Quyên góp ## 📱 WeChat Official Account
Theo dõi tài khoản chính thức để nhận các cập nhật và tin tức mới nhất.
<img src="_docs/img/weixin.png" width="200" />
## 💝 Quyên góp
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn! Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
## Trưng bày ## 🖼️ Trưng bày
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## Development ## 👨‍💻 Development
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`. Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
> Mã giao diện nằm trong thư mục `ui/flutter`. > Mã giao diện nằm trong thư mục `ui/flutter`.
### Environment ### 🌍 Environment
1. Golang 1.23+ 1. Golang 1.24+
2. Flutter 3.24+ 2. Flutter 3.38+
### Clone ### 📋 Clone
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### Đóng góp ### 🤝 Đóng góp
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md) Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
### Xây dựng ### 🏗️ Xây dựng
#### Desktop #### Desktop
@@ -210,7 +274,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## Tín dụng ## ❤️ Tín dụng
### Người đóng góp ### Người đóng góp
+147 -83
View File
@@ -1,10 +1,11 @@
# [![](_docs/img/banner.png)](https://gopeed.com) # [![](_docs/img/banner.svg)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html)
[![WeChat](https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7-07C160?logo=wechat&logoColor=white)](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -13,94 +14,151 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 介绍 ## 🚀 介绍
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。 Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/) 访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## 下载 ## ⬇️ 下载
<table> <table>
<thead>
<tr>
<th colspan="2">Windows</th>
<th>MacOS</th>
<th colspan="4">Linux</th>
<th>Android</th>
<th>iOS</th>
<th>Docker</th>
<th colspan="2">Qnap</th>
<th colspan="8">Web</th>
</tr>
<tr>
<td><code>EXE</code></td>
<td><code>Portable</code></td>
<td><code>DMG</code></td>
<td><code>Flathub</code></td>
<td><code>SNAP</code></td>
<td><code>DEB</code></td>
<td><code>AppImage</code></td>
<td><code>APK</code></td>
<td><code>IPA</code></td>
<td>-</td>
<td colspan="2"><code>QPKG</code></td>
<td colspan="3"><code>Windows</code></td>
<td colspan="2"><code>MacOS</code></td>
<td colspan="3"><code>Linux</code></td>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td> <td>amd64</td>
<td>amd64</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
<td>-</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>amd64</td>
<td>arm64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
</tr> </tr>
<tr> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td> <td><code>Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td> </tr>
<td><a href="https://snapcraft.io/gopeed">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td> <td rowspan="3">🍎 MacOS</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">前往</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">前往</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">前往</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">前往</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">前往</a></td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html) 更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
### 命令行工具 ### 🛠️ 命令行工具
使用`go install`安装: 使用`go install`安装:
@@ -108,40 +166,46 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## 浏览器扩展 ## 🔌 浏览器扩展
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension) Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## 赞助 ## 📱 微信公众号
关注公众号获取项目最新动态和资讯。
<img src="_docs/img/weixin.png" width="200" />
## 💝 赞助
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢! 如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
## 界面展示 ## 🖼️ 界面展示
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## 开发 ## 👨‍💻 开发
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。 本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
> 前端代码位于`ui/flutter`目录下。 > 前端代码位于`ui/flutter`目录下。
### 环境要求 ### 🌍 环境要求
1. Golang 1.23+ 1. Golang 1.24+
2. Flutter 3.24+ 2. Flutter 3.38+
### 克隆项目 ### 📋 克隆项目
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### 贡献代码 ### 🤝 贡献代码
请参考[贡献指南](CONTRIBUTING_zh-CN.md) 请参考[贡献指南](CONTRIBUTING_zh-CN.md)
### 编译 ### 🏗️ 编译
#### 桌面端 #### 桌面端
@@ -214,7 +278,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## 感谢 ## ❤️ 感谢
### 贡献者 ### 贡献者
+146 -81
View File
@@ -1,10 +1,11 @@
# [![](_docs/img/banner.png)](https://gopeed.com) # [![](_docs/img/banner.svg)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases) [![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![WeChat](https://img.shields.io/badge/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7-07C160?logo=wechat&logoColor=white)](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -13,94 +14,152 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 簡介 ## 🚀 簡介
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。 Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/) 前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
## 下載 ## ⬇️ 下載
<table> <table>
<thead>
<tr>
<th colspan="2">Windows</th>
<th>MacOS</th>
<th colspan="4">Linux</th>
<th>Android</th>
<th>iOS</th>
<th>Docker</th>
<th colspan="2">Qnap</th>
<th colspan="8">Web</th>
</tr>
<tr>
<td><code>EXE</code></td>
<td><code>Portable</code></td>
<td><code>DMG</code></td>
<td><code>Flathub</code></td>
<td><code>SNAP</code></td>
<td><code>DEB</code></td>
<td><code>AppImage</code></td>
<td><code>APK</code></td>
<td><code>IPA</code></td>
<td>-</td>
<td colspan="2"><code>QPKG</code></td>
<td colspan="3"><code>Windows</code></td>
<td colspan="2"><code>MacOS</code></td>
<td colspan="3"><code>Linux</code></td>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td> <td>amd64</td>
<td>amd64</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
<td>-</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>amd64</td>
<td>arm64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>amd64</td>
<td>386</td>
</tr> </tr>
<tr> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td> <td><code>Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td> </tr>
<td><a href="https://snapcraft.io/gopeed">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td> <td rowspan="3">🍎 MacOS</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">前往</a></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">前往</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">前往</a></td> <td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">前往</a></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">前往</a></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">前往</a></td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html) 更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
### 使用 CLI 安裝 ### 🛠️ 使用 CLI 安裝
使用`go install`安裝: 使用`go install`安裝:
@@ -108,36 +167,42 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## 贊助 ## 📱 微信公眾號
關注公眾號獲取項目最新動態和資訊。
<img src="_docs/img/weixin.png" width="200" />
## 💝 贊助
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝! 如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
## 軟體介面 ## 🖼️ 軟體介面
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## 開發 ## 👨‍💻 開發
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。 該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
> 前端代碼位於`ui/flutter`目錄內。 > 前端代碼位於`ui/flutter`目錄內。
### 開發環境 ### 🌍 開發環境
1. Golang 1.23+ 1. Golang 1.24+
2. Flutter 3.24+ 2. Flutter 3.38+
### 克隆項目 ### 📋 克隆項目
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### 協助開發 ### 🤝 協助開發
請參考[協助指南](CONTRIBUTING_zh-TW.md) 請參考[協助指南](CONTRIBUTING_zh-TW.md)
### 編譯 ### 🏗️ 編譯
#### 桌面端 #### 桌面端
@@ -210,7 +275,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## 感謝 ## ❤️ 感謝
### 貢獻者 ### 貢獻者
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

+54
View File
@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 400" width="1200" height="400">
<rect width="1200" height="400" fill="#040605"/>
<g fill="#5dc76c">
<rect x="-100" y="50" width="150" height="2" opacity="0.6">
<animate attributeName="x" from="-200" to="1300" dur="0.8s" repeatCount="indefinite"/>
</rect>
<rect x="-200" y="150" width="200" height="1" opacity="0.4">
<animate attributeName="x" from="-300" to="1300" dur="1.2s" repeatCount="indefinite"/>
</rect>
<rect x="-50" y="250" width="180" height="3" opacity="0.8">
<animate attributeName="x" from="-200" to="1300" dur="0.6s" repeatCount="indefinite"/>
</rect>
<rect x="-150" y="350" width="220" height="2" opacity="0.5">
<animate attributeName="x" from="-300" to="1300" dur="1s" repeatCount="indefinite"/>
</rect>
<rect x="-100" y="100" width="100" height="1" opacity="0.7">
<animate attributeName="x" from="-150" to="1300" dur="0.9s" repeatCount="indefinite"/>
</rect>
<rect x="-250" y="300" width="250" height="2" opacity="0.3">
<animate attributeName="x" from="-400" to="1300" dur="1.5s" repeatCount="indefinite"/>
</rect>
</g>
<defs>
<filter id="logoGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="12" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<g transform="translate(600, 160)">
<!-- Floating animation -->
<g>
<animateTransform attributeName="transform" type="translate" values="0,-8; 0,8; 0,-8" dur="3s" repeatCount="indefinite" />
<!-- Pulsing glow behind logo -->
<circle cx="0" cy="0" r="45" fill="#5dc76c" filter="url(#logoGlow)" opacity="0.3">
<animate attributeName="opacity" values="0.1; 0.6; 0.1" dur="2s" repeatCount="indefinite" />
<animate attributeName="r" values="40; 65; 40" dur="2s" repeatCount="indefinite" />
</circle>
<!-- Breathing scale animation -->
<g>
<animateTransform attributeName="transform" type="scale" values="0.95; 1.05; 0.95" dur="2s" repeatCount="indefinite" />
<g transform="scale(0.12) translate(-512, -512)">
<path style="opacity:0.998" fill="#5dc76c" d="M 951.5,763.5 C 940.739,755.276 929.405,747.776 917.5,741C 908.477,736.319 899.143,732.485 889.5,729.5C 885.844,625.779 840.844,545.613 754.5,489C 674.802,441.453 590.135,430.12 500.5,455C 419.755,480.605 359.588,530.771 320,605.5C 278.585,565.816 230.752,555.649 176.5,575C 131.948,594.935 106.448,629.102 100,677.5C 99.3333,685.833 99.3333,694.167 100,702.5C 102.081,714.824 105.081,726.824 109,738.5C 95.4882,745.492 82.9882,753.826 71.5,763.5C 37.7121,705.639 16.5454,643.306 8,576.5C -9.41698,416.759 35.9164,278.093 144,160.5C 244.661,58.2827 367.161,5.94939 511.5,3.5C 678.975,7.48829 814.142,74.8216 917,205.5C 1011.33,336.426 1040,480.759 1003,638.5C 991.564,682.671 974.397,724.337 951.5,763.5 Z"/>
<path style="opacity:0.999" fill="#fefefe" d="M 951.5,763.5 C 949.28,768.272 946.78,772.938 944,777.5C 938.305,786.913 932.138,795.913 925.5,804.5C 915.954,796.888 905.288,791.055 893.5,787C 878.282,782.958 863.115,778.791 848,774.5C 854.013,716.891 841.346,663.891 810,615.5C 768.341,553.228 711.174,512.395 638.5,493C 560.311,479.566 490.311,497.566 428.5,547C 411.963,560.536 397.13,575.703 384,592.5C 366.745,616.923 348.911,640.923 330.5,664.5C 310.135,651.302 290.469,637.135 271.5,622C 238.448,602.307 206.782,604.307 176.5,628C 159.375,642.752 148.875,661.252 145,683.5C 143.968,692.716 144.968,701.716 148,710.5C 154.334,725.339 160.167,740.339 165.5,755.5C 138.118,764.688 114.451,779.688 94.5,800.5C 85.653,788.968 77.9863,776.635 71.5,763.5C 82.9882,753.826 95.4882,745.492 109,738.5C 105.081,726.824 102.081,714.824 100,702.5C 99.3333,694.167 99.3333,685.833 100,677.5C 106.448,629.102 131.948,594.935 176.5,575C 230.752,555.649 278.585,565.816 320,605.5C 359.588,530.771 419.755,480.605 500.5,455C 590.135,430.12 674.802,441.453 754.5,489C 840.844,545.613 885.844,625.779 889.5,729.5C 899.143,732.485 908.477,736.319 917.5,741C 929.405,747.776 940.739,755.276 951.5,763.5 Z"/>
<path style="opacity:0.998" fill="#5dc76c" d="M 925.5,804.5 C 850.791,908.608 750.791,975.775 625.5,1006C 609.308,1009.7 592.975,1012.53 576.5,1014.5C 615.708,969.938 655.042,925.438 694.5,881C 664.168,880.5 633.835,880.333 603.5,880.5C 603.5,811.167 603.5,741.833 603.5,672.5C 542.167,672.5 480.833,672.5 419.5,672.5C 419.5,741.5 419.5,810.5 419.5,879.5C 388.493,879.167 357.493,879.5 326.5,880.5C 326.624,881.107 326.957,881.44 327.5,881.5C 366.805,925.808 406.138,970.141 445.5,1014.5C 318.63,996.317 212.796,939.651 128,844.5C 115.85,830.525 104.683,815.859 94.5,800.5C 114.451,779.688 138.118,764.688 165.5,755.5C 160.167,740.339 154.334,725.339 148,710.5C 144.968,701.716 143.968,692.716 145,683.5C 148.875,661.252 159.375,642.752 176.5,628C 206.782,604.307 238.448,602.307 271.5,622C 290.469,637.135 310.135,651.302 330.5,664.5C 348.911,640.923 366.745,616.923 384,592.5C 397.13,575.703 411.963,560.536 428.5,547C 490.311,497.566 560.311,479.566 638.5,493C 711.174,512.395 768.341,553.228 810,615.5C 841.346,663.891 854.013,716.891 848,774.5C 863.115,778.791 878.282,782.958 893.5,787C 905.288,791.055 915.954,796.888 925.5,804.5 Z"/>
<path style="opacity:0.999" fill="#fefefe" d="M 576.5,1014.5 C 532.836,1019.82 489.169,1019.82 445.5,1014.5C 406.138,970.141 366.805,925.808 327.5,881.5C 358.167,880.833 388.833,880.167 419.5,879.5C 419.5,810.5 419.5,741.5 419.5,672.5C 480.833,672.5 542.167,672.5 603.5,672.5C 603.5,741.833 603.5,811.167 603.5,880.5C 633.835,880.333 664.168,880.5 694.5,881C 655.042,925.438 615.708,969.938 576.5,1014.5 Z"/>
<path style="opacity:1" fill="#ade2b5" d="M 419.5,879.5 C 388.833,880.167 358.167,880.833 327.5,881.5C 326.957,881.44 326.624,881.107 326.5,880.5C 357.493,879.5 388.493,879.167 419.5,879.5 Z"/>
</g>
</g>
</g>
</g>
<text x="600" y="300" font-family="sans-serif" font-size="72" font-style="italic" font-weight="900" text-anchor="middle" fill="#fff" letter-spacing="8">GOPEED</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+1
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
+3 -2
View File
@@ -2,13 +2,14 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"sync"
"github.com/GopeedLab/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"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"
"strings"
"sync"
) )
const progressWidth = 20 const progressWidth = 20
+19
View File
@@ -0,0 +1,19 @@
//go:build !windows
// +build !windows
package main
import (
"net"
"os"
"path/filepath"
)
func Dial() (net.Conn, error) {
// Get binary path
exe, err := os.Executable()
if err != nil {
return nil, err
}
return net.Dial("unix", filepath.Join(filepath.Dir(exe), "gopeed_host.sock"))
}
+11
View File
@@ -0,0 +1,11 @@
package main
import (
"net"
"github.com/Microsoft/go-winio"
)
func Dial() (net.Conn, error) {
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
}
+113 -26
View File
@@ -1,22 +1,24 @@
package main package main
import ( import (
"bytes"
"context"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net"
"net/http"
"os" "os"
"strings" "time"
"github.com/pkg/browser" "github.com/pkg/browser"
"github.com/shirou/gopsutil/v4/process"
) )
const identifier = "gopeed"
type Message struct { type Message struct {
Method string `json:"method"` Method string `json:"method"`
Meta map[string]any `json:"meta"`
Params json.RawMessage `json:"params"` Params json.RawMessage `json:"params"`
} }
@@ -26,42 +28,127 @@ type Response struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }
var apiMap = map[string]func(params json.RawMessage) (data any, err error){ func check() (data bool, err error) {
"ping": func(params json.RawMessage) (data any, err error) { conn, err := Dial()
processes, err := process.Processes() if err != nil {
if err != nil { return false, err
return false, err }
} defer conn.Close()
return true, nil
}
for _, p := range processes { func wakeup(hidden bool) error {
name, err := p.Name() running, _ := check()
if err != nil { if running {
continue return nil
} }
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) { uri := "gopeed:"
return true, nil if hidden {
} uri = uri + "?hidden=true"
}
if err := browser.OpenURL(uri); err != nil {
return err
}
for i := 0; i < 10; i++ {
if ok, _ := check(); ok {
return nil
} }
return false, nil time.Sleep(1 * time.Second)
}
return fmt.Errorf("start gopeed failed")
}
// postToFlutter sends a POST request to Flutter RPC server
func postToFlutter(path string, body []byte, headers map[string]string, timeout time.Duration) (*http.Response, error) {
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return Dial()
},
},
Timeout: timeout,
}
req, err := http.NewRequest("POST", "http://127.0.0.1"+path, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}
return client.Do(req)
}
var apiMap = map[string]func(message *Message) (data any, err error){
"ping": func(message *Message) (data any, err error) {
return check()
}, },
"create": func(params json.RawMessage) (data any, err error) { "wakeup": func(message *Message) (data any, err error) {
var strParams string silent := false
if err = json.Unmarshal(params, &strParams); err != nil { if v, ok := message.Meta["silent"]; ok {
return silent, _ = v.(bool)
} }
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams)) err = wakeup(silent)
return return
}, },
"create": func(message *Message) (data any, err error) {
buf, err := message.Params.MarshalJSON()
if err != nil {
return
}
silent := false
if v, ok := message.Meta["silent"]; ok {
silent, _ = v.(bool)
}
if err := wakeup(silent); err != nil {
return nil, err
}
headers := make(map[string]string)
if message.Meta != nil {
metaJson, _ := json.Marshal(message.Meta)
headers["X-Gopeed-Host-Meta"] = string(metaJson)
}
_, err = postToFlutter("/create", buf, headers, 10*time.Second)
return
},
"forward": func(message *Message) (data any, err error) {
buf, err := message.Params.MarshalJSON()
if err != nil {
return
}
resp, err := postToFlutter("/forward", buf, nil, 60*time.Second)
if err != nil {
return
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var respData map[string]json.RawMessage
if err := json.Unmarshal(respBody, &respData); err != nil {
return nil, err
}
return respData, nil
},
} }
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
func main() { func main() {
for { for {
// Read message length (first 4 bytes) // Read message length (first 4 bytes)
var length uint32 var length uint32
if err := binary.Read(os.Stdin, binary.NativeEndian, &length); err != nil { if err := binary.Read(os.Stdin, binary.NativeEndian, &length); err != nil {
if err == io.EOF { if err == io.EOF {
// Connection closed by Chrome
return return
} }
sendError("Failed to read message length: " + err.Error()) sendError("Failed to read message length: " + err.Error())
@@ -86,7 +173,7 @@ func main() {
var data any var data any
var err error var err error
if handler, ok := apiMap[message.Method]; ok { if handler, ok := apiMap[message.Method]; ok {
data, err = handler(message.Params) data, err = handler(&message)
} else { } else {
err = errors.New("Unknown method: " + message.Method) err = errors.New("Unknown method: " + message.Method)
} }
+115
View File
@@ -0,0 +1,115 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"syscall"
"time"
"github.com/pkg/browser"
"github.com/pkg/errors"
)
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/updater
func main() {
pid := flag.Int("pid", 0, "PID of the process to update")
updateChannel := flag.String("channel", "", "Update channel")
packagePath := flag.String("asset", "", "Path to the package asset")
exeDir := flag.String("exeDir", "", "Directory of the entry executable")
logPath := flag.String("log", "", "Log file path")
flag.Parse()
if *pid == 0 {
log.Println("Invalid PID")
os.Exit(1)
}
if *updateChannel == "" {
log.Println("Invalid update channel")
os.Exit(1)
}
if *logPath != "" {
logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err == nil {
defer logFile.Close()
log.SetOutput(logFile)
}
}
var (
restart bool
err error
)
if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
log.Printf("Update failed: %v\n", err)
os.Exit(1)
}
// Restart the application
if restart {
browser.OpenURL("gopeed:///")
}
// Delete package asset
if *packagePath != "" {
os.Remove(*packagePath)
}
os.Exit(0)
}
func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
killSignalChan := make(chan any, 1)
go func() {
<-killSignalChan
if err = killProcess(pid); err != nil {
log.Printf("Failed to kill process: %v\n", err)
}
if err = waitForProcessExit(pid); err != nil {
log.Printf("Failed to wait for process exit: %v\n", err)
}
}()
log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
return false, errors.Wrap(err, "failed to install package")
}
return
}
func waitForProcessExit(pid int) error {
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
process, err := os.FindProcess(pid)
if err != nil {
// On some systems, error is returned if process doesn't exist
return nil
}
// Send null signal to test if process exists
err = process.Signal(syscall.Signal(0))
if err != nil {
// If error occurs, the process no longer exists
return nil
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("process %d still running after timeout", pid)
}
func killProcess(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
return err
}
return process.Kill()
}
+72
View File
@@ -0,0 +1,72 @@
//go:build darwin
package main
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
return true, installByDmg(killSignalChan, packagePath, destDir)
}
// installByDmg handles macOS dmg package installation
func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error {
// /Applications/Gopeed.app/Contents/MacOS -> /Applications
appPath := getParentDir(getParentDir(getParentDir(destDir)))
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse").Output()
if err != nil {
return err
}
mountPoint := ""
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "/Volumes/") {
// Find the /Volumes/ path in the line
// hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
// or with sequence number: /dev/disk4s1 Apple_HFS /Volumes/Gopeed 1
idx := strings.Index(line, "/Volumes/")
if idx != -1 {
// Extract everything from /Volumes/ onwards and trim whitespace
mountPoint = strings.TrimSpace(line[idx:])
break
}
}
}
if mountPoint == "" {
return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
}
// Detach the mounted DMG
defer exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
if err != nil {
return err
}
if len(matches) == 0 {
return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
}
killSignalChan <- nil
// Copy the new app to the destination
// cp -Rf /Volumes/GoPeed/GoPeed.app /Applications
if err := exec.Command("cp", "-Rf", matches[0], appPath).Run(); err != nil {
return err
}
return nil
}
// Get parent directory safely, handling trailing separators
func getParentDir(path string) string {
// Remove trailing separators if they exist
path = strings.TrimRight(path, string(filepath.Separator))
// Now get the parent directory
return filepath.Dir(path)
}
+72
View File
@@ -0,0 +1,72 @@
//go:build linux
package main
import (
"fmt"
"os/exec"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
killSignalChan <- nil
switch updateChannel {
case "linuxDeb":
return true, installByDeb(packagePath)
case "linuxFlathub":
return true, installByFlathub()
case "linuxSnap":
return true, installBySnap()
default:
return false, fmt.Errorf("unsupported update channel for Linux: %s", updateChannel)
}
}
// executeInTerminal tries to execute a command in one of several common terminal emulators
func executeInTerminal(command string) error {
terminals := []string{
"gnome-terminal", // GNOME
"konsole", // KDE
"xfce4-terminal", // XFCE
"xterm", // X11
}
command = fmt.Sprintf(`echo "Starting update..." && echo "[CMD] %s" && %s`, command, command)
for _, term := range terminals {
if _, err := exec.LookPath(term); err == nil {
var cmd *exec.Cmd
switch term {
case "gnome-terminal", "xfce4-terminal":
cmd = exec.Command(term, "--", "bash", "-c", command)
case "konsole":
cmd = exec.Command(term, "-e", "bash", "-c", command)
case "xterm":
cmd = exec.Command(term, "-e", command)
}
if err := cmd.Start(); err == nil {
return nil
}
}
}
return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
}
// installByDeb installs the .deb package
func installByDeb(packagePath string) error {
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)
return executeInTerminal(command)
}
// installByFlathub updates the application via Flathub
func installByFlathub() error {
command := "flatpak update com.gopeed.Gopeed -y"
return executeInTerminal(command)
}
// installBySnap updates the application via Snap
func installBySnap() error {
command := "sudo snap refresh gopeed"
return executeInTerminal(command)
}
+134
View File
@@ -0,0 +1,134 @@
//go:build windows
package main
import (
"archive/zip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
switch updateChannel {
case "windowsInstaller":
return false, installByInstaller(killSignalChan, packagePath, destDir)
default:
return true, installByPortable(killSignalChan, packagePath, destDir)
}
}
// installByInstaller extracts the installer from the zip file and runs it
func installByInstaller(killSignalChan chan<- any, packagePath, destDir string) error {
// Create a temp directory for extraction
tempDir, err := os.MkdirTemp("", "gopeed_update")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
// Extract the zip file
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
// Find the installer file
var installerPath string
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
// Extract file
path := filepath.Join(tempDir, file.Name)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}
_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
// If this is likely an installer (.exe, .msi), save its path
ext := strings.ToLower(filepath.Ext(file.Name))
if ext == ".exe" || ext == ".msi" {
installerPath = path
}
}
if installerPath == "" {
return fmt.Errorf("no installer found in the update package")
}
// Run the installer
cmd := exec.Command(installerPath)
if err := cmd.Start(); err != nil {
return err
}
killSignalChan <- nil
return nil
}
// installByPortable extracts the portable version to the destination directory
func installByPortable(killSignalChan chan<- any, packagePath, destDir string) error {
killSignalChan <- nil
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
for _, file := range reader.File {
path := filepath.Join(destDir, file.Name)
if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}
_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
}
return nil
}
+166 -51
View File
@@ -3,19 +3,23 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strconv"
"strings"
"github.com/GopeedLab/gopeed/pkg/base"
) )
type args struct { type args struct {
Address *string `json:"address"` Address *string `json:"address"`
Port *int `json:"port"` Port *int `json:"port"`
Username *string `json:"username"` Username *string `json:"username"`
Password *string `json:"password"` Password *string `json:"password"`
ApiToken *string `json:"apiToken"` ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"` StorageDir *string `json:"storageDir"`
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
// DownloadConfig when the first time to start the server, it will be configured as initial value // DownloadConfig when the first time to start the server, it will be configured as initial value
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"` DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
@@ -23,62 +27,173 @@ type args struct {
} }
func parse() *args { func parse() *args {
var cliArgs args cfg := &args{}
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port") cliConfig := loadCliArgs()
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username") loadConfigFile(cfg, *cliConfig.configPath)
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd") loadEnvVars(cfg)
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication") // override with non-default command line arguments
cliArgs.StorageDir = flag.String("d", "", "Storage directory") overrideWithCliArgs(cfg, cliConfig)
cliArgs.configPath = flag.String("c", "./config.json", "Config file path") // set default values
setDefaults(cfg, cliConfig)
return cfg
}
// loadCliArgs parses command line arguments and returns initial config
func loadCliArgs() *args {
cfg := &args{}
cfg.Address = flag.String("A", "0.0.0.0", "Bind Address")
cfg.Port = flag.Int("P", 9999, "Bind Port")
cfg.Username = flag.String("u", "gopeed", "Web Authentication Username")
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
cfg.StorageDir = flag.String("d", "", "Storage directory")
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
cfg.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse() flag.Parse()
// args priority: config file > cli args // Parse white download directories from comma-separated string
cfgArgs := loadConfig(*cliArgs.configPath) if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
if cfgArgs.Address == nil { dirs := strings.Split(*whiteDownloadDirs, ",")
cfgArgs.Address = cliArgs.Address for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
cfg.WhiteDownloadDirs = dirs
} }
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port return cfg
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
if cfgArgs.StorageDir == nil {
cfgArgs.StorageDir = cliArgs.StorageDir
}
return cfgArgs
} }
func loadConfig(path string) *args { // overrideWithCliArgs overrides config with non-empty command line arguments
var args args func overrideWithCliArgs(cfg *args, cliConfig *args) {
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "A":
cfg.Address = cliConfig.Address
case "P":
cfg.Port = cliConfig.Port
case "u":
cfg.Username = cliConfig.Username
case "p":
cfg.Password = cliConfig.Password
case "T":
cfg.ApiToken = cliConfig.ApiToken
case "d":
cfg.StorageDir = cliConfig.StorageDir
case "w":
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
case "c":
cfg.configPath = cliConfig.configPath
}
})
}
if !filepath.IsAbs(path) { // setDefaults sets default values for any unset configuration fields
func setDefaults(cfg *args, cliConfig *args) {
if cfg.Address == nil {
cfg.Address = cliConfig.Address
}
if cfg.Port == nil {
cfg.Port = cliConfig.Port
}
if cfg.Username == nil {
cfg.Username = cliConfig.Username
}
if cfg.Password == nil {
cfg.Password = cliConfig.Password
}
if cfg.ApiToken == nil {
cfg.ApiToken = cliConfig.ApiToken
}
if cfg.StorageDir == nil {
cfg.StorageDir = cliConfig.StorageDir
}
}
// loadConfigFile loads configuration from file
func loadConfigFile(cfg *args, configPath string) {
if !filepath.IsAbs(configPath) {
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { if err != nil {
fmt.Println("config dir get failed, reason:" + err.Error()) return
return &args
} }
path = filepath.Join(dir, path) configPath = filepath.Join(dir, configPath)
} }
file, err := os.ReadFile(path)
file, err := os.ReadFile(configPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return &args return
} }
fmt.Println("config file read failed, reason:" + err.Error()) return
return &args
} }
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error()) if err = json.Unmarshal(file, cfg); err != nil {
return &args return
}
}
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
func loadEnvVars(cfg *args) {
v := reflect.ValueOf(cfg).Elem()
t := reflect.TypeOf(cfg).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
// Get json tag as environment variable suffix
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Remove options like omitempty
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
jsonTag = jsonTag[:commaIdx]
}
// Convert to uppercase and add GOPEED_ prefix
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
envValue := os.Getenv(envKey)
if envValue == "" {
continue
}
// Set value based on field type
if field.Kind() == reflect.Ptr {
if field.IsNil() {
// Create new pointer instance
newVal := reflect.New(field.Type().Elem())
field.Set(newVal)
}
switch field.Type().Elem().Kind() {
case reflect.String:
field.Elem().SetString(envValue)
case reflect.Int:
if intVal, err := strconv.Atoi(envValue); err == nil {
field.Elem().SetInt(int64(intVal))
}
default:
// For complex types like DownloadConfig, try JSON unmarshaling
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
var config base.DownloaderStoreConfig
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
field.Set(reflect.ValueOf(&config))
}
}
}
} else if field.Kind() == reflect.Slice {
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
if field.Type().Elem().Kind() == reflect.String {
dirs := strings.Split(envValue, ",")
for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
field.Set(reflect.ValueOf(dirs))
}
}
} }
fmt.Printf("config file loaded: %s\n", path)
return &args
} }
File diff suppressed because it is too large. Load diff
+19 -17
View File
@@ -6,11 +6,12 @@ package main
import ( import (
"embed" "embed"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
) )
//go:embed dist/* //go:embed dist/*
@@ -23,36 +24,37 @@ func main() {
} }
args := parse() args := parse()
var webBasicAuth *model.WebBasicAuth var webAuth *model.WebAuth
if isNotBlank(args.Username) && isNotBlank(args.Password) { if isNotBlank(args.Username) && isNotBlank(args.Password) {
webBasicAuth = &model.WebBasicAuth{ webAuth = &model.WebAuth{
Username: *args.Username, Username: *args.Username,
Password: *args.Password, Password: *args.Password,
} }
} }
var dir string var storageDir string
if args.StorageDir != nil && *args.StorageDir != "" { if args.StorageDir != nil && *args.StorageDir != "" {
dir = *args.StorageDir storageDir = *args.StorageDir
} else { } else {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
panic(err) panic(err)
} }
dir = filepath.Dir(exe) storageDir = filepath.Join(filepath.Dir(exe), "storage")
} }
cfg := &model.StartConfig{ cfg := &model.StartConfig{
Network: "tcp", Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt, Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"), StorageDir: storageDir,
ApiToken: *args.ApiToken, WhiteDownloadDirs: args.WhiteDownloadDirs,
DownloadConfig: args.DownloadConfig, ApiToken: *args.ApiToken,
ProductionMode: true, DownloadConfig: args.DownloadConfig,
WebEnable: true, ProductionMode: true,
WebFS: sub, WebEnable: true,
WebBasicAuth: webBasicAuth, WebFS: sub,
WebAuth: webAuth,
} }
cmd.Start(cfg) cmd.Start(cfg)
} }
+92 -67
View File
@@ -1,139 +1,164 @@
module github.com/GopeedLab/gopeed module github.com/GopeedLab/gopeed
go 1.23.0 go 1.24.9
toolchain go1.23.1 toolchain go1.24.11
require ( require (
github.com/anacrolix/missinggo/v2 v2.8.0 github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621
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-20240919115326-6c7d1df7ff05 github.com/bodgit/sevenzip v1.6.1
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/go-git/go-git/v5 v5.8.1 github.com/go-git/go-git/v5 v5.8.1
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/imroc/req/v3 v3.52.2
github.com/matoous/go-nanoid/v2 v2.0.0 github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mattn/go-ieproxy v0.0.12 github.com/mattn/go-ieproxy v0.0.12
github.com/mholt/archives v0.1.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
go.etcd.io/bbolt v1.3.11 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
golang.org/x/sync v0.9.0
) )
require ( require (
github.com/ebitengine/purego v0.8.1 // indirect github.com/STARRY-S/zip v0.2.3 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/anacrolix/btree v0.1.1 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/bodgit/plumbing v1.3.0 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.51.0 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sync v0.19.0 // indirect
) )
require ( require (
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
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.9.4 // indirect github.com/RoaringBitmap/roaring v1.9.4 // 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/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/chansync v0.6.0 // indirect github.com/anacrolix/chansync v0.7.0 // indirect
github.com/anacrolix/dht/v2 v2.22.0 // indirect github.com/anacrolix/dht/v2 v2.23.0 // indirect
github.com/anacrolix/envpprof v1.4.0 // indirect github.com/anacrolix/envpprof v1.5.0 // indirect
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect github.com/anacrolix/generics v0.2.0 // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect github.com/anacrolix/go-libutp v1.3.2 // indirect
github.com/anacrolix/log v0.16.0 // indirect github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // 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.1.1 // indirect github.com/anacrolix/mmsg v1.1.1 // indirect
github.com/anacrolix/multiless v0.4.0 // indirect github.com/anacrolix/multiless v0.4.0 // indirect
github.com/anacrolix/stm v0.5.0 // indirect github.com/anacrolix/stm v0.5.0 // indirect
github.com/anacrolix/sync v0.5.3 // indirect github.com/anacrolix/sync v0.6.0 // indirect
github.com/anacrolix/upnp v0.1.4 // indirect github.com/anacrolix/upnp v0.1.4 // indirect
github.com/anacrolix/utp v0.2.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.4.3 // indirect github.com/benbjohnson/immutable v0.4.3 // indirect
github.com/bits-and-blooms/bitset v1.15.0 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect github.com/edsrzf/mmap-go v1.2.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-llsqlite/adapter v0.1.0 // indirect github.com/go-llsqlite/adapter v0.2.0 // indirect
github.com/go-llsqlite/crawshaw v0.5.5 // indirect github.com/go-llsqlite/crawshaw v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // 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.4+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.4+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.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.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/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.3.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.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect github.com/multiformats/go-varint v0.1.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pion/datachannel v1.5.9 // indirect github.com/pion/datachannel v1.6.0 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/ice/v4 v4.0.2 // indirect github.com/pion/ice/v4 v4.2.0 // indirect
github.com/pion/interceptor v0.1.37 // indirect github.com/pion/interceptor v0.1.42 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.14 // indirect github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/rtp v1.8.9 // indirect github.com/pion/rtp v1.10.0 // indirect
github.com/pion/sctp v1.8.33 // indirect github.com/pion/sctp v1.9.1 // indirect
github.com/pion/sdp/v3 v3.0.9 // indirect github.com/pion/sdp/v3 v3.0.17 // indirect
github.com/pion/srtp/v3 v3.0.4 // indirect github.com/pion/srtp/v3 v3.0.10 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/stun/v3 v3.1.1 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.1.4 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect
github.com/pion/webrtc/v4 v4.0.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/protolambda/ctxlock v0.1.0 // indirect github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.24.12
github.com/skeema/knownhosts v1.2.0 // indirect github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect github.com/tidwall/btree v1.8.1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.29.0 // indirect golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.33.0
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.27.0 // indirect golang.org/x/tools v0.40.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.4.1 // indirect
modernc.org/libc v1.61.0 // indirect modernc.org/libc v1.67.4 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.33.1 // indirect modernc.org/sqlite v1.43.0 // indirect
zombiezen.com/go/sqlite v1.4.0 // indirect zombiezen.com/go/sqlite v1.4.2 // indirect
) )
+387 -231
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -24,8 +24,10 @@ type DefaultFileController struct {
func NewController() *Controller { func NewController() *Controller {
return &Controller{ return &Controller{
GetConfig: func(v any) {}, GetConfig: func(v any) {},
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil }, GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
return requestProxy.ToHandler()
},
FileController: &DefaultFileController{}, FileController: &DefaultFileController{},
} }
} }
+8 -6
View File
@@ -1,21 +1,23 @@
package fetcher package fetcher
import ( import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/pkg/base"
"path" "path"
"strings" "strings"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/pkg/base"
) )
// Fetcher defines the interface for a download protocol. // Fetcher defines the interface for a download protocol.
// Each download task will have a corresponding Fetcher instance for the management of the download task // Each download task will have a corresponding Fetcher instance for the management of the download task
type Fetcher interface { type Fetcher interface {
Setup(ctl *controller.Controller) Setup(ctl *controller.Controller)
// Resolve resource info from request Resolve(req *base.Request, opts *base.Options) error
Resolve(req *base.Request) error
// Create ready to download, but not started
Create(opts *base.Options) error
Start() error Start() error
// Patch modifies task-specific data based on the protocol.
// For HTTP: can modify Request info (URL, headers, etc.)
// For BT: can modify SelectFiles (via opts.SelectFiles)
Patch(req *base.Request, opts *base.Options) error
Pause() error Pause() error
Close() error Close() error
+1 -1
View File
@@ -4,7 +4,7 @@ type config struct {
ListenPort int `json:"listenPort"` ListenPort int `json:"listenPort"`
Trackers []string `json:"trackers"` Trackers []string `json:"trackers"`
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped. // SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
SeedKeep bool `json:"seedKeep` SeedKeep bool `json:"seedKeep"`
// SeedRatio is the ratio of uploaded data to downloaded data to seed. // SeedRatio is the ratio of uploaded data to downloaded data to seed.
SeedRatio float64 `json:"seedRatio"` SeedRatio float64 `json:"seedRatio"`
// SeedTime is the time in seconds to seed after downloading is complete. // SeedTime is the time in seconds to seed after downloading is complete.
+125 -59
View File
@@ -4,13 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"io" "io"
"net/url" "net/url"
"os" "os"
@@ -19,15 +12,23 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
) )
var ( var (
client *torrent.Client cfg *torrent.ClientConfig
lock sync.Mutex client *torrent.Client
torrentDirMap = make(map[string]string) lock sync.Mutex
ftMap = make(map[string]*fileTorrentImpl) closeCtx context.Context
closeCtx context.Context closeFunc func()
closeFunc func()
) )
type Fetcher struct { type Fetcher struct {
@@ -70,21 +71,12 @@ func (f *Fetcher) initClient() (err error) {
closeCtx, closeFunc = context.WithCancel(context.Background()) closeCtx, closeFunc = context.WithCancel(context.Background())
} }
cfg := torrent.NewDefaultClientConfig() cfg = torrent.NewDefaultClientConfig()
cfg.Seed = true cfg.Seed = true
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20()) cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version) cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
cfg.ListenPort = f.config.ListenPort cfg.ListenPort = f.config.ListenPort
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy) cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
if dir, ok := torrentDirMap[infoHash.String()]; ok {
ft.setTorrentDir(dir)
}
ftMap[infoHash.String()] = ft
},
})
dnsResolver := &DnsCacheResolver{RefreshTimeout: 5 * time.Minute} dnsResolver := &DnsCacheResolver{RefreshTimeout: 5 * time.Minute}
cfg.TrackerDialContext = dnsResolver.DialContext cfg.TrackerDialContext = dnsResolver.DialContext
client, err = torrent.NewClient(cfg) client, err = torrent.NewClient(cfg)
@@ -99,11 +91,12 @@ func (f *Fetcher) initClient() (err error) {
return return
} }
func (f *Fetcher) Resolve(req *base.Request) error { func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
f.meta.Req = req f.meta.Req = req
f.meta.Opts = opts
if f.meta.Opts == nil {
f.meta.Opts = &base.Options{}
}
if err := f.addTorrent(req, false); err != nil { if err := f.addTorrent(req, false); err != nil {
return err return err
} }
@@ -111,23 +104,13 @@ func (f *Fetcher) Resolve(req *base.Request) error {
return nil return nil
} }
func (f *Fetcher) Create(opts *base.Options) (err error) {
f.meta.Opts = opts
if f.meta.Res != nil {
torrentDirMap[f.meta.Res.Hash] = opts.Path
}
return nil
}
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
if !f.torrentReady.Load() { if !f.torrentReady.Load() {
if err = f.addTorrent(f.meta.Req, false); err != nil { if err = f.addTorrent(f.meta.Req, false); err != nil {
return return
} }
} }
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
ft.setTorrentDir(f.meta.Opts.Path)
}
files := f.torrent.Files() files := f.torrent.Files()
// If the user does not specify the file to download, all files will be downloaded by default // If the user does not specify the file to download, all files will be downloaded by default
if f.data.Progress == nil { if f.data.Progress == nil {
@@ -147,12 +130,12 @@ func (f *Fetcher) Start() (err error) {
file.Download() file.Download()
} }
} }
f.torrent.AllowDataDownload()
return return
} }
func (f *Fetcher) Pause() (err error) { func (f *Fetcher) Pause() (err error) {
f.torrentReady.Store(false) f.torrent.DisallowDataDownload()
f.safeDrop()
return return
} }
@@ -249,6 +232,59 @@ func (f *Fetcher) isDone() bool {
return true return true
} }
// Patch modifies the BT task settings.
// Invalid file indices are silently ignored.
func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error {
if opts == nil {
return nil
}
if opts.SelectFiles != nil {
selectFiles := opts.SelectFiles
// Get file count from resource metadata
fileCount := 0
if f.meta.Res != nil {
fileCount = len(f.meta.Res.Files)
}
// Filter out invalid indices (silently ignore)
validSelectFiles := make([]int, 0, len(selectFiles))
for _, idx := range selectFiles {
if idx >= 0 && idx < fileCount {
validSelectFiles = append(validSelectFiles, idx)
}
}
if f.torrent != nil {
files := f.torrent.Files()
// Cancel all current file downloads first
f.torrent.CancelPieces(0, f.torrent.NumPieces())
// Apply new file selection
if len(validSelectFiles) == len(files) {
f.torrent.DownloadAll()
} else {
for _, selectIndex := range validSelectFiles {
file := files[selectIndex]
file.Download()
}
}
}
f.meta.Opts.SelectFiles = validSelectFiles
// Recalculate the resource size based on new selection
if f.meta.Res != nil {
f.meta.Res.CalcSize(validSelectFiles)
}
// Reset progress tracking for new file selection
f.data.Progress = make(fetcher.Progress, len(validSelectFiles))
}
return nil
}
func (f *Fetcher) updateRes() { func (f *Fetcher) updateRes() {
res := &base.Resource{ res := &base.Resource{
Range: true, Range: true,
@@ -355,12 +391,20 @@ func (f *Fetcher) WaitUpload() (err error) {
} }
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) { func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return
}
if err = f.initClient(); err != nil { if err = f.initClient(); err != nil {
return return
} }
schema := util.ParseSchema(req.URL) schema := util.ParseSchema(req.URL)
privateTorrent := false
var spec *torrent.TorrentSpec
if schema == "MAGNET" { if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(req.URL) spec, err = torrent.TorrentSpecFromMagnetUri(req.URL)
if err != nil {
return
}
} else { } else {
var reader io.Reader var reader io.Reader
if schema == "FILE" { if schema == "FILE" {
@@ -388,34 +432,56 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
if err != nil && !strings.Contains(err.Error(), "expected EOF") { if err != nil && !strings.Contains(err.Error(), "expected EOF") {
return err return err
} }
f.torrent, err = client.AddTorrent(metaInfo)
info, er := metaInfo.UnmarshalInfo()
if er != nil {
return er
}
if info.Private != nil && *info.Private {
privateTorrent = true
}
spec, err = torrent.TorrentSpecFromMetaInfoErr(metaInfo)
if err != nil {
return
}
} }
spec.Storage = storage.NewFileOpts(storage.NewFileClientOpts{
ClientBaseDir: cfg.DataDir,
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
return f.meta.Opts.Path
},
})
f.torrent, _, err = client.AddTorrentSpec(spec)
if err != nil { if err != nil {
return return
} }
// use map to deduplicate // Do not add external tracker to a private torrent.
trackers := make(map[string]bool) if !privateTorrent {
if req.Extra != nil { // use map to deduplicate
extra := req.Extra.(*bt.ReqExtra) trackers := make(map[string]bool)
if len(extra.Trackers) > 0 { if req.Extra != nil {
for _, tracker := range extra.Trackers { extra := req.Extra.(*bt.ReqExtra)
if len(extra.Trackers) > 0 {
for _, tracker := range extra.Trackers {
trackers[tracker] = true
}
}
}
if len(f.config.Trackers) > 0 {
for _, tracker := range f.config.Trackers {
trackers[tracker] = true trackers[tracker] = true
} }
} }
} if len(trackers) > 0 {
if len(f.config.Trackers) > 0 { announceList := make([][]string, 0)
for _, tracker := range f.config.Trackers { for tracker := range trackers {
trackers[tracker] = true announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
} }
} }
if len(trackers) > 0 {
announceList := make([][]string, 0)
for tracker := range trackers {
announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
}
<-f.torrent.GotInfo() <-f.torrent.GotInfo()
f.torrentReady.Store(true) f.torrentReady.Store(true)
+138 -10
View File
@@ -3,17 +3,18 @@ package bt
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"testing" "testing"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
) )
func TestFetcher_Resolve_Torrent(t *testing.T) { func TestFetcher_Resolve_Torrent(t *testing.T) {
@@ -30,7 +31,7 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf) dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
err = fetcher.Resolve(&base.Request{ err = fetcher.Resolve(&base.Request{
URL: dataUri, URL: dataUri,
}) }, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -80,7 +81,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
"udp://tracker.bitsearch.to:1337/announce", "udp://tracker.bitsearch.to:1337/announce",
}, },
}, },
}) }, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -110,7 +111,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
"udp://tracker.bitsearch.to:1337/announce", "udp://tracker.bitsearch.to:1337/announce",
}, },
}, },
}) }, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -144,7 +145,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
t.Run("Resolve Unclean Torrent", func(t *testing.T) { t.Run("Resolve Unclean Torrent", func(t *testing.T) {
err := fetcher.Resolve(&base.Request{ err := fetcher.Resolve(&base.Request{
URL: "./testdata/test.unclean.torrent", URL: "./testdata/test.unclean.torrent",
}) }, nil)
if err != nil { if err != nil {
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err) t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
} }
@@ -155,7 +156,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
uri := "file:///" + file uri := "file:///" + file
err := fetcher.Resolve(&base.Request{ err := fetcher.Resolve(&base.Request{
URL: uri, URL: uri,
}) }, nil)
if err != nil { if err != nil {
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err) t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
} }
@@ -238,3 +239,130 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
fetcher.Setup(newController) fetcher.Setup(newController)
return fetcher return fetcher
} }
// TestFetcher_Patch tests the Patch functionality for BT fetcher.
// It tests modifying selected files after Resolve (without downloading).
func TestFetcher_Patch(t *testing.T) {
f := buildFetcher()
// Resolve a multi-file torrent
err := f.Resolve(&base.Request{
URL: "./testdata/test.torrent",
}, nil)
if err != nil {
t.Fatalf("Resolve failed: %v", err)
}
// Verify initial state: 3 files, all selected by default
meta := f.Meta()
if len(meta.Res.Files) != 3 {
t.Fatalf("Expected 3 files, got %d", len(meta.Res.Files))
}
if len(meta.Opts.SelectFiles) != 3 {
t.Fatalf("Expected 3 selected files, got %d", len(meta.Opts.SelectFiles))
}
// Total size of all files: 107484864
totalSize := int64(107484864)
if meta.Res.Size != totalSize {
t.Fatalf("Expected total size %d, got %d", totalSize, meta.Res.Size)
}
t.Run("Patch with valid indices", func(t *testing.T) {
// Select only file 0 and 2 (c.txt and a.txt)
err := f.Patch(nil, &base.Options{
SelectFiles: []int{0, 2},
})
if err != nil {
t.Fatalf("Patch failed: %v", err)
}
meta := f.Meta()
if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 2}) {
t.Errorf("Expected SelectFiles [0, 2], got %v", meta.Opts.SelectFiles)
}
// Size should be recalculated: c.txt (98501754) + a.txt (78114) = 98579868
expectedSize := int64(98501754 + 78114)
if meta.Res.Size != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size)
}
})
t.Run("Patch with invalid indices are silently ignored", func(t *testing.T) {
// Mix of valid (0, 1) and invalid (-1, 5, 100) indices
err := f.Patch(nil, &base.Options{
SelectFiles: []int{-1, 0, 5, 1, 100},
})
if err != nil {
t.Fatalf("Patch should not return error for invalid indices: %v", err)
}
meta := f.Meta()
// Only valid indices 0 and 1 should remain
if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 1}) {
t.Errorf("Expected SelectFiles [0, 1], got %v", meta.Opts.SelectFiles)
}
// Size should be: c.txt (98501754) + b.txt (8904996) = 107406750
expectedSize := int64(98501754 + 8904996)
if meta.Res.Size != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size)
}
})
t.Run("Patch with all invalid indices results in empty selection", func(t *testing.T) {
err := f.Patch(nil, &base.Options{
SelectFiles: []int{-5, 10, 999},
})
if err != nil {
t.Fatalf("Patch should not return error: %v", err)
}
meta := f.Meta()
if len(meta.Opts.SelectFiles) != 0 {
t.Errorf("Expected empty SelectFiles, got %v", meta.Opts.SelectFiles)
}
// Note: CalcSize with empty selectFiles calculates total size of all files
// This is by design - empty selection in CalcSize means "all files"
// But SelectFiles being empty means no files are selected for download
if meta.Res.Size != totalSize {
t.Errorf("Expected size %d (CalcSize with empty slice = all files), got %d", totalSize, meta.Res.Size)
}
})
t.Run("Patch with nil opts does nothing", func(t *testing.T) {
// First set a known state
f.Patch(nil, &base.Options{SelectFiles: []int{1}})
prevSelectFiles := f.Meta().Opts.SelectFiles
// Patch with nil opts
err := f.Patch(nil, nil)
if err != nil {
t.Fatalf("Patch with nil opts should not fail: %v", err)
}
// Should remain unchanged
if !reflect.DeepEqual(f.Meta().Opts.SelectFiles, prevSelectFiles) {
t.Errorf("SelectFiles should remain unchanged after nil opts Patch")
}
})
t.Run("Patch progress array is resized", func(t *testing.T) {
btFetcher := f.(*Fetcher)
// Initialize progress array
btFetcher.data.Progress = make(fetcher.Progress, 3)
err := f.Patch(nil, &base.Options{
SelectFiles: []int{0, 2},
})
if err != nil {
t.Fatalf("Patch failed: %v", err)
}
if len(btFetcher.data.Progress) != 2 {
t.Errorf("Expected Progress length 2, got %d", len(btFetcher.data.Progress))
}
})
}
-215
View File
@@ -1,215 +0,0 @@
// modify from github.com/anacrolix/torrent/storage/file.go
package bt
import (
"context"
"fmt"
"github.com/anacrolix/torrent/storage"
"io"
"os"
"path/filepath"
"github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/common"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/segments"
)
// File-based storage for torrents, that isn't yet bound to a particular torrent.
type fileClientImpl struct {
opts newFileClientOpts
}
type newFileClientOpts struct {
// The base directory for all downloads.
ClientBaseDir string
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
FilePathMaker storage.FilePathMaker
PieceCompletion storage.PieceCompletion
}
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
if opts.FilePathMaker == nil {
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
var parts []string
if opts.Info.Name != metainfo.NoName {
parts = append(parts, opts.Info.Name)
}
return filepath.Join(append(parts, opts.File.Path...)...)
}
}
if opts.PieceCompletion == nil {
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
if err != nil {
ret = storage.NewMapPieceCompletion()
}
opts.PieceCompletion = ret
}
return fileClientImpl{opts}
}
func (fs fileClientImpl) Close() error {
return fs.opts.PieceCompletion.Close()
}
func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
upvertedFiles := info.UpvertedFiles()
files := make([]file, 0, len(upvertedFiles))
for _, fileInfo := range upvertedFiles {
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
Info: info,
File: &fileInfo,
}))
f := file{
rawPath: filePath,
path: filePath,
length: fileInfo.Length,
}
if f.length == 0 {
err = CreateNativeZeroLengthFile(f.path)
if err != nil {
err = fmt.Errorf("creating zero length file: %w", err)
return
}
}
files = append(files, f)
}
t := &fileTorrentImpl{
files,
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
infoHash,
fs.opts.PieceCompletion,
}
fs.opts.HandleFileTorrent(infoHash, t)
return storage.TorrentImpl{
Piece: t.Piece,
Close: t.Close,
}, nil
}
type file struct {
// The safe, OS-local file path.
rawPath string
path string
length int64
}
type fileTorrentImpl struct {
files []file
segmentLocater segments.Index
infoHash metainfo.Hash
completion storage.PieceCompletion
}
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
// Create a view onto the file-based torrent storage.
_io := fileTorrentImplIO{fts}
// Return the appropriate segments of this.
return &filePieceImpl{
fts,
p,
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
io.NewSectionReader(_io, p.Offset(), p.Length()),
}
}
func (fts *fileTorrentImpl) Close() error {
return nil
}
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
for i, f := range fts.files {
fts.files[i].path = filepath.Join(dir, f.rawPath)
}
}
// A helper to create zero-length files which won't appear for file-orientated storage since no
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
// caller should make sure the file name provided is safe/sanitized.
func CreateNativeZeroLengthFile(name string) error {
os.MkdirAll(filepath.Dir(name), 0o777)
var f io.Closer
f, err := os.Create(name)
if err != nil {
return err
}
return f.Close()
}
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
type fileTorrentImplIO struct {
fts *fileTorrentImpl
}
// Returns EOF on short or missing file.
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
f, err := os.Open(file.path)
if os.IsNotExist(err) {
// File missing is treated the same as a short file.
err = io.EOF
return
}
if err != nil {
return
}
defer f.Close()
// Limit the read to within the expected bounds of this file.
if int64(len(b)) > file.length-off {
b = b[:file.length-off]
}
for off < file.length && len(b) != 0 {
n1, err1 := f.ReadAt(b, off)
b = b[n1:]
n += n1
off += int64(n1)
if n1 == 0 {
err = err1
break
}
}
return
}
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
n += n1
b = b[n1:]
err = err1
return err == nil // && int64(n1) == e.Length
})
if len(b) != 0 && err == nil {
err = io.EOF
}
return
}
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
// log.Printf("write at %v: %v bytes", off, len(p))
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
name := fst.fts.files[i].path
os.MkdirAll(filepath.Dir(name), 0o777)
var f *os.File
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return false
}
var n1 int
n1, err = f.WriteAt(p[:e.Length], e.Start)
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
closeErr := f.Close()
n += n1
p = p[n1:]
if err == nil {
err = closeErr
}
if err == nil && int64(n1) != e.Length {
err = io.ErrShortWrite
}
return err == nil
})
return
}
-36
View File
@@ -1,36 +0,0 @@
// modify from github.com/anacrolix/torrent/storage/file_misc.go
package bt
import "github.com/anacrolix/torrent/metainfo"
type requiredLength struct {
fileIndex int
length int64
}
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
if n == 0 {
return
}
for i, fi := range info.UpvertedFiles() {
if off >= fi.Length {
off -= fi.Length
continue
}
n1 := n
if off+n1 > fi.Length {
n1 = fi.Length - off
}
ret = append(ret, requiredLength{
fileIndex: i,
length: off + n1,
})
n -= n1
if n == 0 {
return
}
off = 0
}
return
}
-61
View File
@@ -1,61 +0,0 @@
// modify from github.com/anacrolix/torrent/storage/file_piece.go
package bt
import (
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
"io"
"log"
"os"
)
type filePieceImpl struct {
*fileTorrentImpl
p metainfo.Piece
io.WriterAt
io.ReaderAt
}
var _ storage.PieceImpl = (*filePieceImpl)(nil)
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
}
func (fs *filePieceImpl) Completion() storage.Completion {
c, err := fs.completion.Get(fs.pieceKey())
if err != nil {
log.Printf("error getting piece completion: %s", err)
c.Ok = false
return c
}
verified := true
if c.Complete {
// If it's allegedly complete, check that its constituent files have the necessary length.
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
s, err := os.Stat(fs.files[fi.fileIndex].path)
if err != nil || s.Size() < fi.length {
verified = false
break
}
}
}
if !verified {
// The completion was wrong, fix it.
c.Complete = false
fs.completion.Set(fs.pieceKey(), false)
}
return c
}
func (fs *filePieceImpl) MarkComplete() error {
return fs.completion.Set(fs.pieceKey(), true)
}
func (fs *filePieceImpl) MarkNotComplete() error {
return fs.completion.Set(fs.pieceKey(), false)
}
File diff suppressed because it is too large. Load diff
+105
View File
@@ -0,0 +1,105 @@
package http
import (
"net/url"
"path"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
)
// ============================================================================
// Fetcher Data (for Store/Restore)
// ============================================================================
type fetcherData struct {
Connections []*connection
RedirectURL string // Saved redirect URL for resume
}
// ============================================================================
// Fetcher Manager
// ============================================================================
type FetcherManager struct {
}
func (fm *FetcherManager) Name() string {
return "http"
}
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
return []*fetcher.SchemeFilter{
{
Type: fetcher.FilterTypeUrl,
Pattern: "HTTP",
},
{
Type: fetcher.FilterTypeUrl,
Pattern: "HTTPS",
},
}
}
func (fm *FetcherManager) Build() fetcher.Fetcher {
return &Fetcher{}
}
func (fm *FetcherManager) ParseName(u string) string {
var name string
url, err := url.Parse(u)
if err != nil {
return ""
}
name = path.Base(url.Path)
if name == "" || name == "/" || name == "." {
name = url.Hostname()
}
return name
}
func (fm *FetcherManager) AutoRename() bool {
return true
}
func (fm *FetcherManager) DefaultConfig() any {
return &config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
Connections: 16,
}
}
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher)
_f.redirectLock.Lock()
redirectURL := _f.redirectURL
_f.redirectLock.Unlock()
return &fetcherData{
Connections: _f.connections,
RedirectURL: redirectURL,
}, nil
}
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
fd := v.(*fetcherData)
fb := &FetcherManager{}
fetcher := fb.Build().(*Fetcher)
fetcher.meta = meta
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Connections) > 0 {
fetcher.connections = fd.Connections
}
// Restore redirect URL for resume
if fd.RedirectURL != "" {
fetcher.redirectURL = fd.RedirectURL
}
return fetcher
}
}
func (fm *FetcherManager) Close() error {
return nil
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,138 @@
package http
import (
"testing"
)
// TestParseFilenameWithAmpersand tests the fix for filenames containing & character
// Issue: filenames with & are HTML-encoded as &amp; and then truncated at the semicolon
func TestParseFilenameWithAmpersand(t *testing.T) {
tests := []struct {
name string
disposition string
want string
}{
{
name: "quoted filename with &amp;",
disposition: `attachment; filename="查询处理&amp;优化.pptx"`,
want: "查询处理&优化.pptx",
},
{
name: "unquoted filename with &amp;",
disposition: `attachment; filename=test&amp;file.txt`,
want: "test&file.txt",
},
{
name: "quoted filename with &amp; and extra params",
disposition: `attachment; filename="test&amp;file.txt"; charset=utf-8`,
want: "test&file.txt",
},
{
name: "filename with multiple HTML entities",
disposition: `attachment; filename="test&amp;&lt;&gt;.txt"`,
want: "test&<>.txt",
},
{
name: "normal filename without entities",
disposition: `attachment; filename="normal.txt"`,
want: "normal.txt",
},
{
name: "filename with actual ampersand (no encoding)",
disposition: `attachment; filename="test&file.txt"`,
want: "test&file.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseFilename(tt.disposition)
if got != tt.want {
t.Errorf("parseFilename() = %q, want %q", got, tt.want)
}
})
}
}
// TestFindParamValueEnd tests the helper function for finding parameter value boundaries
func TestFindParamValueEnd(t *testing.T) {
tests := []struct {
name string
value string
want int
}{
{
name: "quoted value with semicolon inside",
value: `"test&amp;file.txt"; charset=utf-8`,
want: 19, // Position of semicolon after closing quote
},
{
name: "quoted value without semicolon after",
value: `"test.txt"`,
want: -1,
},
{
name: "unquoted value with semicolon",
value: `test.txt; charset=utf-8`,
want: 8,
},
{
name: "unquoted value without semicolon",
value: `test.txt`,
want: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := findParamValueEnd(tt.value)
if got != tt.want {
t.Errorf("findParamValueEnd() = %d, want %d", got, tt.want)
}
})
}
}
// TestUnescapeHTMLEntities tests HTML entity unescaping
func TestUnescapeHTMLEntities(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "ampersand",
in: "test&amp;file.txt",
want: "test&file.txt",
},
{
name: "less than and greater than",
in: "&lt;test&gt;.txt",
want: "<test>.txt",
},
{
name: "quote",
in: "test&quot;file.txt",
want: "test\"file.txt",
},
{
name: "multiple entities",
in: "a&amp;b&lt;c&gt;d&quot;e",
want: "a&b<c>d\"e",
},
{
name: "no entities",
in: "normal.txt",
want: "normal.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := unescapeHTMLEntities(tt.in)
if got != tt.want {
t.Errorf("unescapeHTMLEntities() = %q, want %q", got, tt.want)
}
})
}
}
+474
View File
@@ -0,0 +1,474 @@
package http
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"mime"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"unicode/utf8"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"golang.org/x/text/encoding/simplifiedchinese"
)
type RequestError struct {
Code int
}
func NewRequestError(code int) *RequestError {
return &RequestError{Code: code}
}
func (re *RequestError) Error() string {
return fmt.Sprintf("http request fail, code:%d", re.Code)
}
func isFailureExemptHTTPCode(code int) bool {
if code >= 500 && code <= 599 {
return true
}
switch code {
case 429, 408, 440, 499:
return true
default:
return false
}
}
func shouldCountHTTPFailure(err error) bool {
var re *RequestError
if !errors.As(err, &re) {
return false
}
return !isFailureExemptHTTPCode(re.Code)
}
func extractRequestError(err error) *RequestError {
var re *RequestError
if errors.As(err, &re) {
return re
}
return nil
}
// buildRequest creates an HTTP request using the redirect URL if available.
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
return f.buildRequestWithURL(ctx, req, true)
}
// buildRequestWithOriginalURL creates an HTTP request using the original URL.
// This is used for retrying when the redirect URL has expired.
func (f *Fetcher) buildRequestWithOriginalURL(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
return f.buildRequestWithURL(ctx, req, false)
}
// buildRequestWithURL creates an HTTP request.
// If useRedirect is true and a redirect URL exists, it will be used; otherwise the original URL is used.
func (f *Fetcher) buildRequestWithURL(ctx context.Context, req *base.Request, useRedirect bool) (httpReq *http.Request, err error) {
var reqUrl string
f.redirectLock.Lock()
if useRedirect && f.redirectURL != "" {
reqUrl = f.redirectURL
} else {
reqUrl = req.URL
}
f.redirectLock.Unlock()
var (
method string
body io.Reader
)
headers := http.Header{}
if req.Extra == nil {
method = http.MethodGet
} else {
extra := req.Extra.(*fhttp.ReqExtra)
if extra.Method != "" {
method = extra.Method
} else {
method = http.MethodGet
}
if len(extra.Header) > 0 {
for k, v := range extra.Header {
headers.Set(k, strings.TrimSpace(v))
}
}
if extra.Body != "" {
body = bytes.NewBufferString(extra.Body)
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
headers.Set(base.HttpHeaderUserAgent, strings.TrimSpace(f.config.UserAgent))
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, reqUrl, body)
} else {
httpReq, err = http.NewRequest(method, reqUrl, body)
}
if err != nil {
return
}
httpReq.Header = headers
if host := headers.Get(base.HttpHeaderHost); host != "" {
httpReq.Host = host
}
return httpReq, nil
}
// updateRedirectURL updates the redirect URL from the response.
// This is called when a request using the original URL succeeds after the redirect URL expired.
func (f *Fetcher) updateRedirectURL(url string) {
f.redirectLock.Lock()
f.redirectURL = url
f.redirectLock.Unlock()
}
// hasRedirectURL checks if a redirect URL exists and is different from the original URL.
func (f *Fetcher) hasRedirectURL() bool {
f.redirectLock.Lock()
defer f.redirectLock.Unlock()
return f.redirectURL != "" && f.redirectURL != f.meta.Req.URL
}
// isRedirectExpiredError checks if the error indicates that the redirect URL may have expired.
// This includes 403 (Forbidden), 401 (Unauthorized), 410 (Gone), and network errors.
func isRedirectExpiredError(err error) bool {
if err == nil {
return false
}
// Check for specific HTTP error codes that might indicate URL expiration
if re := extractRequestError(err); re != nil {
switch re.Code {
case 401, 403, 404, 410:
return true
}
}
return false
}
// tryFallbackToOriginalURL attempts to make a request using the original URL
// when the redirect URL has expired. Returns the response if successful.
func (f *Fetcher) tryFallbackToOriginalURL(ctx context.Context, client *http.Client, rangeStart, rangeEnd int64) (*http.Response, error) {
httpReq, err := f.buildRequestWithOriginalURL(ctx, f.meta.Req)
if err != nil {
return nil, err
}
if f.meta.Res.Range && rangeEnd > 0 {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, rangeStart, rangeEnd))
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
resp.Body.Close()
return nil, NewRequestError(resp.StatusCode)
}
return resp, nil
}
// buildClient creates an HTTP client with the default connection timeout.
// Used for resolve phase where we don't have connection time data yet.
func (f *Fetcher) buildClient() *http.Client {
return f.buildClientWithTimeout(connectTimeout)
}
// buildFastFailClient creates an HTTP client with fast-fail timeout.
// Uses max(minFastFailTimeout, maxConnTime) for fast-fail retry during download phase.
func (f *Fetcher) buildFastFailClient() *http.Client {
maxConn := f.maxConnTime.Load()
if maxConn == 0 {
// No successful connection yet, use default timeout
return f.buildClientWithTimeout(connectTimeout)
}
timeout := maxConn
if timeout >= minFastFailTimeout {
// If greater than minFastFailTimeout, increase by 50% for safety margin
timeout = int64(float64(timeout) * 1.5)
} else {
timeout = minFastFailTimeout
}
return f.buildClientWithTimeout(time.Duration(timeout))
}
// buildClientWithTimeout creates an HTTP client with the specified connection timeout.
func (f *Fetcher) buildClientWithTimeout(timeout time.Duration) *http.Client {
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
},
TLSHandshakeTimeout: timeout,
}
jar, _ := cookiejar.New(nil)
return &http.Client{
Transport: transport,
Jar: jar,
}
}
// ============================================================================
// Filename Parsing
// ============================================================================
// parseFilename extracts filename from Content-Disposition header
func parseFilename(contentDisposition string) string {
// Try RFC 5987 extended notation first (filename*=)
if filename := parseFilenameExtended(contentDisposition); filename != "" {
return filename
}
// Try standard MIME parsing
_, params, err := mime.ParseMediaType(contentDisposition)
if err == nil {
if filename := params["filename"]; filename != "" {
return decodeFilenameParam(filename)
}
}
// Fallback to manual parsing
return parseFilenameFallback(contentDisposition)
}
// parseFilenameExtended handles RFC 5987 extended notation (filename*=)
// Format: filename*=charset'language'value (e.g., UTF-8”%E6%B5%8B%E8%AF%95.zip)
func parseFilenameExtended(cd string) string {
lower := strings.ToLower(cd)
idx := strings.Index(lower, "filename*=")
if idx == -1 {
return ""
}
value := cd[idx+len("filename*="):]
// Find the end of the value using proper quote handling
endIdx := findParamValueEnd(value)
if endIdx != -1 {
value = value[:endIdx]
}
value = strings.TrimSpace(value)
// Try charset''encoded format (e.g., UTF-8''%E4%B8%AD%E6%96%87.txt)
parts := strings.SplitN(value, "''", 2)
if len(parts) == 2 {
// Use PathUnescape to handle %2B correctly (should decode to +, not space)
decoded, err := url.PathUnescape(parts[1])
if err == nil {
return decoded
}
}
// Try charset'language'encoded format
parts = strings.SplitN(value, "'", 3)
if len(parts) >= 3 {
// Use PathUnescape to handle %2B correctly (should decode to +, not space)
decoded, err := url.PathUnescape(parts[2])
if err == nil {
return decoded
}
}
return ""
}
// decodeFilenameParam decodes filename parameter value
// Handles HTML entities, MIME encoded-word, URL encoding, and GBK encoding fallback
func decodeFilenameParam(filename string) string {
// First, unescape HTML entities (e.g., &amp; -> &, &lt; -> <, &gt; -> >)
// This must be done before other decoding to handle cases where servers
// HTML-encode special characters in filenames
filename = unescapeHTMLEntities(filename)
// Handle RFC 2047 encoded word (=?charset?encoding?text?=)
if strings.HasPrefix(filename, "=?") {
decoder := new(mime.WordDecoder)
normalizedFilename := strings.Replace(filename, "UTF8", "UTF-8", 1)
if decoded, err := decoder.Decode(normalizedFilename); err == nil {
return decoded
}
}
// Try URL decoding - use PathUnescape for filenames to handle %2B correctly
decoded := util.TryUrlPathUnescape(filename)
// If not valid UTF-8, try GBK decoding (common for Chinese websites)
if !utf8.ValidString(decoded) {
if gbkDecoded := tryDecodeGBK(decoded); gbkDecoded != "" {
return gbkDecoded
}
}
return decoded
}
// unescapeHTMLEntities unescapes common HTML entities in filenames
// This handles cases where servers HTML-encode special characters like & to &amp;
func unescapeHTMLEntities(s string) string {
// Common HTML entities that might appear in filenames
replacements := map[string]string{
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
"&quot;": "\"",
"&#39;": "'",
"&apos;": "'",
}
result := s
for entity, char := range replacements {
result = strings.ReplaceAll(result, entity, char)
}
return result
}
// tryDecodeGBK attempts to decode string as GBK encoding
func tryDecodeGBK(s string) string {
if len(s) == 0 {
return ""
}
decoder := simplifiedchinese.GBK.NewDecoder()
decoded, err := decoder.Bytes([]byte(s))
if err != nil {
return ""
}
result := string(decoded)
if utf8.ValidString(result) {
return result
}
return ""
}
// parseFilenameFallback is a fallback parser for non-standard Content-Disposition
func parseFilenameFallback(cd string) string {
lower := strings.ToLower(cd)
idx := strings.Index(lower, "filename=")
if idx == -1 {
return ""
}
value := cd[idx+len("filename="):]
// Find the end of the value using proper quote handling
endIdx := findParamValueEnd(value)
if endIdx != -1 {
value = value[:endIdx]
}
value = strings.TrimSpace(value)
// Remove surrounding quotes
if len(value) >= 2 {
if (value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'') {
value = value[1 : len(value)-1]
}
}
return decodeFilenameParam(value)
}
// findParamValueEnd finds the end position of a parameter value in a Content-Disposition header.
// It correctly handles quoted values where semicolons inside quotes should not be treated as delimiters.
// It also handles HTML entities in unquoted values (e.g., &amp; should not be split at the semicolon).
// Returns the end index (exclusive) of the value, or -1 if it extends to the end of the string.
func findParamValueEnd(value string) int {
value = strings.TrimSpace(value)
if len(value) == 0 {
return 0
}
// If the value starts with a quote, find the matching closing quote
if value[0] == '"' || value[0] == '\'' {
quote := value[0]
// Find the closing quote, handling escaped quotes
for i := 1; i < len(value); i++ {
if value[i] == quote {
// Check if it's escaped
if i > 0 && value[i-1] == '\\' {
continue
}
// Found closing quote, now look for ; after it
remaining := value[i+1:]
if semiIdx := strings.Index(remaining, ";"); semiIdx != -1 {
return i + 1 + semiIdx
}
return -1 // No semicolon after closing quote
}
}
// No closing quote found, treat rest of string as value
return -1
}
// Unquoted value - find the next semicolon that's not part of an HTML entity
// HTML entities have the pattern &...; (e.g., &amp; &lt; &gt; &quot; &#39;)
for i := 0; i < len(value); i++ {
if value[i] == ';' {
// Check if this semicolon is part of an HTML entity
// Look backwards for an & character
isEntity := false
if i > 0 {
// Look for & before this semicolon (within reasonable distance, max 10 chars)
for j := i - 1; j >= 0 && j >= i-10; j-- {
if value[j] == '&' {
// Found &, this semicolon might be part of an HTML entity
// Check if there are only alphanumeric or # between & and ;
entityChars := value[j+1 : i]
if len(entityChars) > 0 && isValidHTMLEntityChars(entityChars) {
isEntity = true
}
break
}
// If we hit whitespace or another special char, stop looking
if value[j] == ' ' || value[j] == '"' || value[j] == '\'' {
break
}
}
}
if !isEntity {
return i
}
}
}
return -1 // No semicolon, extends to end
}
// isValidHTMLEntityChars checks if a string contains only valid HTML entity characters
// (alphanumeric and #, typically for entities like &amp; &lt; &#39; etc.)
func isValidHTMLEntityChars(s string) bool {
if len(s) == 0 {
return false
}
for _, c := range s {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '#') {
return false
}
}
return true
}
+643 -65
View File
File diff suppressed because it is too large. Load diff
+81 -11
View File
@@ -2,13 +2,14 @@ package base
import ( import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/mattn/go-ieproxy"
"golang.org/x/exp/slices"
"net/http" "net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/mattn/go-ieproxy"
"golang.org/x/exp/slices"
) )
// Request download request // Request download request
@@ -77,7 +78,7 @@ func (r *Resource) Validate() error {
if r.Name == "" { if r.Name == "" {
return fmt.Errorf("invalid resource name") return fmt.Errorf("invalid resource name")
} }
if r.Files == nil || len(r.Files) == 0 { if len(r.Files) == 0 {
return fmt.Errorf("invalid resource files") return fmt.Errorf("invalid resource files")
} }
for _, file := range r.Files { for _, file := range r.Files {
@@ -123,7 +124,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
// if selectFiles is empty, select all files // if selectFiles is empty, select all files
if len(o.SelectFiles) == 0 { if len(o.SelectFiles) == 0 {
o.SelectFiles = make([]int, fileSize) o.SelectFiles = make([]int, fileSize)
for i := 0; i < fileSize; i++ { for i := range fileSize {
o.SelectFiles[i] = i o.SelectFiles[i] = i
} }
} }
@@ -148,7 +149,7 @@ func ParseReqExtra[E any](req *Request) error {
return nil return nil
} }
func ParseOptsExtra[E any](opts *Options) error { func ParseOptExtra[E any](opts *Options) error {
if opts.Extra == nil { if opts.Extra == nil {
return nil return nil
} }
@@ -163,15 +164,30 @@ func ParseOptsExtra[E any](opts *Options) error {
return nil return nil
} }
type CreateTaskBatch struct {
Reqs []*CreateTaskBatchItem `json:"reqs"`
Opts *Options `json:"opts"`
}
type CreateTaskBatchItem struct {
Req *Request `json:"req"`
Opts *Options `json:"opts"`
}
// DownloaderStoreConfig is the config that can restore the downloader. // DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct { type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"` Proxy *DownloaderProxyConfig `json:"proxy"`
Webhook *WebhookConfig `json:"webhook"` // Webhook is the webhook configuration
Script *ScriptConfig `json:"script"` // Script is the script execution configuration
AutoTorrent *AutoTorrentConfig `json:"autoTorrent"` // AutoTorrent is the auto torrent task creation configuration
Archive *ArchiveConfig `json:"archive"` // Archive is the archive extraction configuration
AutoDeleteMissingFileTasks bool `json:"autoDeleteMissingFileTasks"` // AutoDeleteMissingFileTasks enables automatic deletion of tasks with missing files
} }
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig { func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
@@ -184,6 +200,24 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.Proxy == nil { if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{} cfg.Proxy = &DownloaderProxyConfig{}
} }
if cfg.Webhook == nil {
cfg.Webhook = &WebhookConfig{}
}
if cfg.Script == nil {
cfg.Script = &ScriptConfig{}
}
if cfg.AutoTorrent == nil {
cfg.AutoTorrent = &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
}
}
if cfg.Archive == nil {
cfg.Archive = &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
}
}
return cfg return cfg
} }
@@ -206,9 +240,45 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
if cfg.Proxy == nil { if cfg.Proxy == nil {
cfg.Proxy = beforeCfg.Proxy cfg.Proxy = beforeCfg.Proxy
} }
if cfg.Webhook == nil {
cfg.Webhook = beforeCfg.Webhook
}
if cfg.Script == nil {
cfg.Script = beforeCfg.Script
}
if cfg.AutoTorrent == nil {
cfg.AutoTorrent = beforeCfg.AutoTorrent
}
if cfg.Archive == nil {
cfg.Archive = beforeCfg.Archive
}
return cfg return cfg
} }
// WebhookConfig is the webhook configuration
type WebhookConfig struct {
Enable bool `json:"enable"` // Enable is the flag to enable/disable webhooks
URLs []string `json:"urls"` // URLs is the list of webhook URLs
}
// ScriptConfig is the script execution configuration
type ScriptConfig struct {
Enable bool `json:"enable"` // Enable is the flag to enable/disable script execution
Paths []string `json:"paths"` // Paths is the list of script paths to execute
}
// AutoTorrentConfig is the auto torrent task creation configuration
type AutoTorrentConfig struct {
Enable bool `json:"enable"` // Enable enables automatic BT task creation when downloading .torrent files
DeleteAfterDownload bool `json:"deleteAfterDownload"` // DeleteAfterDownload deletes the .torrent file after BT task creation
}
// ArchiveConfig is the archive extraction configuration
type ArchiveConfig struct {
AutoExtract bool `json:"autoExtract"` // AutoExtract enables automatic extraction of archives after download
DeleteAfterExtract bool `json:"deleteAfterExtract"` // DeleteAfterExtract deletes the archive after successful extraction
}
type DownloaderProxyConfig struct { type DownloaderProxyConfig struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
// System is the flag that use system proxy // System is the flag that use system proxy
+174 -1
View File
@@ -18,6 +18,16 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
MaxRunning: 5, MaxRunning: 5,
ProtocolConfig: map[string]any{}, ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{}, Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
Script: &ScriptConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
}, },
}, },
{ {
@@ -29,6 +39,16 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
MaxRunning: 10, MaxRunning: 10,
ProtocolConfig: map[string]any{}, ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{}, Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
Script: &ScriptConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
}, },
}, },
{ {
@@ -43,7 +63,17 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: map[string]any{ ProtocolConfig: map[string]any{
"key": "value", "key": "value",
}, },
Proxy: &DownloaderProxyConfig{}, Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
Script: &ScriptConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
}, },
}, },
{ {
@@ -59,6 +89,64 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
Proxy: &DownloaderProxyConfig{ Proxy: &DownloaderProxyConfig{
Enable: true, Enable: true,
}, },
Webhook: &WebhookConfig{},
Script: &ScriptConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
},
},
{
"Init AutoTorrent",
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
Script: &ScriptConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
},
},
{
"Init Archive",
&DownloaderStoreConfig{
Archive: &ArchiveConfig{
AutoExtract: true,
DeleteAfterExtract: false,
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
Webhook: &WebhookConfig{},
Script: &ScriptConfig{},
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
Archive: &ArchiveConfig{
AutoExtract: true,
DeleteAfterExtract: false,
},
}, },
}, },
} }
@@ -71,6 +159,10 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
ProtocolConfig: tt.fields.ProtocolConfig, ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra, Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy, Proxy: tt.fields.Proxy,
Webhook: tt.fields.Webhook,
Script: tt.fields.Script,
AutoTorrent: tt.fields.AutoTorrent,
Archive: tt.fields.Archive,
} }
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) { if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Init() = %v, want %v", got, tt.want) t.Errorf("Init() = %v, want %v", got, tt.want)
@@ -245,6 +337,84 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
}, },
}, },
}, },
{
"Merge AutoTorrent No Override",
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
},
},
args{
beforeCfg: &DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: false,
DeleteAfterDownload: false,
},
},
},
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
},
},
},
{
"Merge AutoTorrent Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
},
},
&DownloaderStoreConfig{
AutoTorrent: &AutoTorrentConfig{
Enable: true,
DeleteAfterDownload: true,
},
},
},
{
"Merge Archive No Override",
&DownloaderStoreConfig{
Archive: &ArchiveConfig{
AutoExtract: true,
},
},
args{
beforeCfg: &DownloaderStoreConfig{
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
},
},
&DownloaderStoreConfig{
Archive: &ArchiveConfig{
AutoExtract: true,
},
},
},
{
"Merge Archive Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
},
},
&DownloaderStoreConfig{
Archive: &ArchiveConfig{
AutoExtract: false,
DeleteAfterExtract: false,
},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -255,6 +425,9 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
ProtocolConfig: tt.fields.ProtocolConfig, ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra, Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy, Proxy: tt.fields.Proxy,
Webhook: tt.fields.Webhook,
AutoTorrent: tt.fields.AutoTorrent,
Archive: tt.fields.Archive,
} }
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) { if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Merge() = %v, want %v", got, tt.want) t.Errorf("Merge() = %v, want %v", got, tt.want)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+47
View File
@@ -205,6 +205,31 @@ function testTimeout(){
xhr.send(); xhr.send();
}); });
} }
async function testFingerprint(fingerprint,ua){
__gopeed_setFingerprint(fingerprint);
const resp = await fetch(host+'/ua');
const data = await resp.json();
if(!data.user_agent.includes(ua)){
throw new Error('fingerprint test failed, user agent: ' + data.user_agent);
}
}
async function testFingerprintDefault(){
await testFingerprint('none', 'Go')
}
async function testFingerprintChrome(){
await testFingerprint('chrome', 'Chrome')
}
async function testFingerprintFirefox(){
await testFingerprint('firefox', 'Firefox')
}
async function testFingerprintSafari(){
await testFingerprint('safari', 'Safari')
}
`) `)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -288,6 +313,19 @@ function testTimeout(){
if err == nil || err.Error() != "timeout" { if err == nil || err.Error() != "timeout" {
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err) t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
} }
_, err = callTestFun(engine, "testFingerprintChrome")
if err != nil {
t.Fatal("testFingerprintChrome test failed", err)
}
_, err = callTestFun(engine, "testFingerprintFirefox")
if err != nil {
t.Fatal("testFingerprintFirefox test failed", err)
}
_, err = callTestFun(engine, "testFingerprintSafari")
if err != nil {
t.Fatal("testFingerprintSafari test failed", err)
}
} }
func TestFetchWithProxy(t *testing.T) { func TestFetchWithProxy(t *testing.T) {
@@ -470,6 +508,15 @@ func startServer() net.Listener {
w.Write([]byte("ok")) w.Write([]byte("ok"))
} }
}) })
mux.HandleFunc("/ua", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data := map[string]any{
"user_agent": r.UserAgent(),
}
buf, _ := json.Marshal(data)
w.WriteHeader(http.StatusOK)
w.Write(buf)
})
server.Handler = mux server.Handler = mux
go server.Serve(listener) go server.Serve(listener)
return listener return listener
+77 -51
View File
@@ -15,6 +15,7 @@ import (
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util" "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/imroc/req/v3"
) )
const ( const (
@@ -129,7 +130,8 @@ type XMLHttpRequest struct {
requestHeaders http.Header requestHeaders http.Header
responseHeaders http.Header responseHeaders http.Header
aborted bool aborted bool
proxyHandler func(r *http.Request) (*url.URL, error) client *req.Client
fingerprint string
WithCredentials bool `json:"withCredentials"` WithCredentials bool `json:"withCredentials"`
Upload *XMLHttpRequestUpload `json:"upload"` Upload *XMLHttpRequestUpload `json:"upload"`
@@ -163,25 +165,39 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
} }
func (xhr *XMLHttpRequest) Send(data goja.Value) { func (xhr *XMLHttpRequest) Send(data goja.Value) {
var req *http.Request setFingerprint(xhr.client, xhr.fingerprint)
var err error
d := xhr.parseData(data) d := xhr.parseData(data)
var ( var (
contentType string contentType string
contentLength int64 contentLength int64
isStringBody bool isStringBody bool
) )
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
req, err = http.NewRequest(xhr.method, xhr.url, nil) // Create request using req library
} else { reqBuilder := xhr.client.R()
// Set headers first
if xhr.requestHeaders != nil {
for key, values := range xhr.requestHeaders {
if len(values) > 0 {
// Merge multiple values with comma separator (HTTP standard)
mergedValue := strings.Join(values, ", ")
reqBuilder.SetHeader(key, mergedValue)
}
}
}
// Handle request body
if d != nil && xhr.method != "GET" && xhr.method != "HEAD" {
switch v := d.(type) { switch v := d.(type) {
case string: case string:
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v)) reqBuilder.SetBody(v)
contentType = "text/plain;charset=UTF-8" contentType = "text/plain;charset=UTF-8"
contentLength = int64(len(v)) contentLength = int64(len(v))
isStringBody = true isStringBody = true
case *file.File: case *file.File:
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader) reqBuilder.SetBody(v.Reader)
contentType = "application/octet-stream" contentType = "application/octet-stream"
contentLength = v.Size contentLength = v.Size
case *formdata.FormData: case *formdata.FormData:
@@ -203,43 +219,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
defer mw.Close() defer mw.Close()
mw.Send() mw.Send()
}() }()
req, err = http.NewRequest(xhr.method, xhr.url, pr) reqBuilder.SetBody(pr)
contentType = mw.FormDataContentType() contentType = mw.FormDataContentType()
contentLength = mw.Size() contentLength = mw.Size()
} }
} }
if err != nil {
xhr.callOnerror()
return
}
req.Header = xhr.requestHeaders
// Only string body can specify Content-Type header by user // Only string body can specify Content-Type header by user
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") { if contentType != "" && (!isStringBody || xhr.requestHeaders.Get("Content-Type") == "") {
req.Header.Set("Content-Type", contentType) reqBuilder.SetHeader("Content-Type", contentType)
} }
if contentLength > 0 {
req.ContentLength = contentLength // Set timeout
if xhr.Timeout > 0 {
xhr.client.SetTimeout(time.Duration(xhr.Timeout) * time.Millisecond)
} }
transport := &http.Transport{
Proxy: xhr.proxyHandler, // Configure redirect behavior
} xhr.client.SetRedirectPolicy(func(req *http.Request, via []*http.Request) error {
client := &http.Client{ if xhr.Redirect == redirectManual {
Transport: transport, return http.ErrUseLastResponse
Timeout: time.Duration(xhr.Timeout) * time.Millisecond, }
CheckRedirect: func(req *http.Request, via []*http.Request) error { if xhr.Redirect == redirectError {
if xhr.Redirect == redirectManual { return errors.New("redirect failed")
return http.ErrUseLastResponse }
} if len(via) > 20 {
if xhr.Redirect == redirectError { return errors.New("too many redirects")
return errors.New("redirect failed") }
} return nil
if len(via) > 20 { })
return errors.New("too many redirects")
} // Execute request
return nil resp, err := reqBuilder.Send(xhr.method, xhr.url)
},
}
resp, err := client.Do(req)
if err != nil { if err != nil {
// handle timeout error // handle timeout error
var ne net.Error var ne net.Error
@@ -254,35 +265,37 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
xhr.callOnerror() xhr.callOnerror()
return return
} }
defer resp.Body.Close()
xhr.Upload.callOnprogress(contentLength, contentLength) xhr.Upload.callOnprogress(contentLength, contentLength)
if !xhr.aborted { if !xhr.aborted {
xhr.Upload.callOnload() xhr.Upload.callOnload()
} }
responseUrl := resp.Request.URL // Set response URL (final URL after redirects)
responseUrl.Fragment = "" if resp.Response != nil && resp.Response.Request != nil && resp.Response.Request.URL != nil {
xhr.ResponseUrl = responseUrl.String() responseUrl := resp.Response.Request.URL
responseUrl.Fragment = ""
xhr.ResponseUrl = responseUrl.String()
} else {
xhr.ResponseUrl = xhr.url
}
// Set response headers
xhr.responseHeaders = resp.Header xhr.responseHeaders = resp.Header
xhr.Status = resp.StatusCode xhr.Status = resp.StatusCode
xhr.StatusText = resp.Status xhr.StatusText = resp.Status
xhr.doReadystatechange(2) xhr.doReadystatechange(2)
buf, err := io.ReadAll(resp.Body)
if err != nil { bodyBytes := resp.Bytes()
xhr.callOnerror()
return
}
xhr.doReadystatechange(3) xhr.doReadystatechange(3)
xhr.Response = string(buf) xhr.Response = string(bodyBytes)
xhr.ResponseText = xhr.Response xhr.ResponseText = xhr.Response
xhr.doReadystatechange(4) xhr.doReadystatechange(4)
respBodyLen := int64(len(buf)) respBodyLen := int64(len(bodyBytes))
xhr.callOnprogress(respBodyLen, respBodyLen) xhr.callOnprogress(respBodyLen, respBodyLen)
if !xhr.aborted { if !xhr.aborted {
xhr.callOnload() xhr.callOnload()
} }
return
} }
func (xhr *XMLHttpRequest) Abort() { func (xhr *XMLHttpRequest) Abort() {
@@ -293,6 +306,9 @@ func (xhr *XMLHttpRequest) Abort() {
} }
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string { func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
if xhr.responseHeaders == nil {
return ""
}
return strings.Join(xhr.responseHeaders.Values(key), ", ") return strings.Join(xhr.responseHeaders.Values(key), ", ")
} }
@@ -359,8 +375,15 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
return instanceValue return instanceValue
}) })
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
// Create req client with proxy support
client := req.C()
if proxyHandler != nil {
client.SetProxy(proxyHandler)
}
instance := &XMLHttpRequest{ instance := &XMLHttpRequest{
proxyHandler: proxyHandler, client: client,
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
Upload: &XMLHttpRequestUpload{ Upload: &XMLHttpRequestUpload{
EventProp: &EventProp{ EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)), eventListeners: make(map[string]func(event *ProgressEvent)),
@@ -374,6 +397,9 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
instanceValue.SetPrototype(call.This.Prototype()) instanceValue.SetPrototype(call.This.Prototype())
return instanceValue return instanceValue
}) })
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
runtime.Set(FingerprintMagicKey, fingerprint)
})
if err := runtime.Set("ProgressEvent", progressEvent); err != nil { if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
return err return err
} }
@@ -0,0 +1,24 @@
package xhr
import "github.com/imroc/req/v3"
type Fingerprint string
const (
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
fingerprintChrome = "chrome"
fingerprintFirefox = "firefox"
fingerprintSafari = "safari"
)
func setFingerprint(client *req.Client, fingerprint string) {
switch fingerprint {
case fingerprintChrome:
client.ImpersonateChrome()
case fingerprintFirefox:
client.ImpersonateFirefox()
case fingerprintSafari:
client.ImpersonateSafari()
}
}
Loaded 50 of 187 files, more files were not shown because too many files have changed in this diff. Show more