mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 12:36:37 +08:00
support multiple roots per library
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
@@ -20,6 +23,9 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
if err := ensurePerformanceIndexes(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureLibraryRootsCompatibility(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if isSQLite(db) {
|
||||
return ensureMediaSearchIndex(db)
|
||||
}
|
||||
@@ -52,6 +58,7 @@ func ensurePerformanceIndexes(db *gorm.DB) error {
|
||||
statements := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_media_library_created_active ON media(library_id, created_at DESC) WHERE deleted_at IS NULL`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_media_library_episode_active ON media(library_id, season_num, episode_num, created_at DESC) WHERE deleted_at IS NULL`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_media_library_root_active ON media(library_id, library_root_id) WHERE deleted_at IS NULL`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_media_series_active ON media(series_id, season_num, episode_num) WHERE deleted_at IS NULL`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_favorites_user_media_active ON favorites(user_id, media_id) WHERE deleted_at IS NULL`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_playback_histories_user_media_active ON playback_histories(user_id, media_id, watched_at DESC) WHERE deleted_at IS NULL`,
|
||||
@@ -77,6 +84,107 @@ func ensurePerformanceIndexes(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureLibraryRootsCompatibility(db *gorm.DB) error {
|
||||
var libraries []model.Library
|
||||
if err := db.Find(&libraries).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, lib := range libraries {
|
||||
if strings.TrimSpace(lib.Path) == "" {
|
||||
continue
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&model.LibraryRoot{}).Where("library_id = ?", lib.ID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
root := model.LibraryRoot{
|
||||
LibraryID: lib.ID,
|
||||
Name: firstLibraryRootLabel(lib.Path),
|
||||
Path: lib.Path,
|
||||
Enabled: lib.Enabled,
|
||||
SortOrder: 0,
|
||||
}
|
||||
if err := db.Create(&root).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backfillLibraryRootMedia(db, lib, root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func backfillLibraryRootMedia(db *gorm.DB, lib model.Library, root model.LibraryRoot) error {
|
||||
var rows []model.Media
|
||||
if err := db.Unscoped().
|
||||
Model(&model.Media{}).
|
||||
Select("id", "path").
|
||||
Where("library_id = ? AND (library_root_id = '' OR library_root_id IS NULL)", lib.ID).
|
||||
Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rootPath := strings.TrimSpace(root.Path)
|
||||
for _, row := range rows {
|
||||
rel, ok := relativePathWithinRoot(row.Path, rootPath)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if err := db.Unscoped().Model(&model.Media{}).Where("id = ?", row.ID).Updates(map[string]any{
|
||||
"library_root_id": root.ID,
|
||||
"relative_path": rel,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func relativePathWithinRoot(pathValue, root string) (string, bool) {
|
||||
pathValue = strings.TrimSpace(pathValue)
|
||||
root = strings.TrimSpace(root)
|
||||
if pathValue == "" || root == "" {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(root), "cloud://") || strings.HasPrefix(strings.ToLower(pathValue), "cloud://") {
|
||||
prefix := strings.TrimRight(root, "/") + "/"
|
||||
if strings.EqualFold(pathValue, root) {
|
||||
return "", true
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(pathValue), strings.ToLower(prefix)) {
|
||||
return strings.TrimPrefix(pathValue, prefix), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
cleanPath := filepath.Clean(pathValue)
|
||||
cleanRoot := filepath.Clean(root)
|
||||
rel, err := filepath.Rel(cleanRoot, cleanPath)
|
||||
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
||||
return "", false
|
||||
}
|
||||
return rel, true
|
||||
}
|
||||
|
||||
func firstLibraryRootLabel(pathValue string) string {
|
||||
pathValue = strings.TrimSpace(pathValue)
|
||||
if pathValue == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(pathValue), "cloud://") {
|
||||
parts := strings.Split(strings.Trim(pathValue, "/"), "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
}
|
||||
base := filepath.Base(filepath.Clean(pathValue))
|
||||
if base == "." || base == string(filepath.Separator) {
|
||||
return pathValue
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// mediaSearchIndexSchemaVersion identifies the physical FTS index layout.
|
||||
// v2 aligns FTS rowids with media rowids and keeps the index current with
|
||||
// triggers.
|
||||
|
||||
Reference in New Issue
Block a user