App v0.2.27 and gateway 0.1.23

Skip Intro from Emby's own chapter markers, trickplay seek previews from
BIF files, a server-composed home hero ranked on Radarr/Sonarr dates and
review scores, and My Alerts as its own page behind the user picker.

Related titles now degrade at every step instead of returning empty, and
the "+" is back on Manage users so a second viewer can be added from the
launcher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ponzischeme89
2026-08-07 10:44:17 +12:00
co-authored by Claude Opus 5
parent 4a4df7a73c
commit 80c304d86b
62 changed files with 6095 additions and 255 deletions
+36
View File
@@ -14,6 +14,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -465,6 +466,41 @@ func (c *Client) ImageResponse(ctx context.Context, cred Credentials, itemID, im
return resp, nil
}
// TrickplayBytes reads part of a title's preview-thumbnail file (BIF).
//
// It is always a ranged read, because the whole file is megabytes and nothing here ever
// wants all of it: callers take the index off the front, then one frame at a time. Emby
// answers ranges on this route correctly but does not say so — the response advertises
// "Accept-Ranges: none" and a Content-Length taken from the media file — so the request is
// made on the strength of the 206 rather than on what the headers promise, and a server
// that ignored the range would simply return the head of the file, which is the index a
// caller wanted anyway.
func (c *Client) TrickplayBytes(
ctx context.Context, cred Credentials, itemID string, width int, from, to int64,
) ([]byte, error) {
params := url.Values{}
params.Set("Width", strconv.Itoa(width))
path := "/Videos/" + url.PathEscape(itemID) + "/index.bif"
req, err := c.newRequest(ctx, http.MethodGet, path, params, cred, nil)
if err != nil {
return nil, err
}
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", from, to))
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)}
}
// Cap the read at what was asked for. A 200 means the range was ignored and the whole
// file is on its way, which must not become a multi-megabyte read on a seek.
return io.ReadAll(io.LimitReader(resp.Body, to-from+1))
}
// 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 {