mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 05:46:38 +08:00
refactor: split modules and harden scraping workflows
This commit is contained in:
@@ -18,7 +18,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -142,184 +141,6 @@ type Match struct {
|
||||
NSFW bool `json:"nsfw,omitempty"`
|
||||
}
|
||||
|
||||
type tmdbMovieSearchResult struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
OriginalTitle string `json:"original_title"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
Overview string `json:"overview"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
VoteAverage float32 `json:"vote_average"`
|
||||
GenreIDs []int `json:"genre_ids"`
|
||||
}
|
||||
|
||||
type tmdbTVSearchResult struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
OriginCountry []string `json:"origin_country"`
|
||||
Overview string `json:"overview"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
FirstAirDate string `json:"first_air_date"`
|
||||
VoteAverage float32 `json:"vote_average"`
|
||||
GenreIDs []int `json:"genre_ids"`
|
||||
}
|
||||
|
||||
// SearchMovie issues `/search/movie` and returns the best match, or nil
|
||||
// when no result is found. The `year` argument is optional (0 = any).
|
||||
func (t *TMDbProvider) SearchMovie(ctx context.Context, query string, year int) (*Match, error) {
|
||||
matches, err := t.SearchMovieCandidates(ctx, query, year)
|
||||
if err != nil || len(matches) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
// SearchMovieCandidates returns the first TMDb result page as manual-scrape
|
||||
// candidates. Automatic scrape still uses SearchMovie's first-result behavior,
|
||||
// while manual correction can show alternatives when the top result is wrong.
|
||||
func (t *TMDbProvider) SearchMovieCandidates(ctx context.Context, query string, year int) ([]*Match, error) {
|
||||
if query == "" {
|
||||
return nil, errors.New("empty query")
|
||||
}
|
||||
|
||||
// Resolve API key from config or database
|
||||
apiKey := t.resolveAPIKey(ctx)
|
||||
if apiKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
base := t.resolveBaseURL(ctx)
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("api_key", apiKey)
|
||||
q.Set("query", query)
|
||||
q.Set("language", "zh-CN")
|
||||
q.Set("include_adult", "false")
|
||||
if year > 0 {
|
||||
q.Set("year", fmt.Sprintf("%d", year))
|
||||
}
|
||||
u := base + "/search/movie?" + q.Encode()
|
||||
|
||||
type page struct {
|
||||
Results []tmdbMovieSearchResult `json:"results"`
|
||||
}
|
||||
|
||||
var p page
|
||||
if err := t.getJSON(ctx, u, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Results) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]*Match, 0, len(p.Results))
|
||||
for _, r := range p.Results {
|
||||
out = append(out, t.movieSearchResultToMatch(r))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t *TMDbProvider) movieSearchResultToMatch(r tmdbMovieSearchResult) *Match {
|
||||
m := &Match{
|
||||
TMDbID: r.ID,
|
||||
Title: r.Title,
|
||||
OriginalName: r.OriginalTitle,
|
||||
Overview: r.Overview,
|
||||
Rating: r.VoteAverage,
|
||||
Languages: nonEmptyStrings(r.OriginalLanguage),
|
||||
Genres: genreIDStrings(r.GenreIDs),
|
||||
}
|
||||
if r.PosterPath != "" {
|
||||
m.PosterURL = t.imgCDN + "/w500" + r.PosterPath
|
||||
}
|
||||
if r.BackdropPath != "" {
|
||||
m.BackdropURL = t.imgCDN + "/w1280" + r.BackdropPath
|
||||
}
|
||||
if len(r.ReleaseDate) >= 4 {
|
||||
_, _ = fmt.Sscanf(r.ReleaseDate[:4], "%d", &m.Year)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// SearchTV issues `/search/tv` and returns the best match. Used by anime /
|
||||
// tv libraries before falling back to SearchMovie.
|
||||
func (t *TMDbProvider) SearchTV(ctx context.Context, query string, year int) (*Match, error) {
|
||||
matches, err := t.SearchTVCandidates(ctx, query, year)
|
||||
if err != nil || len(matches) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return matches[0], nil
|
||||
}
|
||||
|
||||
// SearchTVCandidates returns the first TMDb TV result page for manual scrape.
|
||||
func (t *TMDbProvider) SearchTVCandidates(ctx context.Context, query string, year int) ([]*Match, error) {
|
||||
if query == "" {
|
||||
return nil, errors.New("empty query")
|
||||
}
|
||||
|
||||
apiKey := t.resolveAPIKey(ctx)
|
||||
if apiKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
base := t.resolveBaseURL(ctx)
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("api_key", apiKey)
|
||||
q.Set("query", query)
|
||||
q.Set("language", "zh-CN")
|
||||
q.Set("include_adult", "false")
|
||||
if year > 0 {
|
||||
q.Set("first_air_date_year", fmt.Sprintf("%d", year))
|
||||
}
|
||||
u := base + "/search/tv?" + q.Encode()
|
||||
|
||||
type page struct {
|
||||
Results []tmdbTVSearchResult `json:"results"`
|
||||
}
|
||||
|
||||
var p page
|
||||
if err := t.getJSON(ctx, u, &p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(p.Results) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]*Match, 0, len(p.Results))
|
||||
for _, r := range p.Results {
|
||||
out = append(out, t.tvSearchResultToMatch(r))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t *TMDbProvider) tvSearchResultToMatch(r tmdbTVSearchResult) *Match {
|
||||
m := &Match{
|
||||
TMDbID: r.ID,
|
||||
Title: r.Name,
|
||||
OriginalName: r.OriginalName,
|
||||
Overview: r.Overview,
|
||||
Rating: r.VoteAverage,
|
||||
Languages: nonEmptyStrings(r.OriginalLanguage),
|
||||
Countries: deduplicate(r.OriginCountry),
|
||||
Genres: genreIDStrings(r.GenreIDs),
|
||||
}
|
||||
if m.Title == "" {
|
||||
m.Title = r.OriginalName
|
||||
}
|
||||
if r.PosterPath != "" {
|
||||
m.PosterURL = t.imgCDN + "/w500" + r.PosterPath
|
||||
}
|
||||
if r.BackdropPath != "" {
|
||||
m.BackdropURL = t.imgCDN + "/w1280" + r.BackdropPath
|
||||
}
|
||||
if len(r.FirstAirDate) >= 4 {
|
||||
_, _ = fmt.Sscanf(r.FirstAirDate[:4], "%d", &m.Year)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (t *TMDbProvider) getJSON(ctx context.Context, url string, out any) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user