11package db
22
33import (
4+ "encoding/json"
5+ "strings"
6+
47 "github.com/OpenListTeam/OpenList/v4/internal/model"
58 "gorm.io/gorm"
69)
@@ -12,12 +15,9 @@ func GetMediaConfig(mediaType model.MediaType) (*model.MediaConfig, error) {
1215 var cfg model.MediaConfig
1316 result := db .Where ("media_type = ?" , mediaType ).First (& cfg )
1417 if result .Error == gorm .ErrRecordNotFound {
15- // 返回默认配置
1618 return & model.MediaConfig {
1719 MediaType : mediaType ,
1820 Enabled : false ,
19- ScanPath : "/" ,
20- PathMerge : false ,
2121 }, nil
2222 }
2323 return & cfg , result .Error
@@ -41,18 +41,56 @@ func SaveMediaConfig(cfg *model.MediaConfig) error {
4141 return db .Save (cfg ).Error
4242}
4343
44+ // ==================== MediaScanPath ====================
45+
46+ // ListMediaScanPaths 获取指定媒体类型的所有扫描路径
47+ func ListMediaScanPaths (mediaType model.MediaType ) ([]model.MediaScanPath , error ) {
48+ var paths []model.MediaScanPath
49+ tx := db .Model (& model.MediaScanPath {})
50+ if mediaType != "" {
51+ tx = tx .Where ("media_type = ?" , mediaType )
52+ }
53+ err := tx .Order ("id asc" ).Find (& paths ).Error
54+ return paths , err
55+ }
56+
57+ // GetMediaScanPath 按ID获取扫描路径
58+ func GetMediaScanPath (id uint ) (* model.MediaScanPath , error ) {
59+ var p model.MediaScanPath
60+ err := db .First (& p , id ).Error
61+ return & p , err
62+ }
63+
64+ // CreateMediaScanPath 创建扫描路径
65+ func CreateMediaScanPath (p * model.MediaScanPath ) error {
66+ return db .Create (p ).Error
67+ }
68+
69+ // UpdateMediaScanPath 更新扫描路径
70+ func UpdateMediaScanPath (p * model.MediaScanPath ) error {
71+ return db .Save (p ).Error
72+ }
73+
74+ // DeleteMediaScanPath 删除扫描路径(硬删除)
75+ func DeleteMediaScanPath (id uint ) error {
76+ return db .Unscoped ().Delete (& model.MediaScanPath {}, id ).Error
77+ }
78+
4479// ==================== MediaItem ====================
4580
4681// MediaItemQuery 媒体条目查询参数
4782type MediaItemQuery struct {
48- MediaType model.MediaType
49- FolderPath string
50- Hidden * bool
51- Keyword string
52- OrderBy string // "name", "date", "size"
53- OrderDir string // "asc", "desc"
54- Page int
55- PageSize int
83+ MediaType model.MediaType
84+ ScanPathID uint // 按扫描路径ID筛选
85+ FolderPath string // 按文件夹路径筛选
86+ TypeTag string // 按类型标签筛选(电影、电视剧等)
87+ ContentTag string // 按内容标签筛选(喜剧、惊悚等)
88+ Hidden * bool
89+ Keyword string
90+ OrderBy string // "name", "date", "size"
91+ OrderDir string // "asc", "desc"
92+ Page int
93+ PageSize int
5694}
5795
5896// ListMediaItems 分页查询媒体条目
@@ -61,6 +99,9 @@ func ListMediaItems(q MediaItemQuery) ([]model.MediaItem, int64, error) {
6199 if q .MediaType != "" {
62100 tx = tx .Where ("media_type = ?" , q .MediaType )
63101 }
102+ if q .ScanPathID > 0 {
103+ tx = tx .Where ("scan_path_id = ?" , q .ScanPathID )
104+ }
64105 if q .FolderPath != "" {
65106 tx = tx .Where ("folder_path = ?" , q .FolderPath )
66107 }
@@ -71,6 +112,16 @@ func ListMediaItems(q MediaItemQuery) ([]model.MediaItem, int64, error) {
71112 like := "%" + q .Keyword + "%"
72113 tx = tx .Where ("file_name LIKE ? OR scraped_name LIKE ?" , like , like )
73114 }
115+ // 按类型标签筛选(通过关联扫描路径的type_tag)
116+ if q .TypeTag != "" {
117+ tx = tx .Joins ("JOIN media_scan_paths ON media_scan_paths.id = media_items.scan_path_id" ).
118+ Where ("media_scan_paths.type_tag = ?" , q .TypeTag )
119+ }
120+ // 按内容标签筛选(通过关联扫描路径的content_tags)
121+ if q .ContentTag != "" {
122+ tx = tx .Joins ("JOIN media_scan_paths ON media_scan_paths.id = media_items.scan_path_id" ).
123+ Where ("media_scan_paths.content_tags LIKE ?" , "%" + q .ContentTag + "%" )
124+ }
74125
75126 var total int64
76127 if err := tx .Count (& total ).Error ; err != nil {
@@ -114,18 +165,18 @@ func GetMediaItemByID(id uint) (*model.MediaItem, error) {
114165 return & item , err
115166}
116167
117- // GetMediaItemByPath 按文件路径获取媒体条目
118- func GetMediaItemByPath ( filePath string ) (* model.MediaItem , error ) {
168+ // GetMediaItemByFolderPath 按文件夹路径获取媒体条目(用于合并模式)
169+ func GetMediaItemByFolderPath ( folderPath string ) (* model.MediaItem , error ) {
119170 var item model.MediaItem
120- result := db .Where ("file_path = ?" , filePath ).First (& item )
171+ result := db .Where ("folder_path = ?" , folderPath ).First (& item )
121172 return & item , result .Error
122173}
123174
124- // CreateOrUpdateMediaItem 创建或更新媒体条目(按 file_path 唯一 )
175+ // CreateOrUpdateMediaItem 创建或更新媒体条目(按 folder_path + file_name + album_name 组合唯一 )
125176// 更新时保留已有的刮削数据,避免重新扫描时把已刮削的字段清空
126177func CreateOrUpdateMediaItem (item * model.MediaItem ) error {
127178 var existing model.MediaItem
128- result := db .Where ("file_path = ?" , item .FilePath ).First (& existing )
179+ result := db .Where ("folder_path = ? AND file_name = ? AND album_name = ? " , item .FolderPath , item . FileName , item . AlbumName ).First (& existing )
129180 if result .Error == gorm .ErrRecordNotFound {
130181 return db .Create (item ).Error
131182 }
@@ -171,6 +222,11 @@ func ClearMediaItems(mediaType model.MediaType) error {
171222 return db .Unscoped ().Where ("media_type = ?" , mediaType ).Delete (& model.MediaItem {}).Error
172223}
173224
225+ // ClearMediaItemsByScanPath 硬删除指定扫描路径的所有媒体条目
226+ func ClearMediaItemsByScanPath (scanPathID uint ) error {
227+ return db .Unscoped ().Where ("scan_path_id = ?" , scanPathID ).Delete (& model.MediaItem {}).Error
228+ }
229+
174230// ListAlbums 列出所有专辑(音乐专用)
175231func ListAlbums (q MediaItemQuery ) ([]AlbumInfo , int64 , error ) {
176232 type albumRow struct {
@@ -179,6 +235,7 @@ func ListAlbums(q MediaItemQuery) ([]AlbumInfo, int64, error) {
179235 Cover string
180236 ReleaseDate string
181237 TrackCount int
238+ ScanPathID uint
182239 }
183240
184241 // 构建基础查询
@@ -187,9 +244,12 @@ func ListAlbums(q MediaItemQuery) ([]AlbumInfo, int64, error) {
187244 if q .Hidden != nil {
188245 baseQuery = baseQuery .Where ("hidden = ?" , * q .Hidden )
189246 }
247+ if q .ScanPathID > 0 {
248+ baseQuery = baseQuery .Where ("scan_path_id = ?" , q .ScanPathID )
249+ }
190250 if q .Keyword != "" {
191251 like := "%" + q .Keyword + "%"
192- baseQuery = baseQuery .Where ("album_name LIKE ? OR album_artist LIKE ?" , like , like )
252+ baseQuery = baseQuery .Where ("album_name LIKE ? OR album_artist LIKE ? OR scraped_name LIKE ?" , like , like , like )
193253 }
194254
195255 // 统计分组数(用子查询)
@@ -209,7 +269,7 @@ func ListAlbums(q MediaItemQuery) ([]AlbumInfo, int64, error) {
209269 }
210270
211271 tx := baseQuery .
212- Select ("album_name, album_artist, MAX(cover) as cover, MAX(release_date) as release_date, COUNT(*) as track_count" ).
272+ Select ("album_name, album_artist, MAX(cover) as cover, MAX(release_date) as release_date, COUNT(*) as track_count, MAX(scan_path_id) as scan_path_id " ).
213273 Group ("album_name, album_artist" ).
214274 Offset ((q .Page - 1 ) * q .PageSize ).Limit (q .PageSize )
215275
@@ -226,6 +286,7 @@ func ListAlbums(q MediaItemQuery) ([]AlbumInfo, int64, error) {
226286 Cover : r .Cover ,
227287 ReleaseDate : r .ReleaseDate ,
228288 TrackCount : r .TrackCount ,
289+ ScanPathID : r .ScanPathID ,
229290 }
230291 }
231292 return albums , total , nil
@@ -238,23 +299,63 @@ type AlbumInfo struct {
238299 Cover string `json:"cover"`
239300 ReleaseDate string `json:"release_date"`
240301 TrackCount int `json:"track_count"`
302+ ScanPathID uint `json:"scan_path_id"`
241303}
242304
243305// GetAlbumTracks 获取专辑曲目列表
306+ // 支持两种模式:
307+ // 1. 普通模式(is_folder=false):直接返回独立文件记录
308+ // 2. 合并文件夹模式(is_folder=true):把 episodes 展开成虚拟 MediaItem 列表
309+ // 展开后每条记录的 folder_path = 原folder_path/file_name(文件夹实际路径),file_name = episode.FileName
244310func GetAlbumTracks (albumName , albumArtist string ) ([]model.MediaItem , error ) {
245311 var items []model.MediaItem
246312 tx := db .Where ("media_type = ?" , model .MediaTypeMusic )
247313 if albumName != "" {
248314 tx = tx .Where ("album_name = ?" , albumName )
249315 } else {
250- // album_name 为空时,查询该艺术家下所有无专辑名的曲目
251316 tx = tx .Where ("(album_name = '' OR album_name IS NULL)" )
252317 }
253318 if albumArtist != "" {
254319 tx = tx .Where ("album_artist = ?" , albumArtist )
255320 }
256321 err := tx .Order ("track_number asc" ).Find (& items ).Error
257- return items , err
322+ if err != nil {
323+ return nil , err
324+ }
325+
326+ // 展开合并文件夹条目的 episodes
327+ var result []model.MediaItem
328+ for _ , item := range items {
329+ if ! item .IsFolder || item .Episodes == "" {
330+ result = append (result , item )
331+ continue
332+ }
333+ // 解析 episodes
334+ type EpisodeInfo struct {
335+ FileName string `json:"file_name"`
336+ Index int `json:"index"`
337+ Title string `json:"title"`
338+ }
339+ var eps []EpisodeInfo
340+ if err := json .Unmarshal ([]byte (item .Episodes ), & eps ); err != nil || len (eps ) == 0 {
341+ // 解析失败则跳过该条目(不返回文件夹本身,避免播放路径错误)
342+ continue
343+ }
344+ // 文件夹实际路径 = folder_path + "/" + file_name
345+ actualDir := strings .TrimRight (item .FolderPath , "/" ) + "/" + item .FileName
346+ for _ , ep := range eps {
347+ track := item // 复制基础信息(封面、专辑名、艺术家等)
348+ track .ID = 0
349+ track .IsFolder = false
350+ track .FolderPath = actualDir
351+ track .FileName = ep .FileName
352+ track .TrackNumber = ep .Index
353+ track .ScrapedName = ep .Title
354+ track .Episodes = ""
355+ result = append (result , track )
356+ }
357+ }
358+ return result , nil
258359}
259360
260361// ListFolderPaths 列出指定媒体类型下的所有文件夹路径(目录浏览模式)
0 commit comments