chore: rename module from torrentclaw-cli to unarr

- Rename Go module path github.com/torrentclaw/torrentclaw-cli → github.com/torrentclaw/unarr
- Update all imports, ldflags, scripts, docs, and Docker config
- Add GitHub Actions release workflow (goreleaser on tag push)
This commit is contained in:
Deivid Soto 2026-03-30 13:06:07 +02:00
parent 9cc806d11f
commit 5a7449b9e6
58 changed files with 166 additions and 141 deletions

View file

@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
func TestDebridAvailable(t *testing.T) {

View file

@ -5,7 +5,7 @@ import (
"log"
"sync"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
// ManagerConfig holds download manager settings.
@ -24,6 +24,7 @@ type Manager struct {
activeMu sync.RWMutex
active map[string]*Task
cancels map[string]context.CancelFunc // per-task cancel functions
sem chan struct{}
wg sync.WaitGroup
@ -45,6 +46,7 @@ func NewManager(cfg ManagerConfig, reporter *ProgressReporter, downloaders ...Do
reporter: reporter,
downloaders: dlMap,
active: make(map[string]*Task),
cancels: make(map[string]context.CancelFunc),
sem: make(chan struct{}, cfg.MaxConcurrent),
}
}
@ -53,8 +55,12 @@ func NewManager(cfg ManagerConfig, reporter *ProgressReporter, downloaders ...Do
func (m *Manager) Submit(ctx context.Context, at agent.Task) {
task := NewTaskFromAgent(at)
// Per-task cancellable context so CancelTask can unblock the goroutine
taskCtx, taskCancel := context.WithCancel(ctx)
m.activeMu.Lock()
m.active[task.ID] = task
m.cancels[task.ID] = taskCancel
m.activeMu.Unlock()
m.reporter.Track(task)
@ -65,7 +71,8 @@ func (m *Manager) Submit(ctx context.Context, at agent.Task) {
m.wg.Add(1)
go func() {
defer m.wg.Done()
m.processTask(ctx, task)
defer taskCancel()
m.processTask(taskCtx, task)
}()
return
}
@ -74,6 +81,7 @@ func (m *Manager) Submit(ctx context.Context, at agent.Task) {
select {
case m.sem <- struct{}{}:
case <-ctx.Done():
taskCancel()
return
}
@ -81,7 +89,8 @@ func (m *Manager) Submit(ctx context.Context, at agent.Task) {
go func() {
defer m.wg.Done()
defer func() { <-m.sem }()
m.processTask(ctx, task)
defer taskCancel()
m.processTask(taskCtx, task)
}()
}
@ -119,12 +128,19 @@ func (m *Manager) ActiveTasks() []*Task {
func (m *Manager) CancelTask(taskID string) {
m.activeMu.RLock()
task, ok := m.active[taskID]
cancel := m.cancels[taskID]
m.activeMu.RUnlock()
if !ok {
return
}
// Cancel the task's context first — this unblocks the goroutine
// (e.g. stuck waiting for metadata) so it exits and releases the semaphore slot.
if cancel != nil {
cancel()
}
if dl, exists := m.downloaders[task.ResolvedMethod]; exists {
dl.Pause(taskID) // stop download, keep files
}
@ -141,12 +157,17 @@ func (m *Manager) CancelTask(taskID string) {
func (m *Manager) PauseTask(taskID string) {
m.activeMu.RLock()
task, ok := m.active[taskID]
cancel := m.cancels[taskID]
m.activeMu.RUnlock()
if !ok {
return
}
if cancel != nil {
cancel()
}
if dl, exists := m.downloaders[task.ResolvedMethod]; exists {
dl.Pause(taskID) // stop download, keep files for resume
}
@ -159,12 +180,17 @@ func (m *Manager) PauseTask(taskID string) {
func (m *Manager) CancelAndDeleteFiles(taskID string) {
m.activeMu.RLock()
task, ok := m.active[taskID]
cancel := m.cancels[taskID]
m.activeMu.RUnlock()
if !ok {
return
}
if cancel != nil {
cancel()
}
if dl, exists := m.downloaders[task.ResolvedMethod]; exists {
dl.Cancel(taskID) // stop download + delete files
}
@ -204,8 +230,12 @@ func (m *Manager) Shutdown(ctx context.Context) {
}
}
// Clean active map
// Clean active map and cancel functions
m.activeMu.Lock()
for id, cancel := range m.cancels {
cancel()
delete(m.cancels, id)
}
m.active = make(map[string]*Task)
m.activeMu.Unlock()
}
@ -214,6 +244,7 @@ func (m *Manager) processTask(ctx context.Context, task *Task) {
defer func() {
m.activeMu.Lock()
delete(m.active, task.ID)
delete(m.cancels, task.ID)
m.activeMu.Unlock()
}()

View file

@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
func TestManagerSubmitAndWait(t *testing.T) {

View file

@ -6,7 +6,7 @@ import (
"sync"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
// ActionFunc is called when the server signals an action on a task.

View file

@ -8,7 +8,7 @@ import (
"testing"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
// ---------------------------------------------------------------------------

View file

@ -5,7 +5,7 @@ import (
"sync"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
// TaskStatus represents the current state of a download task.

View file

@ -3,7 +3,7 @@ package engine
import (
"testing"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/unarr/internal/agent"
)
func TestNewTaskFromAgent(t *testing.T) {

View file

@ -16,7 +16,7 @@ import (
"github.com/anacrolix/dht/v2/krpc"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/storage"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/unarr/internal/config"
"golang.org/x/time/rate"
)

View file

@ -20,11 +20,13 @@ type UPnPMapping struct {
// setupUPnP discovers the gateway, maps the port, and gets the public IP.
// Returns nil if UPnP is not available or fails.
func setupUPnP(internalPort int) (*UPnPMapping, error) {
devices := upnp.Discover(0, 5*time.Second, alog.Logger{})
log.Println("stream: discovering UPnP gateway (10s timeout)...")
devices := upnp.Discover(0, 10*time.Second, alog.Logger{})
if len(devices) == 0 {
return nil, fmt.Errorf("no UPnP devices found")
return nil, fmt.Errorf("no UPnP devices found (is UPnP enabled on your router?)")
}
log.Printf("stream: found %d UPnP device(s), using %s", len(devices), devices[0].ID())
device := devices[0]
// Get public IP
@ -32,13 +34,15 @@ func setupUPnP(internalPort int) (*UPnPMapping, error) {
if err != nil {
return nil, fmt.Errorf("get external IP: %w", err)
}
log.Printf("stream: public IP via UPnP: %s", externalIP)
// Map port (0 = let router choose external port, 2h lease)
// Map port (same internal/external, 2h lease)
mappedPort, err := device.AddPortMapping(upnp.TCP, internalPort, internalPort, "unarr stream", 2*time.Hour)
if err != nil {
return nil, fmt.Errorf("add port mapping: %w", err)
return nil, fmt.Errorf("add port mapping %d: %w", internalPort, err)
}
log.Printf("stream: UPnP port mapped %s:%d -> local:%d (2h lease)", externalIP, mappedPort, internalPort)
return &UPnPMapping{
ExternalIP: externalIP.String(),
ExternalPort: mappedPort,

View file

@ -10,12 +10,12 @@ import (
"sync"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/download"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/nntp"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/nzb"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/postprocess"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/usenet/download"
"github.com/torrentclaw/unarr/internal/usenet/nntp"
"github.com/torrentclaw/unarr/internal/usenet/nzb"
"github.com/torrentclaw/unarr/internal/usenet/postprocess"
)
// activeDownload holds the state for a single in-progress usenet download.