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:
co-authored by
Claude Opus 5
parent
4a4df7a73c
commit
80c304d86b
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user