aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-02-24 15:02:38 +0530
committerBobby <[email protected]>2026-02-24 15:02:38 +0530
commitc6ff27b989047cf0af8d6cf2aa86c8e80547cf10 (patch)
tree55206173780ba073611d72fca135fedfd0322cb4
parent17b77153a862ad1eb3babe1e34e748363ac9916c (diff)
downloadmetachan-c6ff27b989047cf0af8d6cf2aa86c8e80547cf10.tar.xz
metachan-c6ff27b989047cf0af8d6cf2aa86c8e80547cf10.zip
Add GetAnimeEpisodes endpoint and implement episode retrieval logic
-rw-r--r--controllers/anime.go18
-rw-r--r--repositories/anime.go47
-rw-r--r--router/router.go1
-rw-r--r--services/anime.go57
-rw-r--r--utils/api/streaming/streaming.go50
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 {