7 Commits
v1.1.6 ... main

Author SHA1 Message Date
f5b1f100e2 fix: handle TorBox 'checking' state as downloading, add symlink completion logging
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m10s
- Add 'checking' to the list of downloading states in getTorboxStatus()
  so TorBox torrents in transitional state don't get marked as error
- Add debug log before calling onSuccess to trace state update flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:56:14 -08:00
4cf8246550 fix: add safety check to force state update after symlink processing
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m16s
Add a fallback in onSuccess that ensures the torrent state is set to
'pausedUP' after updateTorrent returns. This catches edge cases where
updateTorrent might not set the state correctly.

The check:
1. Verifies state after updateTorrent returns
2. If state is not 'pausedUP' but we have a valid symlink path, force update
3. Logs a warning with diagnostic info when fallback triggers

This should definitively fix the TorBox downloads stuck in 'downloading'
state issue (dcy-355).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:47:43 -08:00
3f96382e76 debug: add comprehensive logging for TorBox state transition
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m13s
Add logging at key points in the download flow to diagnose why TorBox
downloads get stuck in 'downloading' state despite completion:

- Log initial state at processFiles start
- Log when download loop exits
- Log when onSuccess callback is invoked
- Log condition evaluation in updateTorrent
- Log when state is set to pausedUP

Also fix missing return after onFailed for empty symlink path edge case.

Part of dcy-355 investigation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:46:33 -08:00
43a94118f4 debug: add logging and case-insensitive check for TorBox status
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m15s
Added debug logging to see actual DownloadState values from TorBox API.
Also made status comparison case-insensitive.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:30:23 -08:00
38d59cb01d fix: treat TorBox cached/completed status as downloaded
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m11s
For instant/cached TorBox downloads, the API returns DownloadFinished=false
(no download happened - content was already cached) with DownloadState="cached"
or "completed". These should be treated as "downloaded" since content is
immediately available.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:27:05 -08:00
b306248db6 fix: directly set pausedUP state when debrid reports downloaded
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m27s
Previous fix relied on IsReady() calculation which might still fail
due to edge cases with progress/AmountLeft values. This fix directly
sets state to pausedUP when debridTorrent.Status == "downloaded" and
TorrentPath is set, bypassing the IsReady() check entirely.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:16:41 -08:00
3a5289cb1d fix: TorBox downloads stuck in 'downloading' state
All checks were successful
CI/CD / Build & Push Docker Image (push) Successful in 1m17s
Fixed race condition where TorBox reports DownloadFinished=true but
Progress < 1.0, causing IsReady() to return false and state to stay
"downloading" instead of transitioning to "pausedUP".

Also added Gitea CI workflow to push images to internal registry.

Fixes: dcy-355

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:57:36 -08:00
4 changed files with 154 additions and 4 deletions

55
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,55 @@
name: CI/CD
on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
workflow_dispatch:
permissions:
contents: read
actions: write
jobs:
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
outputs:
image_tag: ${{ steps.meta.outputs.tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate image metadata
id: meta
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
echo "tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "Image will be tagged: ${SHORT_SHA}"
- name: Login to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login registry.johnogle.info -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
registry.johnogle.info/johno/decypharr:${{ steps.meta.outputs.tag }}
registry.johnogle.info/johno/decypharr:latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.meta.outputs.tag }}
CHANNEL=dev

9
.gitignore vendored
View File

@@ -19,4 +19,11 @@ auth.json
node_modules/
.venv/
.stignore
.stfolder/**
.stfolder/**
# Gas Town (added by gt)
.runtime/
.claude/
.logs/
.beads/
state.json

View File

@@ -171,13 +171,24 @@ func (tb *Torbox) SubmitMagnet(torrent *types.Torrent) (*types.Torrent, error) {
}
func (tb *Torbox) getTorboxStatus(status string, finished bool) string {
if finished {
// Log raw values for debugging
tb.logger.Debug().
Str("download_state", status).
Bool("download_finished", finished).
Msg("getTorboxStatus called")
// For cached/completed torrents, content is immediately available even if
// DownloadFinished=false (no download actually happened - it was already cached)
// Use case-insensitive comparison for safety
statusLower := strings.ToLower(status)
if finished || statusLower == "cached" || statusLower == "completed" {
return "downloaded"
}
downloading := []string{"completed", "cached", "paused", "downloading", "uploading",
downloading := []string{"paused", "downloading", "uploading",
"checkingResumeData", "metaDL", "pausedUP", "queuedUP", "checkingUP",
"forcedUP", "allocating", "downloading", "metaDL", "pausedDL",
"queuedDL", "checkingDL", "forcedDL", "checkingResumeData", "moving"}
"queuedDL", "checkingDL", "forcedDL", "checkingResumeData", "moving",
"checking"}
var determinedStatus string
switch {

View File

@@ -53,6 +53,12 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
return
}
s.logger.Debug().
Str("torrent_name", debridTorrent.Name).
Str("debrid_status", debridTorrent.Status).
Str("torrent_state", torrent.State).
Msg("processFiles started")
deb := s.debrid.Debrid(debridTorrent.Debrid)
client := deb.Client()
downloadingStatuses := client.GetDownloadingStatus()
@@ -96,6 +102,12 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
nextInterval := min(s.refreshInterval*2, 30*time.Second)
backoff.Reset(nextInterval)
}
s.logger.Debug().
Str("torrent_name", debridTorrent.Name).
Str("debrid_status", debridTorrent.Status).
Msg("Download loop exited, proceeding to post-processing")
var torrentSymlinkPath, torrentRclonePath string
debridTorrent.Arr = _arr
@@ -114,8 +126,28 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
}
onSuccess := func(torrentSymlinkPath string) {
s.logger.Debug().
Str("torrent_name", debridTorrent.Name).
Str("symlink_path", torrentSymlinkPath).
Str("debrid_status", debridTorrent.Status).
Msg("onSuccess called")
torrent.TorrentPath = torrentSymlinkPath
s.updateTorrent(torrent, debridTorrent)
// Safety check: ensure state is set correctly after updateTorrent
// This catches any edge cases where updateTorrent doesn't set the state
if torrent.State != "pausedUP" && torrentSymlinkPath != "" {
s.logger.Warn().
Str("torrent_name", debridTorrent.Name).
Str("current_state", torrent.State).
Str("debrid_status", debridTorrent.Status).
Msg("State not pausedUP after updateTorrent, forcing state update")
torrent.State = "pausedUP"
torrent.Progress = 1.0
torrent.AmountLeft = 0
s.torrents.Update(torrent)
}
s.logger.Info().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
go importReq.markAsCompleted(torrent, debridTorrent) // Mark the import request as completed, send callback if needed
@@ -201,7 +233,12 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
if torrentSymlinkPath == "" {
err = fmt.Errorf("symlink path is empty for %s", debridTorrent.Name)
onFailed(err)
return
}
s.logger.Debug().
Str("torrent_name", debridTorrent.Name).
Str("symlink_path", torrentSymlinkPath).
Msg("Symlink processing complete, calling onSuccess")
onSuccess(torrentSymlinkPath)
return
case "download":
@@ -270,6 +307,12 @@ func (s *Store) partialTorrentUpdate(t *Torrent, debridTorrent *types.Torrent) *
if math.IsNaN(progress) || math.IsInf(progress, 0) {
progress = 0
}
// When debrid reports download complete, force progress to 100% to ensure
// IsReady() returns true. This fixes a race condition where TorBox can report
// DownloadFinished=true but Progress < 1.0, causing state to stay "downloading".
if debridTorrent.Status == "downloaded" {
progress = 1.0
}
sizeCompleted := int64(float64(totalSize) * progress)
var speed int64
@@ -314,6 +357,13 @@ func (s *Store) updateTorrent(t *Torrent, debridTorrent *types.Torrent) *Torrent
return t
}
s.logger.Debug().
Str("torrent_name", t.Name).
Str("debrid_status", debridTorrent.Status).
Str("torrent_path", t.TorrentPath).
Str("current_state", t.State).
Msg("updateTorrent called")
if debridClient := s.debrid.Clients()[debridTorrent.Debrid]; debridClient != nil {
if debridTorrent.Status != "downloaded" {
_ = debridClient.UpdateTorrent(debridTorrent)
@@ -322,7 +372,34 @@ func (s *Store) updateTorrent(t *Torrent, debridTorrent *types.Torrent) *Torrent
t = s.partialTorrentUpdate(t, debridTorrent)
t.ContentPath = t.TorrentPath
// When debrid reports download complete and we have a path, mark as ready.
// This is a direct fix for TorBox where IsReady() might fail due to
// progress/AmountLeft calculation issues.
if debridTorrent.Status == "downloaded" && t.TorrentPath != "" {
s.logger.Debug().
Str("torrent_name", t.Name).
Msg("Setting state to pausedUP (downloaded + path)")
t.State = "pausedUP"
t.Progress = 1.0
t.AmountLeft = 0
s.torrents.Update(t)
return t
}
// Log why the primary condition failed
s.logger.Debug().
Str("torrent_name", t.Name).
Str("debrid_status", debridTorrent.Status).
Str("torrent_path", t.TorrentPath).
Bool("is_ready", t.IsReady()).
Float64("progress", t.Progress).
Int64("amount_left", t.AmountLeft).
Msg("Primary pausedUP condition failed, checking IsReady")
if t.IsReady() {
s.logger.Debug().
Str("torrent_name", t.Name).
Msg("Setting state to pausedUP (IsReady=true)")
t.State = "pausedUP"
s.torrents.Update(t)
return t