diff --git a/cmd/videnc/main.go b/cmd/videnc/main.go index e972ee6..150cbf2 100644 --- a/cmd/videnc/main.go +++ b/cmd/videnc/main.go @@ -146,12 +146,13 @@ func processFile(inputPath string, cfg *types.Config, enc *encoder.Encoder, meta outputPath := filepath.Join(outputDir, "output.mkv") var outFilename string - if meta.IMDBID == "" || meta.Title == "Unknown" { - outFilename = fmt.Sprintf("%s.nometadata.mkv", generateRandomString(8)) - } else if isSeries { + switch { + case isSeries && meta.Collection != "": outFilename = fmt.Sprintf("%s.S%sE%s.mkv", sanitizeFilename(meta.Collection), season, episode) - } else { + case !isSeries && meta.IMDBID != "" && meta.Title != "Unknown": outFilename = fmt.Sprintf("%s.%s.mkv", sanitizeFilename(meta.Title), meta.IMDBID) + default: + outFilename = fmt.Sprintf("%s.nometadata.mkv", generateRandomString(8)) } finalOutput := filepath.Join(cfg.Paths.Output, outFilename) diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index 421a88c..b8d39e5 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -3,8 +3,10 @@ package metadata import ( "encoding/json" "fmt" + "io" "net/http" "regexp" + "strconv" "strings" "time" @@ -13,7 +15,7 @@ import ( const ( omdbAPIURL = "https://www.omdbapi.com/" - tvmazeAPIURL = "https://api.tvmaze.com/" + tvmazeAPIURL = "https://api.tvmaze.com" ) var ( @@ -38,11 +40,10 @@ type TVMazeShowResponse struct { } type TVMazeEpisode struct { - Name string `json:"name"` - Season int `json:"season"` - Number int `json:"number"` - Airdate string `json:"airdate"` - Show TVMazeShowResponse `json:"show"` + Name string `json:"name"` + Season int `json:"season"` + Number int `json:"number"` + Airdate string `json:"airdate"` } type Client struct { @@ -94,13 +95,29 @@ func (c *Client) FetchMovieMetadata(imdbID string) (*types.Metadata, error) { } defer resp.Body.Close() + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + return nil, fmt.Errorf("reading OMDb body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("OMDb HTTP %d: %s", resp.StatusCode, snippet(body)) + } + var result OMDbResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("decoding OMDb response: %w", err) + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("decoding OMDb response: %w (body: %s)", err, snippet(body)) } if result.Response != "True" { - return nil, fmt.Errorf("OMDb error: %s", result.Error) + if result.Error != "" { + return nil, fmt.Errorf("OMDb error: %s", result.Error) + } + return nil, fmt.Errorf("OMDb non-True response: %s", snippet(body)) + } + + if result.Title == "" || result.IMDBID == "" { + return nil, fmt.Errorf("OMDb response missing Title/imdbID (schema drift?): %s", snippet(body)) } dateReleased := formatDate(result.Year) @@ -114,39 +131,75 @@ func (c *Client) FetchMovieMetadata(imdbID string) (*types.Metadata, error) { } func (c *Client) FetchSeriesMetadata(tvmazeID, season, episode string) (*types.Metadata, error) { - url := fmt.Sprintf("%s/shows/%s/episodebynumber?season=%s&number=%s", tvmazeAPIURL, tvmazeID, season, episode) - - resp, err := c.httpClient.Get(url) + seasonNum, err := strconv.Atoi(season) if err != nil { - return nil, fmt.Errorf("fetching TVmaze: %w", err) + return nil, fmt.Errorf("parsing season %q: %w", season, err) + } + episodeNum, err := strconv.Atoi(episode) + if err != nil { + return nil, fmt.Errorf("parsing episode %q: %w", episode, err) } - defer resp.Body.Close() + epURL := fmt.Sprintf("%s/shows/%s/episodebynumber?season=%d&number=%d", tvmazeAPIURL, tvmazeID, seasonNum, episodeNum) var ep TVMazeEpisode - if err := json.NewDecoder(resp.Body).Decode(&ep); err != nil { - return nil, fmt.Errorf("decoding TVmaze response: %w", err) + if err := c.fetchTVMazeJSON(epURL, &ep); err != nil { + return nil, fmt.Errorf("TVmaze episode %s S%dE%d: %w", tvmazeID, seasonNum, episodeNum, err) } - dateReleased := formatDate(ep.Airdate) + showURL := fmt.Sprintf("%s/shows/%s", tvmazeAPIURL, tvmazeID) + var show TVMazeShowResponse + if err := c.fetchTVMazeJSON(showURL, &show); err != nil { + return nil, fmt.Errorf("TVmaze show %s: %w", tvmazeID, err) + } - var imdbID string - if ep.Show.Extern.IMDB != "" { - imdbID = ep.Show.Extern.IMDB + if ep.Name == "" || show.Name == "" { + return nil, fmt.Errorf("TVmaze response missing episode/show name (schema drift?) for %s S%dE%d", tvmazeID, seasonNum, episodeNum) } return &types.Metadata{ Title: ep.Name, - Collection: ep.Show.Name, + Collection: show.Name, Season: season, Episode: episode, - DateReleased: dateReleased, - IMDBID: imdbID, + DateReleased: formatDate(ep.Airdate), + IMDBID: show.Extern.IMDB, TVMazeID: tvmazeID, IsSeries: true, OriginalMedia: types.MediaTypeBluRay, }, nil } +func (c *Client) fetchTVMazeJSON(url string, v interface{}) error { + resp, err := c.httpClient.Get(url) + if err != nil { + return fmt.Errorf("GET: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024)) + if err != nil { + return fmt.Errorf("read body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP %d: %s", resp.StatusCode, snippet(body)) + } + + if err := json.Unmarshal(body, v); err != nil { + return fmt.Errorf("decode: %w (body: %s)", err, snippet(body)) + } + return nil +} + +func snippet(body []byte) string { + s := strings.TrimSpace(string(body)) + const max = 200 + if len(s) > max { + s = s[:max] + "…" + } + return s +} + func formatDate(date string) string { if date == "" || date == "N/A" { return ""