mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-13 00:26:37 +08:00
feat(organize/scan): transfer modes, seeding-safe relocation, inode dedup, incremental scanning
- Organizer: add move/copy/hardlink/symlink transfer modes (default move), per-request target_path/transfer_mode overrides, and honor organize.target_dir / organize.transfer_mode settings. - keep_seeding (default on): escalate move->hardlink (cross-device->copy) so the qBittorrent source stays in place and continues seeding after organize. - qBittorrent SetLocation + POST /downloads/relocate to migrate whole torrents while keeping them seeding. - Scanner: FileID (device:inode) hardlink dedup to avoid duplicate recognition and double-counted storage; extract single-file ingest. - Watcher: recursive watch + incremental per-file ingest/remove instead of full re-scan; periodic full library scan now gated behind scan.periodic_enabled (default off) to reduce disk wear. - Telegram bot: handle callback_query in polling, declare allowed_updates in webhook, answer callbacks. - Frontend: settings for transfer mode / keep_seeding / periodic scan; organize panel target dir + transfer mode overrides. - Tests for transfer modes, organizer resolution, SetLocation, inode dedup, incremental ingest/remove. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -454,6 +454,54 @@ func (q *QBitClient) Delete(ctx context.Context, hash string, deleteFiles bool)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocation moves a torrent's data to a new save directory via
|
||||
// POST /api/v2/torrents/setLocation. qBittorrent performs the physical move
|
||||
// itself and keeps seeding from the new location — this is the seeding-safe
|
||||
// way to relocate downloaded PT files. location must be an absolute path the
|
||||
// qBittorrent process can write to.
|
||||
func (q *QBitClient) SetLocation(ctx context.Context, hash, location string) error {
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
return errors.New("qbittorrent setLocation: empty hash")
|
||||
}
|
||||
if strings.TrimSpace(location) == "" {
|
||||
return errors.New("qbittorrent setLocation: empty location")
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
if err := q.ensureAuth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("hashes", hash)
|
||||
form.Set("location", location)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||||
strings.TrimRight(q.cfg.BaseURL, "/")+"/api/v2/torrents/setLocation",
|
||||
strings.NewReader(form.Encode()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", q.cfg.BaseURL)
|
||||
resp, err := q.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
return errors.New("qbittorrent setLocation: 保存路径无效")
|
||||
}
|
||||
if resp.StatusCode == http.StatusConflict {
|
||||
return errors.New("qbittorrent setLocation: 无法写入目标路径 (权限或磁盘问题)")
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("qbittorrent setLocation: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
q.log.Info("qbittorrent: torrent relocated", zap.String("hash", hash), zap.String("location", location))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureAuth makes sure we have a valid SID cookie. Cheap on the happy
|
||||
// path; logs in transparently otherwise.
|
||||
func (q *QBitClient) ensureAuth(ctx context.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user