Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/api/arrs_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -322,6 +323,18 @@ func (s *Server) handleArrsWebhook(c *fiber.Ctx) error {
var releaseDate *time.Time
var sourceNzb *string

// Handle Rename specifically: try to find and re-link old record
if req.EventType == "Rename" {
fileName := filepath.Base(normalizedPath)
// Try to find a record with the same filename but currently under /complete/
// or with a NULL library_path
if err := s.healthRepo.RelinkFileByFilename(c.Context(), fileName, normalizedPath, path); err == nil {
slog.InfoContext(c.Context(), "Successfully re-linked health record during Rename webhook",
"filename", fileName, "new_library_path", path)
continue // Successfully re-linked, no need to add new
}
}

// Try to read metadata to get release date
if s.metadataService != nil {
meta, err := s.metadataService.ReadFileMetadata(normalizedPath)
Expand Down
90 changes: 82 additions & 8 deletions internal/database/health_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,75 @@ func (r *HealthRepository) GetUnhealthyFiles(ctx context.Context, limit int) ([]
return nil, fmt.Errorf("failed to iterate unhealthy files: %w", err)
}

return files, nil
}
return files, nil



}



// RelinkFileByFilename attempts to find a health record with the same filename and update its paths

func (r *HealthRepository) RelinkFileByFilename(ctx context.Context, fileName, newVirtualPath, newLibraryPath string) error {

newVirtualPath = strings.TrimPrefix(newVirtualPath, "/")



// We look for a record that:

// 1. Ends with the same filename

// 2. Is NOT already at the correct path

// 3. Either has a NULL library_path or is in /complete/

query := `

UPDATE file_health

SET file_path = ?,

library_path = ?,

updated_at = datetime('now')

WHERE (file_path LIKE '%' || ? OR library_path LIKE '%' || ?)

AND file_path != ?

AND (library_path IS NULL OR library_path LIKE '%/complete/%' OR library_path LIKE 'complete/%')

`



result, err := r.db.ExecContext(ctx, query, newVirtualPath, newLibraryPath, fileName, fileName, newVirtualPath)

if err != nil {

return err

}



rows, _ := result.RowsAffected()

if rows == 0 {

return fmt.Errorf("no matching record found to re-link")

}



return nil

}



// SetPriority sets the priority for a file health record
func (r *HealthRepository) SetPriority(ctx context.Context, id int64, priority HealthPriority) error {
Expand Down Expand Up @@ -1442,14 +1509,21 @@ func (r *HealthRepository) RenameHealthRecord(ctx context.Context, oldPath, newP
query := `
UPDATE file_health
SET file_path = ?,
library_path = CASE
WHEN library_path = ? THEN ?
ELSE library_path
END,
updated_at = datetime('now')
WHERE file_path = ?
`

_, err := r.db.ExecContext(ctx, query, newPath, oldPath)
if err != nil {
return fmt.Errorf("failed to rename health record: %w", err)
}
_, err := r.db.ExecContext(ctx, query, newPath, oldPath, newPath, oldPath)
if err != nil {
return fmt.Errorf("failed to rename health record: %w", err)
}

return nil
}
return nil

}


58 changes: 38 additions & 20 deletions internal/health/library_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,37 @@ func (lsw *LibrarySyncWorker) SyncLibrary(ctx context.Context, dryRun bool) *Dry
path := mountRelativePath

p.Go(func() {
// Check if needs to be added
if it, exists := dbPathSet[path]; !exists || it.LibraryPath == nil {
// Look up library path from our map
libraryPath := lsw.getLibraryPath(path, filesInUse)
// Check if needs to be added or repaired
recordExists := false
var existingRecord *database.AutomaticHealthCheckRecord
if it, exists := dbPathSet[path]; exists {
recordExists = true
existingRecord = &it
}

// Look up library path from our map
libraryPath := lsw.getLibraryPath(path, filesInUse)

// Fast Path Recovery: If record exists but has no library path,
// try to see if it's at the expected location even if not in filesInUse
if recordExists && existingRecord.LibraryPath == nil && libraryPath == nil {
expectedPath := filepath.Join(cfg.MountPath, path)
if _, err := os.Stat(expectedPath); err == nil {
// Found it at the expected location!
libStr := expectedPath
libraryPath = &libStr
slog.InfoContext(ctx, "Recovered library path for record",
"path", path, "recovered_location", expectedPath)

// Update DB immediately for this recovered path
if err := lsw.healthRepo.UpdateLibraryPath(ctx, path, expectedPath); err != nil {
slog.ErrorContext(ctx, "Failed to update recovered library path",
"path", path, "error", err)
}
}
}

if !recordExists || (existingRecord.LibraryPath == nil && libraryPath != nil) {
record, err := lsw.processMetadataForSync(ctx, path, libraryPath)
if err != nil {
slog.ErrorContext(ctx, "Failed to read metadata",
Expand Down Expand Up @@ -656,8 +682,8 @@ func (lsw *LibrarySyncWorker) SyncLibrary(ctx context.Context, dryRun bool) *Dry
// Wait for all workers to complete
p.Wait()

// Additional cleanup of orphaned metadata files if enabled
metadataDeletedCount := 0
// Additional cleanup of orphaned database records if enabled
// We no longer delete metadata files here for safety.
if shouldCleanup {
// We already have libraryFiles from earlier in the function
for relativeMountPath := range metaFileSet {
Expand All @@ -670,25 +696,17 @@ func (lsw *LibrarySyncWorker) SyncLibrary(ctx context.Context, dryRun bool) *Dry
libraryPath := lsw.getLibraryPath(relativeMountPath, filesInUse)

if libraryPath == nil {
if !dryRun {
deleteSourceNzb := false
if cfg.Metadata.DeleteSourceNzbOnRemoval != nil {
deleteSourceNzb = *cfg.Metadata.DeleteSourceNzbOnRemoval
}
err := lsw.metadataService.DeleteFileMetadataWithSourceNzb(ctx, relativeMountPath, deleteSourceNzb)
if err != nil {
slog.ErrorContext(ctx, "Failed to delete metadata file", "error", err)
continue
}
// Remove from our set so the database cleanup step below knows it's gone
delete(metaFileSet, relativeMountPath)
}
metadataDeletedCount++
// We used to delete metadata here, but it's removed for safety.
// If a file is missing from the library, we just let the database cleanup handle it later
// or keep the metadata so it can be re-linked if found.
slog.DebugContext(ctx, "File missing from library, but preserving metadata for safety",
"path", relativeMountPath)
}
}
}

// Cleanup orphaned library files (symlinks and STRM files without metadata)
metadataDeletedCount := 0
libraryFilesDeletedCount := 0
libraryDirsDeletedCount := 0

Expand Down
1 change: 1 addition & 0 deletions internal/nzbfilesystem/metadata_remote_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ func (mrf *MetadataRemoteFile) RenameFile(ctx context.Context, oldName, newName
if err := os.Rename(oldDirPath, newDirPath); err != nil {
return false, fmt.Errorf("failed to rename directory: %w", err)
}

return true, nil
}

Expand Down
Loading