Ztorrent/internal/stream/server.go

142 lines
3.1 KiB
Go

package stream
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/veggiedefender/torrent-client/internal/torrent"
)
// Server handles HTTP streaming for a torrent
type Server struct {
server *http.Server
engine *torrent.Engine
}
func NewServer(engine *torrent.Engine) *Server {
s := &Server{
engine: engine,
}
mux := http.NewServeMux()
mux.HandleFunc("/stream", s.handleStream)
s.server = &http.Server{
Addr: ":8080",
Handler: mux,
}
return s
}
func (s *Server) Start() {
go func() {
log.Println("Starting stream server on http://localhost:8080/stream")
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("Stream server error: %v", err)
}
}()
}
func (s *Server) Stop() {
if s.server != nil {
s.server.Shutdown(context.Background())
}
}
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
status := s.engine.Status()
if !status.Loaded {
http.Error(w, "Torrent not loaded", http.StatusNotFound)
return
}
partPath := s.engine.PartFilePath()
if partPath == "" {
http.Error(w, "Part file not ready", http.StatusServiceUnavailable)
return
}
file, err := os.Open(partPath)
if err != nil {
http.Error(w, "Failed to open file", http.StatusInternalServerError)
return
}
// Note: We don't defer file.Close() here because ServeContent uses the Seeker asynchronously,
// wait, http.ServeContent does NOT close it, but it finishes before returning!
// So we MUST defer file.Close().
defer file.Close()
reader := &streamReader{
file: file,
engine: s.engine,
pieceLength: status.PieceLength,
length: int64(status.Length),
offset: 0,
}
w.Header().Set("Content-Type", "video/mp4") // TODO: detect properly, default to mp4
w.Header().Set("Accept-Ranges", "bytes")
http.ServeContent(w, r, status.Name, time.Now(), reader)
}
// streamReader wraps a file and blocks until requested pieces are downloaded
type streamReader struct {
file *os.File
engine *torrent.Engine
pieceLength int
length int64
offset int64
}
func (r *streamReader) Read(p []byte) (n int, err error) {
if r.offset >= r.length {
return 0, io.EOF
}
// Calculate which piece we are trying to read
pieceIndex := int(r.offset / int64(r.pieceLength))
// Polling loop to wait for the piece
for !r.engine.HasPiece(pieceIndex) {
status := r.engine.Status()
if status.Phase == "failed" || status.Phase == "stopped" {
return 0, fmt.Errorf("torrent stopped or failed")
}
time.Sleep(200 * time.Millisecond)
}
// Read from the actual file
n, err = r.file.ReadAt(p, r.offset)
if n > 0 {
r.offset += int64(n)
}
return n, err
}
func (r *streamReader) Seek(offset int64, whence int) (int64, error) {
var newOffset int64
switch whence {
case io.SeekStart:
newOffset = offset
case io.SeekCurrent:
newOffset = r.offset + offset
case io.SeekEnd:
newOffset = r.length + offset
default:
return 0, fmt.Errorf("invalid whence: %d", whence)
}
if newOffset < 0 {
return 0, fmt.Errorf("negative offset")
}
r.offset = newOffset
return r.offset, nil
}