From c6ff27b989047cf0af8d6cf2aa86c8e80547cf10 Mon Sep 17 00:00:00 2001 From: Bobby <30593201+luciferreeves@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:02:38 +0530 Subject: Add GetAnimeEpisodes endpoint and implement episode retrieval logic --- controllers/anime.go | 18 +++++++++++++ repositories/anime.go | 47 +++++++++++++++++++++++++++++++++ router/router.go | 1 + services/anime.go | 57 ++++++++++++++++++++++++++++++++++++---- utils/api/streaming/streaming.go | 50 +++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 5 deletions(-) diff --git a/controllers/anime.go b/controllers/anime.go index 3f5f692..85b1d72 100644 --- a/controllers/anime.go +++ b/controllers/anime.go @@ -27,6 +27,24 @@ func GetAnime(c *fiber.Ctx) error { return c.JSON(anime) } +func GetAnimeEpisodes(c *fiber.Ctx) error { + id := meta.Request(c).MustHave().Param("id") + provider := meta.Request(c).Default("mal").Query("provider") + + switch provider { + case "mal", "anilist": + default: + return BadRequest(c, errors.New("invalid provider")) + } + + episodes, err := repositories.GetAnimeEpisodes(enums.MappingType(provider), id) + if err != nil { + return NotFound(c, err) + } + + return c.JSON(episodes) +} + // -- Old Code Below -- // import ( diff --git a/repositories/anime.go b/repositories/anime.go index 9a17542..4960e6c 100644 --- a/repositories/anime.go +++ b/repositories/anime.go @@ -6,6 +6,7 @@ import ( "metachan/entities" "metachan/enums" "metachan/utils/logger" + "time" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -209,6 +210,52 @@ func SaveEpisodeSkipTimes(episodeID string, skipTimes []entities.EpisodeSkipTime return nil } +func GetAnimeEpisodes[T idType](maptype enums.MappingType, id T) ([]entities.Episode, error) { + mapping, err := GetAnimeMapping(maptype, id) + if err != nil { + return nil, errors.New("anime not found") + } + + var anime entities.Anime + if err := DB.Where("mapping_id = ?", mapping.ID).Select("id").First(&anime).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, errors.New("anime not found") + } + + var episodes []entities.Episode + result := DB. + Preload("Title"). + Preload("SkipTimes"). + Preload("StreamInfo"). + Preload("StreamInfo.SubSources"). + Preload("StreamInfo.DubSources"). + Where("anime_id = ?", anime.ID). + Order("episode_number asc"). + Find(&episodes) + + if result.Error != nil { + return nil, errors.New("failed to fetch episodes") + } + + return episodes, nil +} + +func SaveEpisodeStreamInfo(animeID uint, episodeID string, info *entities.StreamInfo) error { + info.AnimeID = animeID + info.EpisodeID = episodeID + info.LastFetch = time.Now() + + var existing entities.StreamInfo + if DB.Where("episode_id = ? AND anime_id = ?", episodeID, animeID).First(&existing).Error == nil { + info.ID = existing.ID + DB.Where("stream_info_id = ?", existing.ID).Delete(&entities.StreamingSource{}) + } + + return DB.Session(&gorm.Session{FullSaveAssociations: true}).Save(info).Error +} + func GetAllAnimeStubs() ([]animeStub, error) { var stubs []animeStub if err := DB.Model(&entities.Anime{}).Select("mal_id, updated_at").Scan(&stubs).Error; err != nil { diff --git a/router/router.go b/router/router.go index 870992b..4d4f832 100644 --- a/router/router.go +++ b/router/router.go @@ -13,6 +13,7 @@ func Initialize(router *fiber.App) { // Anime routes animeRouter := router.Group("/anime") animeRouter.Get("/:id", controllers.GetAnime) + animeRouter.Get("/:id/episodes", controllers.GetAnimeEpisodes) // Anime routes // animeRouter := router.Group("/a") diff --git a/services/anime.go b/services/anime.go index de19aa5..ec2b99c 100644 --- a/services/anime.go +++ b/services/anime.go @@ -1,6 +1,7 @@ package services import ( + "crypto/md5" "fmt" "metachan/entities" "metachan/enums" @@ -315,6 +316,12 @@ func applyJikanData(anime *entities.Anime, jikanAnime *types.JikanAnimeResponse, } } + titleForID := je.Title + if titleForID == "" { + titleForID = je.TitleRomaji + } + episode.EpisodeID = generateEpisodeID(anime.MALID, je.MALID, titleForID) + anime.Episodes = append(anime.Episodes, episode) } @@ -486,11 +493,11 @@ func applyStreamingData(anime *entities.Anime) { return } - logger.Infof("AnimeService", "Fetching streaming counts for: %s", searchTitle) subCount, dubCount, err := streaming.GetStreamingCounts(searchTitle) - if err != nil { - if anime.Title.English != "" && anime.Title.English != searchTitle { - subCount, dubCount, err = streaming.GetStreamingCounts(anime.Title.English) + if err != nil && anime.Title.English != "" && anime.Title.English != searchTitle { + subCount, dubCount, err = streaming.GetStreamingCounts(anime.Title.English) + if err == nil { + searchTitle = anime.Title.English } } @@ -501,7 +508,39 @@ func applyStreamingData(anime *entities.Anime) { anime.SubbedCount = subCount anime.DubbedCount = dubCount - logger.Infof("AnimeService", "Streaming counts - Subbed: %d, Dubbed: %d", subCount, dubCount) + + if len(anime.Episodes) > 0 { + epNums := make([]int, len(anime.Episodes)) + for i, ep := range anime.Episodes { + epNums[i] = ep.EpisodeNumber + } + sourcesMap, err := streaming.FetchAllEpisodeSources(searchTitle, epNums) + if err == nil { + for i := range anime.Episodes { + ep := &anime.Episodes[i] + if s, ok := sourcesMap[ep.EpisodeNumber]; ok { + sub := make([]entities.StreamingSource, len(s.Sub)) + for j, src := range s.Sub { + sub[j] = entities.StreamingSource{URL: src.URL, Server: src.Server, Type: src.Type} + } + dub := make([]entities.StreamingSource, len(s.Dub)) + for j, src := range s.Dub { + dub[j] = entities.StreamingSource{URL: src.URL, Server: src.Server, Type: src.Type} + } + ep.StreamInfo = &entities.StreamInfo{ + SubSources: sub, + DubSources: dub, + } + } + } + } + } +} + +func generateEpisodeID(malID int, episodeNumber int, title string) string { + unique := fmt.Sprintf("%d-%d-%s", malID, episodeNumber, title) + hash := md5.Sum([]byte(unique)) + return fmt.Sprintf("%x", hash) } func applySeasonData(anime *entities.Anime, mapping *entities.Mapping) { @@ -711,6 +750,14 @@ func saveAnime(anime *entities.Anime, skipTimeMap map[string][]entities.EpisodeS if err := repositories.SaveAnimeEpisodes(anime.ID, anime.Episodes); err != nil { logger.Warnf("AnimeService", "Failed to save episodes: %v", err) } + for i := range anime.Episodes { + ep := &anime.Episodes[i] + if ep.StreamInfo != nil && ep.EpisodeID != "" { + if err := repositories.SaveEpisodeStreamInfo(anime.ID, ep.EpisodeID, ep.StreamInfo); err != nil { + logger.Warnf("AnimeService", "Failed to save stream info for episode %s: %v", ep.EpisodeID, err) + } + } + } } if len(anime.Characters) > 0 { diff --git a/utils/api/streaming/streaming.go b/utils/api/streaming/streaming.go index f08a2d3..4e06515 100644 --- a/utils/api/streaming/streaming.go +++ b/utils/api/streaming/streaming.go @@ -514,6 +514,56 @@ func GetStreamingSources(title string, episodeNumber int) (*types.StreamAnimeStr return streaming, nil } +func FetchAllEpisodeSources(title string, episodeNumbers []int) (map[int]*types.StreamAnimeStreaming, error) { + searchResults, err := SearchAnime(title) + if err != nil || len(searchResults) == 0 { + return nil, errors.New("no streaming sources found") + } + + bestMatch := searchResults[0] + + subSet := make(map[string]bool) + dubSet := make(map[string]bool) + + if bestMatch.SubEpisodes > 0 { + if eps, err := GetEpisodesList(bestMatch.ID, "sub"); err == nil { + for _, e := range eps { + subSet[e] = true + } + } + } + if bestMatch.DubEpisodes > 0 { + if eps, err := GetEpisodesList(bestMatch.ID, "dub"); err == nil { + for _, e := range eps { + dubSet[e] = true + } + } + } + + result := make(map[int]*types.StreamAnimeStreaming) + for _, epNum := range episodeNumbers { + epStr := fmt.Sprintf("%d", epNum) + s := &types.StreamAnimeStreaming{ + Sub: []types.StreamAnimeStreamingSource{}, + Dub: []types.StreamAnimeStreamingSource{}, + } + if subSet[epStr] { + if sources, err := GetEpisodeLinks(bestMatch.ID, epStr, "sub"); err == nil { + s.Sub = sources + } + } + if dubSet[epStr] { + if sources, err := GetEpisodeLinks(bestMatch.ID, epStr, "dub"); err == nil { + s.Dub = sources + } + } + if len(s.Sub) > 0 || len(s.Dub) > 0 { + result[epNum] = s + } + } + return result, nil +} + func GetStreamingCounts(title string) (int, int, error) { searchResults, err := SearchAnime(title) if err != nil { -- cgit v1.2.3