Skip to content

Commit 1b60e72

Browse files
committed
feat: library pagination
Signed-off-by: ygelfand <yuri@shlitz.com>
1 parent f8e0588 commit 1b60e72

File tree

4 files changed

+103
-26
lines changed

4 files changed

+103
-26
lines changed

cmd/library.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import (
1414
"github.com/ygelfand/plexctl/internal/ui"
1515
)
1616

17+
var (
18+
libraryCount int
19+
libraryPage int
20+
libraryAll bool
21+
)
22+
1723
var libraryCmd = &cobra.Command{
1824
Use: "library",
1925
Short: "Manage libraries",
@@ -52,25 +58,21 @@ var libraryShowCmd = &cobra.Command{
5258
libraryID := args[0]
5359
slog.Debug("SDK: Fetching library content", "library_id", libraryID)
5460

55-
res, err := client.SDK.Content.ListContent(ctx, operations.ListContentRequest{
56-
SectionID: libraryID,
57-
})
61+
allMetadata, err := plex.WalkContent(ctx, libraryAll, libraryPage, libraryCount, plex.LibraryWalker(client, libraryID))
5862
if err != nil {
59-
slog.Error("SDK: Failed to get library items", "library_id", libraryID, "error", err)
60-
return fmt.Errorf("failed to get library items: %w", err)
63+
return err
6164
}
6265

63-
if res.MediaContainerWithMetadata == nil || res.MediaContainerWithMetadata.MediaContainer == nil || len(res.MediaContainerWithMetadata.MediaContainer.Metadata) == 0 {
64-
slog.Debug("SDK: No items found", "library_id", libraryID)
66+
if len(allMetadata) == 0 {
6567
fmt.Println("No items found in this library.")
6668
return nil
6769
}
6870

69-
slog.Debug("SDK: Found items", "library_id", libraryID, "count", len(res.MediaContainerWithMetadata.MediaContainer.Metadata))
71+
slog.Debug("SDK: Found items", "library_id", libraryID, "count", len(allMetadata))
7072
return commands.Print(&presenters.LibraryItemsPresenter{
7173
SectionID: libraryID,
72-
Items: presenters.MapMetadata(res.MediaContainerWithMetadata.MediaContainer.Metadata),
73-
RawData: res.MediaContainerWithMetadata.MediaContainer.Metadata,
74+
Items: presenters.MapMetadata(allMetadata),
75+
RawData: allMetadata,
7476
}, opts)
7577
}),
7678
}
@@ -104,4 +106,8 @@ func init() {
104106
libraryCmd.AddCommand(libraryListCmd)
105107
libraryCmd.AddCommand(libraryShowCmd)
106108
libraryCmd.AddCommand(libraryRefreshCmd)
109+
110+
libraryShowCmd.Flags().IntVar(&libraryCount, "count", 50, "Number of items to return per page")
111+
libraryShowCmd.Flags().IntVar(&libraryPage, "page", 1, "Page number to return")
112+
libraryShowCmd.Flags().BoolVar(&libraryAll, "all", false, "Return all items (overrides count/page)")
107113
}

internal/commands/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ type PlexCtlOptions struct {
55
OutputFormat string
66
Verbosity int
77
Sort string
8+
Count int
9+
Page int
10+
All bool
811
}

internal/commands/wrapper.go

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ import (
1717
// RunnerFunc defines the signature for a command handler that receives a Plex client
1818
type RunnerFunc func(ctx context.Context, client *plex.Client, cmd *cobra.Command, args []string, opts *PlexCtlOptions) error
1919

20-
// RunWithClient wraps a cobra command RunE function to inject a configured Plex client
20+
// RunWithClient wraps a cobra command RunE function to inject a configured Plex client.
21+
// This is for commands that only need a token (like login or discovery).
2122
func RunWithClient(runner RunnerFunc) func(cmd *cobra.Command, args []string) error {
2223
return func(cmd *cobra.Command, args []string) error {
2324
opts := &PlexCtlOptions{
2425
OutputFormat: viper.GetString("output"),
2526
Verbosity: viper.GetInt("verbose"),
2627
Sort: viper.GetString("sort"),
28+
Count: viper.GetInt("count"),
29+
Page: viper.GetInt("page"),
30+
All: viper.GetBool("all"),
2731
}
2832

2933
client, err := plex.NewClient()
@@ -34,22 +38,10 @@ func RunWithClient(runner RunnerFunc) func(cmd *cobra.Command, args []string) er
3438
}
3539
}
3640

37-
// RunWithServer wraps a cobra command RunE function to inject a configured Plex client
38-
// and ensures that a default server has been selected.
41+
// RunWithServer wraps RunWithClient but ensures a default server is selected first.
42+
// This is for commands that interact with specific media or server library sections.
3943
func RunWithServer(runner RunnerFunc) func(cmd *cobra.Command, args []string) error {
40-
return func(cmd *cobra.Command, args []string) error {
41-
opts := &PlexCtlOptions{
42-
OutputFormat: viper.GetString("output"),
43-
Verbosity: viper.GetInt("verbose"),
44-
Sort: viper.GetString("sort"),
45-
}
46-
47-
client, err := plex.NewClient()
48-
if err != nil {
49-
return err
50-
}
51-
return runner(cmd.Context(), client, cmd, args, opts)
52-
}
44+
return RunWithClient(runner)
5345
}
5446

5547
// EnsureActiveServer checks if a server is configured and triggers discovery if not

internal/plex/pagination.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package plex
2+
3+
import (
4+
"context"
5+
6+
"github.com/LukeHagar/plexgo/models/components"
7+
"github.com/LukeHagar/plexgo/models/operations"
8+
"github.com/ygelfand/plexctl/internal/ui"
9+
)
10+
11+
// ContentWalker is a function that fetches a single page of results
12+
type ContentWalker func(ctx context.Context, start, size int) ([]components.Metadata, int64, error)
13+
14+
// WalkContent handles either fetching a single page or walking all pages based on the provided options
15+
func WalkContent(ctx context.Context, all bool, page, count int, walker ContentWalker) ([]components.Metadata, error) {
16+
var allMetadata []components.Metadata
17+
start := 0
18+
size := count
19+
20+
if all {
21+
size = 100 // doesn't seem to make much of a difference how many per page once it gets here
22+
} else {
23+
start = (page - 1) * count
24+
}
25+
26+
for {
27+
metadata, totalSize, err := walker(ctx, start, size)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
allMetadata = append(allMetadata, metadata...)
33+
34+
if !all {
35+
break
36+
}
37+
38+
start += len(metadata)
39+
if totalSize > 0 && int64(start) >= totalSize {
40+
break
41+
}
42+
if len(metadata) == 0 {
43+
break
44+
}
45+
}
46+
47+
return allMetadata, nil
48+
}
49+
50+
// LibraryWalker returns a ContentWalker for a specific library section
51+
func LibraryWalker(client *Client, sectionID string) ContentWalker {
52+
return func(ctx context.Context, start, size int) ([]components.Metadata, int64, error) {
53+
req := operations.ListContentRequest{
54+
SectionID: sectionID,
55+
XPlexContainerStart: ui.Ptr(start),
56+
XPlexContainerSize: ui.Ptr(size),
57+
}
58+
59+
res, err := client.SDK.Content.ListContent(ctx, req)
60+
if err != nil {
61+
return nil, 0, err
62+
}
63+
64+
if res.MediaContainerWithMetadata == nil || res.MediaContainerWithMetadata.MediaContainer == nil {
65+
return nil, 0, nil
66+
}
67+
68+
mc := res.MediaContainerWithMetadata.MediaContainer
69+
total := int64(0)
70+
if mc.TotalSize != nil {
71+
total = *mc.TotalSize
72+
}
73+
74+
return mc.Metadata, total, nil
75+
}
76+
}

0 commit comments

Comments
 (0)