Item-shaped results now omit empty fields and drop low-value metadata (overview, lyrics, file_path, creation_date, premiere_date, bitrate); absent numeric fields drop the key instead of emitting "". Player sessions report hh:mm:ss times only; playlists drop overview/date_created. The lyrics_or_description search parameter is removed, along with the unused DTO fields, and upstream Fields requests are slimmed to match. Module path renamed to git.i3omb.com/gronod/emby-mcp.
149 lines
4.4 KiB
Go
149 lines
4.4 KiB
Go
package emby
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// GetChildren lists the direct children of an item: seasons of a series,
|
|
// episodes of a season, tracks of an album, or folder contents. Unlike
|
|
// GetItems it does not restrict MediaTypes, so container items (Series,
|
|
// Season, Folder) are returned too.
|
|
func (c *Client) GetChildren(ctx context.Context, userID, parentID string) ([]MediaItem, error) {
|
|
if parentID == "" {
|
|
return nil, fmt.Errorf("the 'item_id' parameter cannot be empty")
|
|
}
|
|
q := url.Values{
|
|
"Recursive": {"false"},
|
|
"ParentId": {parentID},
|
|
"Fields": {itemExtraFields},
|
|
"SortBy": {"SortName"},
|
|
}
|
|
var res QueryResult[BaseItemDto]
|
|
if err := c.Get(ctx, "/Users/"+userID+"/Items", q, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]MediaItem, 0, len(res.Items))
|
|
for _, it := range res.Items {
|
|
items = append(items, toMediaItem(&it))
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetSeasons lists the seasons of a series.
|
|
func (c *Client) GetSeasons(ctx context.Context, userID, seriesID string) ([]MediaItem, error) {
|
|
if seriesID == "" {
|
|
return nil, fmt.Errorf("the 'series_id' parameter cannot be empty")
|
|
}
|
|
q := url.Values{
|
|
"UserId": {userID},
|
|
"Fields": {itemExtraFields},
|
|
}
|
|
var res QueryResult[BaseItemDto]
|
|
if err := c.Get(ctx, "/Shows/"+seriesID+"/Seasons", q, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]MediaItem, 0, len(res.Items))
|
|
for _, it := range res.Items {
|
|
items = append(items, toMediaItem(&it))
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetEpisodes lists the episodes of a series, optionally restricted to one
|
|
// season. Episodes are sorted by season/episode number and include per-user
|
|
// watch state.
|
|
func (c *Client) GetEpisodes(ctx context.Context, userID, seriesID, seasonID string) ([]MediaItem, error) {
|
|
if seriesID == "" {
|
|
return nil, fmt.Errorf("the 'series_id' parameter cannot be empty")
|
|
}
|
|
q := url.Values{
|
|
"UserId": {userID},
|
|
"Fields": {itemExtraFields},
|
|
"SortBy": {"ParentIndexNumber,IndexNumber"},
|
|
"EnableUserData": {"true"},
|
|
}
|
|
if seasonID != "" {
|
|
q.Set("SeasonId", seasonID)
|
|
}
|
|
var res QueryResult[BaseItemDto]
|
|
if err := c.Get(ctx, "/Shows/"+seriesID+"/Episodes", q, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]MediaItem, 0, len(res.Items))
|
|
for _, it := range res.Items {
|
|
items = append(items, toMediaItem(&it))
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetNextUp returns the user's next-up episode for a series: the first episode
|
|
// that is not fully played, including partially-watched resume candidates.
|
|
// Returns nil when the series has no next-up episode (e.g. fully watched).
|
|
func (c *Client) GetNextUp(ctx context.Context, userID, seriesID string) (*MediaItem, error) {
|
|
q := url.Values{
|
|
"UserId": {userID},
|
|
"SeriesId": {seriesID},
|
|
"Fields": {itemExtraFields},
|
|
"EnableUserData": {"true"},
|
|
"Limit": {"1"},
|
|
}
|
|
var res QueryResult[BaseItemDto]
|
|
if err := c.Get(ctx, "/Shows/NextUp", q, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(res.Items) == 0 {
|
|
return nil, nil
|
|
}
|
|
mi := toMediaItem(&res.Items[0])
|
|
return &mi, nil
|
|
}
|
|
|
|
// GetLatestEpisode returns the most recently premiered episode of a series
|
|
// that is actually present (virtual/missing placeholders are skipped).
|
|
// Returns nil when the series has no episodes.
|
|
func (c *Client) GetLatestEpisode(ctx context.Context, userID, seriesID string) (*MediaItem, error) {
|
|
q := url.Values{
|
|
"UserId": {userID},
|
|
"Fields": {itemExtraFields},
|
|
"SortBy": {"PremiereDate"},
|
|
"SortOrder": {"Descending"},
|
|
"Limit": {"10"},
|
|
"EnableUserData": {"true"},
|
|
}
|
|
var res QueryResult[BaseItemDto]
|
|
if err := c.Get(ctx, "/Shows/"+seriesID+"/Episodes", q, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range res.Items {
|
|
if strings.EqualFold(res.Items[i].LocationType, "Virtual") {
|
|
continue
|
|
}
|
|
mi := toMediaItem(&res.Items[i])
|
|
return &mi, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// FindSeries resolves a series by name across all libraries. An exact
|
|
// case-insensitive name match wins; otherwise all candidates are returned so
|
|
// the caller can disambiguate.
|
|
func (c *Client) FindSeries(ctx context.Context, userID, name string) (exact *MediaItem, candidates []MediaItem, err error) {
|
|
items, err := c.GetItems(ctx, userID, "", ItemQuery{
|
|
SearchTerm: name,
|
|
IncludeItemTypes: "Series",
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for i := range items {
|
|
if strings.EqualFold(items[i].Title, name) {
|
|
mi := items[i]
|
|
return &mi, nil, nil
|
|
}
|
|
}
|
|
return nil, items, nil
|
|
}
|