package handler import ( "fmt" "io" "log" "mal/internal/domain" "mal/pkg/net/proxytransport" "net/http" "strconv" "strings" "sync" "github.com/gin-gonic/gin" ) type PlaybackHandler struct { svc domain.PlaybackService animeSvc domain.AnimeService proxyClient *http.Client streamingClient *http.Client subtitleCache sync.Map } func NewPlaybackHandler(svc domain.PlaybackService, animeSvc domain.AnimeService) *PlaybackHandler { return &PlaybackHandler{ svc: svc, animeSvc: animeSvc, proxyClient: proxytransport.NewClient(), streamingClient: proxytransport.NewStreamingClient(), } } func (h *PlaybackHandler) Register(r *gin.Engine) { log.Println("Registering playback routes") r.GET("/anime/:id/watch", h.HandleWatchPage) r.POST("/api/watch-progress", h.HandleSaveProgress) r.POST("/api/watch-complete", h.HandleWatchComplete) r.GET("/api/watch/thumbnails/:animeId", h.HandleEpisodeThumbnails) r.GET("/watch/proxy/stream", h.HandleProxyStream) r.GET("/watch/proxy/subtitle", h.HandleProxySubtitle) } func (h *PlaybackHandler) HandleWatchPage(c *gin.Context) { log.Printf("Route /anime/:id/watch triggered for ID: %s", c.Param("id")) id, _ := strconv.Atoi(c.Param("id")) ep := c.DefaultQuery("ep", "1") mode := c.DefaultQuery("mode", "sub") user, _ := c.Get("User") userID := "" if u, ok := user.(*domain.User); ok { userID = u.ID } data, err := h.svc.BuildWatchData(c.Request.Context(), id, []string{}, ep, mode, userID) if err != nil { log.Printf("BuildWatchData failed for ID %d: %v", id, err) // Try to at least get anime info for the error page anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id) c.HTML(http.StatusOK, "watch.gohtml", gin.H{ "Error": err.Error(), "Anime": anime, "Episodes": []domain.EpisodeData{}, "CurrentPath": c.Request.URL.Path, "User": user, "CurrentEpID": ep, "WatchData": map[string]any{"Episodes": []domain.EpisodeData{}, "Providers": []any{}}, }) return } log.Printf("BuildWatchData succeeded for ID %d", id) // Merge data from service with handler-specific context responseData := gin.H{ "User": user, "CurrentPath": c.Request.URL.Path, } for k, v := range data { responseData[k] = v } c.HTML(http.StatusOK, "watch.gohtml", responseData) log.Printf("c.HTML finished for ID %d", id) } func (h *PlaybackHandler) HandleSaveProgress(c *gin.Context) { user, _ := c.Get("User") userID := "" if u, ok := user.(*domain.User); ok { userID = u.ID } var req struct { MalID int64 `json:"mal_id"` Episode int `json:"episode"` TimeSeconds float64 `json:"time_seconds"` } if err := c.ShouldBindJSON(&req); err != nil { c.Status(http.StatusBadRequest) return } err := h.svc.SaveProgress(c.Request.Context(), userID, req.MalID, req.Episode, req.TimeSeconds) if err != nil { c.Status(http.StatusInternalServerError) return } c.Status(http.StatusOK) } func (h *PlaybackHandler) HandleWatchComplete(c *gin.Context) { user, _ := c.Get("User") userID := "" if u, ok := user.(*domain.User); ok { userID = u.ID } var req struct { MalID int64 `json:"mal_id"` Episode int `json:"episode"` } if err := c.ShouldBindJSON(&req); err != nil { c.Status(http.StatusBadRequest) return } err := h.svc.CompleteAnime(c.Request.Context(), userID, req.MalID) if err != nil { c.Status(http.StatusInternalServerError) return } c.Status(http.StatusOK) } func (h *PlaybackHandler) HandleEpisodeThumbnails(c *gin.Context) { id, err := strconv.Atoi(c.Param("animeId")) if err != nil { c.Status(http.StatusBadRequest) return } allEpisodes, err := h.animeSvc.GetAllEpisodes(c.Request.Context(), id) if err != nil { log.Printf("failed to fetch thumbnails/episodes: %v", err) } anime, _ := h.animeSvc.GetAnimeByID(c.Request.Context(), id) if anime.Episodes > 0 && anime.Episodes > len(allEpisodes) { epMap := make(map[int]domain.EpisodeData) for _, ep := range allEpisodes { epMap[ep.MalID] = ep } var filled []domain.EpisodeData for i := 1; i <= anime.Episodes; i++ { if ep, ok := epMap[i]; ok { filled = append(filled, ep) } else { filled = append(filled, domain.EpisodeData{ MalID: i, Title: fmt.Sprintf("Episode %d", i), }) } } allEpisodes = filled } type Result struct { MalID int `json:"mal_id"` Title string `json:"title"` } results := make([]Result, len(allEpisodes)) for i, ep := range allEpisodes { results[i] = Result{ MalID: ep.MalID, Title: ep.Title, } } c.JSON(http.StatusOK, results) } func (h *PlaybackHandler) HandleProxyStream(c *gin.Context) { token := c.Query("token") if token == "" { c.Status(http.StatusBadRequest) return } targetURL, referer, err := h.svc.ResolveProxyToken(token) if err != nil { log.Printf("proxy token error: %v", err) c.Status(http.StatusForbidden) return } req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, targetURL, nil) if err != nil { c.Status(http.StatusBadGateway) return } if referer != "" { req.Header.Set("Referer", referer) } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0") resp, err := h.streamingClient.Do(req) if err != nil { log.Printf("proxy stream fetch error: %v", err) c.Status(http.StatusBadGateway) return } defer func() { _ = resp.Body.Close() }() for k, v := range resp.Header { c.Header(k, v[0]) } c.Status(resp.StatusCode) _, _ = io.Copy(c.Writer, resp.Body) } type cachedSubtitle struct { data []byte contentType string } func (h *PlaybackHandler) HandleProxySubtitle(c *gin.Context) { token := c.Query("token") if token == "" { c.Status(http.StatusBadRequest) return } targetURL, referer, err := h.svc.ResolveProxyToken(token) if err != nil { log.Printf("proxy subtitle token error: %v", err) c.Status(http.StatusForbidden) return } if cached, ok := h.subtitleCache.Load(targetURL); ok { entry := cached.(cachedSubtitle) c.Data(http.StatusOK, entry.contentType, entry.data) return } req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, targetURL, nil) if err != nil { c.Status(http.StatusBadGateway) return } if referer != "" { req.Header.Set("Referer", referer) } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0") resp, err := h.proxyClient.Do(req) if err != nil { log.Printf("proxy subtitle fetch error: %v", err) c.Status(http.StatusBadGateway) return } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(io.LimitReader(resp.Body, 2*1024*1024)) if err != nil { log.Printf("proxy subtitle read error: %v", err) c.Status(http.StatusBadGateway) return } contentType := resp.Header.Get("Content-Type") if contentType == "" { contentType = detectSubtitleType(targetURL) } h.subtitleCache.Store(targetURL, cachedSubtitle{data: body, contentType: contentType}) c.Data(http.StatusOK, contentType, body) } func detectSubtitleType(url string) string { lower := strings.ToLower(url) switch { case strings.Contains(lower, ".vtt"): return "text/vtt" case strings.Contains(lower, ".srt"): return "text/plain; charset=utf-8" case strings.Contains(lower, ".ass") || strings.Contains(lower, ".ssa"): return "text/plain; charset=utf-8" default: return "text/plain; charset=utf-8" } }