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:
parent
9cc806d11f
commit
5a7449b9e6
58 changed files with 166 additions and 141 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/torrentclaw/torrentclaw-cli/internal/agent"
|
||||
"github.com/torrentclaw/unarr/internal/agent"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue