feat: 添加媒体播放功能,支持基于短时效token的播放入口及相关API接口
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/tenant/dto"
|
||||
"quyun/v2/app/requests"
|
||||
@@ -104,13 +108,32 @@ func (*content) previewAssets(ctx fiber.Ctx, tenant *models.Tenant, user *models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playables := make([]*dto.ContentPlayableAsset, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
token, expiresAt, err := services.MediaDelivery.CreatePlayToken(tenant.ID, contentID, asset.ID, consts.ContentAssetRolePreview, user.ID, 0, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta json.RawMessage
|
||||
if len(asset.Meta) > 0 {
|
||||
meta = json.RawMessage(asset.Meta)
|
||||
}
|
||||
playables = append(playables, &dto.ContentPlayableAsset{
|
||||
AssetID: asset.ID,
|
||||
Type: asset.Type,
|
||||
PlayURL: "/t/" + tenant.Code + "/v1/media/play?token=" + url.QueryEscape(token),
|
||||
ExpiresAt: expiresAt,
|
||||
Meta: meta,
|
||||
})
|
||||
}
|
||||
|
||||
previewSeconds := int32(detail.Content.PreviewSeconds)
|
||||
if previewSeconds <= 0 {
|
||||
previewSeconds = consts.DefaultContentPreviewSeconds
|
||||
}
|
||||
return &dto.ContentAssetsResponse{
|
||||
Content: detail.Content,
|
||||
Assets: assets,
|
||||
Assets: playables,
|
||||
PreviewSeconds: previewSeconds,
|
||||
}, nil
|
||||
}
|
||||
@@ -150,8 +173,27 @@ func (*content) mainAssets(ctx fiber.Ctx, tenant *models.Tenant, user *models.Us
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playables := make([]*dto.ContentPlayableAsset, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
token, expiresAt, err := services.MediaDelivery.CreatePlayToken(tenant.ID, contentID, asset.ID, consts.ContentAssetRoleMain, user.ID, 0, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta json.RawMessage
|
||||
if len(asset.Meta) > 0 {
|
||||
meta = json.RawMessage(asset.Meta)
|
||||
}
|
||||
playables = append(playables, &dto.ContentPlayableAsset{
|
||||
AssetID: asset.ID,
|
||||
Type: asset.Type,
|
||||
PlayURL: "/t/" + tenant.Code + "/v1/media/play?token=" + url.QueryEscape(token),
|
||||
ExpiresAt: expiresAt,
|
||||
Meta: meta,
|
||||
})
|
||||
}
|
||||
|
||||
return &dto.ContentAssetsResponse{
|
||||
Content: detail.Content,
|
||||
Assets: assets,
|
||||
Assets: playables,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ type ContentDetail struct {
|
||||
type ContentAssetsResponse struct {
|
||||
// Content is the content entity.
|
||||
Content *models.Content `json:"content,omitempty"`
|
||||
// Assets is the list of media assets for the requested role (preview/main).
|
||||
Assets []*models.MediaAsset `json:"assets,omitempty"`
|
||||
// Assets is the list of playable assets for the requested role (preview/main).
|
||||
Assets []*ContentPlayableAsset `json:"assets,omitempty"`
|
||||
// PreviewSeconds indicates the max preview duration (only meaningful for preview response).
|
||||
PreviewSeconds int32 `json:"preview_seconds,omitempty"`
|
||||
}
|
||||
|
||||
23
backend/app/http/tenant/dto/content_asset_play.go
Normal file
23
backend/app/http/tenant/dto/content_asset_play.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"quyun/v2/pkg/consts"
|
||||
)
|
||||
|
||||
// ContentPlayableAsset is a deliverable media asset item with short-lived play URL/token.
|
||||
type ContentPlayableAsset struct {
|
||||
AssetID int64 `json:"asset_id"`
|
||||
Type consts.MediaAssetType `json:"type"`
|
||||
|
||||
// PlayURL is a short-lived URL; do NOT expose bucket/object_key directly.
|
||||
PlayURL string `json:"play_url"`
|
||||
|
||||
// ExpiresAt indicates when PlayURL/token expires; optional.
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
|
||||
// Meta is a display-safe whitelist (currently passthrough JSON); optional.
|
||||
Meta json.RawMessage `json:"meta,omitempty"`
|
||||
}
|
||||
38
backend/app/http/tenant_media/play.go
Normal file
38
backend/app/http/tenant_media/play.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package tenant_media
|
||||
|
||||
import (
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// media provides media play endpoints (token-based, no JWT required).
|
||||
//
|
||||
// @provider
|
||||
type media struct{}
|
||||
|
||||
// play
|
||||
//
|
||||
// @Summary 媒体播放入口(短时效 token)
|
||||
// @Tags TenantMedia
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tenantCode path string true "Tenant Code"
|
||||
// @Param token query string true "Play token"
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/media/play [get]
|
||||
// @Bind tenant local key(tenant)
|
||||
// @Bind token query
|
||||
func (*media) play(ctx fiber.Ctx, tenant *models.Tenant, token string) error {
|
||||
log.WithFields(log.Fields{
|
||||
"tenant_id": tenant.ID,
|
||||
}).Info("tenant_media.play")
|
||||
|
||||
location, err := services.MediaDelivery.ResolvePlayRedirect(ctx.Context(), tenant.ID, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Redirect().To(location)
|
||||
}
|
||||
37
backend/app/http/tenant_media/provider.gen.go
Executable file
37
backend/app/http/tenant_media/provider.gen.go
Executable file
@@ -0,0 +1,37 @@
|
||||
package tenant_media
|
||||
|
||||
import (
|
||||
"quyun/v2/app/middlewares"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func() (*media, error) {
|
||||
obj := &media{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
media *media,
|
||||
middlewares *middlewares.Middlewares,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
media: media,
|
||||
middlewares: middlewares,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
backend/app/http/tenant_media/routes.gen.go
Normal file
53
backend/app/http/tenant_media/routes.gen.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Code generated by atomctl. DO NOT EDIT.
|
||||
|
||||
// Package tenant_media provides HTTP route definitions and registration
|
||||
// for the quyun/v2 application.
|
||||
package tenant_media
|
||||
|
||||
import (
|
||||
"quyun/v2/app/middlewares"
|
||||
"quyun/v2/database/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
. "go.ipao.vip/atom/fen"
|
||||
)
|
||||
|
||||
// Routes implements the HttpRoute contract and provides route registration
|
||||
// for all controllers in the tenant_media module.
|
||||
//
|
||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||
type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
middlewares *middlewares.Middlewares
|
||||
// Controller instances
|
||||
media *media
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
func (r *Routes) Prepare() error {
|
||||
r.log = log.WithField("module", "routes.tenant_media")
|
||||
r.log.Info("Initializing routes module")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for this routes provider.
|
||||
func (r *Routes) Name() string {
|
||||
return "tenant_media"
|
||||
}
|
||||
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: media
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/media/play -> media.play")
|
||||
router.Get("/t/:tenantCode/v1/media/play"[len(r.Path()):], Func2(
|
||||
r.media.play,
|
||||
Local[*models.Tenant]("tenant"),
|
||||
QueryParam[string]("token"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
11
backend/app/http/tenant_media/routes.manual.go
Normal file
11
backend/app/http/tenant_media/routes.manual.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package tenant_media
|
||||
|
||||
func (r *Routes) Path() string {
|
||||
return "/t/:tenantCode/v1"
|
||||
}
|
||||
|
||||
func (r *Routes) Middlewares() []any {
|
||||
return []any{
|
||||
r.middlewares.TenantResolve,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package tenant_public
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
tenant_dto "quyun/v2/app/http/tenant/dto"
|
||||
"quyun/v2/app/requests"
|
||||
@@ -124,6 +128,25 @@ func (*content) previewAssets(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playables := make([]*tenant_dto.ContentPlayableAsset, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
token, expiresAt, err := services.MediaDelivery.CreatePlayToken(tenant.ID, contentID, asset.ID, consts.ContentAssetRolePreview, uid, 0, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta json.RawMessage
|
||||
if len(asset.Meta) > 0 {
|
||||
meta = json.RawMessage(asset.Meta)
|
||||
}
|
||||
playables = append(playables, &tenant_dto.ContentPlayableAsset{
|
||||
AssetID: asset.ID,
|
||||
Type: asset.Type,
|
||||
PlayURL: "/t/" + tenant.Code + "/v1/media/play?token=" + url.QueryEscape(token),
|
||||
ExpiresAt: expiresAt,
|
||||
Meta: meta,
|
||||
})
|
||||
}
|
||||
|
||||
previewSeconds := int32(detail.Content.PreviewSeconds)
|
||||
if previewSeconds <= 0 {
|
||||
previewSeconds = consts.DefaultContentPreviewSeconds
|
||||
@@ -131,7 +154,7 @@ func (*content) previewAssets(
|
||||
|
||||
return &tenant_dto.ContentAssetsResponse{
|
||||
Content: detail.Content,
|
||||
Assets: assets,
|
||||
Assets: playables,
|
||||
PreviewSeconds: previewSeconds,
|
||||
}, nil
|
||||
}
|
||||
@@ -174,8 +197,27 @@ func (*content) mainAssets(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
playables := make([]*tenant_dto.ContentPlayableAsset, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
token, expiresAt, err := services.MediaDelivery.CreatePlayToken(tenant.ID, contentID, asset.ID, consts.ContentAssetRoleMain, uid, 0, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta json.RawMessage
|
||||
if len(asset.Meta) > 0 {
|
||||
meta = json.RawMessage(asset.Meta)
|
||||
}
|
||||
playables = append(playables, &tenant_dto.ContentPlayableAsset{
|
||||
AssetID: asset.ID,
|
||||
Type: asset.Type,
|
||||
PlayURL: "/t/" + tenant.Code + "/v1/media/play?token=" + url.QueryEscape(token),
|
||||
ExpiresAt: expiresAt,
|
||||
Meta: meta,
|
||||
})
|
||||
}
|
||||
|
||||
return &tenant_dto.ContentAssetsResponse{
|
||||
Content: detail.Content,
|
||||
Assets: assets,
|
||||
Assets: playables,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user