unarr/internal/cmd/strm_handler.go
Deivid Soto 6adf1e2c4c feat(mediaserver): Plex/Jellyfin/Emby auto-refresh + .strm instant mode
Sprint 1 — Auto-refresh after download:
- New [[mediaserver]] TOML section with kind/url/token/sections
- mediaserver.Refresh() fans out to Plex (partial via section ID auto-mapping
  from file path prefix) and Jellyfin/Emby (full library scan)
- Manager.OnFinalized callback wired in daemon to trigger refresh after
  organize() completes — keeps engine package free of mediaserver dep
- New unarr mediaserver {setup,list,remove,test} commands
- unarr init wizard offers to configure refresh when a server is detected

Sprint 2 — .strm instant mode (cloud + agent):
- Mode strm-to-library handled in daemon dispatch: writes a one-line .strm
  file pointing to the cloud-resolved debrid HTTPS URL, then triggers refresh
- engine.WriteStrm + StrmDestForTask mirror organize()'s naming so Plex/Jellyfin
  see the expected folder structure (Movies/Title (Year)/, TV Shows/Show/Season XX/)
- Atomic write (temp + rename) so partial files never get indexed
- Reports completed/failed status to the cloud via existing agent client
2026-05-05 20:35:08 +02:00

70 lines
2.3 KiB
Go

package cmd
import (
"context"
"log"
"github.com/torrentclaw/unarr/internal/agent"
"github.com/torrentclaw/unarr/internal/config"
"github.com/torrentclaw/unarr/internal/engine"
"github.com/torrentclaw/unarr/internal/mediaserver"
)
// handleStrmToLibrary processes a Mode="strm-to-library" task by writing a
// one-line .strm file to the user's media library and triggering a
// Plex/Jellyfin/Emby refresh. No actual download happens; the .strm points
// at the cloud-resolved debrid HTTPS URL, and the media server streams from
// there when the user presses play.
//
// Reports completion (or failure) back to the cloud via the agent client.
func handleStrmToLibrary(ctx context.Context, t agent.Task, cfg config.Config, agentClient *agent.Client) {
short := agent.ShortID(t.ID)
if t.DirectURL == "" {
log.Printf("[%s] strm-to-library: missing directUrl from server", short)
reportStrmFailure(ctx, agentClient, t.ID, "missing directUrl")
return
}
organizeCfg := engine.OrganizeConfig{
Enabled: cfg.Organize.Enabled,
MoviesDir: cfg.Organize.MoviesDir,
TVShowsDir: cfg.Organize.TVShowsDir,
OutputDir: cfg.Download.Dir,
}
finalPath, err := engine.WriteStrm(t, organizeCfg)
if err != nil {
log.Printf("[%s] strm-to-library write failed: %v", short, err)
reportStrmFailure(ctx, agentClient, t.ID, err.Error())
return
}
log.Printf("[%s] strm-to-library wrote %s", short, finalPath)
// Trigger media-server refresh if any are configured. Errors are logged
// inside Refresh and never propagate — the .strm is on disk, so the
// next periodic scan would pick it up regardless.
if len(cfg.MediaServers) > 0 {
mediaserver.Refresh(cfg.MediaServers, finalPath)
}
if _, reportErr := agentClient.ReportStatus(ctx, agent.StatusUpdate{
TaskID: t.ID,
Status: "completed",
Progress: 100,
FilePath: finalPath,
}); reportErr != nil {
log.Printf("[%s] strm-to-library: status report failed: %v", short, reportErr)
}
}
func reportStrmFailure(ctx context.Context, agentClient *agent.Client, taskID, msg string) {
if _, err := agentClient.ReportStatus(ctx, agent.StatusUpdate{
TaskID: taskID,
Status: "failed",
ErrorMessage: msg,
}); err != nil {
log.Printf("[%s] strm failure report failed: %v", agent.ShortID(taskID), err)
}
}