mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1588 lines
48 KiB
Go
1588 lines
48 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/synctv-org/synctv/internal/cache"
|
|
"github.com/synctv-org/synctv/internal/conf"
|
|
"github.com/synctv-org/synctv/internal/db"
|
|
dbModel "github.com/synctv-org/synctv/internal/model"
|
|
"github.com/synctv-org/synctv/internal/op"
|
|
"github.com/synctv-org/synctv/internal/rtmp"
|
|
"github.com/synctv-org/synctv/internal/settings"
|
|
"github.com/synctv-org/synctv/internal/vendor"
|
|
"github.com/synctv-org/synctv/server/model"
|
|
"github.com/synctv-org/synctv/utils"
|
|
"github.com/synctv-org/vendors/api/alist"
|
|
"github.com/synctv-org/vendors/api/emby"
|
|
uhc "github.com/zijiren233/go-uhc"
|
|
"github.com/zijiren233/livelib/protocol/hls"
|
|
"github.com/zijiren233/livelib/protocol/httpflv"
|
|
"github.com/zijiren233/stream"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
func GetPageItems[T any](ctx *gin.Context, items []T) ([]T, error) {
|
|
page, max, err := utils.GetPageAndMax(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return utils.GetPageItems(items, page, max), nil
|
|
}
|
|
|
|
// func MovieList(ctx *gin.Context) {
|
|
// room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
// user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
// log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
// page, max, err := utils.GetPageAndMax(ctx)
|
|
// if err != nil {
|
|
// log.Errorf("get page and max error: %v", err)
|
|
// ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
// return
|
|
// }
|
|
|
|
// currentResp, err := genCurrentResp(ctx, user, room)
|
|
// if err != nil {
|
|
// log.Errorf("gen current resp error: %v", err)
|
|
// ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
// return
|
|
// }
|
|
|
|
// m := room.GetMoviesWithPage(page, max)
|
|
// mresp := make([]model.MovieResp, len(m))
|
|
// for i, v := range m {
|
|
// mresp[i] = model.MovieResp{
|
|
// Id: v.Movie.ID,
|
|
// Base: v.Movie.Base,
|
|
// Creator: op.GetUserName(v.Movie.CreatorID),
|
|
// }
|
|
// // hide url and headers when proxy
|
|
// if user.ID != v.Movie.CreatorID && v.Movie.Base.Proxy {
|
|
// mresp[i].Base.Url = ""
|
|
// mresp[i].Base.Headers = nil
|
|
// }
|
|
// }
|
|
|
|
// ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{
|
|
// "current": currentResp,
|
|
// "total": room.GetMoviesCount(),
|
|
// "movies": mresp,
|
|
// }))
|
|
// }
|
|
|
|
func genMovieInfo(
|
|
ctx context.Context,
|
|
user *op.User,
|
|
opMovie *op.Movie,
|
|
userAgent,
|
|
userToken string,
|
|
) (*model.Movie, error) {
|
|
if opMovie == nil || opMovie.ID == "" {
|
|
return &model.Movie{}, nil
|
|
}
|
|
if opMovie.IsFolder {
|
|
if !opMovie.IsDynamicFolder() {
|
|
return nil, errors.New("movie is static folder, can't get movie info")
|
|
}
|
|
}
|
|
var movie = opMovie.Movie.Clone()
|
|
if movie.MovieBase.VendorInfo.Vendor != "" {
|
|
vendorMovie, err := genVendorMovie(ctx, user, opMovie, userAgent, userToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
movie = vendorMovie
|
|
} else if movie.MovieBase.RtmpSource || movie.MovieBase.Live && movie.MovieBase.Proxy {
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/live/hls/list/%s.m3u8?token=%s", movie.ID, userToken)
|
|
movie.MovieBase.Type = "m3u8"
|
|
movie.MoreSources = append(movie.MoreSources, &dbModel.MoreSource{
|
|
Name: "flv",
|
|
Url: fmt.Sprintf("/api/movie/live/flv/%s.flv?token=%s", movie.ID, userToken),
|
|
Type: "flv",
|
|
})
|
|
movie.MovieBase.Headers = nil
|
|
} else if movie.MovieBase.Proxy {
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
|
|
movie.MovieBase.Headers = nil
|
|
}
|
|
if movie.MovieBase.Type == "" && movie.MovieBase.Url != "" {
|
|
movie.MovieBase.Type = utils.GetUrlExtension(movie.MovieBase.Url)
|
|
}
|
|
for _, v := range movie.MoreSources {
|
|
if v.Type == "" {
|
|
v.Type = utils.GetUrlExtension(v.Url)
|
|
}
|
|
}
|
|
resp := &model.Movie{
|
|
Id: movie.ID,
|
|
CreatedAt: movie.CreatedAt.UnixMilli(),
|
|
Base: movie.MovieBase,
|
|
Creator: op.GetUserName(movie.CreatorID),
|
|
CreatorId: movie.CreatorID,
|
|
SubPath: opMovie.SubPath(),
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func genCurrentRespWithCurrent(ctx context.Context, user *op.User, room *op.Room, userAgent string, userToken string) (*model.CurrentMovieResp, error) {
|
|
current := room.Current()
|
|
if current.Movie.ID == "" {
|
|
return &model.CurrentMovieResp{
|
|
Movie: &model.Movie{},
|
|
}, nil
|
|
}
|
|
opMovie, err := room.GetMovieByID(current.Movie.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get current movie error: %w", err)
|
|
}
|
|
mr, err := genMovieInfo(ctx, user, opMovie, userAgent, userToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gen current movie info error: %w", err)
|
|
}
|
|
resp := &model.CurrentMovieResp{
|
|
Status: current.UpdateStatus(),
|
|
Movie: mr,
|
|
ExpireId: opMovie.ExpireId(),
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func CurrentMovie(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
currentResp, err := genCurrentRespWithCurrent(ctx, user, room, ctx.GetHeader("User-Agent"), ctx.MustGet("token").(string))
|
|
if err != nil {
|
|
log.Errorf("gen current resp error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, model.NewApiDataResp(currentResp))
|
|
}
|
|
|
|
func Movies(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
if !user.HasRoomPermission(room, dbModel.PermissionGetMovieList) {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorResp(dbModel.ErrNoPermission))
|
|
return
|
|
}
|
|
|
|
id := ctx.Query("id")
|
|
if len(id) != 0 && len(id) != 32 {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id length must be 0 or 32"))
|
|
return
|
|
}
|
|
|
|
page, max, err := utils.GetPageAndMax(ctx)
|
|
if err != nil {
|
|
log.Errorf("get page and max error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
if id != "" {
|
|
mv, err := room.GetMovieByID(id)
|
|
if err != nil {
|
|
log.Errorf("get room movie by id error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if !mv.IsFolder {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("parent id is not folder"))
|
|
return
|
|
}
|
|
if mv.IsDynamicFolder() {
|
|
resp, err := listVendorDynamicMovie(ctx, user, room, mv.Movie, ctx.Query("subPath"), page, max)
|
|
if err != nil {
|
|
log.Errorf("vendor dynamic movie list error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
|
|
return
|
|
}
|
|
}
|
|
|
|
m, total, err := user.GetRoomMoviesWithPage(room, page, max, id)
|
|
if err != nil {
|
|
log.Errorf("get room movies with page error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
paths, err := getParentMoviePath(room, id)
|
|
if err != nil {
|
|
log.Errorf("get parent movie path error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
resp := &model.MoviesResp{
|
|
Total: total,
|
|
Movies: make([]*model.Movie, len(m)),
|
|
Paths: paths,
|
|
}
|
|
|
|
for i, v := range m {
|
|
resp.Movies[i] = &model.Movie{
|
|
Id: v.ID,
|
|
CreatedAt: v.CreatedAt.UnixMilli(),
|
|
Base: v.MovieBase,
|
|
Creator: op.GetUserName(v.CreatorID),
|
|
CreatorId: v.CreatorID,
|
|
}
|
|
// hide url and headers when proxy
|
|
if user.ID != v.CreatorID && v.MovieBase.Proxy {
|
|
resp.Movies[i].Base.Url = ""
|
|
resp.Movies[i].Base.Headers = nil
|
|
}
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, model.NewApiDataResp(resp))
|
|
}
|
|
|
|
func getParentMoviePath(room *op.Room, id string) ([]*model.MoviePath, error) {
|
|
paths := []*model.MoviePath{
|
|
{
|
|
Name: "Home",
|
|
ID: "",
|
|
},
|
|
}
|
|
if id == "" {
|
|
return paths, nil
|
|
}
|
|
for id != "" {
|
|
p, err := room.GetMovieByID(id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get movie by id error: %w", err)
|
|
}
|
|
paths = append(paths, &model.MoviePath{
|
|
Name: p.MovieBase.Name,
|
|
ID: p.ID,
|
|
})
|
|
id = p.ParentID.String()
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func listVendorDynamicMovie(ctx context.Context, reqUser *op.User, room *op.Room, movie *dbModel.Movie, subPath string, page, max int) (*model.MoviesResp, error) {
|
|
if reqUser.ID != movie.CreatorID {
|
|
return nil, fmt.Errorf("list vendor dynamic folder error: %w", dbModel.ErrNoPermission)
|
|
}
|
|
// creatorE, err := op.LoadOrInitUserByID(movie.CreatorID)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
user := reqUser
|
|
|
|
paths, err := getParentMoviePath(room, movie.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get parent movie path error: %w", err)
|
|
}
|
|
resp := &model.MoviesResp{
|
|
Paths: paths,
|
|
Dynamic: true,
|
|
}
|
|
|
|
switch movie.MovieBase.VendorInfo.Vendor {
|
|
case dbModel.VendorAlist:
|
|
serverID, truePath, err := movie.VendorInfo.Alist.ServerIDAndFilePath()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load alist server id error: %w", err)
|
|
}
|
|
newPath := path.Join(truePath, subPath)
|
|
// check new path is in parent path
|
|
if !strings.HasPrefix(newPath, truePath) {
|
|
return nil, fmt.Errorf("sub path is not in parent path")
|
|
}
|
|
truePath = newPath
|
|
aucd, err := user.AlistCache().LoadOrStore(ctx, serverID)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound("vendor")) {
|
|
return nil, errors.New("alist server not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
var cli = vendor.LoadAlistClient(movie.VendorInfo.Backend)
|
|
data, err := cli.FsList(ctx, &alist.FsListReq{
|
|
Token: aucd.Token,
|
|
Password: movie.VendorInfo.Alist.Password,
|
|
Path: truePath,
|
|
Host: aucd.Host,
|
|
Refresh: false,
|
|
Page: uint64(page),
|
|
PerPage: uint64(max),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Total = int64(data.Total)
|
|
resp.Movies = make([]*model.Movie, len(data.Content))
|
|
for i, flr := range data.Content {
|
|
resp.Movies[i] = &model.Movie{
|
|
Id: movie.ID,
|
|
CreatedAt: movie.CreatedAt.UnixMilli(),
|
|
Creator: op.GetUserName(movie.CreatorID),
|
|
CreatorId: movie.CreatorID,
|
|
SubPath: fmt.Sprintf("/%s", strings.Trim(fmt.Sprintf("%s/%s", subPath, flr.Name), "/")),
|
|
Base: dbModel.MovieBase{
|
|
Name: flr.Name,
|
|
IsFolder: flr.IsDir,
|
|
ParentID: dbModel.EmptyNullString(movie.ID),
|
|
VendorInfo: dbModel.VendorInfo{
|
|
Vendor: dbModel.VendorAlist,
|
|
Backend: movie.VendorInfo.Backend,
|
|
Alist: &dbModel.AlistStreamingInfo{
|
|
Path: dbModel.FormatAlistPath(serverID, fmt.Sprintf("/%s", strings.Trim(fmt.Sprintf("%s/%s", truePath, flr.Name), "/"))),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
resp.Paths = model.GenDefaultSubPaths(subPath, true, resp.Paths...)
|
|
|
|
case dbModel.VendorEmby:
|
|
serverID, truePath, err := movie.VendorInfo.Emby.ServerIDAndFilePath()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load emby server id error: %w", err)
|
|
}
|
|
if subPath != "" {
|
|
truePath = subPath
|
|
}
|
|
aucd, err := user.EmbyCache().LoadOrStore(ctx, serverID)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound("vendor")) {
|
|
return nil, errors.New("emby server not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
var cli = vendor.LoadEmbyClient(movie.VendorInfo.Backend)
|
|
data, err := cli.FsList(ctx, &emby.FsListReq{
|
|
Host: aucd.Host,
|
|
Path: truePath,
|
|
Token: aucd.ApiKey,
|
|
UserId: aucd.UserID,
|
|
Limit: uint64(max),
|
|
StartIndex: uint64((page - 1) * max),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("emby fs list error: %w", err)
|
|
}
|
|
resp.Total = int64(data.Total)
|
|
resp.Movies = make([]*model.Movie, len(data.Items))
|
|
for i, flr := range data.Items {
|
|
resp.Movies[i] = &model.Movie{
|
|
Id: movie.ID,
|
|
CreatedAt: movie.CreatedAt.UnixMilli(),
|
|
Creator: op.GetUserName(movie.CreatorID),
|
|
CreatorId: movie.CreatorID,
|
|
SubPath: flr.Id,
|
|
Base: dbModel.MovieBase{
|
|
Name: flr.Name,
|
|
IsFolder: flr.IsFolder,
|
|
ParentID: dbModel.EmptyNullString(movie.ID),
|
|
VendorInfo: dbModel.VendorInfo{
|
|
Vendor: dbModel.VendorEmby,
|
|
Backend: movie.VendorInfo.Backend,
|
|
Emby: &dbModel.EmbyStreamingInfo{
|
|
Path: dbModel.FormatEmbyPath(serverID, flr.Id),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("%v vendor not implement list dynamic movie", movie.MovieBase.VendorInfo.Vendor)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func PushMovie(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
req := model.PushMovieReq{}
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
log.Errorf("push movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
m, err := user.AddRoomMovie(room, (*dbModel.MovieBase)(&req))
|
|
if err != nil {
|
|
log.Errorf("push movie error: %v", err)
|
|
if errors.Is(err, dbModel.ErrNoPermission) {
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("push movie error: %w", err),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, model.NewApiDataResp(m))
|
|
}
|
|
|
|
func PushMovies(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
req := model.PushMoviesReq{}
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
var ms []*dbModel.MovieBase = make([]*dbModel.MovieBase, len(req))
|
|
|
|
for i, v := range req {
|
|
ms[i] = (*dbModel.MovieBase)(v)
|
|
}
|
|
|
|
m, err := user.AddRoomMovies(room, ms)
|
|
if err != nil {
|
|
log.Errorf("push movies error: %v", err)
|
|
if errors.Is(err, dbModel.ErrNoPermission) {
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("push movies error: %w", err),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, model.NewApiDataResp(m))
|
|
}
|
|
|
|
func NewPublishKey(ctx *gin.Context) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
if !conf.Conf.Server.Rtmp.Enable {
|
|
log.Errorf("rtmp is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
|
|
return
|
|
}
|
|
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
|
|
req := model.IdReq{}
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
log.Errorf("new publish key error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
movie, err := room.GetMovieByID(req.Id)
|
|
if err != nil {
|
|
log.Errorf("new publish key error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
if movie.Movie.CreatorID != user.ID {
|
|
log.Errorf("new publish key error: %v", dbModel.ErrNoPermission)
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("new publish key error: %w", dbModel.ErrNoPermission),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
|
|
if !movie.Movie.MovieBase.RtmpSource {
|
|
log.Errorf("new publish key error: %v", "only rtmp source movie can get publish key")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("only live movie can get publish key"))
|
|
return
|
|
}
|
|
|
|
token, err := rtmp.NewRtmpAuthorization(movie.Movie.ID)
|
|
if err != nil {
|
|
log.Errorf("new publish key error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
host := settings.CustomPublishHost.Get()
|
|
if host == "" {
|
|
host = HOST.Get()
|
|
}
|
|
if host == "" {
|
|
host = ctx.Request.Host
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{
|
|
"host": host,
|
|
"app": room.ID,
|
|
"token": token,
|
|
}))
|
|
}
|
|
|
|
func EditMovie(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
req := model.EditMovieReq{}
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
log.Errorf("edit movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
if err := user.UpdateRoomMovie(room, req.Id, (*dbModel.MovieBase)(&req.PushMovieReq)); err != nil {
|
|
log.Errorf("edit movie error: %v", err)
|
|
if errors.Is(err, dbModel.ErrNoPermission) {
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("edit movie error: %w", err),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func DelMovie(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
req := model.IdsReq{}
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
log.Errorf("del movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
err := user.DeleteRoomMoviesByID(room, req.Ids)
|
|
if err != nil {
|
|
log.Errorf("del movie error: %v", err)
|
|
if errors.Is(err, dbModel.ErrNoPermission) {
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("del movie error: %w", err),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func ClearMovies(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
|
|
var req model.ClearMoviesReq
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
if err := user.ClearRoomMoviesByParentID(room, req.ParentId); err != nil {
|
|
if errors.Is(err, dbModel.ErrNoPermission) {
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("clear movies error: %w", err),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func SwapMovie(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
|
|
req := model.SwapMovieReq{}
|
|
if err := model.Decode(ctx, &req); err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
if err := user.SwapRoomMoviePositions(room, req.Id1, req.Id2); err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func ChangeCurrentMovie(ctx *gin.Context) {
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
req := model.SetRoomCurrentMovieReq{}
|
|
err := model.Decode(ctx, &req)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
err = user.SetRoomCurrentMovie(room, req.Id, req.SubPath, true)
|
|
if err != nil {
|
|
log.Errorf("change current movie error: %v", err)
|
|
if errors.Is(err, dbModel.ErrNoPermission) {
|
|
ctx.AbortWithStatusJSON(
|
|
http.StatusForbidden,
|
|
model.NewApiErrorResp(
|
|
fmt.Errorf("change current movie error: %w", err),
|
|
),
|
|
)
|
|
return
|
|
}
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func ProxyMovie(ctx *gin.Context) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
if !settings.MovieProxy.Get() {
|
|
log.Errorf("movie proxy is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("movie proxy is not enabled"))
|
|
return
|
|
}
|
|
roomId := ctx.Param("roomId")
|
|
if roomId == "" {
|
|
log.Errorf("room id is empty")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("roomId is empty"))
|
|
return
|
|
}
|
|
|
|
room, err := op.LoadOrInitRoomByID(roomId)
|
|
if err != nil {
|
|
log.Errorf("load or init room by id error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
m, err := room.Value().GetMovieByID(ctx.Param("movieId"))
|
|
if err != nil {
|
|
log.Errorf("get movie by id error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
if m.Movie.MovieBase.VendorInfo.Vendor != "" {
|
|
proxyVendorMovie(ctx, m)
|
|
return
|
|
}
|
|
|
|
if !m.Movie.MovieBase.Proxy || m.Movie.MovieBase.Live || m.Movie.MovieBase.RtmpSource {
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
|
|
return
|
|
}
|
|
|
|
switch m.Movie.MovieBase.Type {
|
|
case "mpd":
|
|
// TODO: cache mpd file
|
|
fallthrough
|
|
default:
|
|
err = proxyURL(ctx, m.Movie.MovieBase.Url, m.Movie.MovieBase.Headers)
|
|
if err != nil {
|
|
log.Errorf("proxy movie error: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// only cache mpd file
|
|
// func initDashCache(ctx context.Context, movie *dbModel.Movie) func() (any, error) {
|
|
// return func() (any, error) {
|
|
// req, err := http.NewRequestWithContext(ctx, http.MethodGet, movie.Base.Url, nil)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// for k, v := range movie.Base.Headers {
|
|
// req.Header.Set(k, v)
|
|
// }
|
|
// req.Header.Set("User-Agent", utils.UA)
|
|
// resp, err := uhc.Do(req)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// defer resp.Body.Close()
|
|
// b, err := io.ReadAll(resp.Body)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// m, err := mpd.ReadFromString(string(b))
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// if len(m.BaseURL) != 0 && !path.IsAbs(m.BaseURL[0]) {
|
|
// result, err := url.JoinPath(path.Dir(movie.Base.Url), m.BaseURL[0])
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// m.BaseURL = []string{result}
|
|
// }
|
|
// s, err := m.WriteToString()
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
// return s, nil
|
|
// }
|
|
// }
|
|
|
|
func proxyURL(ctx *gin.Context, u string, headers map[string]string) error {
|
|
if !settings.AllowProxyToLocal.Get() {
|
|
if l, err := utils.ParseURLIsLocalIP(u); err != nil {
|
|
return fmt.Errorf("check url is local ip error: %w", err)
|
|
} else if l {
|
|
return errors.New("not allow proxy to local")
|
|
}
|
|
}
|
|
ctx2, cf := context.WithCancel(ctx)
|
|
defer cf()
|
|
req, err := http.NewRequestWithContext(ctx2, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("new request error: %w", err)
|
|
}
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
req.Header.Set("Range", ctx.GetHeader("Range"))
|
|
req.Header.Set("Accept-Encoding", ctx.GetHeader("Accept-Encoding"))
|
|
if req.Header.Get("User-Agent") == "" {
|
|
req.Header.Set("User-Agent", utils.UA)
|
|
}
|
|
resp, err := uhc.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request url error: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
ctx.Status(resp.StatusCode)
|
|
ctx.Header("Accept-Ranges", resp.Header.Get("Accept-Ranges"))
|
|
ctx.Header("Cache-Control", resp.Header.Get("Cache-Control"))
|
|
ctx.Header("Content-Length", resp.Header.Get("Content-Length"))
|
|
ctx.Header("Content-Range", resp.Header.Get("Content-Range"))
|
|
ctx.Header("Content-Type", resp.Header.Get("Content-Type"))
|
|
_, err = io.Copy(ctx.Writer, resp.Body)
|
|
if err != nil && err != io.EOF {
|
|
return fmt.Errorf("copy response body error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type FormatErrNotSupportFileType string
|
|
|
|
func (e FormatErrNotSupportFileType) Error() string {
|
|
return fmt.Sprintf("not support file type %s", string(e))
|
|
}
|
|
|
|
func JoinLive(ctx *gin.Context) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
token := ctx.MustGet("token").(string)
|
|
|
|
ctx.Header("Cache-Control", "no-store")
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
movieId := strings.Trim(ctx.Param("movieId"), "/")
|
|
m, err := room.GetMovieByID(movieId)
|
|
if err != nil {
|
|
log.Errorf("join live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
|
|
log.Error("join live error: rtmp is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
|
|
return
|
|
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
|
|
log.Error("join live error: live proxy is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
|
|
return
|
|
}
|
|
channel, err := m.Channel()
|
|
if err != nil {
|
|
log.Errorf("join live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
joinType := ctx.DefaultQuery("type", "auto")
|
|
if joinType == "auto" {
|
|
joinType = m.Movie.MovieBase.Type
|
|
}
|
|
switch joinType {
|
|
case "flv":
|
|
w := httpflv.NewHttpFLVWriter(ctx.Writer)
|
|
defer w.Close()
|
|
err = channel.AddPlayer(w)
|
|
if err != nil {
|
|
log.Errorf("join live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
_ = w.SendPacket()
|
|
case "m3u8":
|
|
b, err := channel.GenM3U8File(func(tsName string) (tsPath string) {
|
|
ext := "ts"
|
|
if settings.TsDisguisedAsPng.Get() {
|
|
ext = "png"
|
|
}
|
|
return fmt.Sprintf("/api/movie/live/hls/data/%s/%s/%s.%s?token=%s", room.ID, movieId, tsName, ext, token)
|
|
})
|
|
if err != nil {
|
|
log.Errorf("join live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Data(http.StatusOK, hls.M3U8ContentType, b)
|
|
default:
|
|
log.Errorf("join live error: %v", FormatErrNotSupportFileType(joinType))
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(fmt.Sprintf("not support join type: %s", joinType)))
|
|
return
|
|
}
|
|
}
|
|
|
|
func JoinFlvLive(ctx *gin.Context) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
ctx.Header("Cache-Control", "no-store")
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
movieId := strings.TrimSuffix(strings.Trim(ctx.Param("movieId"), "/"), ".flv")
|
|
m, err := room.GetMovieByID(movieId)
|
|
if err != nil {
|
|
log.Errorf("join flv live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
|
|
log.Error("join flv live error: rtmp is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
|
|
return
|
|
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
|
|
log.Error("join flv live error: live proxy is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
|
|
return
|
|
}
|
|
channel, err := m.Channel()
|
|
if err != nil {
|
|
log.Errorf("join flv live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
w := httpflv.NewHttpFLVWriter(ctx.Writer)
|
|
defer w.Close()
|
|
err = channel.AddPlayer(w)
|
|
if err != nil {
|
|
log.Errorf("join flv live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
_ = w.SendPacket()
|
|
}
|
|
|
|
func JoinHlsLive(ctx *gin.Context) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
token := ctx.MustGet("token").(string)
|
|
|
|
ctx.Header("Cache-Control", "no-store")
|
|
room := ctx.MustGet("room").(*op.RoomEntry).Value()
|
|
movieId := strings.TrimSuffix(strings.Trim(ctx.Param("movieId"), "/"), ".m3u8")
|
|
m, err := room.GetMovieByID(movieId)
|
|
if err != nil {
|
|
log.Errorf("join hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
|
|
log.Error("join hls live error: rtmp is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
|
|
return
|
|
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
|
|
log.Error("join hls live error: live proxy is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
|
|
return
|
|
}
|
|
channel, err := m.Channel()
|
|
if err != nil {
|
|
log.Errorf("join hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
b, err := channel.GenM3U8File(func(tsName string) (tsPath string) {
|
|
ext := "ts"
|
|
if settings.TsDisguisedAsPng.Get() {
|
|
ext = "png"
|
|
}
|
|
return fmt.Sprintf("/api/movie/live/hls/data/%s/%s/%s.%s?token=%s", room.ID, movieId, tsName, ext, token)
|
|
})
|
|
if err != nil {
|
|
log.Errorf("join hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Data(http.StatusOK, hls.M3U8ContentType, b)
|
|
}
|
|
|
|
func ServeHlsLive(ctx *gin.Context) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
ctx.Header("Cache-Control", "no-store")
|
|
roomId := ctx.Param("roomId")
|
|
roomE, err := op.LoadOrInitRoomByID(roomId)
|
|
if err != nil {
|
|
log.Errorf("serve hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
room := roomE.Value()
|
|
movieId := ctx.Param("movieId")
|
|
m, err := room.GetMovieByID(movieId)
|
|
if err != nil {
|
|
log.Errorf("serve hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if m.Movie.MovieBase.RtmpSource && !conf.Conf.Server.Rtmp.Enable {
|
|
log.Error("serve hls live error: rtmp is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("rtmp is not enabled"))
|
|
return
|
|
} else if m.Movie.MovieBase.Live && !settings.LiveProxy.Get() {
|
|
log.Error("serve hls live error: live proxy is not enabled")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("live proxy is not enabled"))
|
|
return
|
|
}
|
|
channel, err := m.Channel()
|
|
if err != nil {
|
|
log.Errorf("serve hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
dataId := ctx.Param("dataId")
|
|
switch fileExt := filepath.Ext(dataId); fileExt {
|
|
case ".ts":
|
|
if settings.TsDisguisedAsPng.Get() {
|
|
log.Errorf("serve hls live error: %v", FormatErrNotSupportFileType(fileExt))
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt)))
|
|
return
|
|
}
|
|
b, err := channel.GetTsFile(strings.TrimSuffix(dataId, fileExt))
|
|
if err != nil {
|
|
log.Errorf("serve hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Header("Cache-Control", "public, max-age=90")
|
|
ctx.Data(http.StatusOK, hls.TSContentType, b)
|
|
case ".png":
|
|
if !settings.TsDisguisedAsPng.Get() {
|
|
log.Errorf("serve hls live error: %v", FormatErrNotSupportFileType(fileExt))
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt)))
|
|
return
|
|
}
|
|
b, err := channel.GetTsFile(strings.TrimSuffix(dataId, fileExt))
|
|
if err != nil {
|
|
log.Errorf("serve hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Header("Cache-Control", "public, max-age=90")
|
|
img := image.NewGray(image.Rect(0, 0, 1, 1))
|
|
img.Set(1, 1, color.Gray{uint8(rand.Intn(255))})
|
|
cache := bytes.NewBuffer(make([]byte, 0, 71))
|
|
err = png.Encode(cache, img)
|
|
if err != nil {
|
|
log.Errorf("serve hls live error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Data(http.StatusOK, "image/png", append(cache.Bytes(), b...))
|
|
default:
|
|
ctx.Header("Cache-Control", "no-store")
|
|
log.Errorf("serve hls live error: %v", FormatErrNotSupportFileType(fileExt))
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt)))
|
|
}
|
|
}
|
|
|
|
func proxyVendorMovie(ctx *gin.Context, movie *op.Movie) {
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
|
|
switch movie.Movie.MovieBase.VendorInfo.Vendor {
|
|
case dbModel.VendorBilibili:
|
|
if movie.MovieBase.Live {
|
|
data, err := movie.BilibiliCache().Live.Get(ctx)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if len(data) == 0 {
|
|
log.Error("proxy vendor movie error: live data is empty")
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorStringResp("live data is empty"))
|
|
return
|
|
}
|
|
ctx.Data(http.StatusOK, "application/vnd.apple.mpegurl", data)
|
|
return
|
|
} else {
|
|
t := ctx.Query("t")
|
|
switch t {
|
|
case "", "hevc":
|
|
if !movie.Movie.MovieBase.Proxy {
|
|
log.Errorf("proxy vendor movie error: %v", "not support movie proxy")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
|
|
return
|
|
}
|
|
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
mpdC, err := movie.BilibiliCache().SharedMpd.Get(ctx, u.Value().BilibiliCache())
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if id := ctx.Query("id"); id == "" {
|
|
if t == "hevc" {
|
|
s, err := cache.BilibiliMpdToString(mpdC.HevcMpd, ctx.MustGet("token").(string))
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Data(http.StatusOK, "application/dash+xml", stream.StringToBytes(s))
|
|
} else {
|
|
s, err := cache.BilibiliMpdToString(mpdC.Mpd, ctx.MustGet("token").(string))
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
ctx.Data(http.StatusOK, "application/dash+xml", stream.StringToBytes(s))
|
|
}
|
|
return
|
|
} else {
|
|
streamId, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if streamId >= len(mpdC.Urls) {
|
|
log.Errorf("proxy vendor movie error: %v", "stream id out of range")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("stream id out of range"))
|
|
return
|
|
}
|
|
headers := maps.Clone(movie.Movie.MovieBase.Headers)
|
|
if headers == nil {
|
|
headers = map[string]string{
|
|
"Referer": "https://www.bilibili.com",
|
|
"User-Agent": utils.UA,
|
|
}
|
|
} else {
|
|
headers["Referer"] = "https://www.bilibili.com"
|
|
headers["User-Agent"] = utils.UA
|
|
}
|
|
err = proxyURL(ctx, mpdC.Urls[streamId], headers)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie [%s] error: %v", mpdC.Urls[streamId], err)
|
|
}
|
|
return
|
|
}
|
|
case "subtitle":
|
|
id := ctx.Query("n")
|
|
if id == "" {
|
|
log.Errorf("proxy vendor movie error: %v", "n is empty")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("n is empty"))
|
|
return
|
|
}
|
|
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
srtI, err := movie.BilibiliCache().Subtitle.Get(ctx, u.Value().BilibiliCache())
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if s, ok := srtI[id]; ok {
|
|
srtData, err := s.Srt.Get(ctx)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
http.ServeContent(ctx.Writer, ctx.Request, id, time.Now(), bytes.NewReader(srtData))
|
|
return
|
|
} else {
|
|
log.Errorf("proxy vendor movie error: %v", "subtitle not found")
|
|
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorStringResp("subtitle not found"))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
case dbModel.VendorAlist:
|
|
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
data, err := movie.AlistCache().Get(ctx, &cache.AlistMovieCacheFuncArgs{
|
|
UserCache: u.Value().AlistCache(),
|
|
UserAgent: utils.UA,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
switch data.Provider {
|
|
case cache.AlistProviderAli:
|
|
t := ctx.Query("t")
|
|
switch t {
|
|
case "":
|
|
ctx.Data(http.StatusOK, "audio/mpegurl", data.Ali.M3U8ListFile)
|
|
return
|
|
case "subtitle":
|
|
idS := ctx.Query("id")
|
|
if idS == "" {
|
|
log.Errorf("proxy vendor movie error: %v", "id is empty")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id is empty"))
|
|
return
|
|
}
|
|
id, err := strconv.Atoi(idS)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if id >= len(data.Subtitles) {
|
|
log.Errorf("proxy vendor movie error: %v", "id out of range")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range"))
|
|
return
|
|
}
|
|
b, err := data.Subtitles[id].Cache.Get(ctx)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
http.ServeContent(ctx.Writer, ctx.Request, data.Subtitles[id].Name, time.Now(), bytes.NewReader(b))
|
|
}
|
|
|
|
case cache.AlistProvider115:
|
|
fallthrough
|
|
default:
|
|
if !movie.Movie.MovieBase.Proxy {
|
|
log.Errorf("proxy vendor movie error: %v", "not support movie proxy")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
|
|
return
|
|
} else {
|
|
err = proxyURL(ctx, data.URL, nil)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
}
|
|
}
|
|
|
|
}
|
|
return
|
|
|
|
case dbModel.VendorEmby:
|
|
t := ctx.Query("t")
|
|
switch t {
|
|
case "":
|
|
if !movie.Movie.MovieBase.Proxy {
|
|
log.Errorf("proxy vendor movie error: %v", "not support movie proxy")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("not support movie proxy"))
|
|
return
|
|
}
|
|
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
embyC, err := movie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
source, err := strconv.Atoi(ctx.Query("source"))
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if source >= len(embyC.Sources) {
|
|
log.Errorf("proxy vendor movie error: %v", "source out of range")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("source out of range"))
|
|
return
|
|
}
|
|
id, err := strconv.Atoi(ctx.Query("id"))
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if id >= len(embyC.Sources[source].URL) {
|
|
log.Errorf("proxy vendor movie error: %v", "id out of range")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range"))
|
|
return
|
|
}
|
|
if embyC.Sources[source].IsTranscode {
|
|
ctx.Redirect(http.StatusFound, embyC.Sources[source].URL)
|
|
return
|
|
}
|
|
err = proxyURL(ctx, embyC.Sources[source].URL, nil)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
}
|
|
return
|
|
|
|
case "subtitle":
|
|
u, err := op.LoadOrInitUserByID(movie.Movie.CreatorID)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
embyC, err := movie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
source, err := strconv.Atoi(ctx.Query("source"))
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if source >= len(embyC.Sources) {
|
|
log.Errorf("proxy vendor movie error: %v", "source out of range")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("source out of range"))
|
|
return
|
|
}
|
|
id, err := strconv.Atoi(ctx.Query("id"))
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
if id >= len(embyC.Sources[source].Subtitles) {
|
|
log.Errorf("proxy vendor movie error: %v", "id out of range")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("id out of range"))
|
|
return
|
|
}
|
|
data, err := embyC.Sources[source].Subtitles[id].Cache.Get(ctx)
|
|
if err != nil {
|
|
log.Errorf("proxy vendor movie error: %v", err)
|
|
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
http.ServeContent(ctx.Writer, ctx.Request, embyC.Sources[source].Subtitles[id].Name, time.Now(), bytes.NewReader(data))
|
|
return
|
|
}
|
|
|
|
default:
|
|
log.Errorf("proxy vendor movie error: %v", "vendor not support proxy")
|
|
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support proxy"))
|
|
return
|
|
}
|
|
}
|
|
|
|
// user is the api requester
|
|
func genVendorMovie(ctx context.Context, user *op.User, opMovie *op.Movie, userAgent, userToken string) (*dbModel.Movie, error) {
|
|
movie := *opMovie.Movie
|
|
var err error
|
|
switch movie.MovieBase.VendorInfo.Vendor {
|
|
case dbModel.VendorBilibili:
|
|
if movie.IsFolder {
|
|
return nil, fmt.Errorf("bilibili folder not support")
|
|
}
|
|
|
|
bmc := opMovie.BilibiliCache()
|
|
if movie.MovieBase.Live {
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
|
|
movie.MovieBase.Type = "m3u8"
|
|
return &movie, nil
|
|
} else {
|
|
if !movie.MovieBase.Proxy {
|
|
var s string
|
|
if movie.MovieBase.VendorInfo.Bilibili.Shared {
|
|
var u *op.UserEntry
|
|
u, err = op.LoadOrInitUserByID(movie.CreatorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s, err = opMovie.BilibiliCache().NoSharedMovie.LoadOrStore(ctx, movie.CreatorID, u.Value().BilibiliCache())
|
|
} else {
|
|
s, err = opMovie.BilibiliCache().NoSharedMovie.LoadOrStore(ctx, user.ID, user.BilibiliCache())
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
movie.MovieBase.Url = s
|
|
} else {
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
|
|
movie.MovieBase.Type = "mpd"
|
|
movie.MovieBase.MoreSources = []*dbModel.MoreSource{
|
|
{
|
|
Name: "hevc",
|
|
Type: "mpd",
|
|
Url: fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s&t=hevc", movie.RoomID, movie.ID, userToken),
|
|
},
|
|
}
|
|
}
|
|
srt, err := bmc.Subtitle.Get(ctx, user.BilibiliCache())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for k := range srt {
|
|
if movie.MovieBase.Subtitles == nil {
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(srt))
|
|
}
|
|
movie.MovieBase.Subtitles[k] = &dbModel.Subtitle{
|
|
URL: fmt.Sprintf("/api/movie/proxy/%s/%s?t=subtitle&n=%s&token=%s", movie.RoomID, movie.ID, k, userToken),
|
|
Type: "srt",
|
|
}
|
|
}
|
|
return &movie, nil
|
|
}
|
|
|
|
case dbModel.VendorAlist:
|
|
creator, err := op.LoadOrInitUserByID(movie.CreatorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alistCache := opMovie.AlistCache()
|
|
data, err := alistCache.Get(ctx, &cache.AlistMovieCacheFuncArgs{
|
|
UserCache: creator.Value().AlistCache(),
|
|
UserAgent: utils.UA,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, subt := range data.Subtitles {
|
|
if movie.MovieBase.Subtitles == nil {
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
|
|
}
|
|
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
|
|
URL: subt.URL,
|
|
Type: subt.Type,
|
|
}
|
|
}
|
|
|
|
switch data.Provider {
|
|
case cache.AlistProviderAli:
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
|
|
movie.MovieBase.Type = "m3u8"
|
|
|
|
for i, subt := range data.Subtitles {
|
|
if movie.MovieBase.Subtitles == nil {
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
|
|
}
|
|
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
|
|
URL: fmt.Sprintf("/api/movie/proxy/%s/%s?t=subtitle&id=%d&token=%s", movie.RoomID, movie.ID, i, userToken),
|
|
Type: subt.Type,
|
|
}
|
|
}
|
|
|
|
case cache.AlistProvider115:
|
|
if movie.MovieBase.Proxy {
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
|
|
movie.MovieBase.Type = utils.GetUrlExtension(data.URL)
|
|
|
|
// TODO: proxy subtitle
|
|
} else {
|
|
data, err = alistCache.GetRefreshFunc()(ctx, &cache.AlistMovieCacheFuncArgs{
|
|
UserCache: creator.Value().AlistCache(),
|
|
UserAgent: userAgent,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("refresh 115 movie cache error: %w", err)
|
|
}
|
|
movie.MovieBase.Url = data.URL
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Subtitles))
|
|
for _, subt := range data.Subtitles {
|
|
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
|
|
URL: subt.URL,
|
|
Type: subt.Type,
|
|
}
|
|
}
|
|
}
|
|
|
|
default:
|
|
if !movie.MovieBase.Proxy {
|
|
movie.MovieBase.Url = data.URL
|
|
} else {
|
|
movie.MovieBase.Url = fmt.Sprintf("/api/movie/proxy/%s/%s?token=%s", movie.RoomID, movie.ID, userToken)
|
|
movie.MovieBase.Type = utils.GetUrlExtension(data.URL)
|
|
}
|
|
}
|
|
|
|
movie.MovieBase.VendorInfo.Alist.Password = ""
|
|
return &movie, nil
|
|
|
|
case dbModel.VendorEmby:
|
|
u, err := op.LoadOrInitUserByID(movie.CreatorID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := opMovie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !movie.MovieBase.Proxy {
|
|
if len(data.Sources) == 0 {
|
|
return nil, errors.New("no source")
|
|
}
|
|
movie.MovieBase.Url = data.Sources[0].URL
|
|
for _, s := range data.Sources[0].Subtitles {
|
|
if movie.MovieBase.Subtitles == nil {
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(data.Sources[0].Subtitles))
|
|
}
|
|
movie.MovieBase.Subtitles[s.Name] = &dbModel.Subtitle{
|
|
URL: s.URL,
|
|
Type: s.Type,
|
|
}
|
|
}
|
|
for _, s := range data.Sources[1:] {
|
|
movie.MovieBase.MoreSources = append(movie.MovieBase.MoreSources,
|
|
&dbModel.MoreSource{
|
|
Name: s.Name,
|
|
Url: s.URL,
|
|
},
|
|
)
|
|
|
|
for _, subt := range s.Subtitles {
|
|
if movie.MovieBase.Subtitles == nil {
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(s.Subtitles))
|
|
}
|
|
movie.MovieBase.Subtitles[subt.Name] = &dbModel.Subtitle{
|
|
URL: subt.URL,
|
|
Type: subt.Type,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for si, es := range data.Sources {
|
|
if len(es.URL) == 0 {
|
|
if si != len(data.Sources)-1 {
|
|
continue
|
|
}
|
|
if movie.MovieBase.Url == "" {
|
|
return nil, errors.New("no source")
|
|
}
|
|
}
|
|
|
|
rawPath, err := url.JoinPath("/api/movie/proxy", movie.RoomID, movie.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rawQuery := url.Values{}
|
|
rawQuery.Set("source", strconv.Itoa(si))
|
|
rawQuery.Set("token", userToken)
|
|
u := url.URL{
|
|
Path: rawPath,
|
|
RawQuery: rawQuery.Encode(),
|
|
}
|
|
movie.MovieBase.Url = u.String()
|
|
movie.MovieBase.Type = utils.GetUrlExtension(es.URL)
|
|
|
|
if len(es.Subtitles) == 0 {
|
|
continue
|
|
}
|
|
for sbi, s := range es.Subtitles {
|
|
if movie.MovieBase.Subtitles == nil {
|
|
movie.MovieBase.Subtitles = make(map[string]*dbModel.Subtitle, len(es.Subtitles))
|
|
}
|
|
rawQuery := url.Values{}
|
|
rawQuery.Set("t", "subtitle")
|
|
rawQuery.Set("source", strconv.Itoa(si))
|
|
rawQuery.Set("id", strconv.Itoa(sbi))
|
|
rawQuery.Set("token", userToken)
|
|
u := url.URL{
|
|
Path: rawPath,
|
|
RawQuery: rawQuery.Encode(),
|
|
}
|
|
movie.MovieBase.Subtitles[s.Name] = &dbModel.Subtitle{
|
|
URL: u.String(),
|
|
Type: s.Type,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return &movie, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("vendor not implement gen movie url")
|
|
}
|
|
}
|