diff options
| author | Bobby <[email protected]> | 2025-05-09 02:25:54 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-05-09 02:25:54 +0530 |
| commit | cb41c834529f696c2f83adbb41e6b592c89495f3 (patch) | |
| tree | 5180a0c055c2b2f4b55a3374c83405d20e858d78 /utils | |
| parent | 9ebde192fbde6d6d6be2d7d86485eca9a0c5e026 (diff) | |
| download | metachan-cb41c834529f696c2f83adbb41e6b592c89495f3.tar.xz metachan-cb41c834529f696c2f83adbb41e6b592c89495f3.zip | |
refactored types
Diffstat (limited to 'utils')
| -rw-r--r-- | utils/api/anilist/anilist.go (renamed from utils/api/anilist.go) | 23 | ||||
| -rw-r--r-- | utils/api/anilist/types.go | 207 | ||||
| -rw-r--r-- | utils/api/aniskip/aniskip.go (renamed from utils/api/aniskip.go) | 57 | ||||
| -rw-r--r-- | utils/api/aniskip/types.go | 15 | ||||
| -rw-r--r-- | utils/api/jikan/jikan.go (renamed from utils/api/jikan.go) | 23 | ||||
| -rw-r--r-- | utils/api/jikan/types.go | 177 | ||||
| -rw-r--r-- | utils/api/malsync/malsync.go (renamed from utils/api/malsync.go) | 7 | ||||
| -rw-r--r-- | utils/api/malsync/types.go | 31 | ||||
| -rw-r--r-- | utils/api/streaming/streaming.go (renamed from utils/api/streaming.go) | 38 | ||||
| -rw-r--r-- | utils/api/streaming/types.go | 38 | ||||
| -rw-r--r-- | utils/api/tmdb/tmdb.go (renamed from utils/api/tmdb.go) | 82 | ||||
| -rw-r--r-- | utils/api/tmdb/types.go | 54 | ||||
| -rw-r--r-- | utils/api/tvdb/tvdb.go (renamed from utils/api/tvdb.go) | 13 | ||||
| -rw-r--r-- | utils/logger/logger.go | 64 | ||||
| -rw-r--r-- | utils/logger/types.go | 34 |
15 files changed, 698 insertions, 165 deletions
diff --git a/utils/api/anilist.go b/utils/api/anilist/anilist.go index cda860a..afb88e4 100644 --- a/utils/api/anilist.go +++ b/utils/api/anilist/anilist.go @@ -1,10 +1,9 @@ -package api +package anilist import ( "bytes" "encoding/json" "fmt" - "metachan/types" "metachan/utils/logger" "net/http" ) @@ -24,7 +23,7 @@ func NewAniListClient() *AniListClient { } // GetAnime fetches anime details from AniList by ID using a simpler approach -func (c *AniListClient) GetAnime(anilistID int) (*types.AnilistAnimeResponse, error) { +func (c *AniListClient) GetAnime(anilistID int) (*AnilistAnimeResponse, error) { // Create a much simpler request with minimal formatting that might trigger Cloudflare query := ` query ($id: Int) { @@ -92,8 +91,8 @@ func (c *AniListClient) GetAnime(anilistID int) (*types.AnilistAnimeResponse, er } // Log the request for debugging - logger.Log(fmt.Sprintf("Sending request to AniList for ID %d", anilistID), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("Sending request to AniList for ID %d", anilistID), logger.LogOptions{ + Level: logger.Debug, Prefix: "AniList", }) @@ -116,8 +115,8 @@ func (c *AniListClient) GetAnime(anilistID int) (*types.AnilistAnimeResponse, er resp, err = c.client.Do(req) if err != nil { lastErr = err - logger.Log(fmt.Sprintf("AniList request attempt %d failed: %v", i+1, err), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("AniList request attempt %d failed: %v", i+1, err), logger.LogOptions{ + Level: logger.Debug, Prefix: "AniList", }) continue @@ -129,14 +128,14 @@ func (c *AniListClient) GetAnime(anilistID int) (*types.AnilistAnimeResponse, er body := make([]byte, 1024) n, _ := resp.Body.Read(body) lastErr = fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body[:n])) - logger.Log(fmt.Sprintf("AniList returned non-200 status on attempt %d: %v", i+1, lastErr), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("AniList returned non-200 status on attempt %d: %v", i+1, lastErr), logger.LogOptions{ + Level: logger.Debug, Prefix: "AniList", }) continue } - var anilistResponse types.AnilistAnimeResponse + var anilistResponse AnilistAnimeResponse if err := json.NewDecoder(resp.Body).Decode(&anilistResponse); err != nil { lastErr = fmt.Errorf("failed to decode response: %w", err) continue @@ -151,8 +150,8 @@ func (c *AniListClient) GetAnime(anilistID int) (*types.AnilistAnimeResponse, er if anilistResponse.Data.Media.CoverImage.ExtraLarge != "" { logger.Log(fmt.Sprintf("Found cover data - Color: %s, Image: %s", anilistResponse.Data.Media.CoverImage.Color, - anilistResponse.Data.Media.CoverImage.ExtraLarge), types.LogOptions{ - Level: types.Debug, + anilistResponse.Data.Media.CoverImage.ExtraLarge), logger.LogOptions{ + Level: logger.Debug, Prefix: "AniList", }) } diff --git a/utils/api/anilist/types.go b/utils/api/anilist/types.go new file mode 100644 index 0000000..367719b --- /dev/null +++ b/utils/api/anilist/types.go @@ -0,0 +1,207 @@ +package anilist + +// AnilistAnimeResponse represents the response from AniList API +type AnilistAnimeResponse struct { + Data struct { + Media struct { + ID int `json:"id"` + MALID int `json:"idMal"` + Title struct { + Romaji string `json:"romaji"` + English string `json:"english"` + Native string `json:"native"` + UserPreferred string `json:"userPreferred"` + } `json:"title"` + Type string `json:"type"` + Format string `json:"format"` + Status string `json:"status"` + Description string `json:"description"` + StartDate struct { + Year int `json:"year"` + Month int `json:"month"` + Day int `json:"day"` + } `json:"startDate"` + EndDate struct { + Year int `json:"year"` + Month int `json:"month"` + Day int `json:"day"` + } `json:"endDate"` + Season string `json:"season"` + SeasonYear int `json:"seasonYear"` + Episodes int `json:"episodes"` + Duration int `json:"duration"` + Chapters int `json:"chapters"` + Volumes int `json:"volumes"` + CountryOfOrigin string `json:"countryOfOrigin"` + IsLicensed bool `json:"isLicensed"` + Source string `json:"source"` + Hashtag string `json:"hashtag"` + Trailer struct { + ID string `json:"id"` + Site string `json:"site"` + Thumbnail string `json:"thumbnail"` + } `json:"trailer"` + CoverImage struct { + ExtraLarge string `json:"extraLarge"` + Large string `json:"large"` + Medium string `json:"medium"` + Color string `json:"color"` + } `json:"coverImage"` + BannerImage string `json:"bannerImage"` + Genres []string `json:"genres"` + Synonyms []string `json:"synonyms"` + AverageScore int `json:"averageScore"` + MeanScore int `json:"meanScore"` + Popularity int `json:"popularity"` + IsLocked bool `json:"isLocked"` + Trending int `json:"trending"` + Favorites int `json:"favorites"` + Tags []struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + Rank int `json:"rank"` + IsGeneralSpoiler bool `json:"isGeneralSpoiler"` + IsMediaSpoiler bool `json:"isMediaSpoiler"` + IsAdult bool `json:"isAdult"` + } `json:"tags"` + Relations struct { + Edges []struct { + ID int `json:"id"` + RelationType string `json:"relationType"` + Node struct { + ID int `json:"id"` + Title struct { + Romaji string `json:"romaji"` + English string `json:"english"` + Native string `json:"native"` + UserPreferred string `json:"userPreferred"` + } `json:"title"` + Format string `json:"format"` + Type string `json:"type"` + Status string `json:"status"` + CoverImage struct { + ExtraLarge string `json:"extraLarge"` + Large string `json:"large"` + Medium string `json:"medium"` + Color string `json:"color"` + } `json:"coverImage"` + BannerImage string `json:"bannerImage"` + } `json:"node"` + } `json:"edges"` + } `json:"relations"` + Characters struct { + Edges []struct { + Role string `json:"role"` + Node struct { + ID int `json:"id"` + Name struct { + First string `json:"first"` + Last string `json:"last"` + Middle string `json:"middle"` + Full string `json:"full"` + Native string `json:"native"` + UserPreferred string `json:"userPreferred"` + } `json:"name"` + Image struct { + Large string `json:"large"` + Medium string `json:"medium"` + } `json:"image"` + Description string `json:"description"` + Age string `json:"age"` + } `json:"node"` + } `json:"edges"` + } `json:"characters"` + Staff struct { + Edges []struct { + Role string `json:"role"` + Node struct { + ID int `json:"id"` + Name struct { + First string `json:"first"` + Last string `json:"last"` + Middle string `json:"middle"` + Full string `json:"full"` + Native string `json:"native"` + UserPreferred string `json:"userPreferred"` + } `json:"name"` + Image struct { + Large string `json:"large"` + Medium string `json:"medium"` + } `json:"image"` + Description string `json:"description"` + PrimaryOccupations []string `json:"primaryOccupations"` + Gender string `json:"gender"` + Age int `json:"age"` + LanguageV2 string `json:"languageV2"` + } `json:"node"` + } `json:"edges"` + } `json:"staff"` + Studios struct { + Edges []struct { + IsMain bool `json:"isMain"` + Node struct { + ID int `json:"id"` + Name string `json:"name"` + } `json:"node"` + } `json:"edges"` + } `json:"studios"` + IsAdult bool `json:"isAdult"` + NextAiringEpisode struct { + ID int `json:"id"` + AiringAt int `json:"airingAt"` + TimeUntilAiring int `json:"timeUntilAiring"` + Episode int `json:"episode"` + } `json:"nextAiringEpisode"` + AiringSchedule struct { + Nodes []struct { + ID int `json:"id"` + Episode int `json:"episode"` + AiringAt int `json:"airingAt"` + TimeUntilAiring int `json:"timeUntilAiring"` + } `json:"nodes"` + } `json:"airingSchedule"` + Trends struct { + Nodes []struct { + Date int `json:"date"` + Trending int `json:"trending"` + Popularity int `json:"popularity"` + InProgress int `json:"inProgress"` + } `json:"nodes"` + } `json:"trends"` + ExternalLinks []struct { + ID int `json:"id"` + URL string `json:"url"` + Site string `json:"site"` + } `json:"externalLinks"` + StreamingEpisodes []struct { + Title string `json:"title"` + Thumbnail string `json:"thumbnail"` + URL string `json:"url"` + Site string `json:"site"` + } `json:"streamingEpisodes"` + Rankings []struct { + ID int `json:"id"` + Rank int `json:"rank"` + Type string `json:"type"` + Format string `json:"format"` + Year int `json:"year"` + Season string `json:"season"` + AllTime bool `json:"allTime"` + Context string `json:"context"` + } `json:"rankings"` + Stats struct { + ScoreDistribution []struct { + Score int `json:"score"` + Amount int `json:"amount"` + } `json:"scoreDistribution"` + StatusDistribution []struct { + Status string `json:"status"` + Amount int `json:"amount"` + } `json:"statusDistribution"` + } `json:"stats"` + SiteURL string `json:"siteUrl"` + } `json:"media"` + } `json:"data"` +} diff --git a/utils/api/aniskip.go b/utils/api/aniskip/aniskip.go index c5aa462..fb95f47 100644 --- a/utils/api/aniskip.go +++ b/utils/api/aniskip/aniskip.go @@ -1,11 +1,10 @@ -package api +package aniskip import ( "context" "encoding/json" "fmt" "io" - "metachan/types" "metachan/utils/logger" "metachan/utils/ratelimit" "net/http" @@ -22,7 +21,7 @@ type AniSkipClient struct { client *http.Client rateLimiter *ratelimit.RateLimiter maxRetries int - cache map[string][]types.AnimeSkipTimes + cache map[string][]AnimeSkipTimes cacheMutex sync.RWMutex cacheTTL time.Duration cacheTime map[string]time.Time @@ -31,7 +30,7 @@ type AniSkipClient struct { // EpisodeSkipTimesResult contains skip times for a specific episode type EpisodeSkipTimesResult struct { EpisodeNumber int - SkipTimes []types.AnimeSkipTimes + SkipTimes []AnimeSkipTimes } // NewAniSkipClient creates a new client for the AniSkip API @@ -42,7 +41,7 @@ func NewAniSkipClient() *AniSkipClient { }, rateLimiter: ratelimit.NewRateLimiter(10, 10*time.Second), // Conservative rate limit maxRetries: 2, - cache: make(map[string][]types.AnimeSkipTimes), + cache: make(map[string][]AnimeSkipTimes), cacheTime: make(map[string]time.Time), cacheTTL: 24 * time.Hour, // Cache skip times for 24 hours } @@ -54,7 +53,7 @@ func (c *AniSkipClient) getCacheKey(malID, episode int) string { } // getFromCache tries to get skip times from cache -func (c *AniSkipClient) getFromCache(malID, episode int) ([]types.AnimeSkipTimes, bool) { +func (c *AniSkipClient) getFromCache(malID, episode int) ([]AnimeSkipTimes, bool) { key := c.getCacheKey(malID, episode) c.cacheMutex.RLock() @@ -72,7 +71,7 @@ func (c *AniSkipClient) getFromCache(malID, episode int) ([]types.AnimeSkipTimes } // saveToCache saves skip times to cache -func (c *AniSkipClient) saveToCache(malID, episode int, skipTimes []types.AnimeSkipTimes) { +func (c *AniSkipClient) saveToCache(malID, episode int, skipTimes []AnimeSkipTimes) { key := c.getCacheKey(malID, episode) c.cacheMutex.Lock() @@ -83,7 +82,7 @@ func (c *AniSkipClient) saveToCache(malID, episode int, skipTimes []types.AnimeS } // GetSkipTimesForEpisode fetches skip times for a specific anime episode -func (c *AniSkipClient) GetSkipTimesForEpisode(malID, episodeNumber int) ([]types.AnimeSkipTimes, error) { +func (c *AniSkipClient) GetSkipTimesForEpisode(malID, episodeNumber int) ([]AnimeSkipTimes, error) { // Check cache first if skipTimes, found := c.getFromCache(malID, episodeNumber); found { return skipTimes, nil @@ -123,8 +122,8 @@ func (c *AniSkipClient) GetSkipTimesForEpisode(malID, episodeNumber int) ([]type if resp.StatusCode == http.StatusNotFound { // No skip times found, not an error - c.saveToCache(malID, episodeNumber, []types.AnimeSkipTimes{}) - return []types.AnimeSkipTimes{}, nil + c.saveToCache(malID, episodeNumber, []AnimeSkipTimes{}) + return []AnimeSkipTimes{}, nil } if resp.StatusCode != http.StatusOK { @@ -174,14 +173,14 @@ func (c *AniSkipClient) GetSkipTimesForEpisode(malID, episodeNumber int) ([]type // If no results found if !skipResp.Found || len(skipResp.Results) == 0 { - c.saveToCache(malID, episodeNumber, []types.AnimeSkipTimes{}) - return []types.AnimeSkipTimes{}, nil + c.saveToCache(malID, episodeNumber, []AnimeSkipTimes{}) + return []AnimeSkipTimes{}, nil } // Convert to our skip times format - var skipTimes []types.AnimeSkipTimes + var skipTimes []AnimeSkipTimes for _, result := range skipResp.Results { - skipTime := types.AnimeSkipTimes{ + skipTime := AnimeSkipTimes{ SkipType: result.SkipType, StartTime: result.Interval.StartTime, EndTime: result.Interval.EndTime, @@ -197,10 +196,10 @@ func (c *AniSkipClient) GetSkipTimesForEpisode(malID, episodeNumber int) ([]type } // GetSkipTimesForEpisodesBatch fetches skip times for episodes in batches -func (c *AniSkipClient) GetSkipTimesForEpisodesBatch(malID int, episodes []int) (map[int][]types.AnimeSkipTimes, error) { +func (c *AniSkipClient) GetSkipTimesForEpisodesBatch(malID int, episodes []int) (map[int][]AnimeSkipTimes, error) { // If we have fewer than 3 episodes, use individual requests instead if len(episodes) < 3 { - results := make(map[int][]types.AnimeSkipTimes) + results := make(map[int][]AnimeSkipTimes) for _, ep := range episodes { skipTimes, err := c.GetSkipTimesForEpisode(malID, ep) if err != nil { @@ -213,7 +212,7 @@ func (c *AniSkipClient) GetSkipTimesForEpisodesBatch(malID int, episodes []int) // Check if all episodes are cached and return them allCached := true - cachedResults := make(map[int][]types.AnimeSkipTimes) + cachedResults := make(map[int][]AnimeSkipTimes) for _, ep := range episodes { if skipTimes, found := c.getFromCache(malID, ep); found { @@ -318,7 +317,7 @@ func (c *AniSkipClient) GetSkipTimesForEpisodesBatch(malID int, episodes []int) return nil, fmt.Errorf("failed to decode batch response: %w", err) } - results := make(map[int][]types.AnimeSkipTimes) + results := make(map[int][]AnimeSkipTimes) // Process results for epStr, epData := range skipResp { @@ -327,11 +326,11 @@ func (c *AniSkipClient) GetSkipTimesForEpisodesBatch(malID int, episodes []int) continue // Skip if we can't parse the episode number } - var skipTimes []types.AnimeSkipTimes + var skipTimes []AnimeSkipTimes if epData.Found { for _, result := range epData.Results { - skipTimes = append(skipTimes, types.AnimeSkipTimes{ + skipTimes = append(skipTimes, AnimeSkipTimes{ SkipType: result.SkipType, StartTime: result.Interval.StartTime, EndTime: result.Interval.EndTime, @@ -349,16 +348,16 @@ func (c *AniSkipClient) GetSkipTimesForEpisodesBatch(malID int, episodes []int) } // GetSkipTimesForEpisodes fetches skip times for multiple episodes efficiently -func (c *AniSkipClient) GetSkipTimesForEpisodes(malID int, episodeCount int, maxConcurrent int) []types.EpisodeSkipResult { +func (c *AniSkipClient) GetSkipTimesForEpisodes(malID int, episodeCount int, maxConcurrent int) []EpisodeSkipResult { startTime := time.Now() // If episode count is small, just use single endpoint if episodeCount <= 5 { - results := []types.EpisodeSkipResult{} + results := []EpisodeSkipResult{} for i := 1; i <= episodeCount; i++ { skipTimes, err := c.GetSkipTimesForEpisode(malID, i) if err == nil && len(skipTimes) > 0 { - results = append(results, types.EpisodeSkipResult{ + results = append(results, EpisodeSkipResult{ EpisodeNumber: i, SkipTimes: skipTimes, }) @@ -375,7 +374,7 @@ func (c *AniSkipClient) GetSkipTimesForEpisodes(malID int, episodeCount int, max // Batch size - we'll process episodes in batches const batchSize = 25 - var results []types.EpisodeSkipResult + var results []EpisodeSkipResult // Process in batches for i := 0; i < episodeCount; i += batchSize { @@ -387,8 +386,8 @@ func (c *AniSkipClient) GetSkipTimesForEpisodes(malID int, episodeCount int, max batchEpisodes := allEpisodes[i:end] batchResults, err := c.GetSkipTimesForEpisodesBatch(malID, batchEpisodes) if err != nil { - logger.Log(fmt.Sprintf("Error fetching skip times batch %d-%d: %v", i+1, end, err), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("Error fetching skip times batch %d-%d: %v", i+1, end, err), logger.LogOptions{ + Level: logger.Warn, Prefix: "AniSkip", }) continue @@ -397,7 +396,7 @@ func (c *AniSkipClient) GetSkipTimesForEpisodes(malID int, episodeCount int, max // Add results to the final list for epNum, skipTimes := range batchResults { if len(skipTimes) > 0 { - results = append(results, types.EpisodeSkipResult{ + results = append(results, EpisodeSkipResult{ EpisodeNumber: epNum, SkipTimes: skipTimes, }) @@ -406,8 +405,8 @@ func (c *AniSkipClient) GetSkipTimesForEpisodes(malID int, episodeCount int, max } logger.Log(fmt.Sprintf("AniSkip: Fetched skip times for %d episodes of %d in %s", - len(results), episodeCount, time.Since(startTime)), types.LogOptions{ - Level: types.Debug, + len(results), episodeCount, time.Since(startTime)), logger.LogOptions{ + Level: logger.Debug, Prefix: "AniSkip", }) diff --git a/utils/api/aniskip/types.go b/utils/api/aniskip/types.go new file mode 100644 index 0000000..24c9618 --- /dev/null +++ b/utils/api/aniskip/types.go @@ -0,0 +1,15 @@ +package aniskip + +// AnimeSkipTimes represents skip time intervals for anime episodes +type AnimeSkipTimes struct { + SkipType string `json:"skip_type"` + StartTime float64 `json:"start_time"` + EndTime float64 `json:"end_time"` + EpisodeLength float64 `json:"episode_length"` +} + +// EpisodeSkipResult contains skip times for a specific episode +type EpisodeSkipResult struct { + EpisodeNumber int + SkipTimes []AnimeSkipTimes +} diff --git a/utils/api/jikan.go b/utils/api/jikan/jikan.go index 0f9ff83..9dcf00a 100644 --- a/utils/api/jikan.go +++ b/utils/api/jikan/jikan.go @@ -1,4 +1,4 @@ -package api +package jikan import ( "context" @@ -6,7 +6,6 @@ import ( "fmt" "io" "math" - "metachan/types" "metachan/utils/ratelimit" "net/http" "strconv" @@ -121,7 +120,7 @@ func (c *JikanClient) makeRequest(ctx context.Context, url string) ([]byte, erro } // GetAnime fetches basic anime information by MAL ID -func (c *JikanClient) GetAnime(malID int) (*types.JikanAnimeResponse, error) { +func (c *JikanClient) GetAnime(malID int) (*JikanAnimeResponse, error) { apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d", malID) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -132,7 +131,7 @@ func (c *JikanClient) GetAnime(malID int) (*types.JikanAnimeResponse, error) { return nil, fmt.Errorf("failed to get anime data: %w", err) } - var animeResponse types.JikanAnimeResponse + var animeResponse JikanAnimeResponse if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -145,7 +144,7 @@ func (c *JikanClient) GetAnime(malID int) (*types.JikanAnimeResponse, error) { } // GetFullAnime fetches detailed anime information by MAL ID -func (c *JikanClient) GetFullAnime(malID int) (*types.JikanAnimeResponse, error) { +func (c *JikanClient) GetFullAnime(malID int) (*JikanAnimeResponse, error) { apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/full", malID) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -156,7 +155,7 @@ func (c *JikanClient) GetFullAnime(malID int) (*types.JikanAnimeResponse, error) return nil, fmt.Errorf("failed to get anime full data: %w", err) } - var animeResponse types.JikanAnimeResponse + var animeResponse JikanAnimeResponse if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -169,9 +168,9 @@ func (c *JikanClient) GetFullAnime(malID int) (*types.JikanAnimeResponse, error) } // GetAnimeEpisodes fetches all episodes for an anime by MAL ID -func (c *JikanClient) GetAnimeEpisodes(malID int) (*types.JikanAnimeEpisodeResponse, error) { - result := types.JikanAnimeEpisodeResponse{ - Data: []types.JikanAnimeEpisode{}, +func (c *JikanClient) GetAnimeEpisodes(malID int) (*JikanAnimeEpisodeResponse, error) { + result := JikanAnimeEpisodeResponse{ + Data: []JikanAnimeEpisode{}, } maxPages := 25 // Safety limit to avoid excessive requests @@ -198,7 +197,7 @@ func (c *JikanClient) GetAnimeEpisodes(malID int) (*types.JikanAnimeEpisodeRespo return nil, fmt.Errorf("failed to get anime episodes page %d: %w", page, err) } - var pageResponse types.JikanAnimeEpisodeResponse + var pageResponse JikanAnimeEpisodeResponse if err := json.Unmarshal(bodyBytes, &pageResponse); err != nil { // Return what we have if we got some pages successfully if len(result.Data) > 0 { @@ -224,7 +223,7 @@ func (c *JikanClient) GetAnimeEpisodes(malID int) (*types.JikanAnimeEpisodeRespo } // GetAnimeCharacters fetches all characters for an anime by MAL ID -func (c *JikanClient) GetAnimeCharacters(malID int) (*types.JikanAnimeCharacterResponse, error) { +func (c *JikanClient) GetAnimeCharacters(malID int) (*JikanAnimeCharacterResponse, error) { apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/characters", malID) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -235,7 +234,7 @@ func (c *JikanClient) GetAnimeCharacters(malID int) (*types.JikanAnimeCharacterR return nil, fmt.Errorf("failed to get anime characters: %w", err) } - var characterResponse types.JikanAnimeCharacterResponse + var characterResponse JikanAnimeCharacterResponse if err := json.Unmarshal(bodyBytes, &characterResponse); err != nil { return nil, fmt.Errorf("failed to decode characters response: %w", err) } diff --git a/utils/api/jikan/types.go b/utils/api/jikan/types.go new file mode 100644 index 0000000..cbcb543 --- /dev/null +++ b/utils/api/jikan/types.go @@ -0,0 +1,177 @@ +package jikan + +// JikanPagination represents the pagination data in Jikan API responses +type JikanPagination struct { + LastVisiblePage int `json:"last_visible_page"` + HasNextPage bool `json:"has_next_page"` +} + +// JikanGenericMALStructure represents a common structure for various MAL entities +type JikanGenericMALStructure struct { + MALID int `json:"mal_id"` + Type string `json:"type"` + URL string `json:"url"` + Name string `json:"name"` +} + +// JikanAnimeResponse represents the main anime response from Jikan API +type JikanAnimeResponse struct { + Data struct { + MALID int `json:"mal_id"` + URL string `json:"url"` + Images struct { + JPG struct { + ImageURL string `json:"image_url"` + SmallImageURL string `json:"small_image_url"` + LargeImageURL string `json:"large_image_url"` + } `json:"jpg"` + WebP struct { + ImageURL string `json:"image_url"` + SmallImageURL string `json:"small_image_url"` + LargeImageURL string `json:"large_image_url"` + } `json:"webp"` + } `json:"images"` + Trailer struct { + YoutubeID string `json:"youtube_id"` + URL string `json:"url"` + EmbedURL string `json:"embed_url"` + Images struct { + ImageURL string `json:"image_url"` + SmallImageURL string `json:"small_image_url"` + MediumImageURL string `json:"medium_image_url"` + LargeImageURL string `json:"large_image_url"` + MaximumImageURL string `json:"maximum_image_url"` + } `json:"images"` + } `json:"trailer"` + Approved bool `json:"approved"` + Titles []struct { + Type string `json:"type"` + Title string `json:"title"` + } `json:"titles"` + Title string `json:"title"` + TitleEnglish string `json:"title_english"` + TitleJapanese string `json:"title_japanese"` + TitleSynonyms []string `json:"title_synonyms"` + Type string `json:"type"` + Source string `json:"source"` + Episodes int `json:"episodes"` + Status string `json:"status"` + Airing bool `json:"airing"` + Aired struct { + From string `json:"from"` + To string `json:"to"` + Prop struct { + From struct { + Day int `json:"day"` + Month int `json:"month"` + Year int `json:"year"` + } `json:"from"` + To struct { + Day int `json:"day"` + Month int `json:"month"` + Year int `json:"year"` + } `json:"to"` + } `json:"prop"` + String string `json:"string"` + } `json:"aired"` + Duration string `json:"duration"` + Rating string `json:"rating"` + Score float64 `json:"score"` + ScoredBy int `json:"scored_by"` + Rank int `json:"rank"` + Popularity int `json:"popularity"` + Members int `json:"members"` + Favorites int `json:"favorites"` + Synopsis string `json:"synopsis"` + Background string `json:"background"` + Season string `json:"season"` + Year int `json:"year"` + Broadcast struct { + Day string `json:"day"` + Time string `json:"time"` + Timezone string `json:"timezone"` + String string `json:"string"` + } `json:"broadcast"` + Producers []JikanGenericMALStructure `json:"producers"` + Licensors []JikanGenericMALStructure `json:"licensors"` + Studios []JikanGenericMALStructure `json:"studios"` + Genres []JikanGenericMALStructure `json:"genres"` + ExplicitGenres []JikanGenericMALStructure `json:"explicit_genres"` + Themes []JikanGenericMALStructure `json:"themes"` + Demographics []JikanGenericMALStructure `json:"demographics"` + Relations []struct { + Relation string `json:"relation"` + Entry []JikanGenericMALStructure `json:"entry"` + } `json:"relations"` + Theme struct { + Openings []string `json:"openings"` + Endings []string `json:"endings"` + } `json:"theme"` + External []struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"external"` + Streaming []struct { + Name string `json:"name"` + URL string `json:"url"` + } `json:"streaming"` + } `json:"data"` +} + +// JikanAnimeEpisode represents an episode from Jikan API +type JikanAnimeEpisode struct { + MALID int `json:"mal_id"` + URL string `json:"url"` + Title string `json:"title"` + TitleJapanese string `json:"title_japanese"` + TitleRomaji string `json:"title_romaji"` + Aired string `json:"aired"` + Score float64 `json:"score"` + Filler bool `json:"filler"` + Recap bool `json:"recap"` + ForumURL string `json:"forum_url"` +} + +// JikanAnimeEpisodeResponse represents the episodes response from Jikan API +type JikanAnimeEpisodeResponse struct { + Pagination JikanPagination `json:"pagination"` + Data []JikanAnimeEpisode `json:"data"` +} + +// JikanAnimeCharacterResponse represents the characters response from Jikan API +type JikanAnimeCharacterResponse struct { + Data []struct { + Character struct { + MALID int `json:"mal_id"` + URL string `json:"url"` + Images struct { + JPG struct { + ImageURL string `json:"image_url"` + SmallImageURL string `json:"small_image_url"` + } `json:"jpg"` + WebP struct { + ImageURL string `json:"image_url"` + SmallImageURL string `json:"small_image_url"` + } `json:"webp"` + } `json:"images"` + Name string `json:"name"` + } `json:"character"` + Role string `json:"role"` + VoiceActors []struct { + Person struct { + MALID int `json:"mal_id"` + URL string `json:"url"` + Images struct { + JPG struct { + ImageURL string `json:"image_url"` + } `json:"jpg"` + WebP struct { + ImageURL string `json:"image_url"` + } `json:"webp"` + } `json:"images"` + Name string `json:"name"` + } `json:"person"` + Language string `json:"language"` + } `json:"voice_actors"` + } `json:"data"` +} diff --git a/utils/api/malsync.go b/utils/api/malsync/malsync.go index d323841..379c6ad 100644 --- a/utils/api/malsync.go +++ b/utils/api/malsync/malsync.go @@ -1,11 +1,10 @@ -package api +package malsync import ( "context" "encoding/json" "fmt" "io" - "metachan/types" "net/http" "time" ) @@ -31,7 +30,7 @@ func NewMALSyncClient() *MALSyncClient { } // GetAnimeByMALID fetches anime metadata from MALSync by MAL ID -func (c *MALSyncClient) GetAnimeByMALID(malID int) (*types.MALSyncAnimeResponse, error) { +func (c *MALSyncClient) GetAnimeByMALID(malID int) (*MALSyncAnimeResponse, error) { apiURL := fmt.Sprintf("%s/anime/%d", malsyncAPIBaseURL, malID) // Create context with timeout @@ -85,7 +84,7 @@ func (c *MALSyncClient) GetAnimeByMALID(malID int) (*types.MALSyncAnimeResponse, return nil, fmt.Errorf("failed to read response: %w", err) } - var malSyncResponse types.MALSyncAnimeResponse + var malSyncResponse MALSyncAnimeResponse if err := json.Unmarshal(bodyBytes, &malSyncResponse); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } diff --git a/utils/api/malsync/types.go b/utils/api/malsync/types.go new file mode 100644 index 0000000..9944166 --- /dev/null +++ b/utils/api/malsync/types.go @@ -0,0 +1,31 @@ +package malsync + +// MALSyncStreamingSite represents a single streaming site entry in the MALSync API +type MALSyncStreamingSite struct { + ID int `json:"id,omitempty"` + Identifier any `json:"identifier"` + Image string `json:"image,omitempty"` + MalID int `json:"malId,omitempty"` + AniID int `json:"aniId,omitempty"` + Page string `json:"page"` + Title string `json:"title"` + Type string `json:"type"` + URL string `json:"url"` + External bool `json:"external,omitempty"` +} + +// MALSyncSitesCollection represents the nested structure of streaming sites +// Format: map[platformName]map[identifier]siteObject +type MALSyncSitesCollection map[string]map[string]MALSyncStreamingSite + +// MALSyncAnimeResponse is the top-level response from the MALSync API +type MALSyncAnimeResponse struct { + ID int `json:"id"` + Type string `json:"type"` + Title string `json:"title"` + URL string `json:"url"` + Total int `json:"total"` + Image string `json:"image"` + AnidbID int `json:"anidbId,omitempty"` + Sites MALSyncSitesCollection `json:"Sites"` +} diff --git a/utils/api/streaming.go b/utils/api/streaming/streaming.go index 067704e..4d0f625 100644 --- a/utils/api/streaming.go +++ b/utils/api/streaming/streaming.go @@ -1,10 +1,9 @@ -package api +package streaming import ( "encoding/json" "fmt" "maps" - "metachan/types" "metachan/utils/mappers" "net/http" "net/url" @@ -18,21 +17,6 @@ const ( allanimeBaseURL = "https://api.allanime.day/api" ) -// AllAnimeClient provides methods for interacting with the AllAnime API -type AllAnimeClient struct { - client *http.Client - headers http.Header -} - -// StreamingSearchResult represents a search result from AllAnime -type StreamingSearchResult struct { - ID string `json:"_id"` - Name string `json:"name"` - SubEpisodes int `json:"sub_episodes"` - DubEpisodes int `json:"dub_episodes"` - Similarity float64 `json:"similarity"` -} - // NewAllAnimeClient creates a new AllAnime client func NewAllAnimeClient() *AllAnimeClient { headers := http.Header{ @@ -163,7 +147,7 @@ func (c *AllAnimeClient) getClockLink(urlStr string) (string, error) { } // processSourceURL processes a streaming source URL from AllAnime -func (c *AllAnimeClient) processSourceURL(sourceURL, sourceType string) *types.AnimeStreamingSource { +func (c *AllAnimeClient) processSourceURL(sourceURL, sourceType string) *AnimeStreamingSource { var decodedURL string if strings.HasPrefix(sourceURL, "--") { decodedURL = c.decodeURL(sourceURL) @@ -176,7 +160,7 @@ func (c *AllAnimeClient) processSourceURL(sourceURL, sourceType string) *types.A // Check if it's a clock link if strings.Contains(processedURL, "/apivtwo/clock") { if directURL, err := c.getClockLink(processedURL); err == nil { - return &types.AnimeStreamingSource{ + return &AnimeStreamingSource{ URL: directURL, Server: getServerName(sourceType), Type: "direct", @@ -188,7 +172,7 @@ func (c *AllAnimeClient) processSourceURL(sourceURL, sourceType string) *types.A directPatterns := []string{"fast4speed.rsvp", "sharepoint.com", ".m3u8", ".mp4"} for _, pattern := range directPatterns { if strings.Contains(processedURL, pattern) { - return &types.AnimeStreamingSource{ + return &AnimeStreamingSource{ URL: processedURL, Server: getServerName(sourceType), Type: "direct", @@ -197,7 +181,7 @@ func (c *AllAnimeClient) processSourceURL(sourceURL, sourceType string) *types.A } // Return as regular source if not direct - return &types.AnimeStreamingSource{ + return &AnimeStreamingSource{ URL: processedURL, Server: getServerName(sourceType), Type: "embed", @@ -378,7 +362,7 @@ func (c *AllAnimeClient) GetEpisodesList(showID string, mode string) ([]string, } // GetEpisodeLinks gets streaming links for a specific episode -func (c *AllAnimeClient) GetEpisodeLinks(showID, episode, mode string) ([]types.AnimeStreamingSource, error) { +func (c *AllAnimeClient) GetEpisodeLinks(showID, episode, mode string) ([]AnimeStreamingSource, error) { episodeQuery := ` query ($showId: String!, $translationType: VaildTranslationTypeEnumType!, $episodeString: String!) { episode( @@ -424,7 +408,7 @@ func (c *AllAnimeClient) GetEpisodeLinks(showID, episode, mode string) ([]types. episodeData := data["data"].(map[string]any)["episode"].(map[string]any) sourceUrls := episodeData["sourceUrls"].([]any) - var links []types.AnimeStreamingSource + var links []AnimeStreamingSource for _, source := range sourceUrls { sourceMap := source.(map[string]any) if sourceURL, ok := sourceMap["sourceUrl"].(string); ok { @@ -442,7 +426,7 @@ func (c *AllAnimeClient) GetEpisodeLinks(showID, episode, mode string) ([]types. } // GetStreamingSources fetches both sub and dub streaming sources for an anime episode -func (c *AllAnimeClient) GetStreamingSources(title string, episodeNumber int) (*types.AnimeStreaming, error) { +func (c *AllAnimeClient) GetStreamingSources(title string, episodeNumber int) (*AnimeStreaming, error) { // Search for the anime searchResults, err := c.SearchAnime(title) if err != nil { @@ -456,9 +440,9 @@ func (c *AllAnimeClient) GetStreamingSources(title string, episodeNumber int) (* // Use the best match (first result) bestMatch := searchResults[0] - streaming := &types.AnimeStreaming{ - Sub: []types.AnimeStreamingSource{}, - Dub: []types.AnimeStreamingSource{}, + streaming := &AnimeStreaming{ + Sub: []AnimeStreamingSource{}, + Dub: []AnimeStreamingSource{}, } // Get sub episodes if available diff --git a/utils/api/streaming/types.go b/utils/api/streaming/types.go new file mode 100644 index 0000000..1b12da0 --- /dev/null +++ b/utils/api/streaming/types.go @@ -0,0 +1,38 @@ +package streaming + +import "net/http" + +// AllAnimeClient provides methods for interacting with the AllAnime API +type AllAnimeClient struct { + client *http.Client + headers http.Header +} + +// AnimeStreamingSource represents a single streaming source for an episode +type AnimeStreamingSource struct { + URL string `json:"url"` + Server string `json:"server"` + Type string `json:"type"` // direct or embed +} + +// AnimeStreaming represents all available streaming sources for an episode +type AnimeStreaming struct { + Sub []AnimeStreamingSource `json:"sub"` + Dub []AnimeStreamingSource `json:"dub"` +} + +// StreamingSearchResult represents a search result from streaming providers +type StreamingSearchResult struct { + ID string `json:"_id"` + Name string `json:"name"` + SubEpisodes int `json:"sub_episodes"` + DubEpisodes int `json:"dub_episodes"` + Similarity float64 `json:"similarity"` +} + +// EpisodeStreamingResult contains streaming sources for a specific episode +// Used for parallel streaming source fetching +type EpisodeStreamingResult struct { + EpisodeNumber int + Streaming *AnimeStreaming +} diff --git a/utils/api/tmdb.go b/utils/api/tmdb/tmdb.go index 1344080..1c5a87b 100644 --- a/utils/api/tmdb.go +++ b/utils/api/tmdb/tmdb.go @@ -1,4 +1,4 @@ -package api +package tmdb import ( "encoding/json" @@ -31,8 +31,8 @@ func makeRequestWithRetries(req *http.Request, maxRetries int) (*http.Response, sleepTime := backoffTime + jitter logger.Log(fmt.Sprintf("TMDB request retry %d/%d after %v due to: %v", - attempt, maxRetries, sleepTime, lastErr), types.LogOptions{ - Level: types.Debug, + attempt, maxRetries, sleepTime, lastErr), logger.LogOptions{ + Level: logger.Debug, Prefix: "TMDB", }) @@ -120,7 +120,7 @@ func normalizeTitle(title string) string { } // searchTVShowsByTitle searches for TV shows on TMDB by title -func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, countryPriority string) ([]types.TMDBShowResult, error) { +func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, countryPriority string) ([]TMDBShowResult, error) { if config.Config.TMDB.ReadAccessToken == "" { return nil, fmt.Errorf("TMDB is not initialized") } @@ -131,8 +131,8 @@ func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, c query = normalizeTitle(alternativeTitle) } - logger.Log(fmt.Sprintf("Searching TMDB for TV show: %s", query), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("Searching TMDB for TV show: %s", query), logger.LogOptions{ + Level: logger.Debug, Prefix: "TMDB", }) @@ -163,7 +163,7 @@ func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, c } // Parse response - var searchResponse types.TMDBSearchResponse + var searchResponse TMDBSearchResponse if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -171,7 +171,7 @@ func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, c results := searchResponse.Results // Filter results if needed - var filteredResults []types.TMDBShowResult + var filteredResults []TMDBShowResult for _, show := range results { if (isAdult && show.Adult) || (!isAdult && !show.Adult) { filteredResults = append(filteredResults, show) @@ -180,8 +180,8 @@ func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, c // Sort by country priority if specified if countryPriority != "" && len(filteredResults) > 0 { - var prioritizedResults []types.TMDBShowResult - var otherResults []types.TMDBShowResult + var prioritizedResults []TMDBShowResult + var otherResults []TMDBShowResult for _, show := range filteredResults { hasPriority := false @@ -204,13 +204,13 @@ func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, c } if len(filteredResults) == 0 { - logger.Log(fmt.Sprintf("No TMDB shows found for: %s", query), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("No TMDB shows found for: %s", query), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) } else { - logger.Log(fmt.Sprintf("Found %d TMDB shows for: %s", len(filteredResults), query), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("Found %d TMDB shows for: %s", len(filteredResults), query), logger.LogOptions{ + Level: logger.Debug, Prefix: "TMDB", }) } @@ -219,7 +219,7 @@ func searchTVShowsByTitle(title string, alternativeTitle string, isAdult bool, c } // getTVShowDetails gets details for a TV show from TMDB -func getTVShowDetails(showID int) (*types.TMDBShowDetails, error) { +func getTVShowDetails(showID int) (*TMDBShowDetails, error) { if config.Config.TMDB.ReadAccessToken == "" { return nil, fmt.Errorf("TMDB is not initialized") } @@ -246,7 +246,7 @@ func getTVShowDetails(showID int) (*types.TMDBShowDetails, error) { } // Parse response - var showDetails types.TMDBShowDetails + var showDetails TMDBShowDetails if err := json.NewDecoder(resp.Body).Decode(&showDetails); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -255,7 +255,7 @@ func getTVShowDetails(showID int) (*types.TMDBShowDetails, error) { } // getSeasonDetails gets details for a TV season from TMDB -func getSeasonDetails(showID, seasonNumber int) (*types.TMDBSeasonDetails, error) { +func getSeasonDetails(showID, seasonNumber int) (*TMDBSeasonDetails, error) { if config.Config.TMDB.ReadAccessToken == "" { return nil, fmt.Errorf("TMDB is not initialized") } @@ -282,7 +282,7 @@ func getSeasonDetails(showID, seasonNumber int) (*types.TMDBSeasonDetails, error } // Parse response - var seasonDetails types.TMDBSeasonDetails + var seasonDetails TMDBSeasonDetails if err := json.NewDecoder(resp.Body).Decode(&seasonDetails); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -291,12 +291,12 @@ func getSeasonDetails(showID, seasonNumber int) (*types.TMDBSeasonDetails, error } // findBestSeason finds the best matching season for an anime -func findBestSeason(shows []types.TMDBShowResult, title string, episodeCount int, airDate string) (int, int, error) { +func findBestSeason(shows []TMDBShowResult, title string, episodeCount int, airDate string) (int, int, error) { for _, show := range shows { showDetails, err := getTVShowDetails(show.ID) if err != nil { - logger.Log(fmt.Sprintf("Failed to get details for show %d: %v", show.ID, err), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("Failed to get details for show %d: %v", show.ID, err), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) continue @@ -325,8 +325,8 @@ func findBestSeason(shows []types.TMDBShowResult, title string, episodeCount int // If either count or air date matches, consider it a potential match if episodeCountMatches || airDateMatches { logger.Log(fmt.Sprintf("Found matching season for \"%s\": Show ID %d, Season %d", - title, show.ID, season.SeasonNumber), types.LogOptions{ - Level: types.Info, + title, show.ID, season.SeasonNumber), logger.LogOptions{ + Level: logger.Info, Prefix: "TMDB", }) return show.ID, season.SeasonNumber, nil @@ -340,8 +340,8 @@ func findBestSeason(shows []types.TMDBShowResult, title string, episodeCount int // AttachEpisodeDescriptions enriches anime episodes with descriptions and thumbnails from TMDB func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode, alternativeTitle string, tmdbID int) []types.AnimeSingleEpisode { if config.Config.TMDB.ReadAccessToken == "" { - logger.Log("TMDB is not configured, skipping episode description enrichment", types.LogOptions{ - Level: types.Warn, + logger.Log("TMDB is not configured, skipping episode description enrichment", logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) return episodes @@ -351,8 +351,8 @@ func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode return episodes } - logger.Log(fmt.Sprintf("Enriching episodes for: %s", title), types.LogOptions{ - Level: types.Info, + logger.Log(fmt.Sprintf("Enriching episodes for: %s", title), logger.LogOptions{ + Level: logger.Info, Prefix: "TMDB", }) @@ -367,8 +367,8 @@ func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode // Try to get show details and find the best season showDetails, err := getTVShowDetails(showID) if err != nil { - logger.Log(fmt.Sprintf("Failed to get TMDB show details for ID %d: %v", tmdbID, err), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("Failed to get TMDB show details for ID %d: %v", tmdbID, err), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) return episodes @@ -405,24 +405,24 @@ func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode } } - logger.Log(fmt.Sprintf("Using TMDB ID %d with season %d", showID, seasonNumber), types.LogOptions{ - Level: types.Info, + logger.Log(fmt.Sprintf("Using TMDB ID %d with season %d", showID, seasonNumber), logger.LogOptions{ + Level: logger.Info, Prefix: "TMDB", }) } else { // Search for the TV show on TMDB if we don't have a direct ID shows, err := searchTVShowsByTitle(title, alternativeTitle, false, "JP") if err != nil { - logger.Log(fmt.Sprintf("Failed to search TV shows: %v", err), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("Failed to search TV shows: %v", err), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) return episodes } if len(shows) == 0 { - logger.Log(fmt.Sprintf("No TV shows found for: %s", title), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("No TV shows found for: %s", title), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) return episodes @@ -436,8 +436,8 @@ func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode showID, seasonNumber, err = findBestSeason(shows, title, len(episodes), airDate) if err != nil { - logger.Log(fmt.Sprintf("Failed to find best season: %v", err), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("Failed to find best season: %v", err), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) return episodes @@ -447,8 +447,8 @@ func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode // Get season details with episode information seasonDetails, err := getSeasonDetails(showID, seasonNumber) if err != nil { - logger.Log(fmt.Sprintf("Failed to get season details: %v", err), types.LogOptions{ - Level: types.Warn, + logger.Log(fmt.Sprintf("Failed to get season details: %v", err), logger.LogOptions{ + Level: logger.Warn, Prefix: "TMDB", }) return episodes @@ -489,8 +489,8 @@ func AttachEpisodeDescriptions(title string, episodes []types.AnimeSingleEpisode } logger.Log(fmt.Sprintf("Successfully enriched %d episodes with descriptions and %d with thumbnails for: %s", - len(enrichedEpisodes), thumbnailCount, title), types.LogOptions{ - Level: types.Success, + len(enrichedEpisodes), thumbnailCount, title), logger.LogOptions{ + Level: logger.Success, Prefix: "TMDB", }) diff --git a/utils/api/tmdb/types.go b/utils/api/tmdb/types.go new file mode 100644 index 0000000..8743573 --- /dev/null +++ b/utils/api/tmdb/types.go @@ -0,0 +1,54 @@ +package tmdb + +// TMDBShowResult represents a TV show result from TMDB search +type TMDBShowResult struct { + ID int `json:"id"` + Name string `json:"name"` + FirstAirDate string `json:"first_air_date"` + OriginCountry []string `json:"origin_country"` + Adult bool `json:"adult"` +} + +// TMDBSearchResponse represents the response from TMDB search API +type TMDBSearchResponse struct { + Page int `json:"page"` + Results []TMDBShowResult `json:"results"` + TotalPages int `json:"total_pages"` + TotalResults int `json:"total_results"` +} + +// TMDBEpisode represents a TV episode from TMDB +type TMDBEpisode struct { + ID int `json:"id"` + Name string `json:"name"` + Overview string `json:"overview"` + StillPath string `json:"still_path"` + AirDate string `json:"air_date"` + EpisodeNumber int `json:"episode_number"` + SeasonNumber int `json:"season_number"` +} + +// TMDBSeasonDetails represents a TV season from TMDB +type TMDBSeasonDetails struct { + ID int `json:"id"` + AirDate string `json:"air_date"` + EpisodeCount int `json:"episode_count"` + Name string `json:"name"` + Overview string `json:"overview"` + SeasonNumber int `json:"season_number"` + Episodes []TMDBEpisode `json:"episodes"` +} + +// TMDBShowDetails represents a TV show from TMDB +type TMDBShowDetails struct { + ID int `json:"id"` + Name string `json:"name"` + Overview string `json:"overview"` + Seasons []struct { + ID int `json:"id"` + Name string `json:"name"` + SeasonNumber int `json:"season_number"` + EpisodeCount int `json:"episode_count"` + AirDate string `json:"air_date"` + } `json:"seasons"` +} diff --git a/utils/api/tvdb.go b/utils/api/tvdb/tvdb.go index cee3de2..230be54 100644 --- a/utils/api/tvdb.go +++ b/utils/api/tvdb/tvdb.go @@ -4,14 +4,13 @@ import ( "fmt" "metachan/database" "metachan/entities" - "metachan/types" "metachan/utils/logger" ) // FindSeasonMappings finds all anime mappings that belong to the same series based on TVDB ID func FindSeasonMappings(tvdbID int) ([]entities.AnimeMapping, error) { - logger.Log(fmt.Sprintf("Finding season mappings for TVDB ID %d", tvdbID), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("Finding season mappings for TVDB ID %d", tvdbID), logger.LogOptions{ + Level: logger.Debug, Prefix: "TVDB", }) @@ -22,13 +21,13 @@ func FindSeasonMappings(tvdbID int) ([]entities.AnimeMapping, error) { } if len(mappings) == 0 { - logger.Log(fmt.Sprintf("No season mappings found for TVDB ID %d", tvdbID), types.LogOptions{ - Level: types.Debug, + logger.Log(fmt.Sprintf("No season mappings found for TVDB ID %d", tvdbID), logger.LogOptions{ + Level: logger.Debug, Prefix: "TVDB", }) } else { - logger.Log(fmt.Sprintf("Found %d season mappings for TVDB ID %d", len(mappings), tvdbID), types.LogOptions{ - Level: types.Info, + logger.Log(fmt.Sprintf("Found %d season mappings for TVDB ID %d", len(mappings), tvdbID), logger.LogOptions{ + Level: logger.Info, Prefix: "TVDB", }) } diff --git a/utils/logger/logger.go b/utils/logger/logger.go index de2d69c..c5788bc 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -5,8 +5,6 @@ import ( "os" "strings" "time" - - "metachan/types" ) const prefixWidth = 15 @@ -15,47 +13,47 @@ func getTimestamp() string { return time.Now().Format(time.RFC3339) } -func getLevelColor(level types.LogLevel) string { +func getLevelColor(level LogLevel) string { switch level { - case types.Info: - return types.LevelColorInfo - case types.Warn: - return types.LevelColorWarn - case types.Error: - return types.LevelColorError - case types.Debug: - return types.LevelColorDebug - case types.Success: - return types.LevelColorSuccess + case Info: + return LevelColorInfo + case Warn: + return LevelColorWarn + case Error: + return LevelColorError + case Debug: + return LevelColorDebug + case Success: + return LevelColorSuccess default: - return types.LevelColorInfo + return LevelColorInfo } } -func getMessageColor(level types.LogLevel) string { +func getMessageColor(level LogLevel) string { switch level { - case types.Info: - return types.MessageColorInfo - case types.Warn: - return types.MessageColorWarn - case types.Error: - return types.MessageColorError - case types.Debug: - return types.MessageColorDebug - case types.Success: - return types.MessageColorSuccess + case Info: + return MessageColorInfo + case Warn: + return MessageColorWarn + case Error: + return MessageColorError + case Debug: + return MessageColorDebug + case Success: + return MessageColorSuccess default: - return types.MessageColorInfo + return MessageColorInfo } } -func Log(message interface{}, options types.LogOptions) { +func Log(message interface{}, options LogOptions) { var builder strings.Builder if options.Timestamp { - builder.WriteString(types.Gray) + builder.WriteString(Gray) builder.WriteString(getTimestamp()) - builder.WriteString(types.Reset) + builder.WriteString(Reset) builder.WriteString(" ") } @@ -70,11 +68,11 @@ func Log(message interface{}, options types.LogOptions) { padding = strings.Repeat(" ", prefixWidth-totalWidth) } - builder.WriteString(types.Cyan) + builder.WriteString(Cyan) builder.WriteString("[") builder.WriteString(options.Prefix) builder.WriteString("]") - builder.WriteString(types.Reset) + builder.WriteString(Reset) builder.WriteString(padding) builder.WriteString(" ") } @@ -90,10 +88,10 @@ func Log(message interface{}, options types.LogOptions) { builder.WriteString(fmt.Sprintf("%v", msg)) } - builder.WriteString(types.Reset) + builder.WriteString(Reset) builder.WriteString("\n") - if options.Level == types.Error || options.Level == types.Warn { + if options.Level == Error || options.Level == Warn { os.Stderr.WriteString(builder.String()) } else { os.Stdout.WriteString(builder.String()) diff --git a/utils/logger/types.go b/utils/logger/types.go new file mode 100644 index 0000000..5ef310f --- /dev/null +++ b/utils/logger/types.go @@ -0,0 +1,34 @@ +package logger + +type LogLevel string + +const ( + Debug LogLevel = "debug" + Info LogLevel = "info" + Warn LogLevel = "warn" + Error LogLevel = "error" + Success LogLevel = "success" + + Reset = "\033[0m" + Cyan = "\033[36m" + Gray = "\033[90m" + + LevelColorInfo = "\033[34mINFO \033[0m" + LevelColorWarn = "\033[33mWARN \033[0m" + LevelColorError = "\033[31mERROR \033[0m" + LevelColorDebug = "\033[35mDEBUG \033[0m" + LevelColorSuccess = "\033[32mSUCCESS\033[0m" + + MessageColorInfo = "\033[97m" + MessageColorWarn = "\033[33m" + MessageColorError = "\033[31m" + MessageColorDebug = "\033[90m" + MessageColorSuccess = "\033[32m" +) + +type LogOptions struct { + Timestamp bool + Prefix string + Level LogLevel + Fatal bool +} |
