mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
0eb3f104f4
- 新增 emby_remote 提供方:通过 STRM 账号体系配置远程 Emby 地址/用户名/密码
- EmbyService 聚合远程媒体库 Views/Items/详情/最近添加/全局搜索,远程数据不落库
- 条目 ID 统一伪装为 embyremote~{account}~{id},请求按账号路由回远程
- 播放支持账号级 proxy_play 配置:默认客户端直连远程;开启后由 MMTL 反向代理流/字幕
- 图片/元数据全部实时透传远程,播放状态(已看/收藏)透传回远程 Emby
- cloud 包新增 emby Provider(Ping/List/Resolve),账号测试/目录浏览自动生效
- 前端 STRM 页面支持 Emby 远程挂载账号(地址+凭据+代理开关)
116 lines
3.0 KiB
Go
116 lines
3.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/ShukeBta/MMTL/internal/model"
|
|
)
|
|
|
|
// SetFavorite 把 mediaID 标为 userID 的收藏。远程 Emby 条目直接透传到对应
|
|
// 服务器(本地不落库)。
|
|
func (e *EmbyService) SetFavorite(ctx context.Context, userID, mediaID string, favorite bool) error {
|
|
if e.remote != nil && IsEmbyRemoteID(mediaID) {
|
|
acctID, remoteID, _ := DecodeEmbyRemoteID(mediaID)
|
|
if err := e.ProxyRemoteSetFavorite(ctx, acctID, remoteID, favorite); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if favorite {
|
|
var f model.Favorite
|
|
err := e.repo.DB.WithContext(ctx).
|
|
Where("user_id = ? AND media_id = ?", userID, mediaID).First(&f).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return e.repo.DB.WithContext(ctx).Create(&model.Favorite{
|
|
UserID: userID, MediaID: mediaID,
|
|
}).Error
|
|
}
|
|
return err
|
|
}
|
|
return e.repo.DB.WithContext(ctx).
|
|
Where("user_id = ? AND media_id = ?", userID, mediaID).
|
|
Delete(&model.Favorite{}).Error
|
|
}
|
|
|
|
// MarkPlayed 把 mediaID 标为已看(写一个 100% 进度的 history 行)。
|
|
// 远程 Emby 条目直接透传到对应服务器(本地不落库)。
|
|
func (e *EmbyService) MarkPlayed(ctx context.Context, userID, mediaID string, played bool) error {
|
|
if e.remote != nil && IsEmbyRemoteID(mediaID) {
|
|
acctID, remoteID, _ := DecodeEmbyRemoteID(mediaID)
|
|
if err := e.ProxyRemoteSetPlayed(ctx, acctID, remoteID, played); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if !played {
|
|
return e.repo.DB.WithContext(ctx).
|
|
Where("user_id = ? AND media_id = ?", userID, mediaID).
|
|
Delete(&model.PlaybackHistory{}).Error
|
|
}
|
|
m, err := e.repo.Media.FindByID(ctx, mediaID)
|
|
if err != nil || m == nil {
|
|
return errors.New("media not found")
|
|
}
|
|
dur := int64(m.DurationSec) * 1000
|
|
if dur <= 0 {
|
|
dur = 1
|
|
}
|
|
return e.repo.History.Upsert(ctx, &model.PlaybackHistory{
|
|
UserID: userID,
|
|
MediaID: mediaID,
|
|
PositionMs: dur,
|
|
DurationMs: dur,
|
|
WatchedAt: time.Now(),
|
|
Completed: true,
|
|
})
|
|
}
|
|
|
|
// RecordProgress 记录播放进度(来自 Emby 客户端的 /Sessions/Playing/Progress)。
|
|
func (e *EmbyService) RecordProgress(ctx context.Context, userID, mediaID string, positionTicks, runtimeTicks int64) error {
|
|
pos := positionTicks / 10_000
|
|
dur := runtimeTicks / 10_000
|
|
if dur <= 0 {
|
|
// runtimeTicks 缺失时回退到 media.DurationSec
|
|
if m, _ := e.repo.Media.FindByID(ctx, mediaID); m != nil {
|
|
dur = int64(m.DurationSec) * 1000
|
|
}
|
|
}
|
|
completed := dur > 0 && pos >= dur*9/10
|
|
return e.repo.History.Upsert(ctx, &model.PlaybackHistory{
|
|
UserID: userID,
|
|
MediaID: mediaID,
|
|
PositionMs: pos,
|
|
DurationMs: dur,
|
|
WatchedAt: time.Now(),
|
|
Completed: completed,
|
|
})
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
if strings.TrimSpace(s) == "" {
|
|
return []string{}
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func intToStr(v int) string {
|
|
if v == 0 {
|
|
return ""
|
|
}
|
|
return strconv.Itoa(v)
|
|
}
|