320 lines
9.0 KiB
Go
320 lines
9.0 KiB
Go
package directstream
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"seanime/internal/api/anilist"
|
|
"seanime/internal/library/anime"
|
|
"seanime/internal/mkvparser"
|
|
"seanime/internal/nativeplayer"
|
|
"seanime/internal/util"
|
|
"seanime/internal/util/result"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/samber/mo"
|
|
)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Local File
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
var _ Stream = (*LocalFileStream)(nil)
|
|
|
|
// LocalFileStream is a stream that is a local file.
|
|
type LocalFileStream struct {
|
|
BaseStream
|
|
localFile *anime.LocalFile
|
|
}
|
|
|
|
func (s *LocalFileStream) newReader() (io.ReadSeekCloser, error) {
|
|
r, err := os.OpenFile(s.localFile.Path, os.O_RDONLY, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (s *LocalFileStream) Type() nativeplayer.StreamType {
|
|
return nativeplayer.StreamTypeFile
|
|
}
|
|
|
|
func (s *LocalFileStream) LoadContentType() string {
|
|
s.contentTypeOnce.Do(func() {
|
|
// No need to pass a reader because we are not going to read the file
|
|
// Get the mime type from the file extension
|
|
s.contentType = loadContentType(s.localFile.Path)
|
|
})
|
|
|
|
return s.contentType
|
|
}
|
|
|
|
func (s *LocalFileStream) LoadPlaybackInfo() (ret *nativeplayer.PlaybackInfo, err error) {
|
|
s.playbackInfoOnce.Do(func() {
|
|
if s.localFile == nil {
|
|
s.playbackInfo = &nativeplayer.PlaybackInfo{}
|
|
err = fmt.Errorf("local file is not set")
|
|
s.playbackInfoErr = err
|
|
return
|
|
}
|
|
|
|
// Open the file
|
|
fr, err := s.newReader()
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("directstream(file): Failed to open local file")
|
|
s.manager.preStreamError(s, fmt.Errorf("cannot stream local file: %w", err))
|
|
return
|
|
}
|
|
|
|
// Close the file when done
|
|
defer func() {
|
|
if closer, ok := fr.(io.Closer); ok {
|
|
s.logger.Trace().Msg("directstream(file): Closing local file reader")
|
|
_ = closer.Close()
|
|
} else {
|
|
s.logger.Trace().Msg("directstream(file): Local file reader does not implement io.Closer")
|
|
}
|
|
}()
|
|
|
|
// Get the file size
|
|
size, err := fr.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("directstream(file): Failed to get file size")
|
|
s.manager.preStreamError(s, fmt.Errorf("failed to get file size: %w", err))
|
|
return
|
|
}
|
|
_, _ = fr.Seek(0, io.SeekStart)
|
|
|
|
id := uuid.New().String()
|
|
|
|
var entryListData *anime.EntryListData
|
|
if animeCollection, ok := s.manager.animeCollection.Get(); ok {
|
|
if listEntry, ok := animeCollection.GetListEntryFromAnimeId(s.media.ID); ok {
|
|
entryListData = anime.NewEntryListData(listEntry)
|
|
}
|
|
}
|
|
|
|
playbackInfo := nativeplayer.PlaybackInfo{
|
|
ID: id,
|
|
StreamType: s.Type(),
|
|
MimeType: s.LoadContentType(),
|
|
StreamUrl: "{{SERVER_URL}}/api/v1/directstream/stream?id=" + id,
|
|
ContentLength: size,
|
|
MkvMetadata: nil,
|
|
MkvMetadataParser: mo.None[*mkvparser.MetadataParser](),
|
|
Episode: s.episode,
|
|
Media: s.media,
|
|
EntryListData: entryListData,
|
|
}
|
|
|
|
// If the content type is an EBML content type, we can create a metadata parser
|
|
if isEbmlContent(s.LoadContentType()) {
|
|
|
|
parserKey := util.Base64EncodeStr(s.localFile.Path)
|
|
|
|
parser, ok := s.manager.parserCache.Get(parserKey)
|
|
if !ok {
|
|
parser = mkvparser.NewMetadataParser(fr, s.logger)
|
|
s.manager.parserCache.SetT(parserKey, parser, 2*time.Hour)
|
|
}
|
|
|
|
metadata := parser.GetMetadata(context.Background())
|
|
if metadata.Error != nil {
|
|
s.logger.Error().Err(metadata.Error).Msg("directstream(torrent): Failed to get metadata")
|
|
s.manager.preStreamError(s, fmt.Errorf("failed to get metadata: %w", metadata.Error))
|
|
s.playbackInfoErr = fmt.Errorf("failed to get metadata: %w", metadata.Error)
|
|
return
|
|
}
|
|
|
|
playbackInfo.MkvMetadata = metadata
|
|
playbackInfo.MkvMetadataParser = mo.Some(parser)
|
|
}
|
|
|
|
s.playbackInfo = &playbackInfo
|
|
})
|
|
|
|
return s.playbackInfo, s.playbackInfoErr
|
|
}
|
|
|
|
func (s *LocalFileStream) GetAttachmentByName(filename string) (*mkvparser.AttachmentInfo, bool) {
|
|
return getAttachmentByName(s.manager.playbackCtx, s, filename)
|
|
}
|
|
|
|
func (s *LocalFileStream) GetStreamHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
s.logger.Trace().Str("method", r.Method).Msg("directstream: Received request")
|
|
|
|
defer func() {
|
|
s.logger.Trace().Msg("directstream: Request finished")
|
|
}()
|
|
|
|
if r.Method == http.MethodHead {
|
|
// Get the file size
|
|
fileInfo, err := os.Stat(s.localFile.Path)
|
|
if err != nil {
|
|
s.logger.Error().Msg("directstream: Failed to get file info")
|
|
http.Error(w, "Failed to get file info", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Set the content length
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
|
|
w.Header().Set("Content-Type", s.LoadContentType())
|
|
w.Header().Set("Accept-Ranges", "bytes")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", s.localFile.Path))
|
|
w.WriteHeader(http.StatusOK)
|
|
} else {
|
|
ServeLocalFile(w, r, s)
|
|
}
|
|
})
|
|
}
|
|
|
|
func ServeLocalFile(w http.ResponseWriter, r *http.Request, lfStream *LocalFileStream) {
|
|
playbackInfo, err := lfStream.LoadPlaybackInfo()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
size := playbackInfo.ContentLength
|
|
|
|
if isThumbnailRequest(r) {
|
|
reader, err := lfStream.newReader()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
ra, ok := handleRange(w, r, reader, lfStream.localFile.Path, size)
|
|
if !ok {
|
|
return
|
|
}
|
|
serveContentRange(w, r, r.Context(), reader, lfStream.localFile.Path, size, playbackInfo.MimeType, ra)
|
|
return
|
|
}
|
|
|
|
if lfStream.serveContentCancelFunc != nil {
|
|
lfStream.serveContentCancelFunc()
|
|
}
|
|
|
|
ct, cancel := context.WithCancel(lfStream.manager.playbackCtx)
|
|
lfStream.serveContentCancelFunc = cancel
|
|
|
|
reader, err := lfStream.newReader()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
ra, ok := handleRange(w, r, reader, lfStream.localFile.Path, size)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if _, ok := playbackInfo.MkvMetadataParser.Get(); ok {
|
|
// Start a subtitle stream from the current position
|
|
subReader, err := lfStream.newReader()
|
|
if err != nil {
|
|
lfStream.logger.Error().Err(err).Msg("directstream: Failed to create subtitle reader")
|
|
http.Error(w, "Failed to create subtitle reader", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
go lfStream.StartSubtitleStream(lfStream, lfStream.manager.playbackCtx, subReader, ra.Start)
|
|
}
|
|
|
|
serveContentRange(w, r, ct, reader, lfStream.localFile.Path, size, playbackInfo.MimeType, ra)
|
|
}
|
|
|
|
type PlayLocalFileOptions struct {
|
|
ClientId string
|
|
Path string
|
|
LocalFiles []*anime.LocalFile
|
|
}
|
|
|
|
// PlayLocalFile is used by a module to load a new torrent stream.
|
|
func (m *Manager) PlayLocalFile(ctx context.Context, opts PlayLocalFileOptions) error {
|
|
m.playbackMu.Lock()
|
|
defer m.playbackMu.Unlock()
|
|
|
|
animeCollection, ok := m.animeCollection.Get()
|
|
if !ok {
|
|
return fmt.Errorf("cannot play local file, anime collection is not set")
|
|
}
|
|
|
|
// Get the local file
|
|
var lf *anime.LocalFile
|
|
for _, l := range opts.LocalFiles {
|
|
if util.NormalizePath(l.Path) == util.NormalizePath(opts.Path) {
|
|
lf = l
|
|
break
|
|
}
|
|
}
|
|
|
|
if lf == nil {
|
|
return fmt.Errorf("cannot play local file, could not find local file: %s", opts.Path)
|
|
}
|
|
|
|
if lf.MediaId == 0 {
|
|
return fmt.Errorf("local file has not been matched to a media: %s", opts.Path)
|
|
}
|
|
|
|
mId := lf.MediaId
|
|
var media *anilist.BaseAnime
|
|
listEntry, ok := animeCollection.GetListEntryFromAnimeId(mId)
|
|
if ok {
|
|
media = listEntry.Media
|
|
}
|
|
|
|
if media == nil {
|
|
return fmt.Errorf("media not found in anime collection: %d", mId)
|
|
}
|
|
|
|
episodeCollection, err := anime.NewEpisodeCollectionFromLocalFiles(ctx, anime.NewEpisodeCollectionFromLocalFilesOptions{
|
|
LocalFiles: opts.LocalFiles,
|
|
Media: media,
|
|
AnimeCollection: animeCollection,
|
|
Platform: m.platform,
|
|
MetadataProvider: m.metadataProvider,
|
|
Logger: m.Logger,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("cannot play local file, could not create episode collection: %w", err)
|
|
}
|
|
|
|
var episode *anime.Episode
|
|
for _, e := range episodeCollection.Episodes {
|
|
if e.LocalFile != nil && util.NormalizePath(e.LocalFile.Path) == util.NormalizePath(lf.Path) {
|
|
episode = e
|
|
break
|
|
}
|
|
}
|
|
|
|
if episode == nil {
|
|
return fmt.Errorf("cannot play local file, could not find episode for local file: %s", opts.Path)
|
|
}
|
|
|
|
stream := &LocalFileStream{
|
|
localFile: lf,
|
|
BaseStream: BaseStream{
|
|
manager: m,
|
|
logger: m.Logger,
|
|
clientId: opts.ClientId,
|
|
filename: filepath.Base(lf.Path),
|
|
media: media,
|
|
episode: episode,
|
|
episodeCollection: episodeCollection,
|
|
subtitleEventCache: result.NewResultMap[string, *mkvparser.SubtitleEvent](),
|
|
activeSubtitleStreams: result.NewResultMap[string, *SubtitleStream](),
|
|
},
|
|
}
|
|
|
|
m.loadStream(stream)
|
|
|
|
return nil
|
|
}
|