Files
go2tv/httphandlers/httphandlers.go
Alex Ballas 1611920e65 misspell
2024-12-28 16:17:57 +02:00

365 lines
8.9 KiB
Go

package httphandlers
import (
"bytes"
"fmt"
"html"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"github.com/alexballas/go2tv/soapcalls"
"github.com/alexballas/go2tv/soapcalls/utils"
)
// HTTPserver - new http.Server instance.
type HTTPserver struct {
http *http.Server
Mux *http.ServeMux
// We only need to run one ffmpeg
// command at a time, per server instance
ffmpeg *exec.Cmd
handlers map[string]struct {
payload *soapcalls.TVPayload
media interface{}
}
mu sync.Mutex
}
// Screen interface is used to push message back to the user
// as these are returned by the subscriptions.
type Screen interface {
EmitMsg(string)
Fini()
}
// We use this type to be able to test
// the serveContent function without the
// need of os.Open in the tests.
type osFileType struct {
time time.Time
file io.ReadSeeker
path string
}
// AddHandler dynamically adds a new handler. Currently used by the gapless playback logic where we use
// the same server to serve multiple media files.
func (s *HTTPserver) AddHandler(path string, payload *soapcalls.TVPayload, media interface{}) {
s.mu.Lock()
s.handlers[path] = struct {
payload *soapcalls.TVPayload
media interface{}
}{payload: payload, media: media}
s.mu.Unlock()
}
// RemoveHandler dynamically removes a handler.
func (s *HTTPserver) RemoveHandler(path string) {
s.mu.Lock()
delete(s.handlers, path)
s.mu.Unlock()
}
// StartServer will start a HTTP server to serve the selected media files and
// also handle the subscriptions requests from the DMR devices.
func (s *HTTPserver) StartServer(serverStarted chan<- error, media, subtitles interface{},
tvpayload *soapcalls.TVPayload, screen Screen,
) {
mURL, err := url.Parse(tvpayload.MediaURL)
if err != nil {
serverStarted <- fmt.Errorf("failed to parse MediaURL: %w", err)
return
}
sURL, err := url.Parse(tvpayload.SubtitlesURL)
if err != nil {
serverStarted <- fmt.Errorf("failed to parse SubtitlesURL: %w", err)
return
}
// Dynamically add handlers to better support gapless playback where we're
// required to serve new files with our existing HTTP server.
s.AddHandler(mURL.Path, tvpayload, media)
if sURL.Path != "/." && !tvpayload.Transcode {
s.AddHandler(sURL.Path, nil, subtitles)
}
callbackURL, err := url.Parse(tvpayload.CallbackURL)
if err != nil {
serverStarted <- fmt.Errorf("failed to parse CallbackURL: %w", err)
return
}
s.Mux.HandleFunc("/", s.ServeMediaHandler())
s.Mux.HandleFunc(callbackURL.Path, s.callbackHandler(tvpayload, screen))
ln, err := net.Listen("tcp", s.http.Addr)
if err != nil {
serverStarted <- fmt.Errorf("server listen error: %w", err)
return
}
serverStarted <- nil
_ = s.http.Serve(ln)
}
// ServeMediaHandler is a helper method used to properly handle media and subtitle streaming.
func (s *HTTPserver) ServeMediaHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
out, exists := s.handlers[r.URL.Path]
s.mu.Unlock()
if !exists {
http.Error(w, "not exists", http.StatusNotFound)
return
}
switch f := out.media.(type) {
case string:
m, err := os.Open(f)
if err != nil {
http.NotFound(w, r)
return
}
defer m.Close()
info, err := m.Stat()
if err != nil {
http.NotFound(w, r)
return
}
out.media = osFileType{
time: info.ModTime(),
file: m,
path: f,
}
}
serveContent(w, r, out.payload, out.media, s.ffmpeg)
}
}
func (s *HTTPserver) callbackHandler(tv *soapcalls.TVPayload, screen Screen) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
reqParsed, _ := io.ReadAll(req.Body)
sidVal, sidExists := req.Header["Sid"]
if !sidExists {
http.NotFound(w, req)
return
}
if sidVal[0] == "" {
http.NotFound(w, req)
return
}
uuid := strings.TrimPrefix(sidVal[0], "uuid:")
reqParsedUnescape := html.UnescapeString(string(reqParsed))
previousstate, newstate, err := soapcalls.EventNotifyParser(reqParsedUnescape)
if err != nil {
http.NotFound(w, req)
return
}
// Apparently we should ignore the first message
// On some media renderers we receive a STOPPED message
// even before we start streaming.
processStop, err := tv.GetProcessStop(uuid)
if err != nil {
http.NotFound(w, req)
return
}
if !processStop && newstate == "STOPPED" {
tv.SetProcessStopTrue(uuid)
fmt.Fprintf(w, "OK\n")
return
}
if !tv.UpdateMRstate(previousstate, newstate, uuid) {
http.NotFound(w, req)
return
}
switch newstate {
case "PLAYING":
screen.EmitMsg("Playing")
tv.SetProcessStopTrue(uuid)
case "PAUSED_PLAYBACK":
screen.EmitMsg("Paused")
case "STOPPED":
screen.EmitMsg("Stopped")
_ = tv.UnsubscribeSoapCall(uuid)
screen.Fini()
}
}
}
// StopServer forcefully closes the HTTP server.
func (s *HTTPserver) StopServer() {
if s.ffmpeg != nil && s.ffmpeg.Process != nil {
_ = s.ffmpeg.Process.Kill()
}
s.http.Close()
}
// NewServer constractor generates a new HTTPserver type.
func NewServer(a string) *HTTPserver {
mux := http.NewServeMux()
srv := HTTPserver{
http: &http.Server{Addr: a, Handler: mux},
Mux: mux,
ffmpeg: new(exec.Cmd),
handlers: make(map[string]struct {
payload *soapcalls.TVPayload
media interface{}
}),
}
return &srv
}
func serveContent(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayload, mf interface{}, ff *exec.Cmd) {
var isMedia bool
var transcode bool
var seek bool
var mediaType string
if tv != nil {
isMedia = true
transcode = tv.Transcode
mediaType = tv.MediaType
seek = tv.Seekable
}
w.Header()["transferMode.dlna.org"] = []string{"Interactive"}
if isMedia {
w.Header()["transferMode.dlna.org"] = []string{"Streaming"}
w.Header()["realTimeInfo.dlna.org"] = []string{"DLNA.ORG_TLAG=*"}
w.Header()["Content-Type"] = []string{mediaType}
}
switch f := mf.(type) {
case osFileType:
serveContentCustomType(w, r, tv, mediaType, transcode, seek, f, ff)
case []byte:
serveContentBytes(w, r, mediaType, f)
case io.ReadCloser:
serveContentReadClose(w, r, tv, mediaType, transcode, f, ff)
default:
http.NotFound(w, r)
return
}
}
func serveContentBytes(w http.ResponseWriter, r *http.Request, mediaType string, f []byte) {
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
contentFeatures, err := utils.BuildContentFeatures(mediaType, "01", false)
if err != nil {
http.NotFound(w, r)
return
}
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
}
bReader := bytes.NewReader(f)
name := strings.TrimLeft(r.URL.Path, "/")
http.ServeContent(w, r, name, time.Now(), bReader)
}
func serveContentReadClose(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayload, mediaType string, transcode bool, f io.ReadCloser, ff *exec.Cmd) {
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
contentFeatures, err := utils.BuildContentFeatures(mediaType, "00", transcode)
if err != nil {
http.NotFound(w, r)
return
}
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
}
// Since we're dealing with an io.Reader we can't
// allow any HEAD requests that some DMRs trigger.
if transcode && r.Method == http.MethodGet && strings.Contains(mediaType, "video") {
_ = utils.ServeTranscodedStream(w, f, ff, tv.FFmpegPath, tv.FFmpegSubsPath, tv.FFmpegSeek)
return
}
// No seek support
if r.Method == http.MethodGet {
_, _ = io.Copy(w, f)
f.Close()
return
}
}
func serveContentCustomType(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayload, mediaType string, transcode, seek bool, f osFileType, ff *exec.Cmd) {
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
seekflag := "00"
if seek {
seekflag = "01"
}
contentFeatures, err := utils.BuildContentFeatures(mediaType, seekflag, transcode)
if err != nil {
http.NotFound(w, r)
return
}
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
}
if transcode && r.Method == http.MethodGet && strings.Contains(mediaType, "video") {
// Since we're dealing with an io.Reader we can't
// allow any HEAD requests that some DMRs trigger.
var input interface{} = f.file
// The only case where we should expect f.path to be ""
// is only during our unit tests where we emulate the files.
if f.path != "" {
input = f.path
}
_ = utils.ServeTranscodedStream(w, input, ff, tv.FFmpegPath, tv.FFmpegSubsPath, tv.FFmpegSeek)
return
}
name := strings.TrimLeft(r.URL.Path, "/")
if r.Method == http.MethodGet {
http.ServeContent(w, r, name, f.time, f.file)
}
if r.Method == http.MethodHead {
size, err := f.file.Seek(0, io.SeekEnd)
if err != nil {
http.Error(w, "cant get file size", 500)
}
_, err = f.file.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, "cant get file size", 500)
}
w.Header()["Content-Length"] = []string{strconv.FormatInt(size, 10)}
if !f.time.IsZero() && !f.time.Equal(time.Unix(0, 0)) {
w.Header().Set("Last-Modified", f.time.UTC().Format(http.TimeFormat))
}
}
}