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:
Silo Server Migration
2026-05-25 00:27:39 -04:00
co-authored by Claude Opus 4.7
parent e8143d5757
commit 8a86e0cf08
5 changed files with 21 additions and 23 deletions
+1 -1
View File
@@ -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
}
+11 -9
View File
@@ -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
}
+3 -8
View File
@@ -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()
+5 -4
View File
@@ -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"`
+1 -1
View File
@@ -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)