mirror of https://github.com/synctv-org/synctv
parent
b935a0e664
commit
9edef8d683
@ -0,0 +1,26 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package sysnotify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (sn *SysNotify) Init() {
|
||||
sn.c = make(chan os.Signal, 1)
|
||||
signal.Notify(sn.c, syscall.SIGHUP /*1*/, syscall.SIGINT /*2*/, syscall.SIGQUIT /*3*/, syscall.SIGTERM /*15*/, syscall.SIGUSR1 /*10*/, syscall.SIGUSR2 /*12*/)
|
||||
}
|
||||
|
||||
func parseSysNotifyType(s os.Signal) NotifyType {
|
||||
switch s {
|
||||
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM:
|
||||
return NotifyTypeEXIT
|
||||
case syscall.SIGUSR1, syscall.SIGUSR2:
|
||||
return NotifyTypeRELOAD
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package sysnotify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (sn *SysNotify) Init() {
|
||||
sn.c = make(chan os.Signal, 1)
|
||||
signal.Notify(sn.c, syscall.SIGHUP /*1*/, syscall.SIGINT /*2*/, syscall.SIGQUIT /*3*/, syscall.SIGTERM /*15*/)
|
||||
}
|
||||
|
||||
func parseSysNotifyType(s os.Signal) NotifyType {
|
||||
switch s {
|
||||
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM:
|
||||
return NotifyTypeEXIT
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package sysnotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zijiren233/gencontainer/pqueue"
|
||||
"github.com/zijiren233/gencontainer/rwmap"
|
||||
)
|
||||
|
||||
var sysNotify SysNotify
|
||||
|
||||
func Init() {
|
||||
sysNotify.Init()
|
||||
}
|
||||
|
||||
func RegisterSysNotifyTask(priority int, task *Task) error {
|
||||
return sysNotify.RegisterSysNotifyTask(priority, task)
|
||||
}
|
||||
|
||||
func WaitCbk() {
|
||||
sysNotify.WaitCbk()
|
||||
}
|
||||
|
||||
type SysNotify struct {
|
||||
c chan os.Signal
|
||||
taskGroup rwmap.RWMap[NotifyType, *taskQueue]
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
type NotifyType int
|
||||
|
||||
const (
|
||||
NotifyTypeEXIT NotifyType = iota + 1
|
||||
NotifyTypeRELOAD
|
||||
)
|
||||
|
||||
type taskQueue struct {
|
||||
notifyTaskQueue *pqueue.PQueue[*Task]
|
||||
notifyTaskLock sync.Mutex
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Task func() error
|
||||
Name string
|
||||
NotifyType NotifyType
|
||||
}
|
||||
|
||||
func NewSysNotifyTask(name string, notifyType NotifyType, task func() error) *Task {
|
||||
return &Task{
|
||||
Name: name,
|
||||
NotifyType: notifyType,
|
||||
Task: task,
|
||||
}
|
||||
}
|
||||
|
||||
func runTask(tq *taskQueue) {
|
||||
tq.notifyTaskLock.Lock()
|
||||
defer tq.notifyTaskLock.Unlock()
|
||||
for tq.notifyTaskQueue.Len() > 0 {
|
||||
_, task := tq.notifyTaskQueue.Pop()
|
||||
func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Errorf("task: %s panic has returned: %v", task.Name, err)
|
||||
}
|
||||
}()
|
||||
log.Infof("task: %s running", task.Name)
|
||||
if err := task.Task(); err != nil {
|
||||
log.Errorf("task: %s an error occurred: %v", task.Name, err)
|
||||
}
|
||||
log.Infof("task: %s done", task.Name)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (sn *SysNotify) RegisterSysNotifyTask(priority int, task *Task) error {
|
||||
if task == nil || task.Task == nil {
|
||||
return errors.New("task is nil")
|
||||
}
|
||||
if task.NotifyType == 0 {
|
||||
panic("task notify type is 0")
|
||||
}
|
||||
tasks, _ := sn.taskGroup.LoadOrStore(task.NotifyType, &taskQueue{
|
||||
notifyTaskQueue: pqueue.NewMinPriorityQueue[*Task](),
|
||||
})
|
||||
tasks.notifyTaskLock.Lock()
|
||||
defer tasks.notifyTaskLock.Unlock()
|
||||
tasks.notifyTaskQueue.Push(priority, task)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sn *SysNotify) waitCbk() {
|
||||
log.Info("wait sys notify")
|
||||
for s := range sn.c {
|
||||
log.Infof("receive sys notify: %v", s)
|
||||
switch parseSysNotifyType(s) {
|
||||
case NotifyTypeEXIT:
|
||||
tq, ok := sn.taskGroup.Load(NotifyTypeEXIT)
|
||||
if ok {
|
||||
log.Info("task: NotifyTypeEXIT running...")
|
||||
runTask(tq)
|
||||
}
|
||||
return
|
||||
case NotifyTypeRELOAD:
|
||||
tq, ok := sn.taskGroup.Load(NotifyTypeRELOAD)
|
||||
if ok {
|
||||
log.Info("task: NotifyTypeRELOAD running...")
|
||||
runTask(tq)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("task: all done")
|
||||
}
|
||||
|
||||
func (sn *SysNotify) WaitCbk() {
|
||||
sn.once.Do(sn.waitCbk)
|
||||
}
|
@ -0,0 +1,364 @@
|
||||
package vendoralist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"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/db"
|
||||
dbModel "github.com/synctv-org/synctv/internal/model"
|
||||
"github.com/synctv-org/synctv/internal/op"
|
||||
"github.com/synctv-org/synctv/internal/vendor"
|
||||
"github.com/synctv-org/synctv/server/handlers/proxy"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/synctv-org/vendors/api/alist"
|
||||
)
|
||||
|
||||
type AlistVendorService struct {
|
||||
room *op.Room
|
||||
movie *op.Movie
|
||||
}
|
||||
|
||||
func NewAlistVendorService(room *op.Room, movie *op.Movie) (*AlistVendorService, error) {
|
||||
if movie.VendorInfo.Vendor != dbModel.VendorAlist {
|
||||
return nil, fmt.Errorf("alist vendor not support vendor %s", movie.MovieBase.VendorInfo.Vendor)
|
||||
}
|
||||
return &AlistVendorService{
|
||||
room: room,
|
||||
movie: movie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) Client() alist.AlistHTTPServer {
|
||||
return vendor.LoadAlistClient(s.movie.VendorInfo.Backend)
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) ListDynamicMovie(ctx context.Context, reqUser *op.User, subPath string, page, _max int) (*model.MovieList, error) {
|
||||
if reqUser.ID != s.movie.CreatorID {
|
||||
return nil, fmt.Errorf("list vendor dynamic folder error: %w", dbModel.ErrNoPermission)
|
||||
}
|
||||
user := reqUser
|
||||
|
||||
resp := &model.MovieList{
|
||||
Paths: []*model.MoviePath{},
|
||||
}
|
||||
|
||||
serverID, truePath, err := s.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, errors.New("sub path is not in parent path")
|
||||
}
|
||||
truePath = newPath
|
||||
aucd, err := user.AlistCache().LoadOrStore(ctx, serverID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
return nil, errors.New("alist server not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.Client().FsList(ctx, &alist.FsListReq{
|
||||
Token: aucd.Token,
|
||||
Password: s.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: s.movie.ID,
|
||||
CreatedAt: s.movie.CreatedAt.UnixMilli(),
|
||||
Creator: op.GetUserName(s.movie.CreatorID),
|
||||
CreatorID: s.movie.CreatorID,
|
||||
SubPath: "/" + strings.Trim(fmt.Sprintf("%s/%s", subPath, flr.Name), "/"),
|
||||
Base: dbModel.MovieBase{
|
||||
Name: flr.Name,
|
||||
IsFolder: flr.IsDir,
|
||||
ParentID: dbModel.EmptyNullString(s.movie.ID),
|
||||
VendorInfo: dbModel.VendorInfo{
|
||||
Vendor: dbModel.VendorAlist,
|
||||
Backend: s.movie.VendorInfo.Backend,
|
||||
Alist: &dbModel.AlistStreamingInfo{
|
||||
Path: dbModel.FormatAlistPath(serverID,
|
||||
"/"+strings.Trim(fmt.Sprintf("%s/%s", truePath, flr.Name), "/"),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
resp.Paths = model.GenDefaultSubPaths(s.movie.ID, subPath, true)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) ProxyMovie(ctx *gin.Context) {
|
||||
log := ctx.MustGet("log").(*logrus.Entry)
|
||||
|
||||
// Get user and cache data
|
||||
data, err := s.getUserAndCacheData(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different providers
|
||||
switch data.Provider {
|
||||
case cache.AlistProviderAli:
|
||||
s.handleAliProvider(ctx, log, data)
|
||||
case cache.AlistProvider115:
|
||||
fallthrough
|
||||
default:
|
||||
s.handleDefaultProvider(ctx, log, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) getUserAndCacheData(ctx *gin.Context) (*cache.AlistMovieCacheData, error) {
|
||||
u, err := op.LoadOrInitUserByID(s.movie.Movie.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := s.movie.AlistCache().Get(ctx, &cache.AlistMovieCacheFuncArgs{
|
||||
UserCache: u.Value().AlistCache(),
|
||||
UserAgent: utils.UA,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) handleAliProvider(ctx *gin.Context, log *logrus.Entry, data *cache.AlistMovieCacheData) {
|
||||
t := ctx.Query("t")
|
||||
switch t {
|
||||
case "":
|
||||
ctx.Data(http.StatusOK, "audio/mpegurl", data.Ali.M3U8ListFile)
|
||||
case "raw":
|
||||
s.proxyURL(ctx, log, data.URL)
|
||||
case "subtitle":
|
||||
s.handleSubtitle(ctx, log, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) handleDefaultProvider(ctx *gin.Context, log *logrus.Entry, data *cache.AlistMovieCacheData) {
|
||||
if !s.movie.Movie.MovieBase.Proxy {
|
||||
log.Errorf("proxy vendor movie error: %v", "proxy is not enabled")
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp("proxy is not enabled"))
|
||||
return
|
||||
}
|
||||
s.proxyURL(ctx, log, data.URL)
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) proxyURL(ctx *gin.Context, log *logrus.Entry, url string) {
|
||||
err := proxy.AutoProxyURL(ctx,
|
||||
url,
|
||||
s.movie.MovieBase.Type,
|
||||
nil,
|
||||
ctx.GetString("token"),
|
||||
s.movie.RoomID,
|
||||
s.movie.ID,
|
||||
proxy.WithProxyURLCache(true),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) handleSubtitle(ctx *gin.Context, log *logrus.Entry, data *cache.AlistMovieCacheData) {
|
||||
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))
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) GenMovieInfo(ctx context.Context, user *op.User, userAgent, userToken string) (*dbModel.Movie, error) {
|
||||
if s.movie.Proxy {
|
||||
return s.GenProxyMovieInfo(ctx, user, userAgent, userToken)
|
||||
}
|
||||
|
||||
movie := s.movie.Clone()
|
||||
var err error
|
||||
|
||||
creator, err := op.LoadOrInitUserByID(movie.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alistCache := s.movie.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/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = "m3u8"
|
||||
|
||||
rawStreamURL := data.URL
|
||||
movie.MovieBase.MoreSources = []*dbModel.MoreSource{
|
||||
{
|
||||
Name: "raw",
|
||||
Type: utils.GetURLExtension(movie.MovieBase.VendorInfo.Alist.Path),
|
||||
URL: rawStreamURL,
|
||||
},
|
||||
}
|
||||
|
||||
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/room/movie/proxy/%s?t=subtitle&id=%d&token=%s&roomId=%s", movie.ID, i, userToken, movie.RoomID),
|
||||
Type: subt.Type,
|
||||
}
|
||||
}
|
||||
|
||||
case cache.AlistProvider115:
|
||||
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:
|
||||
movie.MovieBase.URL = data.URL
|
||||
}
|
||||
|
||||
movie.MovieBase.VendorInfo.Alist.Password = ""
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
func (s *AlistVendorService) GenProxyMovieInfo(ctx context.Context, user *op.User, userAgent, userToken string) (*dbModel.Movie, error) {
|
||||
movie := s.movie.Clone()
|
||||
var err error
|
||||
|
||||
creator, err := op.LoadOrInitUserByID(movie.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alistCache := s.movie.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/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = "m3u8"
|
||||
|
||||
rawStreamURL := fmt.Sprintf("/api/room/movie/proxy/%s?t=raw&token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.MoreSources = []*dbModel.MoreSource{
|
||||
{
|
||||
Name: "raw",
|
||||
Type: utils.GetURLExtension(movie.MovieBase.VendorInfo.Alist.Path),
|
||||
URL: rawStreamURL,
|
||||
},
|
||||
}
|
||||
|
||||
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/room/movie/proxy/%s?t=subtitle&id=%d&token=%s&roomId=%s", movie.ID, i, userToken, movie.RoomID),
|
||||
Type: subt.Type,
|
||||
}
|
||||
}
|
||||
|
||||
case cache.AlistProvider115:
|
||||
movie.MovieBase.URL = fmt.Sprintf("/api/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = utils.GetURLExtension(data.URL)
|
||||
|
||||
// TODO: proxy subtitle
|
||||
|
||||
default:
|
||||
movie.MovieBase.URL = fmt.Sprintf("/api/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = utils.GetURLExtension(data.URL)
|
||||
}
|
||||
|
||||
movie.MovieBase.VendorInfo.Alist.Password = ""
|
||||
return movie, nil
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
package vendoralist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
"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/vendor"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/synctv-org/vendors/api/alist"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
Path string `json:"path"`
|
||||
Password string `json:"password"`
|
||||
Refresh bool `json:"refresh"`
|
||||
}
|
||||
|
||||
func (r *ListReq) Validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
type AlistFileItem struct {
|
||||
*model.Item
|
||||
Size uint64 `json:"size"`
|
||||
Modified uint64 `json:"modified"`
|
||||
}
|
||||
|
||||
type AlistFSListResp = model.VendorFSListResp[*AlistFileItem]
|
||||
|
||||
func List(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
req := ListReq{}
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
page, size, err := utils.GetPageAndMax(ctx)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Path == "" {
|
||||
socpes := [](func(*gorm.DB) *gorm.DB){
|
||||
db.OrderByCreatedAtAsc,
|
||||
}
|
||||
|
||||
total, err := db.GetAlistVendorsCount(user.ID, socpes...)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
if total == 0 {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("alist server not found"))
|
||||
return
|
||||
}
|
||||
|
||||
ev, err := db.GetAlistVendors(user.ID, append(socpes, db.Paginate(page, size))...)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("alist server not found"))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if total == 1 {
|
||||
req.Path = ev[0].ServerID + "/"
|
||||
goto AlistFSListResp
|
||||
}
|
||||
|
||||
resp := AlistFSListResp{
|
||||
Paths: []*model.Path{
|
||||
{
|
||||
Name: "",
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
Total: uint64(total),
|
||||
}
|
||||
|
||||
for _, evi := range ev {
|
||||
resp.Items = append(resp.Items, &AlistFileItem{
|
||||
Item: &model.Item{
|
||||
Name: evi.Host,
|
||||
Path: evi.ServerID + `/`,
|
||||
IsDir: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
AlistFSListResp:
|
||||
|
||||
var serverID string
|
||||
serverID, req.Path, err = dbModel.GetAlistServerIDFromPath(req.Path)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.Path, "/") {
|
||||
req.Path = "/" + req.Path
|
||||
}
|
||||
|
||||
aucd, err := user.AlistCache().LoadOrStore(ctx, serverID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("alist server not found"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
cli := vendor.LoadAlistClient(ctx.Query("backend"))
|
||||
data, err := cli.FsList(ctx, &alist.FsListReq{
|
||||
Token: aucd.Token,
|
||||
Password: req.Password,
|
||||
Path: req.Path,
|
||||
Host: aucd.Host,
|
||||
Refresh: req.Refresh,
|
||||
Page: uint64(page),
|
||||
PerPage: uint64(size),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req.Path = strings.Trim(req.Path, "/")
|
||||
resp := AlistFSListResp{
|
||||
Total: data.Total,
|
||||
Paths: model.GenDefaultPaths(req.Path, true,
|
||||
&model.Path{
|
||||
Name: "",
|
||||
Path: "",
|
||||
},
|
||||
&model.Path{
|
||||
Name: aucd.Host,
|
||||
Path: aucd.ServerID + "/",
|
||||
}),
|
||||
}
|
||||
for _, flr := range data.Content {
|
||||
resp.Items = append(resp.Items, &AlistFileItem{
|
||||
Item: &model.Item{
|
||||
Name: flr.Name,
|
||||
Path: fmt.Sprintf("%s/%s", aucd.ServerID, strings.Trim(fmt.Sprintf("%s/%s", req.Path, flr.Name), "/")),
|
||||
IsDir: flr.IsDir,
|
||||
},
|
||||
Size: flr.Size,
|
||||
Modified: flr.Modified,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&resp))
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package vendoralist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/internal/cache"
|
||||
"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/server/model"
|
||||
)
|
||||
|
||||
type LoginReq struct {
|
||||
Host string `json:"host"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
HashedPassword string `json:"hashedPassword"`
|
||||
}
|
||||
|
||||
func (r *LoginReq) Validate() error {
|
||||
if r.Host == "" {
|
||||
return errors.New("host is required")
|
||||
}
|
||||
url, err := url.Parse(r.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if url.Scheme != "http" && url.Scheme != "https" {
|
||||
return errors.New("host is invalid")
|
||||
}
|
||||
r.Host = strings.TrimRight(url.String(), "/")
|
||||
if r.Password != "" && r.HashedPassword != "" {
|
||||
return errors.New("password and hashedPassword can't be both set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LoginReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
func Login(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
req := LoginReq{}
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(req.Password + `-https://github.com/alist-org/alist`))
|
||||
req.HashedPassword = hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
backend := ctx.Query("backend")
|
||||
|
||||
data, err := cache.AlistAuthorizationCacheWithConfigInitFunc(ctx, &dbModel.AlistVendor{
|
||||
Host: req.Host,
|
||||
Username: req.Username,
|
||||
HashedPassword: []byte(req.HashedPassword),
|
||||
Backend: backend,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.CreateOrSaveAlistVendor(&dbModel.AlistVendor{
|
||||
UserID: user.ID,
|
||||
ServerID: data.ServerID,
|
||||
Backend: data.Backend,
|
||||
Host: data.Host,
|
||||
Username: req.Username,
|
||||
HashedPassword: []byte(req.HashedPassword),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = user.AlistCache().StoreOrRefreshWithDynamicFunc(ctx, data.ServerID, func(ctx context.Context, key string, args ...struct{}) (*cache.AlistUserCacheData, error) {
|
||||
return data, nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func Logout(ctx *gin.Context) {
|
||||
log := ctx.MustGet("log").(*logrus.Entry)
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
var req model.ServerIDReq
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
err := db.DeleteAlistVendor(user.ID, req.ServerID)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if rc, ok := user.AlistCache().LoadCache(req.ServerID); ok {
|
||||
err = rc.Clear(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("clear alist cache error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package vendoralist
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/internal/db"
|
||||
"github.com/synctv-org/synctv/internal/op"
|
||||
"github.com/synctv-org/synctv/internal/vendor"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/vendors/api/alist"
|
||||
)
|
||||
|
||||
type AlistMeResp = model.VendorMeResp[*alist.MeResp]
|
||||
|
||||
func Me(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
serverID := ctx.Query("serverID")
|
||||
if serverID == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(errors.New("serverID is required")))
|
||||
return
|
||||
}
|
||||
|
||||
aucd, err := user.AlistCache().LoadOrStore(ctx, serverID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("alist server not found"))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := vendor.LoadAlistClient(aucd.Backend).Me(ctx, &alist.MeReq{
|
||||
Host: aucd.Host,
|
||||
Token: aucd.Token,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&AlistMeResp{
|
||||
IsLogin: true,
|
||||
Info: resp,
|
||||
}))
|
||||
}
|
||||
|
||||
type AlistBindsResp []*struct {
|
||||
ServerID string `json:"serverId"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
func Binds(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
ev, err := db.GetAlistVendors(user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&AlistMeResp{
|
||||
IsLogin: false,
|
||||
}))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
resp := make(AlistBindsResp, len(ev))
|
||||
for i, v := range ev {
|
||||
resp[i] = &struct {
|
||||
ServerID string `json:"serverId"`
|
||||
Host string `json:"host"`
|
||||
}{
|
||||
ServerID: v.ServerID,
|
||||
Host: v.Host,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
package vendorbilibili
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/internal/cache"
|
||||
dbModel "github.com/synctv-org/synctv/internal/model"
|
||||
"github.com/synctv-org/synctv/internal/op"
|
||||
"github.com/synctv-org/synctv/internal/vendor"
|
||||
"github.com/synctv-org/synctv/server/handlers/proxy"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/synctv-org/vendors/api/bilibili"
|
||||
"github.com/zijiren233/stream"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type BilibiliVendorService struct {
|
||||
room *op.Room
|
||||
movie *op.Movie
|
||||
}
|
||||
|
||||
func NewBilibiliVendorService(room *op.Room, movie *op.Movie) (*BilibiliVendorService, error) {
|
||||
if movie.VendorInfo.Vendor != dbModel.VendorBilibili {
|
||||
return nil, fmt.Errorf("bilibili vendor not support vendor %s", movie.MovieBase.VendorInfo.Vendor)
|
||||
}
|
||||
return &BilibiliVendorService{
|
||||
room: room,
|
||||
movie: movie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) Client() bilibili.BilibiliHTTPServer {
|
||||
return vendor.LoadBilibiliClient(s.movie.VendorInfo.Backend)
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) ListDynamicMovie(ctx context.Context, reqUser *op.User, subPath string, page, _max int) (*model.MovieList, error) {
|
||||
return nil, errors.New("bilibili vendor not support list dynamic movie")
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) ProxyMovie(ctx *gin.Context) {
|
||||
log := ctx.MustGet("log").(*logrus.Entry)
|
||||
|
||||
if s.movie.MovieBase.Live {
|
||||
s.handleLiveProxy(ctx, log)
|
||||
return
|
||||
}
|
||||
|
||||
t := ctx.Query("t")
|
||||
switch t {
|
||||
case "", "hevc":
|
||||
s.handleVideoProxy(ctx, log, t)
|
||||
case "subtitle":
|
||||
s.handleSubtitleProxy(ctx, log)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) handleLiveProxy(ctx *gin.Context, log *logrus.Entry) {
|
||||
data, err := s.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)
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) handleVideoProxy(ctx *gin.Context, log *logrus.Entry, t string) {
|
||||
if !s.movie.Movie.MovieBase.Proxy {
|
||||
log.Errorf("proxy vendor movie error: %v", "proxy is not enabled")
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp("proxy is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
u, err := op.LoadOrInitUserByID(s.movie.Movie.CreatorID)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
mpdC, err := s.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
|
||||
}
|
||||
|
||||
id := ctx.Query("id")
|
||||
if id == "" {
|
||||
s.handleMpdProxy(ctx, log, t, mpdC)
|
||||
return
|
||||
}
|
||||
|
||||
s.handleStreamProxy(ctx, log, id, mpdC)
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) handleMpdProxy(ctx *gin.Context, log *logrus.Entry, t string, mpdC *cache.BilibiliMpdCache) {
|
||||
var mpd string
|
||||
var err error
|
||||
if t == "hevc" {
|
||||
mpd, err = cache.BilibiliMpdToString(mpdC.HevcMpd, ctx.MustGet("token").(string))
|
||||
} else {
|
||||
mpd, 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(mpd))
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) handleStreamProxy(ctx *gin.Context, log *logrus.Entry, id string, mpdC *cache.BilibiliMpdCache) {
|
||||
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 := s.getProxyHeaders()
|
||||
err = proxy.URL(ctx,
|
||||
mpdC.URLs[streamID],
|
||||
headers,
|
||||
proxy.WithProxyURLCache(true),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie [%s] error: %v", mpdC.URLs[streamID], err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) getProxyHeaders() map[string]string {
|
||||
headers := maps.Clone(s.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
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) handleSubtitleProxy(ctx *gin.Context, log *logrus.Entry) {
|
||||
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(s.movie.Movie.CreatorID)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
srtI, err := s.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
|
||||
}
|
||||
|
||||
log.Errorf("proxy vendor movie error: %v", "subtitle not found")
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewAPIErrorStringResp("subtitle not found"))
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) GenMovieInfo(ctx context.Context, user *op.User, userAgent, userToken string) (*dbModel.Movie, error) {
|
||||
if s.movie.Proxy {
|
||||
return s.GenProxyMovieInfo(ctx, user, userAgent, userToken)
|
||||
}
|
||||
|
||||
movie := s.movie.Clone()
|
||||
var err error
|
||||
if movie.IsFolder {
|
||||
return nil, errors.New("bilibili folder not support")
|
||||
}
|
||||
|
||||
bmc := s.movie.BilibiliCache()
|
||||
if movie.MovieBase.Live {
|
||||
movie.MovieBase.URL = fmt.Sprintf("/api/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = "m3u8"
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
var str string
|
||||
if movie.MovieBase.VendorInfo.Bilibili.Shared {
|
||||
var u *op.UserEntry
|
||||
u, err = op.LoadOrInitUserByID(movie.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
str, err = s.movie.BilibiliCache().NoSharedMovie.LoadOrStore(ctx, movie.CreatorID, u.Value().BilibiliCache())
|
||||
} else {
|
||||
str, err = s.movie.BilibiliCache().NoSharedMovie.LoadOrStore(ctx, user.ID, user.BilibiliCache())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
movie.MovieBase.URL = str
|
||||
|
||||
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/room/movie/proxy/%s?t=subtitle&n=%s&token=%s&roomId=%s", movie.ID, k, userToken, movie.RoomID),
|
||||
Type: "srt",
|
||||
}
|
||||
}
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
func (s *BilibiliVendorService) GenProxyMovieInfo(ctx context.Context, user *op.User, userAgent, userToken string) (*dbModel.Movie, error) {
|
||||
movie := s.movie.Clone()
|
||||
var err error
|
||||
if movie.IsFolder {
|
||||
return nil, errors.New("bilibili folder not support")
|
||||
}
|
||||
|
||||
bmc := s.movie.BilibiliCache()
|
||||
if movie.MovieBase.Live {
|
||||
movie.MovieBase.URL = fmt.Sprintf("/api/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = "m3u8"
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
movie.MovieBase.URL = fmt.Sprintf("/api/room/movie/proxy/%s?token=%s&roomId=%s", movie.ID, userToken, movie.RoomID)
|
||||
movie.MovieBase.Type = "mpd"
|
||||
movie.MovieBase.MoreSources = []*dbModel.MoreSource{
|
||||
{
|
||||
Name: "hevc",
|
||||
Type: "mpd",
|
||||
URL: fmt.Sprintf("/api/room/movie/proxy/%s?token=%s&t=hevc&roomId=%s", movie.ID, userToken, movie.RoomID),
|
||||
},
|
||||
}
|
||||
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/room/movie/proxy/%s?t=subtitle&n=%s&token=%s&roomId=%s", movie.ID, k, userToken, movie.RoomID),
|
||||
Type: "srt",
|
||||
}
|
||||
}
|
||||
return movie, nil
|
||||
}
|
@ -0,0 +1,243 @@
|
||||
package vendorbilibili
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/internal/cache"
|
||||
"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/vendor"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/synctv-org/vendors/api/bilibili"
|
||||
)
|
||||
|
||||
func NewQRCode(ctx *gin.Context) {
|
||||
r, err := vendor.LoadBilibiliClient("").NewQRCode(ctx, &bilibili.Empty{})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(r))
|
||||
}
|
||||
|
||||
type QRCodeLoginReq struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func (r *QRCodeLoginReq) Validate() error {
|
||||
if r.Key == "" {
|
||||
return errors.New("key is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *QRCodeLoginReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
func LoginWithQR(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
req := QRCodeLoginReq{}
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
backend := ctx.Query("backend")
|
||||
resp, err := vendor.LoadBilibiliClient(backend).LoginWithQRCode(ctx, &bilibili.LoginWithQRCodeReq{
|
||||
Key: req.Key,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
switch resp.Status {
|
||||
case bilibili.QRCodeStatus_EXPIRED:
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(gin.H{
|
||||
"status": "expired",
|
||||
}))
|
||||
return
|
||||
case bilibili.QRCodeStatus_SCANNED:
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(gin.H{
|
||||
"status": "scanned",
|
||||
}))
|
||||
return
|
||||
case bilibili.QRCodeStatus_NOTSCANNED:
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(gin.H{
|
||||
"status": "notScanned",
|
||||
}))
|
||||
return
|
||||
case bilibili.QRCodeStatus_SUCCESS:
|
||||
_, err = db.CreateOrSaveBilibiliVendor(&dbModel.BilibiliVendor{
|
||||
UserID: user.ID,
|
||||
Cookies: resp.Cookies,
|
||||
Backend: backend,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
_, err = user.BilibiliCache().Data().Refresh(ctx, func(ctx context.Context, args ...struct{}) (*cache.BilibiliUserCacheData, error) {
|
||||
return &cache.BilibiliUserCacheData{
|
||||
Backend: backend,
|
||||
Cookies: utils.MapToHTTPCookie(resp.Cookies),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(gin.H{
|
||||
"status": "success",
|
||||
}))
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorStringResp("unknown status"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewCaptcha(ctx *gin.Context) {
|
||||
r, err := vendor.LoadBilibiliClient("").NewCaptcha(ctx, &bilibili.Empty{})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(r))
|
||||
}
|
||||
|
||||
type SMSReq struct {
|
||||
Token string `json:"token"`
|
||||
Challenge string `json:"challenge"`
|
||||
V string `json:"validate"`
|
||||
Telephone string `json:"telephone"`
|
||||
}
|
||||
|
||||
func (r *SMSReq) Validate() error {
|
||||
if r.Token == "" {
|
||||
return errors.New("token is empty")
|
||||
}
|
||||
if r.Challenge == "" {
|
||||
return errors.New("challenge is empty")
|
||||
}
|
||||
if r.V == "" {
|
||||
return errors.New("validate is empty")
|
||||
}
|
||||
if r.Telephone == "" {
|
||||
return errors.New("telephone is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SMSReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
func NewSMS(ctx *gin.Context) {
|
||||
var req SMSReq
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
r, err := vendor.LoadBilibiliClient("").NewSMS(ctx, &bilibili.NewSMSReq{
|
||||
Phone: req.Telephone,
|
||||
Token: req.Token,
|
||||
Challenge: req.Challenge,
|
||||
Validate: req.V,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(gin.H{
|
||||
"captchaKey": r.CaptchaKey,
|
||||
}))
|
||||
}
|
||||
|
||||
type SMSLoginReq struct {
|
||||
Telephone string `json:"telephone"`
|
||||
CaptchaKey string `json:"captchaKey"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (r *SMSLoginReq) Validate() error {
|
||||
if r.Telephone == "" {
|
||||
return errors.New("telephone is empty")
|
||||
}
|
||||
if r.CaptchaKey == "" {
|
||||
return errors.New("captchaKey is empty")
|
||||
}
|
||||
if r.Code == "" {
|
||||
return errors.New("code is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SMSLoginReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
func LoginWithSMS(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
var req SMSLoginReq
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
backend := ctx.Query("backend")
|
||||
c, err := vendor.LoadBilibiliClient(backend).LoginWithSMS(ctx, &bilibili.LoginWithSMSReq{
|
||||
Phone: req.Telephone,
|
||||
CaptchaKey: req.CaptchaKey,
|
||||
Code: req.Code,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
_, err = db.CreateOrSaveBilibiliVendor(&dbModel.BilibiliVendor{
|
||||
UserID: user.ID,
|
||||
Backend: backend,
|
||||
Cookies: c.Cookies,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
_, err = user.BilibiliCache().Data().Refresh(ctx, func(ctx context.Context, args ...struct{}) (*cache.BilibiliUserCacheData, error) {
|
||||
return &cache.BilibiliUserCacheData{
|
||||
Backend: backend,
|
||||
Cookies: utils.MapToHTTPCookie(c.Cookies),
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func Logout(ctx *gin.Context) {
|
||||
log := ctx.MustGet("log").(*log.Entry)
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
err := db.DeleteBilibiliVendor(user.ID)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
err = user.BilibiliCache().Clear(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("clear bilibili cache: %v", err)
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package vendorbilibili
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/internal/db"
|
||||
"github.com/synctv-org/synctv/internal/op"
|
||||
"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/bilibili"
|
||||
)
|
||||
|
||||
type BilibiliMeResp = model.VendorMeResp[*bilibili.UserInfoResp]
|
||||
|
||||
func Me(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
bucd, err := user.BilibiliCache().Get(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&BilibiliMeResp{
|
||||
IsLogin: false,
|
||||
}))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
if len(bucd.Cookies) == 0 {
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&BilibiliMeResp{
|
||||
IsLogin: false,
|
||||
}))
|
||||
return
|
||||
}
|
||||
resp, err := vendor.LoadBilibiliClient(bucd.Backend).UserInfo(ctx, &bilibili.UserInfoReq{
|
||||
Cookies: utils.HTTPCookieToMap(bucd.Cookies),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&BilibiliMeResp{
|
||||
IsLogin: resp.IsLogin,
|
||||
Info: resp,
|
||||
}))
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package vendorbilibili
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
"github.com/synctv-org/synctv/internal/db"
|
||||
"github.com/synctv-org/synctv/internal/op"
|
||||
"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/bilibili"
|
||||
)
|
||||
|
||||
type ParseReq struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (r *ParseReq) Validate() error {
|
||||
if r.URL == "" {
|
||||
return errors.New("url is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ParseReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
func Parse(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
req := ParseReq{}
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
cli := vendor.LoadBilibiliClient(ctx.Query("backend"))
|
||||
|
||||
resp, err := cli.Match(ctx, &bilibili.MatchReq{
|
||||
Url: req.URL,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
// can be no login
|
||||
var cookies []*http.Cookie
|
||||
bucd, err := user.BilibiliCache().Get(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
cookies = bucd.Cookies
|
||||
}
|
||||
|
||||
switch resp.Type {
|
||||
case "bv":
|
||||
resp, err := cli.ParseVideoPage(ctx, &bilibili.ParseVideoPageReq{
|
||||
Cookies: utils.HTTPCookieToMap(cookies),
|
||||
Bvid: resp.Id,
|
||||
Sections: ctx.DefaultQuery("sections", "false") == "true",
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
case "av":
|
||||
aid, err := strconv.ParseUint(resp.Id, 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
resp, err := cli.ParseVideoPage(ctx, &bilibili.ParseVideoPageReq{
|
||||
Cookies: utils.HTTPCookieToMap(cookies),
|
||||
Aid: aid,
|
||||
Sections: ctx.DefaultQuery("sections", "false") == "true",
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
case "ep":
|
||||
epid, err := strconv.ParseUint(resp.Id, 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
resp, err := cli.ParsePGCPage(ctx, &bilibili.ParsePGCPageReq{
|
||||
Cookies: utils.HTTPCookieToMap(cookies),
|
||||
Epid: epid,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
case "ss":
|
||||
ssid, err := strconv.ParseUint(resp.Id, 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
resp, err := cli.ParsePGCPage(ctx, &bilibili.ParsePGCPageReq{
|
||||
Cookies: utils.HTTPCookieToMap(cookies),
|
||||
Ssid: ssid,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
case "live":
|
||||
roomid, err := strconv.ParseUint(resp.Id, 10, 64)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
resp, err := cli.ParseLivePage(ctx, &bilibili.ParseLivePageReq{
|
||||
// Cookies: utils.HttpCookieToMap(cookies), // maybe no need login
|
||||
RoomID: roomid,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorStringResp("unknown match type "+resp.Type))
|
||||
return
|
||||
}
|
||||
}
|
@ -0,0 +1,344 @@
|
||||
package vendoremby
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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/vendor"
|
||||
"github.com/synctv-org/synctv/server/handlers/proxy"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/synctv-org/vendors/api/emby"
|
||||
)
|
||||
|
||||
type EmbyVendorService struct {
|
||||
room *op.Room
|
||||
movie *op.Movie
|
||||
}
|
||||
|
||||
func NewEmbyVendorService(room *op.Room, movie *op.Movie) (*EmbyVendorService, error) {
|
||||
if movie.VendorInfo.Vendor != dbModel.VendorEmby {
|
||||
return nil, fmt.Errorf("emby vendor not support vendor %s", movie.MovieBase.VendorInfo.Vendor)
|
||||
}
|
||||
return &EmbyVendorService{
|
||||
room: room,
|
||||
movie: movie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) Client() emby.EmbyHTTPServer {
|
||||
return vendor.LoadEmbyClient(s.movie.VendorInfo.Backend)
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) ListDynamicMovie(ctx context.Context, reqUser *op.User, subPath string, page, _max int) (*model.MovieList, error) {
|
||||
if reqUser.ID != s.movie.CreatorID {
|
||||
return nil, fmt.Errorf("list vendor dynamic folder error: %w", dbModel.ErrNoPermission)
|
||||
}
|
||||
user := reqUser
|
||||
|
||||
resp := &model.MovieList{
|
||||
Paths: []*model.MoviePath{},
|
||||
}
|
||||
|
||||
serverID, truePath, err := s.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.NotFoundError(db.ErrVendorNotFound)) {
|
||||
return nil, errors.New("emby server not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.Client().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: s.movie.ID,
|
||||
CreatedAt: s.movie.CreatedAt.UnixMilli(),
|
||||
Creator: op.GetUserName(s.movie.CreatorID),
|
||||
CreatorID: s.movie.CreatorID,
|
||||
SubPath: flr.Id,
|
||||
Base: dbModel.MovieBase{
|
||||
Name: flr.Name,
|
||||
IsFolder: flr.IsFolder,
|
||||
ParentID: dbModel.EmptyNullString(s.movie.ID),
|
||||
VendorInfo: dbModel.VendorInfo{
|
||||
Vendor: dbModel.VendorEmby,
|
||||
Backend: s.movie.VendorInfo.Backend,
|
||||
Emby: &dbModel.EmbyStreamingInfo{
|
||||
Path: dbModel.FormatEmbyPath(serverID, flr.Id),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) handleProxyMovie(ctx *gin.Context) {
|
||||
log := ctx.MustGet("log").(*log.Entry)
|
||||
|
||||
if !s.movie.Movie.MovieBase.Proxy {
|
||||
log.Errorf("proxy vendor movie error: %v", "proxy is not enabled")
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp("proxy is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
u, err := op.LoadOrInitUserByID(s.movie.Movie.CreatorID)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
embyC, err := s.movie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if len(embyC.Sources) == 0 {
|
||||
log.Errorf("proxy vendor movie error: %v", "no source")
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp("no source"))
|
||||
return
|
||||
}
|
||||
|
||||
source, err := strconv.Atoi(ctx.Query("source"))
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp(err.Error()))
|
||||
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
|
||||
}
|
||||
|
||||
if embyC.Sources[source].IsTranscode {
|
||||
ctx.Redirect(http.StatusFound, embyC.Sources[source].URL)
|
||||
return
|
||||
}
|
||||
|
||||
// ignore DeviceId, PlaySessionId as cache key
|
||||
sourceCacheKey, err := url.Parse(embyC.Sources[source].URL)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
query := sourceCacheKey.Query()
|
||||
query.Del("DeviceId")
|
||||
query.Del("PlaySessionId")
|
||||
sourceCacheKey.RawQuery = query.Encode()
|
||||
|
||||
err = proxy.AutoProxyURL(ctx,
|
||||
embyC.Sources[source].URL,
|
||||
"",
|
||||
nil,
|
||||
ctx.GetString("token"),
|
||||
s.movie.RoomID,
|
||||
s.movie.ID,
|
||||
proxy.WithProxyURLCache(true),
|
||||
proxy.WithProxyURLCacheKey(sourceCacheKey.String()),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("proxy vendor movie error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) handleSubtitle(ctx *gin.Context) error {
|
||||
u, err := op.LoadOrInitUserByID(s.movie.Movie.CreatorID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
embyC, err := s.movie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
source, err := strconv.Atoi(ctx.Query("source"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if source >= len(embyC.Sources) {
|
||||
return errors.New("source out of range")
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(ctx.Query("id"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id >= len(embyC.Sources[source].Subtitles) {
|
||||
return errors.New("id out of range")
|
||||
}
|
||||
|
||||
data, err := embyC.Sources[source].Subtitles[id].Cache.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.ServeContent(ctx.Writer, ctx.Request, embyC.Sources[source].Subtitles[id].Name, time.Now(), bytes.NewReader(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) ProxyMovie(ctx *gin.Context) {
|
||||
switch t := ctx.Query("t"); t {
|
||||
case "":
|
||||
s.handleProxyMovie(ctx)
|
||||
case "subtitle":
|
||||
s.handleSubtitle(ctx)
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp(fmt.Sprintf("unknown proxy type: %s", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) GenMovieInfo(ctx context.Context, user *op.User, userAgent, userToken string) (*dbModel.Movie, error) {
|
||||
if s.movie.Proxy {
|
||||
return s.GenProxyMovieInfo(ctx, user, userAgent, userToken)
|
||||
}
|
||||
|
||||
movie := s.movie.Clone()
|
||||
var err error
|
||||
|
||||
u, err := op.LoadOrInitUserByID(movie.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.movie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
func (s *EmbyVendorService) GenProxyMovieInfo(ctx context.Context, user *op.User, userAgent, userToken string) (*dbModel.Movie, error) {
|
||||
movie := s.movie.Clone()
|
||||
var err error
|
||||
|
||||
u, err := op.LoadOrInitUserByID(movie.CreatorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.movie.EmbyCache().Get(ctx, u.Value().EmbyCache())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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/room/movie/proxy", movie.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawQuery := url.Values{}
|
||||
rawQuery.Set("source", strconv.Itoa(si))
|
||||
rawQuery.Set("token", userToken)
|
||||
rawQuery.Set("roomId", movie.RoomID)
|
||||
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)
|
||||
rawQuery.Set("roomId", movie.RoomID)
|
||||
u := url.URL{
|
||||
Path: rawPath,
|
||||
RawQuery: rawQuery.Encode(),
|
||||
}
|
||||
movie.MovieBase.Subtitles[s.Name] = &dbModel.Subtitle{
|
||||
URL: u.String(),
|
||||
Type: s.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return movie, nil
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package vendoremby
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
"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/vendor"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/synctv-org/vendors/api/emby"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
Path string `json:"path"`
|
||||
Keywords string `json:"keywords"`
|
||||
}
|
||||
|
||||
func (r *ListReq) Validate() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
type EmbyFileItem struct {
|
||||
*model.Item
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type EmbyFSListResp = model.VendorFSListResp[*EmbyFileItem]
|
||||
|
||||
func List(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
req := ListReq{}
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
page, size, err := utils.GetPageAndMax(ctx)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Path == "" {
|
||||
if req.Keywords != "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorStringResp("keywords is not supported when not choose server (server id is empty)"))
|
||||
return
|
||||
}
|
||||
socpes := [](func(*gorm.DB) *gorm.DB){
|
||||
db.OrderByCreatedAtAsc,
|
||||
}
|
||||
|
||||
total, err := db.GetEmbyVendorsCount(user.ID, socpes...)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
if total == 0 {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("emby server not found"))
|
||||
return
|
||||
}
|
||||
|
||||
ev, err := db.GetEmbyVendors(user.ID, append(socpes, db.Paginate(page, size))...)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("emby server not found"))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if total == 1 {
|
||||
req.Path = ev[0].ServerID + "/"
|
||||
goto EmbyFSListResp
|
||||
}
|
||||
|
||||
resp := EmbyFSListResp{
|
||||
Paths: []*model.Path{
|
||||
{
|
||||
Name: "",
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
Total: uint64(total),
|
||||
}
|
||||
|
||||
for _, evi := range ev {
|
||||
resp.Items = append(resp.Items, &EmbyFileItem{
|
||||
Item: &model.Item{
|
||||
Name: evi.Host,
|
||||
Path: evi.ServerID + `/`,
|
||||
IsDir: true,
|
||||
},
|
||||
Type: "server",
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
EmbyFSListResp:
|
||||
|
||||
var serverID string
|
||||
serverID, req.Path, err = dbModel.GetEmbyServerIDFromPath(req.Path)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
aucd, err := user.EmbyCache().LoadOrStore(ctx, serverID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("emby server not found"))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
cli := vendor.LoadEmbyClient(ctx.Query("backend"))
|
||||
data, err := cli.FsList(ctx, &emby.FsListReq{
|
||||
Host: aucd.Host,
|
||||
Path: req.Path,
|
||||
Token: aucd.APIKey,
|
||||
UserId: aucd.UserID,
|
||||
Limit: uint64(size),
|
||||
StartIndex: uint64((page - 1) * size),
|
||||
SearchTerm: req.Keywords,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(fmt.Errorf("emby fs list error: %w", err)))
|
||||
return
|
||||
}
|
||||
|
||||
var resp EmbyFSListResp = EmbyFSListResp{
|
||||
Paths: []*model.Path{
|
||||
{},
|
||||
},
|
||||
}
|
||||
for _, p := range data.Paths {
|
||||
n := p.Name
|
||||
if p.Path == "1" {
|
||||
n = aucd.Host
|
||||
}
|
||||
resp.Paths = append(resp.Paths, &model.Path{
|
||||
Name: n,
|
||||
Path: fmt.Sprintf("%s/%s", aucd.ServerID, p.Path),
|
||||
})
|
||||
}
|
||||
for _, i := range data.Items {
|
||||
resp.Items = append(resp.Items, &EmbyFileItem{
|
||||
Item: &model.Item{
|
||||
Name: i.Name,
|
||||
Path: fmt.Sprintf("%s/%s", aucd.ServerID, i.Id),
|
||||
IsDir: i.IsFolder,
|
||||
},
|
||||
Type: i.Type,
|
||||
})
|
||||
}
|
||||
|
||||
resp.Total = data.Total
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package vendoremby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
"github.com/synctv-org/synctv/internal/cache"
|
||||
"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/vendor"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/vendors/api/emby"
|
||||
)
|
||||
|
||||
type LoginReq struct {
|
||||
Host string `json:"host"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (r *LoginReq) Validate() error {
|
||||
if r.Host == "" {
|
||||
return errors.New("host is required")
|
||||
}
|
||||
url, err := url.Parse(r.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if url.Scheme != "http" && url.Scheme != "https" {
|
||||
return errors.New("host is invalid")
|
||||
}
|
||||
r.Host = strings.TrimRight(url.String(), "/")
|
||||
if r.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LoginReq) Decode(ctx *gin.Context) error {
|
||||
return json.NewDecoder(ctx.Request.Body).Decode(r)
|
||||
}
|
||||
|
||||
func Login(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
req := LoginReq{}
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
backend := ctx.Query("backend")
|
||||
cli := vendor.LoadEmbyClient(backend)
|
||||
|
||||
data, err := cli.Login(ctx, &emby.LoginReq{
|
||||
Host: req.Host,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if data.ServerId == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorStringResp("serverID is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.CreateOrSaveEmbyVendor(&dbModel.EmbyVendor{
|
||||
UserID: user.ID,
|
||||
ServerID: data.ServerId,
|
||||
Host: req.Host,
|
||||
APIKey: data.Token,
|
||||
Backend: backend,
|
||||
EmbyUserID: data.UserId,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = user.EmbyCache().StoreOrRefreshWithDynamicFunc(ctx, data.ServerId, func(ctx context.Context, key string) (*cache.EmbyUserCacheData, error) {
|
||||
return &cache.EmbyUserCacheData{
|
||||
Host: req.Host,
|
||||
ServerID: key,
|
||||
APIKey: data.Token,
|
||||
Backend: backend,
|
||||
UserID: data.UserId,
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func Logout(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
var req model.ServerIDReq
|
||||
if err := model.Decode(ctx, &req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
err := db.DeleteEmbyVendor(user.ID, req.ServerID)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
eucd, ok := user.EmbyCache().LoadCache(req.ServerID)
|
||||
if ok {
|
||||
eucdr, _ := eucd.Raw()
|
||||
go logoutEmby(eucdr)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func logoutEmby(eucd *cache.EmbyUserCacheData) {
|
||||
if eucd == nil || eucd.APIKey == "" {
|
||||
return
|
||||
}
|
||||
_, _ = vendor.LoadEmbyClient(eucd.Backend).Logout(context.Background(), &emby.LogoutReq{
|
||||
Host: eucd.Host,
|
||||
Token: eucd.APIKey,
|
||||
})
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package vendoremby
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/internal/db"
|
||||
"github.com/synctv-org/synctv/internal/op"
|
||||
"github.com/synctv-org/synctv/internal/vendor"
|
||||
"github.com/synctv-org/synctv/server/model"
|
||||
"github.com/synctv-org/vendors/api/emby"
|
||||
)
|
||||
|
||||
type EmbyMeResp = model.VendorMeResp[*emby.SystemInfoResp]
|
||||
|
||||
func Me(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
serverID := ctx.Query("serverID")
|
||||
if serverID == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewAPIErrorResp(errors.New("serverID is required")))
|
||||
return
|
||||
}
|
||||
|
||||
eucd, err := user.EmbyCache().LoadOrStore(ctx, serverID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusBadRequest, model.NewAPIErrorStringResp("emby server not found"))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := vendor.LoadEmbyClient(eucd.Backend).GetSystemInfo(ctx, &emby.SystemInfoReq{
|
||||
Host: eucd.Host,
|
||||
Token: eucd.APIKey,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&EmbyMeResp{
|
||||
IsLogin: true,
|
||||
Info: data,
|
||||
}))
|
||||
}
|
||||
|
||||
type EmbyBindsResp []*struct {
|
||||
ServerID string `json:"serverId"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
func Binds(ctx *gin.Context) {
|
||||
user := ctx.MustGet("user").(*op.UserEntry).Value()
|
||||
|
||||
ev, err := db.GetEmbyVendors(user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFoundError(db.ErrVendorNotFound)) {
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(&EmbyMeResp{
|
||||
IsLogin: false,
|
||||
}))
|
||||
return
|
||||
}
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewAPIErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
resp := make(EmbyBindsResp, len(ev))
|
||||
for i, v := range ev {
|
||||
resp[i] = &struct {
|
||||
ServerID string `json:"serverId"`
|
||||
Host string `json:"host"`
|
||||
}{
|
||||
ServerID: v.ServerID,
|
||||
Host: v.Host,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, model.NewAPIDataResp(resp))
|
||||
}
|
Loading…
Reference in New Issue