node build fixed
This commit is contained in:
319
seanime-2.9.10/internal/directstream/localfile.go
Normal file
319
seanime-2.9.10/internal/directstream/localfile.go
Normal file
@@ -0,0 +1,319 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user