mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-11-13 22:20:47 +08:00
* feat(func): media library
* feat(func): media library
* feat(func): media library
* feat(func): media library build test
* feat(func): media library build test
* feat(func): media library fix remote fetch error
* feat(func): media library fix remote fetch error
* feat(func): media library fix remote fetch error
* feat(func): media library support select
* feat(func): media library support select
* feat(func): media library support select
* feat(func): media library support select
* feat(func): media library support change api url
* feat(func): media library support change api url
* feat(func): media library support change api url
* feat(media): 并发刮削(默认 5)+ 单条落库
增强 TMDB 搜索:模糊匹配 + 多候选关键词
管理页面新增导入/导出媒体库(全部 / 按扫描路径)
缩略图路径默认值改为 imgs
* feat(media): 清空刮削、删除已失效
* fix(media): fix api proxy path
* fix(media): fix lrc file display & music id3
* fix(media): fix media scan sub path error
* feat(media): support cloud codec via ffmpeg
* feat(media): support cloud codec via ffmpeg
* feat(media): support cloud codec via ffmpeg
* feat(media): support cloud codec via ffmpeg
* feat(media): 提升刮削精准度,支持模糊刮削
* fix(media): clean up imports and improve FetchFileReader logic
* feat(media): 提升刮削精准度,支持模糊刮削
* feat(media): 刮削支持自定义刮削数量,避免太占性能
* feat(media): 刮削支持自定义刮削数量,避免太占性能
* feat(media): 刮削修复写入作品名问题
* fix(media): return 404 on missing item_id and fix unscraped item query
- Validate GetMediaItemByID result before spawning goroutine in
StartMediaScrape; return 404 when item_id > 0 but item is not found
- Replace `description` check with `plot` check in GetUnscrappedItems
since scrapers populate Plot, not Description, to avoid re-selecting
already-scraped items on every run
* Apply remaining changes
* feat(media): add master switch, app keys, STRM, Emby and OpenSubsonic APIs
* feat(media): allow any logged-in user to manage their own media app keys
* feat(media): allow signing arbitrary media path for local player
* fix(media): always return an array for media status types
* fix(media): persist per-type scraper config and apply it to thumbnails
* feat(media): add local thumbnail endpoint and cover proxy with fallback gateway
* fix(media): allow guest media access, per-file local thumbnails and optional cover proxy
* feat(media): thumbnail storage modes and STRM same-directory output
* feat(media): Google Books/OpenLibrary sources and remote cover download
* chore(media): rename transcode cache dir to ctmp and log thumbnail storage dir
* feat(media): progress and favorites API shared with Emby/Subsonic
* chore: ignore runtime thumbnail directory
* feat(media): add preferred scrape language and namespace protocol endpoints under @
* fix(media): harden anonymous emby images, pagination and cover proxy SSRF
* fix(emby): allow anonymous user discovery and login before auth
* chore: drop redundant media build workflow and protocol doc
* fix(media): ID3v2.4 syncsafe frame size and preserve Hidden flag on rescan
* fix(media): address security review (path traversal, guest bypass, cover XSS, auth rate limit, SSRF, quotas, migration)
* fix(media): 清理失效条目时只删除确认不存在的记录
此前 fs.Get 返回任何错误都视为失效并硬删除,驱动故障/超时/鉴权失败时会误删有效媒体记录。现在仅 errs.IsObjectNotFound 才删除,其余错误跳过并在响应中返回 skipped 计数。
* fix(transcode): 播放凭据与 Worker 回写凭据分离,并收紧 profile 与鉴权校验
- 新增 Job.PlayToken(json 标签为 -),master/playlist/切片只认播放凭据,播放器不再拿到可上传、可结束任务的 callback_token
- Worker 上传切片时校验 profile 必须属于任务声明的档位
- TCMaster 与 WorkerJobFinish 改用恒定时间比较,避免时序侧信道
* fix(transcode): 含冒号的本地文件名可转码,补基址缺失与重新激活配额
- ValidSourceInput 不再按冒号位置判协议:本地输入只要求以 / 开头或 Windows 盘符路径,concat:/subfile: 等前缀仍被拒且由 -protocol_whitelist 兜底(修复 /mnt/x/Star Wars: IV.mkv 被误判)
- trustedInternalBaseURL 返回空串时直接 503 并给出明确原因,不再拼出 /d/xxx 让 ffmpeg 报文件不存在
- 远程输入白名单收窄为 http,https,tcp,tls(去掉 file 与 crypto)
- Reactivate 复用同一套单用户配额,避免「先让任务闲置取消、再批量激活」绕过 SubmitLimited;配额不足返回 429
- OwnerID 改为配额键:普通用户 user:<id>,访客 guest:<clientIP>,避免单个匿名访客占满全部匿名配额;补充对应单测
* fix(media): 收紧缩略图解码内存、跳过超长条目可见化,并补 CI 单测
- MaxImagePixels 从 64MP 降到 32MP,宽高改 int64 相乘防 32 位溢出,解码阶段加 12 路全局信号量
- 封面解码失败退回原始字节时限制 512KB,避免十几 MB 的 base64 写进 cover 列(豆瓣/本地书籍三处)
- 扫描进度新增 Skipped 计数,入库被跳过的超长路径会在完成消息与日志中体现
- 数据库迁移失败日志补充 OPENLIST_MEDIA_MIGRATE_WIPE=1 的处置提示
- Subsonic 认证只对「确实带了凭据却校验失败」计数,裸探测不再刷新 IP 锁定
- goexif 从 indirect 调整为直接依赖(internal/media/scraper/image.go 直接 import)
- CI 新增 go test ./internal/db/... ./internal/transcode/... ./internal/media/...
* fix(transcode): 配额/队列满时仍放行已缓存切片,并同步 worker 协议白名单
- TCSegment/TCPlaylist: Reactivate 返回 ErrUserQuota/ErrQueueFull 时不再直接 429,改为标记 quotaBlocked,缓存命中的切片与 playlist 仍可读,只有缓存缺失才返回 429
- Reactivate: 补充全局排队上限 (MaxPendingJobs) 检查,避免批量激活顶爆队列,并补单测
- cmd/worker: 远程输入协议白名单与 master 同步为 http,https,tcp,tls(去掉 file/crypto)
- scraper: decodeSem 容量 12 -> 3,避免信号量大于默认刮削并发而形同失效
* feat(emby): 完善客户端兼容层并修复认证锁定,Subsonic 复用统一密钥校验
- Emby: 用户 ID 改用 GUID 并兼容数字/旧 ID;补齐 /Users/{id}/Authenticate、Sessions 与 WebSocket、首页分区、合集/播放列表、字幕流与歌词、Series/Season 结构
- Emby: 版本号返回客户端可接受的兼容版本,/Users/Public 返回可登录用户以渲染用户选择
- Subsonic: 支持 enc: 十六进制密码编码;app key 校验与 Emby 共用 LookUpAppKeyUser
- 限流: 认证被拒时不再刷新锁定过期时间,避免客户端持续轮询把 IP 永久锁死
* fix(media): 认证失败计数增加用户名维度,防止伪造 X-Forwarded-For 绕过锁定
- media_ratelimit: 新增 mediaAuthLockedFor / mediaAuthFailedFor / mediaAuthSucceededFor,IP 与用户名双维度计数;达到上限同样锁定 5 分钟且被拒时不续期
- emby: 按用户名登录与按 id 登录都检查用户名维度;失败计入两个维度,成功清除两个维度
- subsonic: subAuth 与 429 分支改用双维度判定;成功时用真实用户名清理计数(apiKey 认证时 u 参数为空)
- 仍保留主干 5 次/5 分钟的策略,代理信任范围(SetTrustedProxies)留给项目层面单独处理
* fix(media): 匿名封面按访客身份过滤,认证日志脱敏并收紧限流计数
- embyImage: 匿名封面不再走 loadEmbyLibraryAll(全库可见),改为 loadEmbyLibrary(guest):
hidden / base_path 之外 / meta 受保护的条目都不可见,访客被禁用时直接 401
- embyUnauthorized: 只记录 Path、UA 与是否带 token,避免 query 里的 Pw/Password/api_key
和 X-Emby-Authorization 里的设备令牌写进日志
- media_ratelimit: 失败计数 Set 时带 5min TTL,避免 IP/用户名 key 无限增长(用户名来自请求参数)
- auth.loginHash: 锁定期间不再续期,失败计数同样带 TTL,与媒体侧语义一致
- /System/Endpoint: 按 RemoteAddr 判断 IsLocal/IsInNetwork;/System/Info 不再返回真实监听端口
* feat(media): PDF 批注持久化
- 新增 MediaAnnotation 模型:按 (user_id, item_id, remote_id) 唯一,anchor 原样保存查看器批注对象,不重复建模页码/坐标
- GET/POST /fs/media/annotations:读取当前用户批注、按 remote_id 增量 upsert/delete(单条 512KB、单次 8MB、最多 500 条)
- 批注为用户私有数据:未登录返回空列表,访客写入 403,并校验条目可见性与访问权限
- 条目删除 / 清空类型 / 清空扫描路径 / 批量删除时,在同一事务内清理对应批注
* fix(media): 选集签名前校验最终文件权限;转码在 ffprobe 前预检配额
- GetMediaStreamURL / GetMediaStrm: 解析选集后对最终 rawPath 再执行 CanUserAccessPath,避免「目录可见但 meta 隐藏规则禁止其中某个文件」时仍能拿到有效签名(签名之后下载链路不会重跑隐藏规则)
- TranscodePlay: ProbeSource 之前调用 Scheduler.Capacity 预检配额,已满直接 429,不再先起最长 30s 的 ffprobe
- 新增探测并发闸门(全局 8 / 每用户 3),覆盖预检与 SubmitLimited 之间的并发窗口;用 defer 释放,探测 panic 也不会占死槽位
- 补 Capacity 单测
---------
Co-authored-by: j2rong4cn <j2rong@qq.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: pikachuren <pikachuren@example.com>
368 lines
16 KiB
Go
368 lines
16 KiB
Go
package server
|
|
|
|
import (
|
|
"github.com/OpenListTeam/OpenList/v4/cmd/flags"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/conf"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/message"
|
|
multipartPkg "github.com/OpenListTeam/OpenList/v4/internal/multipart"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/sign"
|
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
|
"github.com/OpenListTeam/OpenList/v4/server/common"
|
|
"github.com/OpenListTeam/OpenList/v4/server/handles"
|
|
"github.com/OpenListTeam/OpenList/v4/server/middlewares"
|
|
"github.com/OpenListTeam/OpenList/v4/server/static"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func Init(e *gin.Engine) {
|
|
e.ContextWithFallback = true
|
|
if !utils.SliceContains([]string{"", "/"}, conf.URL.Path) {
|
|
e.GET("/", func(c *gin.Context) {
|
|
c.Redirect(302, conf.URL.Path)
|
|
})
|
|
}
|
|
Cors(e)
|
|
g := e.Group(conf.URL.Path)
|
|
if conf.Conf.Scheme.HttpPort != -1 && conf.Conf.Scheme.HttpsPort != -1 && conf.Conf.Scheme.ForceHttps {
|
|
e.Use(middlewares.ForceHttps)
|
|
}
|
|
g.Any("/ping", func(c *gin.Context) {
|
|
c.String(200, "pong")
|
|
})
|
|
g.GET("/favicon.ico", handles.Favicon)
|
|
g.GET("/robots.txt", handles.Robots)
|
|
g.GET("/manifest.json", static.ManifestJSON)
|
|
g.GET("/i/:link_name", handles.Plist)
|
|
common.SecretKey = []byte(conf.Conf.JwtSecret)
|
|
g.Use(middlewares.StoragesLoaded)
|
|
if conf.Conf.MaxConnections > 0 {
|
|
g.Use(middlewares.MaxAllowed(conf.Conf.MaxConnections))
|
|
}
|
|
WebDav(g.Group("/dav"))
|
|
S3(g.Group("/s3"))
|
|
MCP(g)
|
|
|
|
downloadLimiter := middlewares.DownloadRateLimiter(stream.ClientDownloadLimit)
|
|
signCheck := middlewares.Down(sign.Verify)
|
|
g.GET("/d/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Down)
|
|
g.GET("/p/*path", middlewares.PathParse, signCheck, downloadLimiter, handles.Proxy)
|
|
g.HEAD("/d/*path", middlewares.PathParse, signCheck, handles.Down)
|
|
g.HEAD("/p/*path", middlewares.PathParse, signCheck, handles.Proxy)
|
|
archiveSignCheck := middlewares.Down(sign.VerifyArchive)
|
|
g.GET("/ad/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveDown)
|
|
g.GET("/ap/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveProxy)
|
|
g.GET("/ae/*path", middlewares.PathParse, archiveSignCheck, downloadLimiter, handles.ArchiveInternalExtract)
|
|
g.HEAD("/ad/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveDown)
|
|
g.HEAD("/ap/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveProxy)
|
|
g.HEAD("/ae/*path", middlewares.PathParse, archiveSignCheck, handles.ArchiveInternalExtract)
|
|
|
|
g.GET("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown)
|
|
g.GET("/sd/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingDown)
|
|
g.HEAD("/sd/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingDown)
|
|
g.HEAD("/sd/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingDown)
|
|
g.GET("/sad/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract)
|
|
g.GET("/sad/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, downloadLimiter, handles.SharingArchiveExtract)
|
|
g.HEAD("/sad/:sid", middlewares.EmptyPathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract)
|
|
g.HEAD("/sad/:sid/*path", middlewares.PathParse, middlewares.SharingIdParse, handles.SharingArchiveExtract)
|
|
|
|
// 转码播放:master/variant playlist 与切片下发;token 已包含在路径中作为鉴权
|
|
g.GET("/tc/:job/:token/master.m3u8", handles.TCMaster)
|
|
g.GET("/tc/:job/:token/:profile/playlist.m3u8", handles.TCPlaylist)
|
|
g.GET("/tc/:job/:token/:profile/:seg", handles.TCSegment)
|
|
|
|
api := g.Group("/api")
|
|
auth := api.Group("", middlewares.Auth(false))
|
|
webauthn := api.Group("/authn", middlewares.Authn)
|
|
|
|
api.POST("/auth/login", handles.Login)
|
|
api.POST("/auth/login/hash", handles.LoginHash)
|
|
api.POST("/auth/login/ldap", handles.LoginLdap)
|
|
auth.GET("/me", handles.CurrentUser)
|
|
auth.POST("/me/update", handles.UpdateCurrent)
|
|
// 媒体应用密钥(每个登录用户都可以为自己的客户端签发密钥)
|
|
auth.GET("/me/appkey/list", handles.ListMediaAppKeys)
|
|
auth.POST("/me/appkey/create", handles.CreateMediaAppKey)
|
|
auth.POST("/me/appkey/delete", handles.DeleteMediaAppKey)
|
|
auth.GET("/me/sshkey/list", handles.ListMyPublicKey)
|
|
auth.POST("/me/sshkey/add", handles.AddMyPublicKey)
|
|
auth.POST("/me/sshkey/delete", handles.DeleteMyPublicKey)
|
|
auth.POST("/auth/2fa/generate", handles.Generate2FA)
|
|
auth.POST("/auth/2fa/verify", handles.Verify2FA)
|
|
auth.GET("/auth/logout", handles.LogOut)
|
|
|
|
// auth
|
|
api.GET("/auth/sso", handles.SSOLoginRedirect)
|
|
api.GET("/auth/sso_callback", handles.SSOLoginCallback)
|
|
api.GET("/auth/get_sso_id", handles.SSOLoginCallback)
|
|
api.GET("/auth/sso_get_token", handles.SSOLoginCallback)
|
|
|
|
// webauthn
|
|
api.GET("/authn/webauthn_begin_login", handles.BeginAuthnLogin)
|
|
api.POST("/authn/webauthn_finish_login", handles.FinishAuthnLogin)
|
|
webauthn.GET("/webauthn_begin_registration", handles.BeginAuthnRegistration)
|
|
webauthn.POST("/webauthn_finish_registration", handles.FinishAuthnRegistration)
|
|
webauthn.POST("/delete_authn", handles.DeleteAuthnLogin)
|
|
webauthn.GET("/getcredentials", handles.GetAuthnCredentials)
|
|
|
|
// no need auth
|
|
public := api.Group("/public")
|
|
public.Any("/settings", handles.PublicSettings)
|
|
public.GET("/media_status", handles.PublicMediaStatus)
|
|
// 封面代理与本地缩略图(媒体库页面使用,免鉴权)
|
|
public.GET("/media/proxy", handles.PublicMediaProxy)
|
|
public.GET("/media/thumb", handles.PublicMediaThumb)
|
|
public.Any("/offline_download_tools", handles.OfflineDownloadTools)
|
|
public.Any("/archive_extensions", handles.ArchiveExtensions)
|
|
public.Any("/init_status", handles.InitStatus)
|
|
public.POST("/init/setup", handles.InitSetup)
|
|
|
|
// 转码 Worker 协议端点(远程 Worker 节点对接,由共享密钥/token 鉴权,无需登录)
|
|
transcodeWorker := api.Group("/transcode/worker")
|
|
transcodeWorker.POST("/register", handles.WorkerRegister)
|
|
transcodeWorker.POST("/heartbeat", handles.WorkerHeartbeat)
|
|
transcodeWorker.POST("/claim", handles.WorkerClaim)
|
|
transcodeWorker.PUT("/segment", handles.WorkerSegmentUpload)
|
|
transcodeWorker.POST("/job/finish", handles.WorkerJobFinish)
|
|
|
|
// Emby 兼容 API(自带鉴权与开关判断,路径大小写由 handler 归一化)
|
|
// 使用 @ 前缀,避免与用户自己创建的 /emby、/rest 目录冲突
|
|
g.Any("/@emby/*path", handles.EmbyAPI)
|
|
g.Any("/@mediabrowser/*path", handles.EmbyAPI)
|
|
// OpenSubsonic API(自带鉴权与开关判断)
|
|
g.Any("/@rest/*path", handles.SubsonicAPI)
|
|
|
|
_fs(auth.Group("/fs"))
|
|
// 媒体库公开 API:与文件浏览一致,允许访客访问(访客仍受自身 base_path/权限限制)
|
|
mediaPublic := auth.Group("/fs/media", handles.RequireMediaEnabled)
|
|
mediaPublic.GET("/list", handles.PublicListMedia)
|
|
mediaPublic.GET("/item/:id", handles.PublicGetMedia)
|
|
mediaPublic.GET("/albums", handles.PublicListAlbums)
|
|
mediaPublic.GET("/album", handles.PublicGetAlbum)
|
|
mediaPublic.GET("/folders", handles.PublicListFolders)
|
|
mediaPublic.GET("/scan_paths", handles.PublicListScanPaths)
|
|
mediaPublic.GET("/strm", handles.GetMediaStrm)
|
|
mediaPublic.GET("/stream_url", handles.GetMediaStreamURL)
|
|
// 播放进度与收藏(与 Emby / Subsonic 共用同一份用户数据)
|
|
mediaPublic.GET("/userdata", handles.GetMediaUserDataBatch)
|
|
mediaPublic.GET("/progress", handles.GetMediaProgress)
|
|
mediaPublic.POST("/progress", handles.SaveMediaProgress)
|
|
mediaPublic.POST("/favorite", handles.SetMediaFavorite)
|
|
mediaPublic.GET("/favorites", handles.ListMediaFavoritesAPI)
|
|
mediaPublic.GET("/resume", handles.ListMediaResumeAPI)
|
|
// 阅读器批注(登录用户私有数据;访客只读返回空列表,写入直接 403)
|
|
mediaPublic.GET("/annotations", handles.ListMediaAnnotationsAPI)
|
|
mediaPublic.POST("/annotations", handles.SyncMediaAnnotationsAPI)
|
|
fsAndShare(api.Group("/fs", middlewares.Auth(true)))
|
|
_task(auth.Group("/task", middlewares.AuthNotGuest))
|
|
_sharing(auth.Group("/share", middlewares.AuthNotGuest))
|
|
admin(auth.Group("/admin", middlewares.AuthAdmin))
|
|
if flags.Debug || flags.Dev {
|
|
debug(g.Group("/debug"))
|
|
}
|
|
static.Static(g, func(handlers ...gin.HandlerFunc) {
|
|
e.NoRoute(handlers...)
|
|
})
|
|
}
|
|
|
|
func admin(g *gin.RouterGroup) {
|
|
meta := g.Group("/meta")
|
|
meta.GET("/list", handles.ListMetas)
|
|
meta.GET("/get", handles.GetMeta)
|
|
meta.POST("/create", handles.CreateMeta)
|
|
meta.POST("/update", handles.UpdateMeta)
|
|
meta.POST("/delete", handles.DeleteMeta)
|
|
|
|
user := g.Group("/user")
|
|
user.GET("/list", handles.ListUsers)
|
|
user.GET("/get", handles.GetUser)
|
|
user.POST("/create", handles.CreateUser)
|
|
user.POST("/update", handles.UpdateUser)
|
|
user.POST("/cancel_2fa", handles.Cancel2FAById)
|
|
user.POST("/delete", handles.DeleteUser)
|
|
user.POST("/del_cache", handles.DelUserCache)
|
|
user.GET("/sshkey/list", handles.ListPublicKeys)
|
|
user.POST("/sshkey/delete", handles.DeletePublicKey)
|
|
|
|
storage := g.Group("/storage")
|
|
storage.GET("/list", handles.ListStorages)
|
|
storage.GET("/get", handles.GetStorage)
|
|
storage.POST("/create", handles.CreateStorage)
|
|
storage.POST("/update", handles.UpdateStorage)
|
|
storage.POST("/delete", handles.DeleteStorage)
|
|
storage.POST("/enable", handles.EnableStorage)
|
|
storage.POST("/disable", handles.DisableStorage)
|
|
storage.POST("/load_all", handles.LoadAllStorages)
|
|
|
|
driver := g.Group("/driver")
|
|
driver.GET("/list", handles.ListDriverInfo)
|
|
driver.GET("/names", handles.ListDriverNames)
|
|
driver.GET("/info", handles.GetDriverInfo)
|
|
|
|
setting := g.Group("/setting")
|
|
setting.GET("/get", handles.GetSetting)
|
|
setting.GET("/list", handles.ListSettings)
|
|
setting.POST("/save", handles.SaveSettings)
|
|
setting.POST("/delete", handles.DeleteSetting)
|
|
setting.POST("/default", handles.DefaultSettings)
|
|
setting.POST("/reset_token", handles.ResetToken)
|
|
setting.POST("/set_aria2", handles.SetAria2)
|
|
setting.POST("/set_qbit", handles.SetQbittorrent)
|
|
setting.POST("/set_transmission", handles.SetTransmission)
|
|
setting.POST("/set_115", handles.Set115)
|
|
setting.POST("/set_115_open", handles.Set115Open)
|
|
setting.POST("/set_123_pan", handles.Set123Pan)
|
|
setting.POST("/set_123_open", handles.Set123Open)
|
|
setting.POST("/set_pikpak", handles.SetPikPak)
|
|
setting.POST("/set_thunder", handles.SetThunder)
|
|
setting.POST("/set_thunderx", handles.SetThunderX)
|
|
setting.POST("/set_thunder_browser", handles.SetThunderBrowser)
|
|
setting.POST("/set_guangyapan", handles.SetGuangYaPan)
|
|
|
|
// retain /admin/task API to ensure compatibility with legacy automation scripts
|
|
_task(g.Group("/task"))
|
|
|
|
ms := g.Group("/message")
|
|
ms.POST("/get", message.HttpInstance.GetHandle)
|
|
ms.POST("/send", message.HttpInstance.SendHandle)
|
|
|
|
index := g.Group("/index")
|
|
index.POST("/build", middlewares.SearchIndex, handles.BuildIndex)
|
|
index.POST("/update", middlewares.SearchIndex, handles.UpdateIndex)
|
|
index.POST("/stop", middlewares.SearchIndex, handles.StopIndex)
|
|
index.POST("/clear", middlewares.SearchIndex, handles.ClearIndex)
|
|
index.GET("/progress", middlewares.SearchIndex, handles.GetProgress)
|
|
|
|
scan := g.Group("/scan")
|
|
scan.POST("/start", handles.StartManualScan)
|
|
scan.POST("/stop", handles.StopManualScan)
|
|
scan.GET("/progress", handles.GetManualScanProgress)
|
|
|
|
// 媒体库管理
|
|
mediaAdmin := g.Group("/media")
|
|
mediaAdmin.GET("/config/list", handles.ListMediaConfigs)
|
|
mediaAdmin.POST("/config/save", handles.SaveMediaConfig)
|
|
mediaAdmin.GET("/items", handles.ListMediaItemsAdmin)
|
|
mediaAdmin.POST("/items/update", handles.UpdateMediaItemAdmin)
|
|
mediaAdmin.POST("/items/delete", handles.DeleteMediaItemAdmin)
|
|
mediaAdmin.POST("/scan/start", handles.StartMediaScan)
|
|
mediaAdmin.GET("/scan/progress", handles.GetMediaScanProgress)
|
|
mediaAdmin.POST("/scrape/start", handles.StartMediaScrape)
|
|
mediaAdmin.GET("/scrape/stats", handles.GetMediaScrapeStats)
|
|
mediaAdmin.POST("/clear", handles.ClearMediaDB)
|
|
mediaAdmin.POST("/clear_scrape", handles.ClearMediaScrape)
|
|
mediaAdmin.POST("/delete_invalid", handles.DeleteInvalidMedia)
|
|
// 扫描路径管理
|
|
mediaAdmin.GET("/scan_paths", handles.ListMediaScanPaths)
|
|
mediaAdmin.POST("/scan_paths/create", handles.CreateMediaScanPath)
|
|
mediaAdmin.POST("/scan_paths/update", handles.UpdateMediaScanPath)
|
|
mediaAdmin.POST("/scan_paths/delete", handles.DeleteMediaScanPath)
|
|
mediaAdmin.POST("/scan_paths/clear", handles.ClearMediaScanPathDB)
|
|
// 导入 / 导出
|
|
mediaAdmin.GET("/export", handles.ExportMediaDB)
|
|
mediaAdmin.POST("/import", handles.ImportMediaDB)
|
|
// 媒体应用密钥(Subsonic/Emby/MoviePilot 等无交互客户端)
|
|
mediaAdmin.GET("/appkey/list", handles.ListMediaAppKeys)
|
|
mediaAdmin.POST("/appkey/create", handles.CreateMediaAppKey)
|
|
mediaAdmin.POST("/appkey/delete", handles.DeleteMediaAppKey)
|
|
// STRM 批量生成
|
|
mediaAdmin.POST("/strm/generate", handles.GenerateMediaStrm)
|
|
|
|
// 转码管理
|
|
transcodeAdmin := g.Group("/transcode")
|
|
transcodeAdmin.GET("/workers", handles.ListTranscodeWorkers)
|
|
transcodeAdmin.GET("/jobs", handles.ListTranscodeJobs)
|
|
transcodeAdmin.POST("/cancel", handles.CancelTranscodeJob)
|
|
}
|
|
|
|
func fsAndShare(g *gin.RouterGroup) {
|
|
g.Any("/list", handles.FsListSplit)
|
|
g.Any("/get", handles.FsGetSplit)
|
|
a := g.Group("/archive")
|
|
a.Any("/meta", handles.FsArchiveMetaSplit)
|
|
a.Any("/list", handles.FsArchiveListSplit)
|
|
|
|
// 转码 - 用户态:申请播放
|
|
g.POST("/transcode/play", handles.RequireMediaEnabled, handles.TranscodePlay)
|
|
}
|
|
|
|
func _fs(g *gin.RouterGroup) {
|
|
g.Any("/search", middlewares.SearchIndex, handles.Search)
|
|
g.Any("/other", handles.FsOther)
|
|
g.Any("/dirs", handles.FsDirs)
|
|
g.POST("/mkdir", handles.FsMkdir)
|
|
g.POST("/rename", handles.FsRename)
|
|
g.POST("/batch_rename", handles.FsBatchRename)
|
|
g.POST("/regex_rename", handles.FsRegexRename)
|
|
g.POST("/move", handles.FsMove)
|
|
g.POST("/recursive_move", handles.FsRecursiveMove)
|
|
g.POST("/copy", handles.FsCopy)
|
|
g.POST("/remove", handles.FsRemove)
|
|
g.POST("/remove_empty_directory", handles.FsRemoveEmptyDirectory)
|
|
uploadLimiter := middlewares.UploadRateLimiter(stream.ClientUploadLimit)
|
|
g.PUT("/put", middlewares.FsUp, uploadLimiter, handles.FsStream)
|
|
g.PUT("/form", middlewares.FsUp, uploadLimiter, handles.FsForm)
|
|
multipartPkg.DefaultManager.StartGC() // reclaim ring files orphaned by a previous run
|
|
multipart := g.Group("/multipart")
|
|
multipart.POST("/init", middlewares.FsUp, handles.MultipartInit)
|
|
multipart.PUT("/chunk", uploadLimiter, handles.MultipartChunk)
|
|
multipart.POST("/complete", handles.MultipartComplete)
|
|
multipart.GET("/status", handles.MultipartStatus)
|
|
multipart.POST("/abort", handles.MultipartAbort)
|
|
g.POST("/link", middlewares.AuthAdmin, handles.Link)
|
|
// g.POST("/add_aria2", handles.AddOfflineDownload)
|
|
// g.POST("/add_qbit", handles.AddQbittorrent)
|
|
// g.POST("/add_transmission", handles.SetTransmission)
|
|
g.POST("/add_offline_download", handles.AddOfflineDownload)
|
|
g.POST("/archive/decompress", handles.FsArchiveDecompress)
|
|
// Torrent 相关接口
|
|
g.POST("/torrent/parse", handles.ParseTorrent)
|
|
g.POST("/torrent/upload_parse", handles.UploadTorrentAndParse)
|
|
g.POST("/torrent/rapid_upload", handles.TorrentRapidUpload)
|
|
g.POST("/torrent/generate", handles.GenerateTorrentForPath)
|
|
// Unified transfer seed APIs. Legacy torrent routes above remain supported.
|
|
seed := g.Group("/seed")
|
|
seed.POST("/parse", handles.ParseSeed)
|
|
seed.POST("/upload_parse", handles.UploadSeedAndParse)
|
|
seed.POST("/generate", handles.GenerateSeedForPaths)
|
|
seed.POST("/convert", handles.ConvertSeed)
|
|
seed.POST("/diagnose", handles.DiagnoseSeed)
|
|
seed.POST("/capabilities", handles.SeedCapabilities)
|
|
seed.POST("/rapid_upload", handles.QuickSaveSeed)
|
|
seed.POST("/offline_download", handles.QuickSaveSeed)
|
|
seed.POST("/update", handles.UpdateSeed)
|
|
seed.POST("/quick_save", handles.QuickSaveSeed)
|
|
seed.POST("/update_channels", handles.UpdateSeedChannels)
|
|
// Direct upload (client-side upload to storage)
|
|
g.POST("/get_direct_upload_info", handles.FsGetDirectUploadInfo)
|
|
}
|
|
|
|
func _task(g *gin.RouterGroup) {
|
|
handles.SetupTaskRoute(g)
|
|
}
|
|
|
|
func _sharing(g *gin.RouterGroup) {
|
|
g.Any("/list", handles.ListSharings)
|
|
g.GET("/get", handles.GetSharing)
|
|
g.POST("/create", handles.CreateSharing)
|
|
g.POST("/update", handles.UpdateSharing)
|
|
g.POST("/delete", handles.DeleteSharing)
|
|
g.POST("/enable", handles.SetEnableSharing(false))
|
|
g.POST("/disable", handles.SetEnableSharing(true))
|
|
}
|
|
|
|
func Cors(r *gin.Engine) {
|
|
config := cors.DefaultConfig()
|
|
// config.AllowAllOrigins = true
|
|
config.AllowOrigins = conf.Conf.Cors.AllowOrigins
|
|
config.AllowHeaders = conf.Conf.Cors.AllowHeaders
|
|
config.AllowMethods = conf.Conf.Cors.AllowMethods
|
|
r.Use(cors.New(config))
|
|
}
|
|
|
|
func InitS3(e *gin.Engine) {
|
|
Cors(e)
|
|
S3Server(e.Group("/"))
|
|
}
|