mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
265 lines
13 KiB
Go
265 lines
13 KiB
Go
package handler
|
||
|
||
import (
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"github.com/truewhile/MeBox/internal/middleware"
|
||
"github.com/truewhile/MeBox/internal/service"
|
||
)
|
||
|
||
// registerEmbyRoutes 在 r 上挂双前缀("" + "/emby")的 Emby 兼容路由。
|
||
func registerEmbyRoutes(r *gin.Engine, jwtSecret string, svc *service.Container) {
|
||
for _, prefix := range []string{"/emby", ""} {
|
||
grp := r.Group(prefix)
|
||
grp.Use(embyNoStoreHeaders())
|
||
|
||
registerEmbyRootRoutes(grp, prefix, svc)
|
||
registerEmbyPublicRoutes(grp, jwtSecret, svc)
|
||
registerEmbyPublicImageRoutes(grp, svc)
|
||
|
||
// 鉴权后端点
|
||
auth := grp.Group("", embyAuthRequiredWithSessionFallback(jwtSecret), activeEmbyUserRequired(svc), embyRealtimeSessionActivity(svc))
|
||
registerEmbyAuthenticatedRoutes(auth, prefix, svc)
|
||
}
|
||
|
||
// 为没有设置 NoRoute 的 Engine 提供 Emby 路径前缀与大小写纠偏(如单测或独立挂载环境)
|
||
r.NoRoute(func(c *gin.Context) {
|
||
if TryHandleEmbyNormalizedRoute(c, r) {
|
||
return
|
||
}
|
||
c.Status(404)
|
||
})
|
||
}
|
||
|
||
type embyRouteHandlerFactory func(*service.Container) gin.HandlerFunc
|
||
|
||
func embyNoStoreHeaders() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
c.Header("Cache-Control", "no-store")
|
||
c.Header("Pragma", "no-cache")
|
||
c.Header("Expires", "0")
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
func registerEmbyRootRoutes(grp *gin.RouterGroup, prefix string, svc *service.Container) {
|
||
if prefix != "/emby" {
|
||
return
|
||
}
|
||
grp.GET("", embyRootHandler(svc))
|
||
grp.HEAD("", embyRootHandler(svc))
|
||
grp.GET("/", embyRootHandler(svc))
|
||
grp.HEAD("/", embyRootHandler(svc))
|
||
}
|
||
|
||
func registerEmbyPublicRoutes(grp *gin.RouterGroup, jwtSecret string, svc *service.Container) {
|
||
registerEmbyPublicSystemRoutes(grp, svc)
|
||
registerEmbyPublicSessionRoutes(grp, jwtSecret, svc)
|
||
registerEmbyPublicClientRoutes(grp, jwtSecret, svc)
|
||
}
|
||
|
||
func registerEmbyPublicSystemRoutes(grp *gin.RouterGroup, svc *service.Container) {
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/System/Info/Public", "/system/info/public"}, embySystemInfoPublicHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/System/Info", "/system/info"}, embySystemInfoHandler)
|
||
registerEmbyGetRoutes(grp, svc, []string{"/System/Endpoint", "/system/endpoint"}, embySystemEndpointHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/System/Ext/ServerDomains", "/system/ext/serverdomains"}, embyServerDomainsHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/System/Configuration/Public", "/system/configuration/public"}, embyPublicServerConfigurationHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/Startup/Configuration", "/startup/configuration"}, embyStartupConfigurationHandler)
|
||
registerEmbyPostRoutes(grp, svc, []string{"/Startup/Complete", "/startup/complete"}, embyNoContentHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/QuickConnect/Enabled", "/quickconnect/enabled"}, embyQuickConnectEnabledHandler)
|
||
for _, path := range []string{"/System/Ping", "/system/ping"} {
|
||
grp.GET(path, embyPingHandler(svc))
|
||
grp.HEAD(path, embyPingHandler(svc))
|
||
grp.POST(path, embyPingHandler(svc))
|
||
}
|
||
}
|
||
|
||
func registerEmbyPublicSessionRoutes(grp *gin.RouterGroup, jwtSecret string, svc *service.Container) {
|
||
for _, path := range []string{
|
||
"/Sessions/Capabilities", "/Sessions/Capabilities/Full",
|
||
"/sessions/capabilities", "/sessions/capabilities/full",
|
||
} {
|
||
grp.POST(path, embySessionCapabilitiesHandler(svc, jwtSecret))
|
||
}
|
||
|
||
// 30/min per IP: many Emby clients sit behind a single NAT/reverse-proxy
|
||
// IP, so a low limit would throttle legitimate logins into 429s.
|
||
embyLoginLimiter := middleware.NewRateLimiter(30, 1*time.Minute)
|
||
for _, path := range []string{"/Users/AuthenticateByName", "/Users/authenticatebyname", "/users/AuthenticateByName", "/users/authenticatebyname"} {
|
||
grp.POST(path, middleware.RateLimit(embyLoginLimiter), embyAuthByNameHandler(svc))
|
||
}
|
||
|
||
registerEmbyGetRoutes(grp, svc, []string{"/Users/Public", "/users/public"}, embyPublicUsersHandler)
|
||
}
|
||
|
||
func registerEmbyPublicClientRoutes(grp *gin.RouterGroup, jwtSecret string, svc *service.Container) {
|
||
registerEmbyGetRoutes(grp, svc, []string{"/Branding/Configuration", "/branding/configuration"}, embyBrandingConfigHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/Branding/Css", "/branding/css"}, embyBrandingCSSHandler)
|
||
registerEmbyGetRoutes(grp, svc, []string{"/Localization/Options", "/localization/options"}, embyLocalizationOptionsHandler)
|
||
registerEmbyGetRoutes(grp, svc, []string{"/Localization/Cultures", "/Localization/cultures", "/localization/cultures"}, embyLocalizationCulturesHandler)
|
||
registerEmbyGetHeadRoutes(grp, svc, []string{"/CustomCssJS/Scripts", "/customcssjs/scripts"}, embyCustomCSSJSScriptsHandler)
|
||
for _, path := range []string{"/embywebsocket", "/EmbyWebSocket"} {
|
||
grp.GET(path, embyWebSocketHandler(svc, jwtSecret))
|
||
grp.HEAD(path, embyNoContentHandler(svc))
|
||
}
|
||
for _, path := range []string{"/Sessions/Logout", "/sessions/logout"} {
|
||
grp.POST(path, embySessionLogoutHandler(svc, jwtSecret))
|
||
}
|
||
grp.GET("/DisplayPreferences/:id", embyDisplayPreferencesHandler(svc))
|
||
grp.POST("/DisplayPreferences/:id", embySaveDisplayPreferencesHandler(svc))
|
||
grp.GET("/displaypreferences/:id", embyDisplayPreferencesHandler(svc))
|
||
grp.POST("/displaypreferences/:id", embySaveDisplayPreferencesHandler(svc))
|
||
}
|
||
|
||
func registerEmbyPublicImageRoutes(grp *gin.RouterGroup, svc *service.Container) {
|
||
// 图片公开(Infuse 缓存 URL 时会丢 token)
|
||
grp.GET("/Items/:id/Images/:type", embyItemImageHandler(svc))
|
||
grp.GET("/Items/:id/Images/:type/:index", embyItemImageHandler(svc))
|
||
grp.HEAD("/Items/:id/Images/:type", embyItemImageHandler(svc))
|
||
grp.GET("/items/:id/images/:type", embyItemImageHandler(svc))
|
||
grp.GET("/items/:id/images/:type/:index", embyItemImageHandler(svc))
|
||
grp.HEAD("/items/:id/images/:type", embyItemImageHandler(svc))
|
||
}
|
||
|
||
func registerEmbyGetRoutes(grp *gin.RouterGroup, svc *service.Container, paths []string, factory embyRouteHandlerFactory) {
|
||
for _, path := range paths {
|
||
grp.GET(path, factory(svc))
|
||
}
|
||
}
|
||
|
||
func registerEmbyGetHeadRoutes(grp *gin.RouterGroup, svc *service.Container, paths []string, factory embyRouteHandlerFactory) {
|
||
for _, path := range paths {
|
||
grp.GET(path, factory(svc))
|
||
grp.HEAD(path, factory(svc))
|
||
}
|
||
}
|
||
|
||
func registerEmbyPostRoutes(grp *gin.RouterGroup, svc *service.Container, paths []string, factory embyRouteHandlerFactory) {
|
||
for _, path := range paths {
|
||
grp.POST(path, factory(svc))
|
||
}
|
||
}
|
||
|
||
func registerEmbyAuthenticatedRoutes(auth *gin.RouterGroup, prefix string, svc *service.Container) {
|
||
registerEmbyAuthenticatedUserRoutes(auth, svc)
|
||
registerEmbyAuthenticatedItemRoutes(auth, svc)
|
||
registerEmbyAuthenticatedPlaybackRoutes(auth, prefix, svc)
|
||
registerEmbyAuthenticatedProgressRoutes(auth, svc)
|
||
registerEmbyAuthenticatedUserDataRoutes(auth, svc)
|
||
registerEmbyAuthenticatedSystemRoutes(auth, svc)
|
||
registerLowercaseEmbyAuthRoutes(auth, svc)
|
||
}
|
||
|
||
func registerEmbyAuthenticatedUserRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
||
auth.GET("/Users/Me", embyMeHandler(svc))
|
||
auth.GET("/Users", embyListUsersHandler(svc))
|
||
auth.GET("/Users/:userId", embyGetUserByIDHandler(svc))
|
||
auth.GET("/Users/:userId/Views", embyViewsHandler(svc))
|
||
auth.GET("/Library/MediaFolders", embyViewsHandler(svc))
|
||
auth.GET("/Library/VirtualFolders", embyVirtualFoldersHandler(svc))
|
||
auth.GET("/Library/SelectableMediaFolders", embyVirtualFoldersHandler(svc))
|
||
}
|
||
|
||
func registerEmbyAuthenticatedItemRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
||
auth.GET("/Items", embyItemsHandler(svc))
|
||
auth.GET("/Users/:userId/Items", embyItemsHandler(svc))
|
||
auth.GET("/Items/Counts", embyItemsCountsHandler(svc))
|
||
auth.GET("/Users/:userId/Items/Counts", embyItemsCountsHandler(svc))
|
||
auth.GET("/Items/Latest", embyLatestItemsHandler(svc))
|
||
auth.GET("/Items/Resume", embyResumeItemsHandler(svc))
|
||
auth.GET("/Users/:userId/Items/Resume", embyResumeItemsHandler(svc))
|
||
auth.GET("/UserItems/Resume", embyResumeItemsHandler(svc))
|
||
auth.GET("/Items/:id", embyItemByIDHandler(svc))
|
||
auth.GET("/Users/:userId/Items/:id", embyUserItemByIDHandler(svc))
|
||
auth.GET("/Shows/:id/Seasons", embyShowSeasonsHandler(svc))
|
||
auth.GET("/Shows/:id/Episodes", embyShowEpisodesHandler(svc))
|
||
auth.GET("/Users/:userId/Shows/:id/Seasons", embyShowSeasonsHandler(svc))
|
||
auth.GET("/Users/:userId/Shows/:id/Episodes", embyShowEpisodesHandler(svc))
|
||
auth.GET("/Shows/NextUp", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Users/:userId/Shows/NextUp", embyEmptyItemsHandler(svc))
|
||
auth.GET("/MediaSegments/:id", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Artists", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Persons", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Genres", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Shows/Upcoming", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Users/:userId/Shows/Upcoming", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Items/:id/Similar", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Items/:id/ThumbnailSet", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Items/:id/ThemeMedia", embyThemeMediaHandler(svc))
|
||
auth.GET("/Users/:userId/Items/:id/SpecialFeatures", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Users/:userId/Items/:id/Intros", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Items/:id/SpecialFeatures", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Items/:id/Intros", embyEmptyItemsHandler(svc))
|
||
auth.GET("/api/danmu/:id/raw", embyDanmuRawHandler(svc))
|
||
}
|
||
|
||
func registerEmbyAuthenticatedPlaybackRoutes(auth *gin.RouterGroup, prefix string, svc *service.Container) {
|
||
auth.GET("/Items/:id/PlaybackInfo", embyPlaybackInfoHandler(svc))
|
||
auth.POST("/Items/:id/PlaybackInfo", embyPlaybackInfoHandler(svc))
|
||
auth.GET("/Users/:userId/Items/:id/PlaybackInfo", embyPlaybackInfoHandler(svc))
|
||
auth.POST("/Users/:userId/Items/:id/PlaybackInfo", embyPlaybackInfoHandler(svc))
|
||
|
||
registerEmbyVideoStreamRoutes(auth, svc, "/Videos")
|
||
auth.GET("/Videos/:id/Subtitles/:index/Stream", embySubtitleStreamHandler(svc))
|
||
auth.HEAD("/Videos/:id/Subtitles/:index/Stream", embySubtitleStreamHandler(svc))
|
||
auth.GET("/Users/:userId/Videos/:id/Subtitles/:index/Stream", embySubtitleStreamHandler(svc))
|
||
auth.HEAD("/Users/:userId/Videos/:id/Subtitles/:index/Stream", embySubtitleStreamHandler(svc))
|
||
// Official Emby/Swagger shape:
|
||
// /Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/Stream.{Format}
|
||
// The mediaSourceId segment is a bare path param. We name it :seg (matching
|
||
// the HLS /Videos/:id/:seg catch-all) so gin allows the two routes to
|
||
// coexist — gin permits the same :param name to be both terminal and parent
|
||
// of children, but rejects two different param names at the same position.
|
||
// The subtitle handler ignores this segment (lookup is by :id + :index).
|
||
auth.GET("/Videos/:id/:seg/Subtitles/:index/Stream.:format", embySubtitleStreamHandler(svc))
|
||
auth.HEAD("/Videos/:id/:seg/Subtitles/:index/Stream.:format", embySubtitleStreamHandler(svc))
|
||
auth.GET("/Users/:userId/Videos/:id/:seg/Subtitles/:index/Stream.:format", embySubtitleStreamHandler(svc))
|
||
auth.HEAD("/Users/:userId/Videos/:id/:seg/Subtitles/:index/Stream.:format", embySubtitleStreamHandler(svc))
|
||
if prefix == "/emby" {
|
||
auth.GET("/api/stream/:id", embyVideoStreamHandler(svc, service.CloudPlaybackModeSTRM))
|
||
auth.HEAD("/api/stream/:id", embyVideoStreamHandler(svc, service.CloudPlaybackModeSTRM))
|
||
}
|
||
auth.GET("/Videos/:id/master.m3u8", embyVideoHLSPlaylistHandler(svc))
|
||
auth.HEAD("/Videos/:id/master.m3u8", embyVideoHLSPlaylistHandler(svc))
|
||
auth.GET("/Videos/:id/main.m3u8", embyVideoHLSPlaylistHandler(svc))
|
||
auth.HEAD("/Videos/:id/main.m3u8", embyVideoHLSPlaylistHandler(svc))
|
||
auth.GET("/Videos/:id/:seg", embyVideoHLSSegmentHandler(svc))
|
||
}
|
||
|
||
func registerEmbyVideoStreamRoutes(auth *gin.RouterGroup, svc *service.Container, basePath string) {
|
||
streamHandler := func() gin.HandlerFunc {
|
||
return embyVideoStreamHandler(svc, service.CloudPlaybackModeRedirectProxy)
|
||
}
|
||
for _, path := range []string{"/:id/stream", "/:id/stream.:container", "/:id/original", "/:id/original.:container"} {
|
||
fullPath := basePath + path
|
||
auth.GET(fullPath, streamHandler())
|
||
auth.HEAD(fullPath, streamHandler())
|
||
}
|
||
}
|
||
|
||
func registerEmbyAuthenticatedProgressRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
||
auth.POST("/Sessions/Playing", embyPlayingProgressHandler(svc))
|
||
auth.POST("/Sessions/Playing/Progress", embyPlayingProgressHandler(svc))
|
||
auth.POST("/Sessions/Playing/Stopped", embyPlayingProgressHandler(svc))
|
||
}
|
||
|
||
func registerEmbyAuthenticatedUserDataRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
||
auth.POST("/Users/:userId/FavoriteItems/:itemId", embyFavoriteHandler(svc, true))
|
||
auth.DELETE("/Users/:userId/FavoriteItems/:itemId", embyFavoriteHandler(svc, false))
|
||
auth.POST("/Users/:userId/PlayedItems/:itemId", embyMarkPlayedHandler(svc, true))
|
||
auth.DELETE("/Users/:userId/PlayedItems/:itemId", embyMarkPlayedHandler(svc, false))
|
||
}
|
||
|
||
func registerEmbyAuthenticatedSystemRoutes(auth *gin.RouterGroup, svc *service.Container) {
|
||
auth.GET("/Sessions", embySessionsHandler(svc))
|
||
auth.GET("/System/Configuration", embyServerConfigurationHandler(svc))
|
||
auth.GET("/System/WakeOnLanInfo", embyEmptyArrayHandler(svc))
|
||
auth.GET("/ScheduledTasks", embyEmptyArrayHandler(svc))
|
||
auth.GET("/LiveTv/Recordings", embyEmptyItemsHandler(svc))
|
||
auth.GET("/System/ActivityLog/Entries", embyEmptyItemsHandler(svc))
|
||
auth.GET("/Web/ConfigurationPages", embyEmptyArrayHandler(svc))
|
||
auth.POST("/Users/:userId/Configuration", embyNoContentHandler(svc))
|
||
}
|