update fix NineSong Scene_Audio_Music api

This commit is contained in:
翔 程 2025-04-28 10:18:37 +08:00
parent 0f2f583470
commit ba0dd943c2
27 changed files with 662 additions and 406 deletions

2
.gitignore vendored
View File

@ -39,4 +39,4 @@ NSMusicS-Electron/resources/mpv-x86_64-20241124
NSMusicS-Electron/resources/mpv-0.39.0
NSMusicS-Electron/src_example
NSMusicS-Go/tmp/main.exe
NineSong/tmp/main.exe

View File

@ -7,11 +7,11 @@
APP_ENV=development
# ===== 容器命名配置 | Container naming configuration =====
APP_CONTAINER_NAME=nsmusics # 前端容器名称(可修改)
APP_CONTAINER_NAME=ninesong # 前端容器名称(可修改)
# Front-end container name (modifiable)
WEB_CONTAINER_NAME=nsmusics-go # 后端容器名称(可修改): 请保持WEB_CONTAINER_NAME与BACKEND_SERVICE中域名一致
WEB_CONTAINER_NAME=ninesong-go # 后端容器名称(可修改): 请保持WEB_CONTAINER_NAME与BACKEND_SERVICE中域名一致
# Back-end container name (modifiable)
MONGO_CONTAINER_NAME=nsmusics-mongodb # 数据库容器名称(可修改)
MONGO_CONTAINER_NAME=ninesong-mongodb # 数据库容器名称(可修改)
# Database container name (modifiable)
# ===== 端口配置 | port configuration =====
@ -21,7 +21,7 @@ SERVER_PORT=8082 # 后端端口(可修改): 请
# Back-end port (modifiable): please keep SERVER_PORT consistent with SERVER_ADDRESS
SERVER_ADDRESS=:8082 # 后端地址(可修改): 请保持SERVER_PORT与SERVER_ADDRESS一致
# Back-end address (modifiable): please keep SERVER_PORT consistent with SERVER_ADDRESS
BACKEND_SERVICE=http://nsmusics-go:8082 # 前端请求后端地址(可修改)
BACKEND_SERVICE=http://ninesong-go:8082 # 前端请求后端地址(可修改)
# Front-end request back-end address (modifiable)
CONTEXT_TIMEOUT=10
@ -34,7 +34,7 @@ DB_USER=jiuge01 # 数据库用户名(可修改):此处请设置高
# Database user name (modifiable): please set high-strength fields here. Simple fields or default fields are vulnerable to attack
DB_PASS=jiuge01 # 数据库用户密码(可修改):此处请设置高强度字段,简单字段或默认字段容易遭受攻击
# Database user password (modifiable): please set high-strength fields here. Simple fields or default fields are vulnerable to attack
DB_NAME=nsmusics-go-db
DB_NAME=ninesong-go-db
LIBRARY_PATH=/data/library

View File

@ -0,0 +1,70 @@
package scene_audio_route_api_controller
import (
"net/http"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/gin-gonic/gin"
)
type AlbumController struct {
AlbumUsecase scene_audio_route_interface.AlbumRepository
}
func NewAlbumController(uc scene_audio_route_interface.AlbumRepository) *AlbumController {
return &AlbumController{AlbumUsecase: uc}
}
func (ac *AlbumController) GetAlbumItems(c *gin.Context) {
params := struct {
Start string `form:"start"`
End string `form:"end"`
Sort string `form:"sort"`
Order string `form:"order"`
Search string `form:"search"`
Starred string `form:"starred"`
ArtistID string `form:"artist_id"`
}{
Start: c.DefaultQuery("start", "0"),
End: c.DefaultQuery("end", "50"),
Sort: c.DefaultQuery("sort", "name"),
Order: c.DefaultQuery("order", "asc"),
Search: c.Query("search"),
Starred: c.Query("starred"),
ArtistID: c.Query("artist_id"),
}
albums, err := ac.AlbumUsecase.GetAlbumItems(
c.Request.Context(),
params.End,
params.Order,
params.Sort,
params.Start,
params.Search,
params.Starred,
params.ArtistID,
)
if err != nil {
handleAlbumError(c, err)
return
}
c.JSON(http.StatusOK, scene_audio_route_models.AlbumListResponse{
Albums: albums,
Count: len(albums),
})
}
func handleAlbumError(c *gin.Context, err error) {
switch err.Error() {
case "invalid start parameter",
"invalid end parameter",
"invalid starred parameter",
"invalid artist id format":
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误"})
}
}

View File

@ -0,0 +1,55 @@
package scene_audio_route_api_controller
import (
"net/http"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/gin-gonic/gin"
)
type ArtistController struct {
ArtistUsecase scene_audio_route_interface.ArtistRepository
}
func NewArtistController(uc scene_audio_route_interface.ArtistRepository) *ArtistController {
return &ArtistController{ArtistUsecase: uc}
}
func (c *ArtistController) GetArtists(ctx *gin.Context) {
params := struct {
Start string `form:"start"`
End string `form:"end"`
Sort string `form:"sort"`
Order string `form:"order"`
Search string `form:"search"`
Starred string `form:"starred"`
}{
Start: ctx.DefaultQuery("start", "0"),
End: ctx.DefaultQuery("end", "50"),
Sort: ctx.DefaultQuery("sort", "name"),
Order: ctx.DefaultQuery("order", "asc"),
Search: ctx.Query("search"),
Starred: ctx.Query("starred"),
}
artists, err := c.ArtistUsecase.GetArtistItems(
ctx.Request.Context(),
params.End,
params.Order,
params.Sort,
params.Start,
params.Search,
params.Starred,
)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, scene_audio_route_models.ArtistListResponse{
Artists: artists,
Count: len(artists),
})
}

View File

@ -0,0 +1,78 @@
package scene_audio_route_api_controller
import (
"net/http"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/gin-gonic/gin"
)
type MediaFileController struct {
MediaFileUsecase scene_audio_route_interface.MediaFileRepository
}
func NewMediaFileController(uc scene_audio_route_interface.MediaFileRepository) *MediaFileController {
return &MediaFileController{MediaFileUsecase: uc}
}
func (c *MediaFileController) GetMediaFiles(ctx *gin.Context) {
params := struct {
Start string `form:"start"`
End string `form:"end"`
Sort string `form:"sort"`
Order string `form:"order"`
Search string `form:"search"`
Starred string `form:"starred"`
AlbumID string `form:"album_id"`
ArtistID string `form:"artist_id"`
Year string `form:"year"`
}{
Start: ctx.DefaultQuery("start", "0"),
End: ctx.DefaultQuery("end", "100"),
Sort: ctx.DefaultQuery("sort", "title"),
Order: ctx.DefaultQuery("order", "asc"),
Search: ctx.Query("search"),
Starred: ctx.Query("starred"),
AlbumID: ctx.Query("album_id"),
ArtistID: ctx.Query("artist_id"),
Year: ctx.Query("year"),
}
mediaFiles, err := c.MediaFileUsecase.GetMediaFileItems(
ctx.Request.Context(),
params.End,
params.Order,
params.Sort,
params.Start,
params.Search,
params.Starred,
params.AlbumID,
params.ArtistID,
params.Year,
)
if err != nil {
handleMediaFileError(ctx, err)
return
}
ctx.JSON(http.StatusOK, scene_audio_route_models.MediaFileListResponse{
MediaFiles: mediaFiles,
Count: len(mediaFiles),
})
}
func handleMediaFileError(ctx *gin.Context, err error) {
switch err.Error() {
case "invalid start parameter",
"invalid end parameter",
"invalid starred parameter",
"invalid album id format",
"invalid artist id format",
"year must be integer":
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误"})
}
}

View File

@ -0,0 +1,66 @@
package scene_audio_route_api_controller
import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/gin-gonic/gin"
"net/http"
)
type PlaylistTrackController struct {
PlaylistTrackUsecase scene_audio_route_interface.PlaylistTrackRepository
}
func NewPlaylistTrackController(uc scene_audio_route_interface.PlaylistTrackRepository) *PlaylistTrackController {
return &PlaylistTrackController{PlaylistTrackUsecase: uc}
}
func (c *PlaylistTrackController) GetPlaylistTracks(ctx *gin.Context) {
params := struct {
Start string `form:"start"`
End string `form:"end"`
Sort string `form:"sort"`
Order string `form:"order"`
Search string `form:"search"`
Starred string `form:"starred"`
AlbumId string `form:"albumId"`
ArtistId string `form:"artistId"`
Year string `form:"year"`
PlaylistId string `form:"playlistId"`
}{
Start: ctx.DefaultQuery("start", "0"),
End: ctx.DefaultQuery("end", "50"),
Sort: ctx.DefaultQuery("sort", "created_at"),
Order: ctx.DefaultQuery("order", "asc"),
Search: ctx.Query("search"),
Starred: ctx.Query("starred"),
AlbumId: ctx.Query("albumId"),
ArtistId: ctx.Query("artistId"),
Year: ctx.Query("year"),
PlaylistId: ctx.Param("playlistId"),
}
tracks, err := c.PlaylistTrackUsecase.GetPlaylistTrackItems(
ctx.Request.Context(),
params.End,
params.Order,
params.Sort,
params.Start,
params.Search,
params.Starred,
params.AlbumId,
params.ArtistId,
params.Year,
params.PlaylistId,
)
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, scene_audio_route_models.PlaylistTrackListResponse{
PlaylistTracks: tracks,
Count: len(tracks),
})
}

View File

@ -4,7 +4,8 @@ import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_app/route_app_config"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_app/route_app_library"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_auth"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_file_entity"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_file_entity/scene_audio_db_api_route"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_file_entity/scene_audio_route_api_route"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/route/route_system"
"time"
@ -50,5 +51,7 @@ func RouterPrivate(env *bootstrap.Env, timeout time.Duration, db mongo.Database,
// app library
route_app_library.NewAppMediaFileLibraryRouter(timeout, db, protectedRouter)
// file entity
route_file_entity.NewFileEntityRouter(timeout, db, protectedRouter)
scene_audio_db_api_route.NewFileEntityRouter(timeout, db, protectedRouter)
// scene audio
scene_audio_route_api_route.NewArtistRouter(env, timeout, db, protectedRouter)
}

View File

@ -1,8 +1,8 @@
package route_file_entity
package scene_audio_db_api_route
import (
"fmt"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller/controller_file_entity"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller/controller_file_entity/scene_audio_db_api_controller"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
@ -35,7 +35,7 @@ func NewFileEntityRouter(timeout time.Duration, db mongo.Database, group *gin.Ro
)
// 注册控制器
ctrl := controller_file_entity.NewFileController(uc)
ctrl := scene_audio_db_api_controller.NewFileController(uc)
// 路由配置
group.Use(requestLogger())

View File

@ -0,0 +1,32 @@
package scene_audio_route_api_route
import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller/controller_file_entity/scene_audio_route_api_controller"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/repository/repository_file_entity/scene_audio/scene_audio_route_repository"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/usecase/usecase_file_entity/scene_audio/scene_audio_route_usecase"
"time"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/bootstrap"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"github.com/gin-gonic/gin"
)
func NewAlbumRouter(
env *bootstrap.Env,
timeout time.Duration,
db mongo.Database,
group *gin.RouterGroup,
) {
repo := scene_audio_route_repository.NewAlbumRepository(db, domain.CollectionFileEntityAudioAlbum)
usecase := scene_audio_route_usecase.NewAlbumUsecase(repo, timeout)
ctrl := scene_audio_route_api_controller.NewAlbumController(usecase)
albumGroup := group.Group("/albums")
{
albumGroup.GET("", ctrl.GetAlbumItems)
// 可扩展其他路由
// albumGroup.GET("/:id", ctrl.GetAlbumDetail)
}
}

View File

@ -0,0 +1,28 @@
package scene_audio_route_api_route
import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller/controller_file_entity/scene_audio_route_api_controller"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/repository/repository_file_entity/scene_audio/scene_audio_route_repository"
"time"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/bootstrap"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"github.com/gin-gonic/gin"
)
func NewArtistRouter(
env *bootstrap.Env,
timeout time.Duration,
db mongo.Database,
group *gin.RouterGroup,
) {
repo := scene_audio_route_repository.NewArtistRepository(db, domain.CollectionFileEntityAudioArtist)
ctrl := scene_audio_route_api_controller.NewArtistController(repo)
artistGroup := group.Group("/artists")
{
artistGroup.GET("", ctrl.GetArtists)
}
}

View File

@ -0,0 +1,33 @@
package scene_audio_route_api_route
import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller/controller_file_entity/scene_audio_route_api_controller"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/repository/repository_file_entity/scene_audio/scene_audio_route_repository"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/usecase/usecase_file_entity/scene_audio/scene_audio_route_usecase"
"time"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/bootstrap"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"github.com/gin-gonic/gin"
)
func NewMediaFileRouter(
env *bootstrap.Env,
timeout time.Duration,
db mongo.Database,
group *gin.RouterGroup,
) {
repo := scene_audio_route_repository.NewMediaFileRepository(db, domain.CollectionFileEntityAudioMediaFile)
usecase := scene_audio_route_usecase.NewMediaFileUsecase(repo, timeout)
ctrl := scene_audio_route_api_controller.NewMediaFileController(usecase)
mediaGroup := group.Group("/mediafiles")
{
mediaGroup.GET("", ctrl.GetMediaFiles)
// 可扩展路由示例
// mediaGroup.GET("/:id", ctrl.GetMediaFileDetail)
// mediaGroup.PUT("/:id/star", ctrl.StarMediaFile)
}
}

View File

@ -0,0 +1,27 @@
package scene_audio_route_api_route
import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/api/controller/controller_file_entity/scene_audio_route_api_controller"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/bootstrap"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/repository/repository_file_entity/scene_audio/scene_audio_route_repository"
"github.com/gin-gonic/gin"
"time"
)
func NewPlaylistTrackRouter(
env *bootstrap.Env,
timeout time.Duration,
db mongo.Database,
group *gin.RouterGroup,
) {
repo := scene_audio_route_repository.NewPlaylistTrackRepository(db, domain.CollectionFileEntityPlaylistTrack)
ctrl := scene_audio_route_api_controller.NewPlaylistTrackController(repo)
playlistTrackGroup := group.Group("/playlists/:playlistId/tracks")
{
playlistTrackGroup.GET("", ctrl.GetPlaylistTracks)
}
}

View File

@ -107,6 +107,14 @@ func (si *Initializer) executeInitialization(ctx context.Context) error {
return err
}
if err := si.initFileEntityAudioPlaylist(ctx); err != nil {
return err
}
if err := si.initFileEntityAudioPlaylistTrack(ctx); err != nil {
return err
}
return si.markInitialized(ctx)
}
@ -898,3 +906,40 @@ func (si *Initializer) initFileEntityAudioAnnotation(ctx context.Context) error
return nil
}
func (si *Initializer) initFileEntityAudioPlaylist(ctx context.Context) error {
coll := si.db.Collection(domain.CollectionFileEntityPlaylist)
dummyID := primitive.NewObjectID()
emptyDoc := &scene_audio_db_models.PlaylistMetadata{
ID: dummyID,
}
if _, err := coll.InsertOne(ctx, emptyDoc); err != nil {
return err
}
if _, err := coll.DeleteOne(ctx, bson.M{"_id": dummyID}); err != nil {
return err
}
return nil
}
func (si *Initializer) initFileEntityAudioPlaylistTrack(ctx context.Context) error {
coll := si.db.Collection(domain.CollectionFileEntityPlaylistTrack)
emptyDoc := &scene_audio_db_models.PlaylistTrackMetadata{
ID: 0,
}
if _, err := coll.InsertOne(ctx, emptyDoc); err != nil {
return err
}
if _, err := coll.DeleteOne(ctx, bson.M{"_id": 0}); err != nil {
return err
}
return nil
}

View File

@ -56,3 +56,9 @@ const (
const (
CollectionFileEntityAnnotation = "file_entity_audio_annotation"
)
const (
CollectionFileEntityPlaylist = "file_entity_audio_playlist"
)
const (
CollectionFileEntityPlaylistTrack = "file_entity_audio_playlist_track"
)

View File

@ -30,3 +30,8 @@ type AlbumMetadata struct {
AllArtistIDs string `bson:"all_artist_ids"`
ImageFiles string `bson:"image_files"` // 为空则不存在cover封面从媒体文件中提取
}
type AlbumListResponse struct {
Albums []AlbumMetadata `json:"albums"`
Count int `json:"count"`
}

View File

@ -21,3 +21,8 @@ type ArtistMetadata struct {
ImageFiles string `bson:"image_files"` // 为空则不存在cover封面从媒体文件中提取
}
type ArtistListResponse struct {
Artists []ArtistMetadata `json:"artists"`
Count int `json:"count"`
}

View File

@ -34,22 +34,7 @@ type MediaFileMetadata struct {
Channels int `bson:"channels"`
}
// FilterParams 基础过滤参数
type FilterParams struct {
Genres []string
MinYear int
MaxYear int
ArtistIDs []string
MinDuration float64
MaxDuration float64
AlbumTypes []string
HasLyrics *bool
BitrateRange [2]int
}
// SortParams 排序参数
type SortParams struct {
Field string // 支持字段title, artist, year, duration, bit_rate 等
Ascending bool
Collation string // 排序规则nocase
type MediaFileListResponse struct {
MediaFiles []MediaFileMetadata `json:"media_files"`
Count int `json:"count"`
}

View File

@ -10,3 +10,8 @@ type PlaylistTrackMetadata struct {
AlbumID primitive.ObjectID `bson:"album_id"`
ArtistID primitive.ObjectID `bson:"artist_id"`
}
type PlaylistTrackListResponse struct {
PlaylistTracks []PlaylistTrackMetadata `json:"playlist_tracks"`
Count int `json:"count"`
}

View File

@ -2,18 +2,14 @@ package scene_audio_route_repository
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_db/scene_audio_db_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"strconv"
)
type albumRepository struct {
@ -28,110 +24,63 @@ func NewAlbumRepository(db mongo.Database, collection string) scene_audio_route_
}
}
func (a albumRepository) GetAlbumItems(
func (r *albumRepository) GetAlbumItems(
ctx context.Context,
end, order, sort, start, search, starred, artistId string,
) ([]scene_audio_route_models.AlbumMetadata, error) {
collection := a.db.Collection(a.collection)
collection := r.db.Collection(r.collection)
// 构建复合查询条件
filter := bson.M{}
// 处理艺术家ID过滤
if artistId != "" {
filter["artist_id"] = artistId
}
// 处理搜索条件
if search != "" {
filter["$or"] = []bson.M{
{"name": bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}},
{"artist": bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}},
{"name": primitive.Regex{Pattern: search, Options: "i"}},
{"artist": primitive.Regex{Pattern: search, Options: "i"}},
}
}
// 处理收藏过滤
if starred != "" {
isStarred, err := strconv.ParseBool(starred)
if err != nil {
return nil, errors.New("invalid starred parameter")
return nil, fmt.Errorf("invalid starred parameter: %w", err)
}
filter["starred"] = isStarred
}
// 分页处理
skip, limit := getPagination(start, end)
// 排序处理
sortOption := getSortOption_Album(sort, order)
// 执行查询
findOptions := options.Find().
SetSort(sortOption).
SetSkip(int64(skip)).
SetLimit(int64(limit))
cursor, err := collection.Find(ctx, filter, findOptions)
if err != nil {
return nil, fmt.Errorf("database query failed: %v", err)
}
defer cursor.Close(ctx)
// 使用组合结构体解码数据库模型
var results []struct {
scene_audio_db_models.AlbumMetadata `bson:",inline"`
MergedImageURL string `bson:"-"`
skip, _ := strconv.Atoi(start)
limit, _ := strconv.Atoi(end)
if limit == 0 || limit > 100 {
limit = 50
}
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("failed to decode results: %v", err)
validSortFields := map[string]bool{"name": true, "song_count": true, "created_at": true}
if !validSortFields[sort] {
sort = "name"
}
// 转换为路由模型
routeResults := make([]scene_audio_route_models.AlbumMetadata, len(results))
for i, item := range results {
routeResults[i] = scene_audio_route_models.AlbumMetadata{
ID: item.ID,
Name: item.Name,
ArtistID: item.ArtistID,
Artist: item.Artist,
AlbumArtist: item.AlbumArtist,
MinYear: item.MinYear,
MaxYear: item.MaxYear,
SongCount: item.SongCount,
Duration: item.Duration,
Genre: item.Genre,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
AlbumArtistID: item.AlbumArtistID,
Comment: item.Comment,
AllArtistIDs: item.AllArtistIDs,
ImageFiles: "", // 待填充Music场景Metadata查找资源逻辑
PlayCount: 0, // 待填充Annotation交互逻辑
Rating: 0, // 待填充Annotation交互逻辑
Starred: false, // 待填充Annotation交互逻辑
StarredAt: time.Time{}, // 待填充Annotation交互逻辑
}
}
if routeResults == nil {
return []scene_audio_route_models.AlbumMetadata{}, nil
}
return routeResults, nil
}
// 排序参数处理
func getSortOption_Album(sortField, order string) bson.D {
sortOrder := 1 // 默认升序
sortOrder := 1
if order == "desc" {
sortOrder = -1
}
// 设置默认排序字段
if sortField == "" {
sortField = "name"
opts := options.Find().
SetSort(bson.D{{Key: sort, Value: sortOrder}}).
SetSkip(int64(skip)).
SetLimit(int64(limit))
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
return nil, fmt.Errorf("database query failed: %w", err)
}
defer cursor.Close(ctx)
var results []scene_audio_route_models.AlbumMetadata
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("decode error: %w", err)
}
return bson.D{{Key: sortField, Value: sortOrder}}
return results, nil
}

View File

@ -2,17 +2,13 @@ package scene_audio_route_repository
import (
"context"
"errors"
"fmt"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_db/scene_audio_db_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"strconv"
"time"
)
type artistRepository struct {
@ -27,107 +23,52 @@ func NewArtistRepository(db mongo.Database, collection string) scene_audio_route
}
}
func (a artistRepository) GetArtistItems(
func (r *artistRepository) GetArtistItems(
ctx context.Context,
end, order, sort, start, search, starred string,
) ([]scene_audio_route_models.ArtistMetadata, error) {
collection := a.db.Collection(a.collection)
collection := r.db.Collection(r.collection)
// 构建查询条件
filter := bson.M{}
// 处理搜索条件
if search != "" {
filter["name"] = bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}
filter["name"] = bson.M{"$regex": search, "$options": "i"}
}
// 处理收藏过滤
if starred != "" {
isStarred, err := strconv.ParseBool(starred)
if err != nil {
return nil, errors.New("invalid starred parameter")
return nil, fmt.Errorf("invalid starred parameter: %w", err)
}
filter["starred"] = isStarred
}
// 处理分页
skip, limit := getPagination(start, end)
skip, _ := strconv.Atoi(start)
limit, _ := strconv.Atoi(end)
if limit == 0 || limit > 100 {
limit = 50
}
// 构建排序
sortOption := getSortOption_Artist(sort, order)
// 执行查询
findOptions := options.Find().
SetSort(sortOption).
SetSkip(int64(skip)).
SetLimit(int64(limit))
cursor, err := collection.Find(ctx, filter, findOptions)
if err != nil {
return nil, fmt.Errorf("database query failed: %v", err)
}
defer cursor.Close(ctx)
var results []struct {
scene_audio_db_models.ArtistMetadata `bson:",inline"`
MergedImageURL string `bson:"-"` // 用于接收合并后的值
}
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("failed to decode results: %v", err)
}
// 转换为目标结构
routeResults := make([]scene_audio_route_models.ArtistMetadata, len(results))
for i, item := range results {
routeResults[i] = scene_audio_route_models.ArtistMetadata{
ID: item.ID,
Name: item.Name,
AlbumCount: item.AlbumCount,
SongCount: item.SongCount,
Size: item.Size,
ImageFiles: "", // 待填充Music场景Metadata查找资源逻辑
PlayCount: 0, // 待填充Annotation交互逻辑
Rating: 0, // 待填充Annotation交互逻辑
Starred: false, // 待填充Annotation交互逻辑
StarredAt: time.Time{}, // 待填充Annotation交互逻辑
}
}
// 处理空结果
if routeResults == nil {
return []scene_audio_route_models.ArtistMetadata{}, nil
}
return routeResults, nil
}
// 分页参数处理
func getPagination(start, end string) (skip int, limit int) {
skip, _ = strconv.Atoi(start)
limit, _ = strconv.Atoi(end)
// 设置默认值
if limit == 0 {
limit = 50 // 默认每页20条
}
if skip < 0 {
skip = 0
}
return
}
// 排序参数处理
func getSortOption_Artist(sortField, order string) bson.D {
sortOrder := 1 // 默认升序
sortOrder := 1
if order == "desc" {
sortOrder = -1
}
opts := options.Find().
SetSort(bson.D{{Key: sort, Value: sortOrder}}).
SetSkip(int64(skip)).
SetLimit(int64(limit))
// 设置默认排序字段
if sortField == "" {
sortField = "name"
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
return nil, fmt.Errorf("database query failed: %w", err)
}
defer cursor.Close(ctx)
var results []scene_audio_route_models.ArtistMetadata
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("decode error: %w", err)
}
return bson.D{{Key: sortField, Value: sortOrder}}
return results, nil
}

View File

@ -3,18 +3,13 @@ package scene_audio_route_repository
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_db/scene_audio_db_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"strconv"
)
type mediaFileRepository struct {
@ -29,131 +24,101 @@ func NewMediaFileRepository(db mongo.Database, collection string) scene_audio_ro
}
}
func (m mediaFileRepository) GetMediaFileItems(
func (r *mediaFileRepository) GetMediaFileItems(
ctx context.Context,
end, order, sort, start, search, starred, albumId, artistId, year string,
) ([]scene_audio_route_models.MediaFileMetadata, error) {
collection := m.db.Collection(m.collection)
collection := r.db.Collection(r.collection)
// 构建复合查询条件
filter := bson.M{}
// 处理艺术家ID过滤
if artistId != "" {
filter["artist_id"] = artistId
}
// 处理专辑ID过滤
if albumId != "" {
filter["album_id"] = albumId
}
// 处理年份过滤
if year != "" {
yearInt, err := strconv.Atoi(year)
if err != nil {
return nil, errors.New("invalid year format")
if yearInt, err := strconv.Atoi(year); err == nil {
filter["year"] = yearInt
}
filter["year"] = yearInt
}
// 处理多字段搜索(标题、艺术家、专辑)
if search != "" {
filter["$or"] = []bson.M{
{"title": bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}},
{"artist": bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}},
{"album": bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}},
{"title": bson.M{"$regex": search, "$options": "i"}},
{"artist": bson.M{"$regex": search, "$options": "i"}},
{"album": bson.M{"$regex": search, "$options": "i"}},
}
}
// 处理收藏状态
if starred != "" {
isStarred, err := strconv.ParseBool(starred)
if err != nil {
return nil, errors.New("invalid starred parameter")
return nil, fmt.Errorf("invalid starred parameter: %w", err)
}
filter["starred"] = isStarred
}
// 分页处理
skip, limit := getPagination(start, end)
// 排序处理支持nocase排序规则
sortOption := getSortOption_MediaFile(sort, order)
// 执行查询
findOptions := options.Find().
SetSort(sortOption).
SetSkip(int64(skip)).
SetLimit(int64(limit))
cursor, err := collection.Find(ctx, filter, findOptions)
if err != nil {
return nil, fmt.Errorf("database query failed: %v", err)
}
defer cursor.Close(ctx)
// 使用组合结构体解码数据库模型
var results []struct {
scene_audio_db_models.MediaFileMetadata `bson:",inline"`
MergedImageURL string `bson:"-"`
// 处理分页
skip, _ := strconv.Atoi(start)
limit, _ := strconv.Atoi(end)
if limit == 0 || limit > 100 {
limit = 50
}
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("failed to decode results: %v", err)
// 安全排序字段白名单
validSortFields := map[string]bool{
"title": true, "artist": true,
"year": true, "duration": true,
}
if !validSortFields[sort] {
sort = "title"
}
// 转换为路由模型
routeResults := make([]scene_audio_route_models.MediaFileMetadata, len(results))
for i, item := range results {
routeResults[i] = scene_audio_route_models.MediaFileMetadata{
ID: item.ID,
Path: item.Path,
Title: item.Title,
Album: item.Album,
Artist: item.Artist,
ArtistID: item.ArtistID,
AlbumArtist: item.AlbumArtist,
AlbumID: item.AlbumID,
HasCoverArt: item.HasCoverArt,
Year: item.Year,
Size: item.Size,
Suffix: item.Suffix,
Duration: item.Duration,
BitRate: item.BitRate,
Genre: item.Genre,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
AlbumArtistID: item.AlbumArtistID,
Channels: item.Channels,
PlayCount: 0, // 待填充Annotation交互逻辑
Rating: 0, // 待填充Annotation交互逻辑
Starred: false, // 待填充Annotation交互逻辑
StarredAt: time.Time{}, // 待填充Annotation交互逻辑
}
}
if routeResults == nil {
return []scene_audio_route_models.MediaFileMetadata{}, nil
}
return routeResults, nil
}
// 常规排序处理(仅字段顺序)
func getSortOption_MediaFile(sortField, order string) bson.D {
// 构建排序
sortOrder := 1
if order == "desc" {
sortOrder = -1
}
opts := options.Find().
SetSort(bson.D{{Key: sort, Value: sortOrder}}).
SetSkip(int64(skip)).
SetLimit(int64(limit))
// 设置默认排序字段
if sortField == "" {
sortField = "title" // 保持原默认字段
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
return nil, fmt.Errorf("database query failed: %w", err)
}
defer cursor.Close(ctx)
var results []scene_audio_route_models.MediaFileMetadata
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("decode error: %w", err)
}
// 简化后的排序条件
return bson.D{
{Key: sortField, Value: sortOrder},
// 后处理逻辑
for i := range results {
if !results[i].HasCoverArt {
results[i].HasCoverArt = checkFallbackCover(results[i].AlbumID)
}
}
return results, nil
}
func checkFallbackCover(albumID string) bool {
// 实现1: 简单校验专辑ID格式
if _, err := strconv.Atoi(albumID); err == nil {
return true // 假设有效ID即有封面
}
return false
// 实现2: 实际数据库查询需注入collection
/*
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var result struct{ HasCover bool }
err := collection.FindOne(ctx, bson.M{"album_id": albumID}).Decode(&result)
return err == nil && result.HasCover
*/
}

View File

@ -3,14 +3,11 @@ package scene_audio_route_repository
import (
"context"
"errors"
"fmt"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_db/scene_audio_db_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_interface"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
"github.com/amitshekhariitbhu/go-backend-clean-architecture/mongo"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"strconv"
)
@ -27,100 +24,61 @@ func NewPlaylistTrackRepository(db mongo.Database, collection string) scene_audi
}
}
func (p playlistTrackRepository) GetPlaylistTrackItems(
func (r *playlistTrackRepository) GetPlaylistTrackItems(
ctx context.Context,
end, order, sort, start, search, starred, albumId, artistId, year, playlistId string,
) ([]scene_audio_route_models.PlaylistTrackMetadata, error) {
collection := p.db.Collection(p.collection)
end string, order string, sort string, start string, search string, starred string, albumId string, artistId string, year string,
playlistId string) ([]scene_audio_route_models.PlaylistTrackMetadata, error) {
// 构建基础查询条件
filter := bson.M{"playlist_id": playlistId} // 强制关联播放列表ID
collection := r.db.Collection(r.collection)
filter := bson.M{"playlist_id": playlistId}
// 处理关联实体过滤
if albumId != "" {
if _, err := primitive.ObjectIDFromHex(albumId); err == nil {
filter["album_id"] = albumId
}
filter["album_id"] = albumId
}
if artistId != "" {
if _, err := primitive.ObjectIDFromHex(artistId); err == nil {
filter["artist_id"] = artistId
}
filter["artist_id"] = artistId
}
// 处理播放类型过滤
if search != "" {
filter["$or"] = []bson.M{
{"play_type": bson.M{"$regex": primitive.Regex{Pattern: search, Options: "i"}}},
{"track_name": bson.M{"$regex": search, "$options": "i"}},
}
}
// 处理收藏状态
if starred != "" {
isStarred, err := strconv.ParseBool(starred)
if err != nil {
return nil, errors.New("invalid starred parameter")
return nil, fmt.Errorf("invalid starred parameter: %w", err)
}
filter["starred"] = isStarred
}
// 分页处理
skip, limit := getPagination(start, end)
// 排序处理默认按ID升序
sortOption := getSortOption_PlaylistTrack(sort, order)
// 执行查询
findOptions := options.Find().
SetSort(sortOption).
SetSkip(int64(skip)).
SetLimit(int64(limit))
cursor, err := collection.Find(ctx, filter, findOptions)
if err != nil {
return nil, fmt.Errorf("database query failed: %v", err)
}
defer cursor.Close(ctx)
// 使用组合结构体解码
var results []struct {
scene_audio_db_models.PlaylistTrackMetadata `bson:",inline"`
skip, _ := strconv.Atoi(start)
limit, _ := strconv.Atoi(end)
if limit == 0 || limit > 100 {
limit = 50
}
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("failed to decode results: %v", err)
}
// 转换为路由模型
routeResults := make([]scene_audio_route_models.PlaylistTrackMetadata, len(results))
for i, item := range results {
routeResults[i] = scene_audio_route_models.PlaylistTrackMetadata{
ID: item.ID,
PlaylistID: item.PlaylistID,
PlayType: item.PlayType,
MediaFileID: item.MediaFileID,
AlbumID: item.AlbumID,
ArtistID: item.ArtistID,
}
}
return routeResults, nil
}
// 常规排序处理(仅字段顺序)
func getSortOption_PlaylistTrack(sortField, order string) bson.D {
sortOrder := 1
if order == "desc" {
sortOrder = -1
}
// 设置默认排序字段
if sortField == "" {
sortField = "title" // 保持原默认字段
opts := options.Find().
SetSort(bson.D{{Key: sort, Value: sortOrder}}).
SetSkip(int64(skip)).
SetLimit(int64(limit))
cursor, err := collection.Find(ctx, filter, opts)
if err != nil {
return nil, fmt.Errorf("database query failed: %w", err)
}
defer cursor.Close(ctx)
var results []scene_audio_route_models.PlaylistTrackMetadata
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("decode error: %w", err)
}
// 简化后的排序条件
return bson.D{
{Key: sortField, Value: sortOrder},
}
return results, nil
}

Binary file not shown.

View File

@ -11,39 +11,43 @@ import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
)
type albumUsecase struct {
albumRepo scene_audio_route_interface.AlbumRepository
timeout time.Duration
type AlbumUsecase struct {
repo scene_audio_route_interface.AlbumRepository
timeout time.Duration
}
func NewAlbumUsecase(repo scene_audio_route_interface.AlbumRepository, timeout time.Duration) scene_audio_route_interface.AlbumRepository {
return &albumUsecase{
albumRepo: repo,
timeout: timeout,
func NewAlbumUsecase(repo scene_audio_route_interface.AlbumRepository, timeout time.Duration) *AlbumUsecase {
return &AlbumUsecase{
repo: repo,
timeout: timeout,
}
}
func (uc *albumUsecase) GetAlbumItems(
func (uc *AlbumUsecase) GetAlbumItems(
ctx context.Context,
end, order, sort, start, search, starred, artistId string,
) ([]scene_audio_route_models.AlbumMetadata, error) {
ctx, cancel := context.WithTimeout(ctx, uc.timeout)
defer cancel()
// 参数验证
if _, err := strconv.Atoi(start); err != nil && start != "" {
if _, err := strconv.Atoi(start); start != "" && err != nil {
return nil, errors.New("invalid start parameter")
}
if _, err := strconv.Atoi(end); err != nil && end != "" {
if _, err := strconv.Atoi(end); end != "" && err != nil {
return nil, errors.New("invalid end parameter")
}
// 转换artistId为ObjectID格式
if artistId != "" {
if _, err := primitive.ObjectIDFromHex(artistId); err != nil {
return nil, errors.New("invalid artist id format")
}
}
return uc.albumRepo.GetAlbumItems(ctx, end, order, sort, start, search, starred, artistId)
validSortFields := map[string]bool{"name": true, "song_count": true, "created_at": true}
if !validSortFields[sort] {
sort = "name"
}
return uc.repo.GetAlbumItems(ctx, end, order, sort, start, search, starred, artistId)
}

View File

@ -10,32 +10,38 @@ import (
"github.com/amitshekhariitbhu/go-backend-clean-architecture/domain/domain_file_entity/scene_audio/scene_audio_route/scene_audio_route_models"
)
type artistUsecase struct {
artistRepo scene_audio_route_interface.ArtistRepository
timeout time.Duration
type ArtistUsecase struct {
repo scene_audio_route_interface.ArtistRepository
timeout time.Duration
}
func NewArtistUsecase(repo scene_audio_route_interface.ArtistRepository, timeout time.Duration) scene_audio_route_interface.ArtistRepository {
return &artistUsecase{
artistRepo: repo,
timeout: timeout,
func NewArtistUsecase(repo scene_audio_route_interface.ArtistRepository, timeout time.Duration) *ArtistUsecase {
return &ArtistUsecase{
repo: repo,
timeout: timeout,
}
}
func (uc *artistUsecase) GetArtistItems(
func (uc *ArtistUsecase) GetArtistItems(
ctx context.Context,
end, order, sort, start, search, starred string,
) ([]scene_audio_route_models.ArtistMetadata, error) {
ctx, cancel := context.WithTimeout(ctx, uc.timeout)
defer cancel()
// 参数验证
if _, err := strconv.Atoi(start); err != nil && start != "" {
if _, err := strconv.Atoi(start); start != "" && err != nil {
return nil, errors.New("invalid start parameter")
}
if _, err := strconv.Atoi(end); err != nil && end != "" {
if _, err := strconv.Atoi(end); end != "" && err != nil {
return nil, errors.New("invalid end parameter")
}
return uc.artistRepo.GetArtistItems(ctx, end, order, sort, start, search, starred)
// 参数白名单校验
validSortFields := map[string]bool{"name": true, "album_count": true, "song_count": true}
if !validSortFields[sort] {
sort = "name"
}
return uc.repo.GetArtistItems(ctx, end, order, sort, start, search, starred)
}

View File

@ -17,7 +17,7 @@ type playlistTrackUsecase struct {
timeout time.Duration
}
func NewPlaylistTrackUsecase(repo scene_audio_route_interface.PlaylistTrackRepository, timeout time.Duration) scene_audio_route_interface.PlaylistTrackRepository {
func NewPlaylistTrackUsecase(repo scene_audio_route_interface.PlaylistTrackRepository, timeout time.Duration) *playlistTrackUsecase {
return &playlistTrackUsecase{
repo: repo,
timeout: timeout,
@ -31,29 +31,19 @@ func (uc *playlistTrackUsecase) GetPlaylistTrackItems(
ctx, cancel := context.WithTimeout(ctx, uc.timeout)
defer cancel()
// 参数验证链
validations := []func() error{
func() error {
if _, err := primitive.ObjectIDFromHex(playlistId); err != nil {
return errors.New("invalid playlist id format")
}
return nil
},
func() error {
if _, err := strconv.Atoi(start); err != nil && start != "" {
return errors.New("invalid start parameter")
}
if _, err := strconv.Atoi(end); err != nil && end != "" {
return errors.New("invalid end parameter")
}
return nil
},
if _, err := primitive.ObjectIDFromHex(playlistId); err != nil {
return nil, errors.New("invalid playlist id format")
}
if _, err := strconv.Atoi(start); start != "" && err != nil {
return nil, errors.New("invalid start parameter")
}
if _, err := strconv.Atoi(end); end != "" && err != nil {
return nil, errors.New("invalid end parameter")
}
for _, validate := range validations {
if err := validate(); err != nil {
return nil, err
}
validSortFields := map[string]bool{"created_at": true, "play_order": true}
if !validSortFields[sort] {
sort = "created_at"
}
return uc.repo.GetPlaylistTrackItems(ctx, end, order, sort, start, search, starred, albumId, artistId, year, playlistId)