Files
gronod 2d5763d6c6 feat: slim tool responses, drop lyrics search, rename module (1.0.11)
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.
2026-09-21 23:54:07 +01:00

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
}