aboutsummaryrefslogtreecommitdiff
path: root/utils/api/tvdb
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-01-20 18:00:43 +0530
committerBobby <[email protected]>2026-01-20 18:00:43 +0530
commitdf6cf3edcbb560e7615ad13d8daf4843507eb11e (patch)
tree28856a2023a07b04b8ecdcadc581bd7ec03cfabc /utils/api/tvdb
parent9df5dd0018d942ae1208308a60589e1124e6bc66 (diff)
downloadmetachan-df6cf3edcbb560e7615ad13d8daf4843507eb11e.tar.xz
metachan-df6cf3edcbb560e7615ad13d8daf4843507eb11e.zip
Add TVDB integration for episode retrieval and configuration setup
Diffstat (limited to 'utils/api/tvdb')
-rw-r--r--utils/api/tvdb/tvdb.go185
-rw-r--r--utils/api/tvdb/types.go43
2 files changed, 227 insertions, 1 deletions
diff --git a/utils/api/tvdb/tvdb.go b/utils/api/tvdb/tvdb.go
index 230be54..dd83643 100644
--- a/utils/api/tvdb/tvdb.go
+++ b/utils/api/tvdb/tvdb.go
@@ -1,12 +1,195 @@
-package api
+package tvdb
import (
+ "bytes"
+ "crypto/md5"
+ "encoding/json"
"fmt"
+ "metachan/config"
"metachan/database"
"metachan/entities"
+ "metachan/types"
"metachan/utils/logger"
+ "net/http"
+ "time"
)
+var tvdbToken string
+var tvdbTokenExpiry time.Time
+
+// authenticateTVDB authenticates with TVDB API and returns a token
+func authenticateTVDB() (string, error) {
+ // Check if we have a valid token
+ if tvdbToken != "" && time.Now().Before(tvdbTokenExpiry) {
+ return tvdbToken, nil
+ }
+
+ if config.Config.TVDB.APIKey == "" {
+ return "", fmt.Errorf("TVDB API key is not set")
+ }
+
+ logger.Log("Authenticating with TVDB API", logger.LogOptions{
+ Level: logger.Debug,
+ Prefix: "TVDB",
+ })
+
+ client := &http.Client{Timeout: 10 * time.Second}
+
+ // Create request body with apikey
+ authBody := map[string]string{"apikey": config.Config.TVDB.APIKey}
+ jsonBody, err := json.Marshal(authBody)
+ if err != nil {
+ return "", fmt.Errorf("failed to marshal auth body: %w", err)
+ }
+
+ req, err := http.NewRequest("POST", "https://api4.thetvdb.com/v4/login", bytes.NewBuffer(jsonBody))
+ if err != nil {
+ return "", fmt.Errorf("failed to create auth request: %w", err)
+ }
+
+ req.Header.Add("Content-Type", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("failed to authenticate: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("authentication failed with status: %d", resp.StatusCode)
+ }
+
+ var authResp TVDBAuthResponse
+ if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
+ return "", fmt.Errorf("failed to decode auth response: %w", err)
+ }
+
+ if authResp.Data.Token == "" {
+ return "", fmt.Errorf("no token received from TVDB")
+ }
+
+ // Store token and set expiry (TVDB tokens typically last 30 days, but we'll refresh after 24 hours to be safe)
+ tvdbToken = authResp.Data.Token
+ tvdbTokenExpiry = time.Now().Add(24 * time.Hour)
+
+ logger.Log("Successfully authenticated with TVDB", logger.LogOptions{
+ Level: logger.Success,
+ Prefix: "TVDB",
+ })
+
+ return tvdbToken, nil
+}
+
+// GetSeriesEpisodes fetches all episodes for a TVDB series
+func GetSeriesEpisodes(tvdbID int) ([]TVDBEpisode, error) {
+ token, err := authenticateTVDB()
+ if err != nil {
+ return nil, fmt.Errorf("failed to authenticate with TVDB: %w", err)
+ }
+
+ logger.Log(fmt.Sprintf("Fetching episodes for TVDB series %d", tvdbID), logger.LogOptions{
+ Level: logger.Debug,
+ Prefix: "TVDB",
+ })
+
+ client := &http.Client{Timeout: 15 * time.Second}
+
+ // TVDB v4 API endpoint for episodes
+ url := fmt.Sprintf("https://api4.thetvdb.com/v4/series/%d/episodes/default", tvdbID)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Add("Authorization", "Bearer "+token)
+ req.Header.Add("Accept", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch episodes: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to fetch episodes with status: %d", resp.StatusCode)
+ }
+
+ var episodesResp TVDBEpisodesResponse
+ if err := json.NewDecoder(resp.Body).Decode(&episodesResp); err != nil {
+ return nil, fmt.Errorf("failed to decode episodes response: %w", err)
+ }
+
+ logger.Log(fmt.Sprintf("Successfully fetched %d episodes from TVDB for series %d", len(episodesResp.Data.Episodes), tvdbID), logger.LogOptions{
+ Level: logger.Success,
+ Prefix: "TVDB",
+ })
+
+ return episodesResp.Data.Episodes, nil
+}
+
+// ConvertTVDBEpisodesToAnimeEpisodes converts TVDB episodes to anime episode format
+func ConvertTVDBEpisodesToAnimeEpisodes(tvdbEpisodes []TVDBEpisode) []types.AnimeSingleEpisode {
+ var animeEpisodes []types.AnimeSingleEpisode
+
+ const tvdbImageBaseURL = "https://artworks.thetvdb.com"
+
+ for _, ep := range tvdbEpisodes {
+ // Generate episode ID from name
+ titles := types.EpisodeTitles{
+ English: ep.Name,
+ Japanese: "",
+ Romaji: "",
+ }
+
+ thumbnailURL := ""
+ if ep.Image != "" {
+ thumbnailURL = ep.Image
+ }
+
+ description := ep.Overview
+ if description == "" {
+ description = "No description available"
+ }
+
+ isRecap := false
+ if ep.FinaleType != nil && *ep.FinaleType == "recap" {
+ isRecap = true
+ }
+
+ animeEpisodes = append(animeEpisodes, types.AnimeSingleEpisode{
+ ID: generateEpisodeID(titles),
+ Titles: titles,
+ Description: description,
+ Aired: ep.Aired,
+ ThumbnailURL: thumbnailURL,
+ Score: 0,
+ Filler: false,
+ Recap: isRecap,
+ ForumURL: "",
+ URL: "",
+ })
+ }
+
+ return animeEpisodes
+}
+
+// generateEpisodeID creates a unique episode ID from titles
+func generateEpisodeID(titles types.EpisodeTitles) string {
+ var title string
+ if titles.English != "" {
+ title = titles.English
+ } else if titles.Romaji != "" {
+ title = titles.Romaji
+ } else {
+ title = titles.Japanese
+ }
+
+ // MD5 hash for ID generation to match Jikan episode IDs
+ hash := md5.Sum([]byte(title))
+ return fmt.Sprintf("%x", hash)
+}
+
// 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), logger.LogOptions{
diff --git a/utils/api/tvdb/types.go b/utils/api/tvdb/types.go
new file mode 100644
index 0000000..4922041
--- /dev/null
+++ b/utils/api/tvdb/types.go
@@ -0,0 +1,43 @@
+package tvdb
+
+// TVDBAuthResponse represents the authentication response from TVDB
+type TVDBAuthResponse struct {
+ Status string `json:"status"`
+ Data struct {
+ Token string `json:"token"`
+ } `json:"data"`
+}
+
+// TVDBEpisode represents an episode from TVDB API v4
+type TVDBEpisode struct {
+ ID int `json:"id"`
+ SeriesID int `json:"seriesId"`
+ Name string `json:"name"`
+ Aired string `json:"aired"`
+ Runtime int `json:"runtime"`
+ NameTranslations []string `json:"nameTranslations"`
+ Overview string `json:"overview"`
+ OverviewTranslations []string `json:"overviewTranslations"`
+ Image string `json:"image"`
+ ImageType int `json:"imageType"`
+ IsMovie int `json:"isMovie"`
+ Number int `json:"number"`
+ AbsoluteNumber int `json:"absoluteNumber"`
+ SeasonNumber int `json:"seasonNumber"`
+ LastUpdated string `json:"lastUpdated"`
+ FinaleType *string `json:"finaleType"`
+ AirsBeforeSeason int `json:"airsBeforeSeason"`
+ AirsBeforeEpisode int `json:"airsBeforeEpisode"`
+ Year string `json:"year"`
+}
+
+// TVDBEpisodesData represents the data container for episodes
+type TVDBEpisodesData struct {
+ Episodes []TVDBEpisode `json:"episodes"`
+}
+
+// TVDBEpisodesResponse represents the episodes response from TVDB API v4
+type TVDBEpisodesResponse struct {
+ Status string `json:"status"`
+ Data TVDBEpisodesData `json:"data"`
+}