fix: keep existing library files in place

This commit is contained in:
ShukeBta
2026-05-29 03:04:27 +08:00
parent fc0dc70bad
commit 48d33275cb
7 changed files with 168 additions and 31 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ func importSTRMHandler(svc *service.Container) gin.HandlerFunc {
STRMURL: req.URL,
Container: "strm",
}
if err := svc.Repo.DB.WithContext(c.Request.Context()).Create(m).Error; err != nil {
if err := svc.Repo.Media.Upsert(c.Request.Context(), m); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
+3
View File
@@ -276,6 +276,9 @@ func (r *MediaRepository) Upsert(ctx context.Context, m *model.Media) error {
if m.EpisodeNum > 0 && existing.EpisodeNum != m.EpisodeNum {
updates["episode_num"] = m.EpisodeNum
}
if m.STRMURL != "" {
updates["strm_url"] = m.STRMURL
}
if err := r.db.WithContext(ctx).Unscoped().Model(&model.Media{}).
Where("id = ?", existing.ID).Updates(updates).Error; err != nil {
+21
View File
@@ -150,6 +150,10 @@ func (o *OrganizerService) OrganizeMedia(ctx context.Context, mediaID string) (s
// OrganizeLibrary organizes every media row in a library whose file is
// not already in the expected path structure.
func (o *OrganizerService) OrganizeLibrary(ctx context.Context, libraryID string) (*OrganizeResult, error) {
lib, err := o.repo.Library.FindByID(ctx, libraryID)
if err != nil || lib == nil {
return nil, errors.New("library not found")
}
var rows []model.Media
if err := o.repo.DB.WithContext(ctx).
Where("library_id = ? AND deleted_at IS NULL", libraryID).
@@ -158,6 +162,10 @@ func (o *OrganizerService) OrganizeLibrary(ctx context.Context, libraryID string
}
res := &OrganizeResult{}
for i := range rows {
if pathWithin(rows[i].Path, lib.Path) {
res.Skipped++
continue
}
dst, err := o.OrganizeMedia(ctx, rows[i].ID)
if err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("%s: %s", rows[i].Title, err.Error()))
@@ -290,6 +298,19 @@ func categoryRoot(root, category string) string {
return filepath.Join(root, category)
}
func pathWithin(path, root string) bool {
cleanPath := filepath.Clean(path)
cleanRoot := filepath.Clean(root)
if strings.EqualFold(cleanPath, cleanRoot) {
return true
}
rel, err := filepath.Rel(cleanRoot, cleanPath)
if err != nil {
return false
}
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func mediaTypeRootDir(mediaType string) string {
switch normalizeMediaType(mediaType, "", "") {
case "movie":
+61
View File
@@ -232,3 +232,64 @@ func TestOrganizeMediaDoesNotRepeatCategoryWhenLibraryIsCategoryRoot(t *testing.
t.Fatalf("repeated category path exists or stat failed unexpectedly: %v", err)
}
}
func TestOrganizeLibrarySkipsFilesAlreadyInsideLibrary(t *testing.T) {
root := t.TempDir()
libraryRoot := filepath.Join(root, "media", "电视剧", "国产剧")
sourceDir := filepath.Join(libraryRoot, "Existing Show")
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
t.Fatal(err)
}
source := filepath.Join(sourceDir, "Existing Show S01E02.mkv")
if err := os.WriteFile(source, []byte("episode"), 0o644); err != nil {
t.Fatal(err)
}
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.Library{}, &model.Media{}, &model.Setting{}); err != nil {
t.Fatal(err)
}
repos := repository.New(db)
if err := repos.Setting.Set(t.Context(), "organizer.smart_classify", "true"); err != nil {
t.Fatal(err)
}
lib := model.Library{Name: "国产剧", Path: libraryRoot, Type: "tv", Enabled: true}
if err := repos.Library.Create(t.Context(), &lib); err != nil {
t.Fatal(err)
}
media := model.Media{
LibraryID: lib.ID,
Title: "Existing Show",
Path: source,
Container: "mkv",
Countries: "CN",
SeasonNum: 1,
EpisodeNum: 2,
ScrapeStatus: "matched",
}
if err := repos.Media.Upsert(t.Context(), &media); err != nil {
t.Fatal(err)
}
organizer := NewOrganizerService(&config.Config{}, zap.NewNop(), repos)
result, err := organizer.OrganizeLibrary(t.Context(), lib.ID)
if err != nil {
t.Fatal(err)
}
if result.Organized != 0 || result.Skipped != 1 {
t.Fatalf("result = %+v, want organized=0 skipped=1", result)
}
if _, err := os.Stat(source); err != nil {
t.Fatalf("source should remain untouched: %v", err)
}
var refreshed model.Media
if err := db.First(&refreshed, "id = ?", media.ID).Error; err != nil {
t.Fatal(err)
}
if refreshed.Path != source {
t.Fatalf("media path = %q, want unchanged %q", refreshed.Path, source)
}
}