mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
fix library deletion and strm path handling
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -10,13 +11,63 @@ import (
|
||||
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
||||
)
|
||||
|
||||
func TestDeleteLibraryHardDeletesLibraryRoots(t *testing.T) {
|
||||
db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}, &model.Media{})
|
||||
repos := repository.New(db)
|
||||
rootA := filepath.Join(t.TempDir(), "movies-a")
|
||||
rootB := filepath.Join(t.TempDir(), "movies-b")
|
||||
lib := &model.Library{Name: "电影", Path: rootA, Type: "movie", Enabled: true}
|
||||
if err := repos.Library.CreateWithRoots(t.Context(), lib, []model.LibraryRoot{
|
||||
{Path: rootA, Enabled: true, SortOrder: 0},
|
||||
{Path: rootB, Enabled: true, SortOrder: 1},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Media.Upsert(t.Context(), &model.Media{
|
||||
LibraryID: lib.ID,
|
||||
LibraryRootID: lib.Roots[0].ID,
|
||||
Title: "测试电影",
|
||||
Path: filepath.Join(rootA, "movie.mkv"),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
svc := NewMediaService(&config.Config{}, zap.NewNop(), repos)
|
||||
if err := svc.DeleteLibrary(t.Context(), lib.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var rootCount int64
|
||||
if err := db.Unscoped().Model(&model.LibraryRoot{}).Where("library_id = ?", lib.ID).Count(&rootCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rootCount != 0 {
|
||||
t.Fatalf("library roots should be hard deleted, count=%d", rootCount)
|
||||
}
|
||||
var visibleLibraryCount int64
|
||||
if err := db.Model(&model.Library{}).Where("id = ?", lib.ID).Count(&visibleLibraryCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if visibleLibraryCount != 0 {
|
||||
t.Fatalf("deleted library should not remain visible, count=%d", visibleLibraryCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCloudLibraryPurgesMountWithoutRecycleBin(t *testing.T) {
|
||||
db := newServiceTestDB(t, &model.Library{}, &model.Media{})
|
||||
db := newServiceTestDB(t, &model.Library{}, &model.LibraryRoot{}, &model.Media{})
|
||||
repos := repository.New(db)
|
||||
lib := model.Library{Name: "OpenList · 剑来", Path: "cloud://openlist/Anime/JianLai", Type: "anime", Enabled: true}
|
||||
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.DB.Create(&model.LibraryRoot{
|
||||
LibraryID: lib.ID,
|
||||
Name: "JianLai",
|
||||
Path: lib.Path,
|
||||
Enabled: true,
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Media.Upsert(t.Context(), &model.Media{
|
||||
LibraryID: lib.ID,
|
||||
Title: "剑来",
|
||||
@@ -52,6 +103,13 @@ func TestDeleteCloudLibraryPurgesMountWithoutRecycleBin(t *testing.T) {
|
||||
if libCount != 0 {
|
||||
t.Fatalf("cloud mount library should be purged, count=%d", libCount)
|
||||
}
|
||||
var rootCount int64
|
||||
if err := db.Unscoped().Model(&model.LibraryRoot{}).Where("library_id = ?", lib.ID).Count(&rootCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rootCount != 0 {
|
||||
t.Fatalf("cloud mount roots should be purged, count=%d", rootCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaUpsertBackfillsExternalIDsForPendingCloudRows(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListLibraries returns every library configured on the server.
|
||||
@@ -20,24 +21,39 @@ func (s *MediaService) DeleteLibrary(ctx context.Context, id string) error {
|
||||
}
|
||||
if lib != nil {
|
||||
if _, ok := ParseCloudLibraryMount(lib.Path); ok {
|
||||
if err := s.repo.Media.PurgeByLibrary(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.repo.DB.WithContext(ctx).Where("library_id = ?", id).Delete(&model.LibraryRoot{}).Error
|
||||
err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.Library{}).Error
|
||||
err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Unscoped().Where("library_id = ?", id).Delete(&model.Media{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hardDeleteLibraryRoots(ctx, tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Unscoped().Where("id = ?", id).Delete(&model.Library{}).Error
|
||||
})
|
||||
if err == nil {
|
||||
s.invalidateMediaCache(ctx)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = s.repo.DB.WithContext(ctx).Where("library_id = ?", id).Delete(&model.LibraryRoot{}).Error
|
||||
if err := s.repo.Media.DeleteByLibrary(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.repo.Library.Delete(ctx, id)
|
||||
err = s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("library_id = ?", id).Delete(&model.Media{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := hardDeleteLibraryRoots(ctx, tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.Library{}, "id = ?", id).Error
|
||||
})
|
||||
if err == nil {
|
||||
s.invalidateMediaCache(ctx)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func hardDeleteLibraryRoots(ctx context.Context, tx *gorm.DB, libraryID string) error {
|
||||
if tx == nil || !tx.Migrator().HasTable(&model.LibraryRoot{}) {
|
||||
return nil
|
||||
}
|
||||
return tx.WithContext(ctx).Unscoped().Where("library_id = ?", libraryID).Delete(&model.LibraryRoot{}).Error
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func strmLibrarySpecificOutputDir(base string, lib *model.Library) string {
|
||||
if outputDir == "" || outputDir == "." || lib == nil {
|
||||
return outputDir
|
||||
}
|
||||
outputDir = stripSTRMLibraryCategoryAfterRoot(outputDir)
|
||||
subdir := strmLibraryOutputSubdir(*lib)
|
||||
if subdir == "" || strmPathHasSuffix(outputDir, subdir) || pathAlreadyEndsWith(outputDir, filepath.Base(subdir)) {
|
||||
return outputDir
|
||||
@@ -36,6 +37,34 @@ func strmLibrarySpecificOutputDir(base string, lib *model.Library) string {
|
||||
return filepath.Join(outputDir, subdir)
|
||||
}
|
||||
|
||||
func stripSTRMLibraryCategoryAfterRoot(pathValue string) string {
|
||||
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(pathValue)))
|
||||
parts := strings.Split(normalized, "/")
|
||||
strmIndex := -1
|
||||
for i, part := range parts {
|
||||
if strings.EqualFold(strings.TrimSpace(part), "strm") {
|
||||
strmIndex = i
|
||||
}
|
||||
}
|
||||
if strmIndex < 0 || strmIndex >= len(parts)-1 {
|
||||
return pathValue
|
||||
}
|
||||
tail := make([]string, 0, len(parts)-strmIndex-1)
|
||||
for _, part := range parts[strmIndex+1:] {
|
||||
if strings.TrimSpace(part) != "" {
|
||||
tail = append(tail, part)
|
||||
}
|
||||
}
|
||||
if len(strmCategoryPartsFromPath(tail)) == 0 {
|
||||
return pathValue
|
||||
}
|
||||
root := strings.Join(parts[:strmIndex+1], "/")
|
||||
if root == "" {
|
||||
return pathValue
|
||||
}
|
||||
return filepath.FromSlash(root)
|
||||
}
|
||||
|
||||
func strmPathHasSuffix(pathValue, suffix string) bool {
|
||||
pathValue = strings.Trim(filepath.ToSlash(filepath.Clean(strings.TrimSpace(pathValue))), "/")
|
||||
suffix = strings.Trim(filepath.ToSlash(filepath.Clean(strings.TrimSpace(suffix))), "/")
|
||||
|
||||
@@ -232,6 +232,21 @@ func TestSTRMLibraryOutputSubdirUsesLibraryCategoryPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSTRMLibrarySpecificOutputDirStripsPreviousLibraryCategory(t *testing.T) {
|
||||
base := filepath.Join(t.TempDir(), "strm", "电影", "演唱会")
|
||||
lib := model.Library{
|
||||
Name: "国产剧",
|
||||
Path: BuildCloudLibraryPath("openlist", "/电视剧/国产剧", "/电视剧/国产剧"),
|
||||
Type: "tv",
|
||||
}
|
||||
|
||||
got := strmLibrarySpecificOutputDir(base, &lib)
|
||||
want := filepath.Join(filepath.Dir(filepath.Dir(base)), "电视剧", "国产剧")
|
||||
if got != want {
|
||||
t.Fatalf("strmLibrarySpecificOutputDir() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSTRMForLibraryUsesCategoryDefaultOutputDir(t *testing.T) {
|
||||
db := newServiceTestDB(t, &model.Library{}, &model.Media{}, &model.STRMRecord{}, &model.Setting{})
|
||||
repos := repository.New(db)
|
||||
|
||||
@@ -58,6 +58,8 @@ export function inferSTRMOutputRoot(saved: string, libraries: Library[]) {
|
||||
}
|
||||
if (slashSaved === toSlash(subdir).toLowerCase()) return ''
|
||||
}
|
||||
const inferredBySTRMRoot = stripLibraryCategoryAfterSTRMRoot(normalized)
|
||||
if (inferredBySTRMRoot) return inferredBySTRMRoot
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -133,6 +135,20 @@ function splitPath(raw: string) {
|
||||
return raw.split('/').map((part) => part.trim()).filter((part) => part && part !== '.')
|
||||
}
|
||||
|
||||
function stripLibraryCategoryAfterSTRMRoot(raw: string) {
|
||||
const normalized = toSlash(raw)
|
||||
const parts = normalized.split('/')
|
||||
let strmIndex = -1
|
||||
for (let index = 0; index < parts.length; index += 1) {
|
||||
if (parts[index].trim().toLowerCase() === 'strm') strmIndex = index
|
||||
}
|
||||
if (strmIndex < 0 || strmIndex >= parts.length - 1) return ''
|
||||
const tail = parts.slice(strmIndex + 1).filter((part) => part.trim())
|
||||
const categoryParts = categoryPartsFromPath(tail)
|
||||
if (!categoryParts?.length) return ''
|
||||
return trimPath(parts.slice(0, strmIndex + 1).join('/'))
|
||||
}
|
||||
|
||||
function joinPath(root: string, ...parts: string[]) {
|
||||
return [trimPath(root), ...parts.map(trimPath)].filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
@@ -99,8 +99,9 @@ export function isSeriesCard(card: SeriesCard): boolean {
|
||||
}
|
||||
|
||||
export function seriesTitle(media: Media): string {
|
||||
const title = media.title?.trim()
|
||||
const fromPath = seriesTitleFromPath(media.path)
|
||||
return fromPath || media.title || media.original_name || '未命名节目'
|
||||
return (title && !unsafeEpisodeTitle(title) ? title : '') || fromPath || media.original_name || title || '未命名节目'
|
||||
}
|
||||
|
||||
function normalizeTitle(value?: string): string {
|
||||
|
||||
Reference in New Issue
Block a user