diff --git a/internal/streaming/handler.go b/internal/streaming/handler.go index aca4392..a6b35f3 100644 --- a/internal/streaming/handler.go +++ b/internal/streaming/handler.go @@ -301,22 +301,31 @@ func (h *Handler) HandleStartHLS(w http.ResponseWriter, r *http.Request) { return } + epStr := r.URL.Query().Get("ep") + episode, _ := strconv.Atoi(epStr) + // Check torrent exists if _, ok := h.svc.GetTorrent(hash); !ok { http.Error(w, "torrent not found - start stream first", http.StatusNotFound) return } - session, err := h.svc.StartHLS(r.Context(), hash) + session, err := h.svc.StartHLS(r.Context(), hash, episode) if err != nil { log.Printf("HLS start error: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } + // Use sessionKey for subsequent requests + sessionKey := hash + if episode > 0 { + sessionKey = fmt.Sprintf("%s-ep%d", hash, episode) + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ - "playlist": fmt.Sprintf("/api/stream/hls/%s/playlist.m3u8", hash), + "playlist": fmt.Sprintf("/api/stream/hls/%s/playlist.m3u8", sessionKey), "status": "ready", "output": session.OutputDir, }) diff --git a/internal/streaming/service.go b/internal/streaming/service.go index ebadf3d..c733586 100644 --- a/internal/streaming/service.go +++ b/internal/streaming/service.go @@ -286,6 +286,70 @@ func isVideoFile(path string) bool { return false } +// findEpisodeFile finds the video file matching a specific episode number +// Falls back to largest video file if no match found +func findEpisodeFile(files []*torrent.File, episode int) *torrent.File { + var bestMatch *torrent.File + var fallback *torrent.File + + // Episode patterns to match in filenames + epStr := fmt.Sprintf("%02d", episode) + epStr2 := fmt.Sprintf("%d", episode) + + patterns := []string{ + fmt.Sprintf(" - %s", epStr), // - 01 + fmt.Sprintf(" - %s ", epStr), // - 01 (with space after) + fmt.Sprintf("E%s", epStr), // E01 + fmt.Sprintf("E%s ", epStr), // E01 (with space) + fmt.Sprintf("Episode %s", epStr2), // Episode 1 + fmt.Sprintf("Episode %s", epStr), // Episode 01 + fmt.Sprintf(" %s ", epStr), // standalone 01 + fmt.Sprintf("[%s]", epStr), // [01] + fmt.Sprintf("_%s_", epStr), // _01_ + fmt.Sprintf(".%s.", epStr), // .01. + } + + for _, f := range files { + if !isVideoFile(f.Path()) { + continue + } + + // Track largest video as fallback + if fallback == nil || f.Length() > fallback.Length() { + fallback = f + } + + filename := strings.ToLower(filepath.Base(f.Path())) + + // Check each pattern + for _, pattern := range patterns { + if strings.Contains(filename, strings.ToLower(pattern)) { + // Verify it's not a different episode (e.g., searching for ep 1, don't match ep 10) + // Check character after match isn't a digit + idx := strings.Index(filename, strings.ToLower(pattern)) + if idx >= 0 { + afterIdx := idx + len(pattern) + if afterIdx >= len(filename) || !isDigit(filename[afterIdx]) { + if bestMatch == nil || f.Length() > bestMatch.Length() { + bestMatch = f + } + break + } + } + } + } + } + + if bestMatch != nil { + return bestMatch + } + return fallback +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} + // ParseMagnetHash extracts info hash from a magnet URI func ParseMagnetHash(magnetURI string) (string, error) { spec, err := torrent.TorrentSpecFromMagnetUri(magnetURI) @@ -330,13 +394,20 @@ func (s *Service) GetVideoFilePath(infoHash string) (string, error) { } // StartHLS starts HLS transcoding for a torrent -func (s *Service) StartHLS(ctx context.Context, infoHash string) (*HLSSession, error) { +// If episode > 0, it will try to find the file matching that episode number +func (s *Service) StartHLS(ctx context.Context, infoHash string, episode int) (*HLSSession, error) { if s.hls == nil { return nil, fmt.Errorf("HLS transcoding not available (ffmpeg not found)") } + // Use episode-specific session key if episode specified + sessionKey := infoHash + if episode > 0 { + sessionKey = fmt.Sprintf("%s-ep%d", infoHash, episode) + } + // Check if session already exists - if session, ok := s.hls.GetSession(infoHash); ok { + if session, ok := s.hls.GetSession(sessionKey); ok { return session, nil } @@ -346,12 +417,22 @@ func (s *Service) StartHLS(ctx context.Context, infoHash string) (*HLSSession, e return nil, fmt.Errorf("torrent not found: %s", infoHash) } - // Find the largest video file + // Find the video file - either by episode or largest var videoFile *torrent.File - for _, f := range t.Files() { - if isVideoFile(f.Path()) { - if videoFile == nil || f.Length() > videoFile.Length() { - videoFile = f + if episode > 0 { + videoFile = findEpisodeFile(t.Files(), episode) + if videoFile != nil { + s.logger.Info("found episode file", "episode", episode, "file", videoFile.Path()) + } + } + + // Fallback to largest video file + if videoFile == nil { + for _, f := range t.Files() { + if isVideoFile(f.Path()) { + if videoFile == nil || f.Length() > videoFile.Length() { + videoFile = f + } } } } @@ -368,7 +449,7 @@ func (s *Service) StartHLS(ctx context.Context, infoHash string) (*HLSSession, e // Wait for at least 2MB to be available before starting ffmpeg minBytes := int64(2 * 1024 * 1024) - s.logger.Info("waiting for initial data", "hash", infoHash, "need", minBytes) + s.logger.Info("waiting for initial data", "hash", infoHash, "file", videoFile.Path(), "need", minBytes) ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() @@ -392,7 +473,7 @@ func (s *Service) StartHLS(ctx context.Context, infoHash string) (*HLSSession, e ready: // Start transcoding with the reader piped to ffmpeg - session, err := s.hls.StartSessionWithReader(infoHash, reader) + session, err := s.hls.StartSessionWithReader(sessionKey, reader) if err != nil { return nil, err } @@ -402,7 +483,7 @@ ready: defer cancel() if err := session.WaitReady(waitCtx); err != nil { - s.hls.StopSession(infoHash) + s.hls.StopSession(sessionKey) return nil, fmt.Errorf("HLS not ready: %w", err) } diff --git a/internal/templates/watch.templ b/internal/templates/watch.templ index f3f2503..a6c2812 100644 --- a/internal/templates/watch.templ +++ b/internal/templates/watch.templ @@ -118,6 +118,7 @@ templ WatchPage(anime jikan.Anime, episode int, torrents []nyaa.Torrent) { ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -233,7 +233,7 @@ func TorrentSource(t nyaa.Torrent) templ.Component { var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(t.Magnet) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 322, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 323, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -246,7 +246,7 @@ func TorrentSource(t nyaa.Torrent) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(truncateTitle(t.Title, 60)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 323, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 324, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -259,7 +259,7 @@ func TorrentSource(t nyaa.Torrent) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(t.Size) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 325, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 326, Col: 37} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -272,7 +272,7 @@ func TorrentSource(t nyaa.Torrent) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", t.Seeders)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 326, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watch.templ`, Line: 327, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil {