Fix Repair checks. Handle false positives

This commit is contained in:
Mukhtar Akere
2025-01-23 01:35:28 +01:00
parent 74a55149fc
commit 0b1c1af8b8
5 changed files with 104 additions and 37 deletions

View File

@@ -3,7 +3,6 @@ package arr
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
@@ -15,7 +14,7 @@ func (a *Arr) GetMedia(tvId string) ([]Content, error) {
}
if resp.StatusCode == http.StatusNotFound {
// This is Radarr
log.Printf("Radarr detected")
repairLogger.Info().Msg("Radarr detected")
a.Type = Radarr
return GetMovies(a, tvId)
}

View File

@@ -12,9 +12,15 @@ import (
"sync"
)
var (
repairLogger zerolog.Logger = common.NewLogger("repair", "info", os.Stdout)
)
var repairLogger *zerolog.Logger
func getLogger() *zerolog.Logger {
if repairLogger == nil {
logger := common.NewLogger("repair", common.CONFIG.LogLevel, os.Stdout)
repairLogger = &logger
}
return repairLogger
}
func (a *Arr) SearchMissing(id int) {
var payload interface{}
@@ -37,41 +43,40 @@ func (a *Arr) SearchMissing(id int) {
MovieId: id,
}
default:
repairLogger.Info().Msgf("Unknown arr type: %s", a.Type)
getLogger().Info().Msgf("Unknown arr type: %s", a.Type)
return
}
resp, err := a.Request(http.MethodPost, "api/v3/command", payload)
if err != nil {
repairLogger.Info().Msgf("Failed to search missing: %v", err)
getLogger().Info().Msgf("Failed to search missing: %v", err)
return
}
if statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'; !statusOk {
repairLogger.Info().Msgf("Failed to search missing: %s", resp.Status)
getLogger().Info().Msgf("Failed to search missing: %s", resp.Status)
return
}
}
func (a *Arr) Repair(tmdbId string) error {
repairLogger.Info().Msgf("Starting repair for %s", a.Name)
getLogger().Info().Msgf("Starting repair for %s", a.Name)
media, err := a.GetMedia(tmdbId)
if err != nil {
repairLogger.Info().Msgf("Failed to get %s media: %v", a.Type, err)
getLogger().Info().Msgf("Failed to get %s media: %v", a.Type, err)
return err
}
repairLogger.Info().Msgf("Found %d %s media", len(media), a.Type)
getLogger().Info().Msgf("Found %d %s media", len(media), a.Type)
brokenMedia := a.processMedia(media)
repairLogger.Info().Msgf("Found %d %s broken media files", len(brokenMedia), a.Type)
getLogger().Info().Msgf("Found %d %s broken media files", len(brokenMedia), a.Type)
// Automatic search for missing files
for _, m := range brokenMedia {
repairLogger.Debug().Msgf("Searching missing for %s", m.Title)
getLogger().Debug().Msgf("Searching missing for %s", m.Title)
a.SearchMissing(m.Id)
}
repairLogger.Info().Msgf("Search missing completed for %s", a.Name)
repairLogger.Info().Msgf("Repair completed for %s", a.Name)
getLogger().Info().Msgf("Repair completed for %s", a.Name)
return nil
}
@@ -79,6 +84,11 @@ func (a *Arr) processMedia(media []Content) []Content {
if len(media) <= 1 {
var brokenMedia []Content
for _, m := range media {
// Check if media is accessible
if !a.isMediaAccessible(m) {
getLogger().Debug().Msgf("Skipping media check for %s - parent directory not accessible", m.Title)
continue
}
if a.checkMediaFiles(m) {
brokenMedia = append(brokenMedia, m)
}
@@ -101,6 +111,12 @@ func (a *Arr) processMedia(media []Content) []Content {
go func() {
defer wg.Done()
for m := range jobs {
// Check if media is accessible
// First check if we can access this media's directory
if !a.isMediaAccessible(m) {
getLogger().Debug().Msgf("Skipping media check for %s - parent directory not accessible", m.Title)
continue
}
if a.checkMediaFilesParallel(m) {
results <- m
}
@@ -146,24 +162,25 @@ func (a *Arr) checkMediaFilesParallel(m Content) bool {
go func() {
defer fileWg.Done()
for f := range fileJobs {
repairLogger.Debug().Msgf("Checking file: %s", f.Path)
getLogger().Debug().Msgf("Checking file: %s", f.Path)
isBroken := false
if fileIsSymlinked(f.Path) {
repairLogger.Debug().Msgf("File is symlinked: %s", f.Path)
getLogger().Debug().Msgf("File is symlinked: %s", f.Path)
if !fileIsCorrectSymlink(f.Path) {
repairLogger.Debug().Msgf("File is broken: %s", f.Path)
getLogger().Debug().Msgf("File is broken: %s", f.Path)
isBroken = true
if err := a.DeleteFile(f.Id); err != nil {
repairLogger.Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
getLogger().Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
}
}
} else {
repairLogger.Debug().Msgf("File is not symlinked: %s", f.Path)
getLogger().Debug().Msgf("File is not symlinked: %s", f.Path)
if !fileIsReadable(f.Path) {
repairLogger.Debug().Msgf("File is broken: %s", f.Path)
getLogger().Debug().Msgf("File is broken: %s", f.Path)
isBroken = true
if err := a.DeleteFile(f.Id); err != nil {
repairLogger.Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
getLogger().Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
}
}
}
@@ -201,14 +218,14 @@ func (a *Arr) checkMediaFiles(m Content) bool {
if !fileIsCorrectSymlink(f.Path) {
isBroken = true
if err := a.DeleteFile(f.Id); err != nil {
repairLogger.Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
getLogger().Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
}
}
} else {
if !fileIsReadable(f.Path) {
isBroken = true
if err := a.DeleteFile(f.Id); err != nil {
repairLogger.Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
getLogger().Info().Msgf("Failed to delete file: %s %d: %v", f.Path, f.Id, err)
}
}
}
@@ -216,6 +233,57 @@ func (a *Arr) checkMediaFiles(m Content) bool {
return isBroken
}
func (a *Arr) isMediaAccessible(m Content) bool {
// We're likely to mount the debrid path.
// So instead of checking the arr path, we check the original path
// This is because the arr path is likely to be a symlink
// And we want to check the actual path where the media is stored
// This is to avoid false positives
if len(m.Files) == 0 {
return false
}
// Get the first file to check its target location
file := m.Files[0].Path
var targetPath string
fileInfo, err := os.Lstat(file)
if err != nil {
repairLogger.Debug().Msgf("Cannot stat file %s: %v", file, err)
return false
}
if fileInfo.Mode()&os.ModeSymlink != 0 {
// If it's a symlink, get where it points to
target, err := os.Readlink(file)
if err != nil {
repairLogger.Debug().Msgf("Cannot read symlink %s: %v", file, err)
return false
}
// If the symlink target is relative, make it absolute
if !filepath.IsAbs(target) {
dir := filepath.Dir(file)
target = filepath.Join(dir, target)
}
targetPath = target
} else {
// If it's a regular file, use its path
targetPath = file
}
mediaDir := filepath.Dir(targetPath) // Gets /remote/storage/Movie
parentDir := filepath.Dir(mediaDir) // Gets /remote/storage
_, err = os.Stat(parentDir)
if err != nil {
repairLogger.Debug().Msgf("Parent directory of target not accessible for media %s: %s", m.Title, parentDir)
return false
}
return true
}
func fileIsSymlinked(file string) bool {
info, err := os.Lstat(file)
if err != nil {

View File

@@ -76,7 +76,7 @@
function getStateColor(state) {
const stateColors = {
'downloading': 'bg-primary',
'pausedUP': 'bg-success',
'pausedup': 'bg-success',
'error': 'bg-danger',
};
return stateColors[state?.toLowerCase()] || 'bg-secondary';

View File

@@ -59,7 +59,7 @@
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Repairing...';
let mediaIds = document.getElementById('mediaIds').value.split(',').map(id => id.trim());
try {
const response = await fetch('/internal/repair', {
method: 'POST',
@@ -68,7 +68,7 @@
},
body: JSON.stringify({
arr: document.getElementById('arrSelect').value,
mediaIds: document.getElementById('mediaIds').value.split(',').map(id => id.trim()),
mediaIds: mediaIds,
async: document.getElementById('isAsync').checked
})
});
@@ -77,7 +77,6 @@
const result = await response.json();
alert('Repair process initiated successfully!');
document.getElementById('mediaIds').value = '';
} catch (error) {
alert(`Error starting repair: ${error.message}`);
} finally {

View File

@@ -41,7 +41,7 @@ type ContentResponse struct {
type RepairRequest struct {
ArrName string `json:"arr"`
TVIds string `json:"tvIds"`
MediaIds []string `json:"mediaIds"`
Async bool `json:"async"`
}
@@ -174,10 +174,6 @@ func (u *uiHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
tvids := []string{""}
if req.TVIds != "" {
tvids = strings.Split(req.TVIds, ",")
}
_arr := u.qbit.Arrs.Get(req.ArrName)
arrs := make([]*arr.Arr, 0)
@@ -192,9 +188,14 @@ func (u *uiHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
return
}
mediaIds := req.MediaIds
if len(mediaIds) == 0 {
mediaIds = []string{""}
}
if req.Async {
for _, a := range arrs {
for _, tvId := range tvids {
for _, tvId := range mediaIds {
go func() {
err := a.Repair(tvId)
if err != nil {
@@ -209,7 +210,7 @@ func (u *uiHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
var errs []error
for _, a := range arrs {
for _, tvId := range tvids {
for _, tvId := range mediaIds {
if err := a.Repair(tvId); err != nil {
errs = append(errs, err)
}