Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions internal/cmd/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type LibraryCmd struct {
List LibrariesCmd `cmd:"" help:"List library sections"`
Update LibraryUpdateCmd `cmd:"" help:"Refresh one library section or all sections"`
Clean LibraryCleanCmd `cmd:"" help:"Empty trash for one library section or all sections"`
Status LibraryStatusCmd `cmd:"" help:"Show active library-related tasks and activities"`
Status LibraryStatusCmd `cmd:"" help:"Show active server tasks and activities"`
}

type LibraryUpdateCmd struct {
Expand Down Expand Up @@ -187,11 +187,6 @@ func (c *LibraryStatusCmd) Run(ctx *kong.Context, u *ui.UI, cfg *config.Config)
})
}

if len(items) == 0 {
fmt.Fprintln(u.Err(), "No active library tasks")
return nil
}

return c.output(u.Out(), items)
}

Expand Down
98 changes: 71 additions & 27 deletions internal/plexclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,27 +703,27 @@ func (c *Client) StartAnalysis(ctx context.Context, sectionID string) error {
}

func (c *Client) AnalyzeMetadata(ctx context.Context, ids string) error {
if strings.TrimSpace(ids) == "" {
encodedIDs, err := encodeMetadataIDs(ids)
if err != nil {
return &PlexError{
Op: "AnalyzeMetadata",
Err: fmt.Errorf("ids are required"),
Err: err,
}
}

escapedIDs := url.PathEscape(ids)
return c.runLibraryGlobalAction(ctx, "AnalyzeMetadata", fmt.Sprintf("library/metadata/%s/analyze", escapedIDs), http.MethodPut)
return c.runLibraryGlobalAction(ctx, "AnalyzeMetadata", fmt.Sprintf("library/metadata/%s/analyze", encodedIDs), http.MethodPut)
}

func (c *Client) GenerateThumbs(ctx context.Context, ids string) error {
if strings.TrimSpace(ids) == "" {
encodedIDs, err := encodeMetadataIDs(ids)
if err != nil {
return &PlexError{
Op: "GenerateThumbs",
Err: fmt.Errorf("ids are required"),
Err: err,
}
}

escapedIDs := url.PathEscape(ids)
return c.runLibraryGlobalAction(ctx, "GenerateThumbs", fmt.Sprintf("library/metadata/%s/chapterThumbs", escapedIDs), http.MethodPut)
return c.runLibraryGlobalAction(ctx, "GenerateThumbs", fmt.Sprintf("library/metadata/%s/chapterThumbs", encodedIDs), http.MethodPut)
}

func (c *Client) EmptyTrash(ctx context.Context, sectionID string) error {
Expand Down Expand Up @@ -2508,12 +2508,17 @@ func (c *Client) GetActivities(ctx context.Context) ([]ActivityTask, error) {

activities = nil
for _, a := range container.Activities {
progress, parseErr := parseOptionalFloat(a.ProgressRaw)
if parseErr != nil {
return fmt.Errorf("invalid activity progress %q: %w", a.ProgressRaw, parseErr)
}

activities = append(activities, ActivityTask{
UUID: a.UUID,
Type: a.Type,
Title: a.Title,
Subtitle: a.Subtitle,
Progress: parseOptionalFloat(a.ProgressRaw),
Progress: progress,
Cancellable: parseBoolAttr(a.CancelRaw),
})
}
Expand Down Expand Up @@ -2574,22 +2579,40 @@ func (c *Client) GetBackgroundTasks(ctx context.Context) ([]BackgroundTask, erro
return fmt.Errorf("failed to parse response: %w", err)
}

appendTask := func(rt rawTask) {
appendTask := func(rt rawTask) error {
progress, parseErr := parseOptionalFloat(rt.ProgressRaw)
if parseErr != nil {
return fmt.Errorf("invalid task progress %q: %w", rt.ProgressRaw, parseErr)
}
remaining, parseErr := parseOptionalInt64(rt.RemainingRaw)
if parseErr != nil {
return fmt.Errorf("invalid task remaining %q: %w", rt.RemainingRaw, parseErr)
}
speed, parseErr := parseOptionalFloat(rt.SpeedRaw)
if parseErr != nil {
return fmt.Errorf("invalid task speed %q: %w", rt.SpeedRaw, parseErr)
}

tasks = append(tasks, BackgroundTask{
Type: rt.Type,
Title: rt.Title,
Progress: parseOptionalFloat(rt.ProgressRaw),
Remaining: parseOptionalInt64(rt.RemainingRaw),
Speed: parseOptionalFloat(rt.SpeedRaw),
Progress: progress,
Remaining: remaining,
Speed: speed,
})
return nil
}

tasks = nil
for _, t := range container.TranscodeJobs {
appendTask(t)
if appendErr := appendTask(t); appendErr != nil {
return appendErr
}
}
for _, t := range container.TranscodeSession {
appendTask(t)
if appendErr := appendTask(t); appendErr != nil {
return appendErr
}
}

return nil
Expand All @@ -2604,26 +2627,28 @@ func (c *Client) GetBackgroundTasks(ctx context.Context) ([]BackgroundTask, erro
return tasks, nil
}

func parseOptionalFloat(raw string) *float64 {
if strings.TrimSpace(raw) == "" {
return nil
func parseOptionalFloat(raw string) (*float64, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return nil, nil
}
v, err := strconv.ParseFloat(raw, 64)
v, err := strconv.ParseFloat(trimmed, 64)
if err != nil {
return nil
return nil, err
}
return &v
return &v, nil
}

func parseOptionalInt64(raw string) *int64 {
if strings.TrimSpace(raw) == "" {
return nil
func parseOptionalInt64(raw string) (*int64, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return nil, nil
}
v, err := strconv.ParseInt(raw, 10, 64)
v, err := strconv.ParseInt(trimmed, 10, 64)
if err != nil {
return nil
return nil, err
}
return &v
return &v, nil
}

func parseBoolAttr(raw string) bool {
Expand All @@ -2634,3 +2659,22 @@ func parseBoolAttr(raw string) bool {
return false
}
}

func encodeMetadataIDs(ids string) (string, error) {
trimmed := strings.TrimSpace(ids)
if trimmed == "" {
return "", fmt.Errorf("ids are required")
}

parts := strings.Split(trimmed, ",")
encoded := make([]string, 0, len(parts))
for _, part := range parts {
id := strings.TrimSpace(part)
if id == "" {
return "", fmt.Errorf("ids contain an empty segment")
}
encoded = append(encoded, url.PathEscape(id))
}

return strings.Join(encoded, ","), nil
}
81 changes: 81 additions & 0 deletions internal/plexclient/client_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package plexclient

import "testing"

func TestEncodeMetadataIDs(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{name: "single id", input: "123", want: "123"},
{name: "multiple ids", input: "1,2,3", want: "1,2,3"},
{name: "trimmed ids", input: " 1, 2 ,3 ", want: "1,2,3"},
{name: "escaped segment", input: "abc/def,2", want: "abc%2Fdef,2"},
{name: "empty input", input: "", wantErr: true},
{name: "empty segment", input: "1,,2", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := encodeMetadataIDs(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Fatalf("encodeMetadataIDs(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}

func TestParseOptionalFloat(t *testing.T) {
got, err := parseOptionalFloat("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != nil {
t.Fatalf("expected nil for empty input")
}

got, err = parseOptionalFloat("12.5")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil || *got != 12.5 {
t.Fatalf("expected 12.5, got %#v", got)
}

if _, err = parseOptionalFloat("abc"); err == nil {
t.Fatalf("expected parse error")
}
}

func TestParseOptionalInt64(t *testing.T) {
got, err := parseOptionalInt64("")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != nil {
t.Fatalf("expected nil for empty input")
}

got, err = parseOptionalInt64("42")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil || *got != 42 {
t.Fatalf("expected 42, got %#v", got)
}

if _, err = parseOptionalInt64("abc"); err == nil {
t.Fatalf("expected parse error")
}
}
Loading