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 {