Release v0.2.34

This commit is contained in:
ponzischeme89
2026-08-09 08:25:50 +12:00
parent b1128bcce2
commit fdd9e6cab2
116 changed files with 11418 additions and 879 deletions
+52
View File
@@ -373,6 +373,21 @@ func (c *Client) SetPlayed(ctx context.Context, cred Credentials, itemID string,
return c.userDataCall(ctx, method, path, cred)
}
// HideFromResume removes an item from resume and next-up feeds without marking it played.
func (c *Client) HideFromResume(ctx context.Context, cred Credentials, itemID string) (json.RawMessage, error) {
params := url.Values{"Hide": {"true"}}
path := "/Users/" + url.PathEscape(cred.UserID) + "/Items/" + url.PathEscape(itemID) + "/HideFromResume"
req, err := c.newRequest(ctx, http.MethodPost, path, params, cred, nil)
if err != nil {
return nil, err
}
var raw json.RawMessage
if err := c.do(req, &raw); err != nil {
return nil, err
}
return raw, nil
}
// RefreshItem asks Emby to re-scan one item's files.
//
// It exists for the subtitle download: Bazarr writes the new .srt beside the media file
@@ -501,6 +516,43 @@ func (c *Client) TrickplayBytes(
return io.ReadAll(io.LimitReader(resp.Body, to-from+1))
}
// MaxSubtitleBytes caps a subtitle read. A feature-length SubRip file is tens of
// kilobytes; a megabyte is far past anything genuine and is what an Emby that answered
// with the media file instead would send.
const MaxSubtitleBytes = 4 << 20
// SubtitleBytes reads a text subtitle track's content.
//
// This is the one place the gateway pulls a subtitle *out* of Emby rather than pointing
// the television at it, and it exists so a track can be used as a timing reference. SubRip
// is asked for because that is what the sync reads and writes; Emby converts from whatever
// the container holds.
func (c *Client) SubtitleBytes(
ctx context.Context, cred Credentials, itemID, mediaSourceID string, index int,
) ([]byte, error) {
if mediaSourceID == "" {
mediaSourceID = itemID
}
path := fmt.Sprintf(
"/Videos/%s/%s/Subtitles/%d/Stream.srt",
url.PathEscape(itemID), url.PathEscape(mediaSourceID), index,
)
req, err := c.newRequest(ctx, http.MethodGet, path, nil, cred, nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)}
}
return io.ReadAll(io.LimitReader(resp.Body, MaxSubtitleBytes))
}
// StreamURL is the direct-play URL handed to the TV. It points at the *public* Emby
// address: video never flows through the gateway, only metadata does.
func (c *Client) StreamURL(cred Credentials, itemID string) string {
@@ -0,0 +1,42 @@
package emby
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestHideFromResumeUsesDedicatedEmbyEndpoint(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "unexpected method: "+r.Method, http.StatusBadRequest)
return
}
if r.URL.Path != "/Users/user-1/Items/item-2/HideFromResume" {
http.Error(w, "unexpected path: "+r.URL.Path, http.StatusBadRequest)
return
}
if r.URL.Query().Get("Hide") != "true" {
http.Error(w, "Hide must be true", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"Played":false,"PlaybackPositionTicks":0}`))
}))
defer upstream.Close()
client := New(upstream.URL, upstream.URL, "MbyATV", time.Second)
got, err := client.HideFromResume(
context.Background(),
Credentials{UserID: "user-1", Token: "token"},
"item-2",
)
if err != nil {
t.Fatal(err)
}
if string(got) != `{"Played":false,"PlaybackPositionTicks":0}` {
t.Fatalf("response = %s", got)
}
}