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

@ -6,7 +6,7 @@ import (
"path/filepath"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/unarr/internal/config"
)
// DaemonState is written to disk every heartbeat for external tools to read.

View file

@ -10,9 +10,9 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/ui"
)
func newCleanCmd() *cobra.Command {

View file

@ -11,7 +11,7 @@ import (
"github.com/charmbracelet/huh"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/unarr/internal/config"
)
var configCategories = []string{"downloads", "organization", "notifications", "device", "region", "connection", "advanced"}

View file

@ -13,12 +13,12 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/engine"
"github.com/torrentclaw/torrentclaw-cli/internal/library"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/download"
"github.com/torrentclaw/torrentclaw-cli/internal/upgrade"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/engine"
"github.com/torrentclaw/unarr/internal/library"
"github.com/torrentclaw/unarr/internal/usenet/download"
"github.com/torrentclaw/unarr/internal/upgrade"
)
// newStartCmd creates the top-level `unarr start` command.

View file

@ -9,8 +9,8 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
)
func newDoctorCmd() *cobra.Command {

View file

@ -12,9 +12,9 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/engine"
"github.com/torrentclaw/torrentclaw-cli/internal/parser"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/engine"
"github.com/torrentclaw/unarr/internal/parser"
)
func newDownloadCmd() *cobra.Command {

View file

@ -13,10 +13,10 @@ import (
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/arr"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/mediaserver"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/arr"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/mediaserver"
)
func newInitCmd() *cobra.Command {

View file

@ -9,8 +9,8 @@ import (
"github.com/spf13/cobra"
tc "github.com/torrentclaw/go-client"
"github.com/torrentclaw/torrentclaw-cli/internal/parser"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/parser"
"github.com/torrentclaw/unarr/internal/ui"
)
func newInspectCmd() *cobra.Command {

View file

@ -11,10 +11,10 @@ import (
"github.com/charmbracelet/huh"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/arr"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/mediaserver"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/arr"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/mediaserver"
)
func newMigrateCmd() *cobra.Command {

View file

@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
tc "github.com/torrentclaw/go-client"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/ui"
)
func newPopularCmd() *cobra.Command {

View file

@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
tc "github.com/torrentclaw/go-client"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/ui"
)
func newRecentCmd() *cobra.Command {

View file

@ -9,8 +9,8 @@ import (
"syscall"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
)
// ReloadableConfig holds a reference to the daemon for hot-reload.

View file

@ -2,7 +2,7 @@
package cmd
import "github.com/torrentclaw/torrentclaw-cli/internal/agent"
import "github.com/torrentclaw/unarr/internal/agent"
// ReloadableConfig holds a reference to the daemon for hot-reload.
type ReloadableConfig struct {

View file

@ -6,8 +6,8 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/sentry"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/sentry"
tc "github.com/torrentclaw/go-client"
)
@ -37,7 +37,7 @@ Get started:
unarr start Start the download daemon
Documentation: https://torrentclaw.com/cli
Source: https://github.com/torrentclaw/torrentclaw-cli`,
Source: https://github.com/torrentclaw/unarr`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if noColor || os.Getenv("NO_COLOR") != "" {
color.NoColor = true

View file

@ -12,9 +12,9 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/library"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/library"
)
func newScanCmd() *cobra.Command {

View file

@ -10,7 +10,7 @@ import (
"github.com/spf13/cobra"
tc "github.com/torrentclaw/go-client"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/ui"
)
func newSearchCmd() *cobra.Command {

View file

@ -11,7 +11,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/upgrade"
"github.com/torrentclaw/unarr/internal/upgrade"
)
func newSelfUpdateCmd() *cobra.Command {

View file

@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/ui"
)
func newStatsCmd() *cobra.Command {

View file

@ -12,9 +12,9 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/torrentclaw/torrentclaw-cli/internal/engine"
"github.com/torrentclaw/torrentclaw-cli/internal/parser"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/engine"
"github.com/torrentclaw/unarr/internal/parser"
"github.com/torrentclaw/unarr/internal/ui"
)
func newStreamCmd() *cobra.Command {

View file

@ -8,10 +8,10 @@ import (
"sync"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/torrentclaw-cli/internal/engine"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/engine"
"github.com/torrentclaw/unarr/internal/ui"
)
// streamRegistry tracks active stream tasks and servers for cancellation.

View file

@ -15,7 +15,7 @@ func newStubCmd(name, short string) *cobra.Command {
fmt.Println()
color.New(color.FgYellow).Printf(" ⚠️ '%s' is coming in a future release.\n", name)
fmt.Println()
fmt.Println(" Follow progress at: https://github.com/torrentclaw/torrentclaw-cli")
fmt.Println(" Follow progress at: https://github.com/torrentclaw/unarr")
fmt.Println()
},
}

View file

@ -10,7 +10,7 @@ import (
"github.com/spf13/cobra"
tc "github.com/torrentclaw/go-client"
"github.com/torrentclaw/torrentclaw-cli/internal/ui"
"github.com/torrentclaw/unarr/internal/ui"
)
func newWatchCmd() *cobra.Command {

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.

View file

@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/torrentclaw/torrentclaw-cli/internal/config"
"github.com/torrentclaw/unarr/internal/config"
)
// CachePath returns the default library cache file path.

View file

@ -4,7 +4,7 @@ import (
"regexp"
"strings"
"github.com/torrentclaw/torrentclaw-cli/internal/library/mediainfo"
"github.com/torrentclaw/unarr/internal/library/mediainfo"
)
var (

View file

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

View file

@ -11,8 +11,8 @@ import (
"sync/atomic"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/library/mediainfo"
"github.com/torrentclaw/torrentclaw-cli/internal/parser"
"github.com/torrentclaw/unarr/internal/library/mediainfo"
"github.com/torrentclaw/unarr/internal/parser"
)
// videoExts are file extensions considered as video files.

View file

@ -1,6 +1,6 @@
package library
import "github.com/torrentclaw/torrentclaw-cli/internal/agent"
import "github.com/torrentclaw/unarr/internal/agent"
// BuildSyncItems converts cached library items to sync request items.
// Shared between unarr scan (cmd/scan.go) and auto-scan (cmd/daemon.go).

View file

@ -1,6 +1,6 @@
package library
import "github.com/torrentclaw/torrentclaw-cli/internal/library/mediainfo"
import "github.com/torrentclaw/unarr/internal/library/mediainfo"
// LibraryItem represents a single scanned media file.
type LibraryItem struct {

View file

@ -10,7 +10,7 @@ import (
)
// dsn is injected at build time via ldflags. If empty, Sentry is disabled.
// Set via: -ldflags "-X github.com/torrentclaw/torrentclaw-cli/internal/sentry.dsn=..."
// Set via: -ldflags "-X github.com/torrentclaw/unarr/internal/sentry.dsn=..."
var dsn string
const flushTimeout = 2 * time.Second

View file

@ -12,9 +12,9 @@ import (
"sync/atomic"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/nntp"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/nzb"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/yenc"
"github.com/torrentclaw/unarr/internal/usenet/nntp"
"github.com/torrentclaw/unarr/internal/usenet/nzb"
"github.com/torrentclaw/unarr/internal/usenet/yenc"
)
// Progress is emitted during download.

View file

@ -8,10 +8,10 @@ import (
"testing"
"time"
"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/usenet/download"
"github.com/torrentclaw/unarr/internal/usenet/nntp"
"github.com/torrentclaw/unarr/internal/usenet/nzb"
"github.com/torrentclaw/unarr/internal/usenet/postprocess"
)
// TestE2EDownload is a real end-to-end test that downloads from Usenet.

View file

@ -11,7 +11,7 @@ import (
"sync/atomic"
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/nzb"
"github.com/torrentclaw/unarr/internal/usenet/nzb"
)
// Binary progress file format:

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/torrentclaw/torrentclaw-cli/internal/usenet/nzb"
"github.com/torrentclaw/unarr/internal/usenet/nzb"
)
var fixedPast = time.Now().Add(-30 * 24 * time.Hour)