refactor: tmdb and requests polish
- GetExternalIDs now uses the dedicated /movie/{id}/external_ids and
/tv/{id}/external_ids endpoints instead of fetching the full detail
with append_to_response=external_ids. The dedicated payload is
one or two orders of magnitude smaller for the same fields.
- Document PosterPath/BackdropPath on MediaResult as raw TMDB path
fragments that callers must prefix with the image base URL.
- normalizeCast switches from inline insertion sort to sort.SliceStable.
The output is identical; the new form is one line and O(n log n).
- normalizeIntegration no longer reuses integration.Tags' backing
array via Tags[:0]; the slice is callable code, so reusing the
array would silently corrupt the caller's slice if it kept a
reference. Allocate a fresh slice instead.
- HandleGet now requires a profile, matching the rest of the
/requests user-group handlers. Router middleware enforces this
already, but the inline check is defense-in-depth for any future
remount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e8143d5757
commit
8a86e0cf08
@@ -265,7 +265,7 @@ func (h *RequestsHandler) HandleListMine(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func (h *RequestsHandler) HandleGet(w http.ResponseWriter, r *http.Request) {
|
||||
viewer, ok := requestViewer(w, r, false)
|
||||
viewer, ok := requestViewer(w, r, true)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -885,11 +886,9 @@ func normalizeCast(cast []castEntry) []MediaCastMember {
|
||||
}
|
||||
sorted := make([]castEntry, len(cast))
|
||||
copy(sorted, cast)
|
||||
for i := 1; i < len(sorted); i++ {
|
||||
for j := i; j > 0 && sorted[j].Order < sorted[j-1].Order; j-- {
|
||||
sorted[j], sorted[j-1] = sorted[j-1], sorted[j]
|
||||
}
|
||||
}
|
||||
sort.SliceStable(sorted, func(i, j int) bool {
|
||||
return sorted[i].Order < sorted[j].Order
|
||||
})
|
||||
const maxCast = 24
|
||||
if len(sorted) > maxCast {
|
||||
sorted = sorted[:maxCast]
|
||||
@@ -972,20 +971,23 @@ func pickTVRating(cr *contentRatingsResponse) string {
|
||||
}
|
||||
|
||||
// GetExternalIDs fetches external IDs for a TMDB movie or TV entry.
|
||||
// Uses the dedicated external_ids endpoint instead of append_to_response on
|
||||
// the full detail, which would return a 100+ KB payload to extract a handful
|
||||
// of identifiers.
|
||||
func (c *Client) GetExternalIDs(ctx context.Context, mediaType string, id int) (*ExternalIDs, error) {
|
||||
var path string
|
||||
switch mediaType {
|
||||
case "movie":
|
||||
path = fmt.Sprintf("/movie/%d?append_to_response=external_ids", id)
|
||||
path = fmt.Sprintf("/movie/%d/external_ids", id)
|
||||
case "tv":
|
||||
path = fmt.Sprintf("/tv/%d?append_to_response=external_ids", id)
|
||||
path = fmt.Sprintf("/tv/%d/external_ids", id)
|
||||
default:
|
||||
return nil, fmt.Errorf("tmdb: invalid media type: %q", mediaType)
|
||||
}
|
||||
|
||||
var resp externalIDsResponse
|
||||
var resp ExternalIDs
|
||||
if err := c.doGet(ctx, path, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.ExternalIDs, nil
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@@ -637,23 +637,18 @@ func TestGetCollectionRejectsNonPositiveID(t *testing.T) {
|
||||
|
||||
func TestGetExternalIDs(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/movie/123" {
|
||||
if r.URL.Path != "/movie/123/external_ids" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("append_to_response"); got != "external_ids" {
|
||||
t.Fatalf("append_to_response = %q, want external_ids", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("api_key"); got != "test-key" {
|
||||
t.Fatalf("api_key query = %q, want test-key", got)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"external_ids": {
|
||||
"imdb_id": "tt0133093",
|
||||
"tvdb_id": 12345
|
||||
}
|
||||
"imdb_id": "tt0133093",
|
||||
"tvdb_id": 12345
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
@@ -37,6 +37,11 @@ type CollectionResult struct {
|
||||
|
||||
// MediaResult is a normalized TMDB movie or TV result for request search and
|
||||
// discovery surfaces. MediaType is Silo-facing: "movie" or "series".
|
||||
//
|
||||
// PosterPath and BackdropPath are raw TMDB path fragments (e.g. "/abc.jpg").
|
||||
// Callers must compose the full URL by prepending the TMDB image base
|
||||
// (https://image.tmdb.org/t/p/{size}); the values are not browser-loadable
|
||||
// on their own.
|
||||
type MediaResult struct {
|
||||
ID int
|
||||
MediaType string
|
||||
@@ -157,10 +162,6 @@ type mediaMultiSearchResponse struct {
|
||||
VoteAverage float64 `json:"vote_average"`
|
||||
}
|
||||
|
||||
type externalIDsResponse struct {
|
||||
ExternalIDs *ExternalIDs `json:"external_ids"`
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
StatusMessage string `json:"status_message"`
|
||||
StatusCode int `json:"status_code"`
|
||||
|
||||
@@ -1236,7 +1236,7 @@ func normalizeIntegration(integration Integration) (Integration, error) {
|
||||
if integration.QualityProfileID != nil && *integration.QualityProfileID <= 0 {
|
||||
return Integration{}, fmt.Errorf("%w: quality_profile_id must be positive", ErrInvalidInput)
|
||||
}
|
||||
filteredTags := integration.Tags[:0]
|
||||
filteredTags := make([]int, 0, len(integration.Tags))
|
||||
for _, tag := range integration.Tags {
|
||||
if tag > 0 {
|
||||
filteredTags = append(filteredTags, tag)
|
||||
|
||||
Reference in New Issue
Block a user