Fix Repair checks. Handle false positives
This commit is contained in:
@@ -3,7 +3,6 @@ package arr
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ func (a *Arr) GetMedia(tvId string) ([]Content, error) {
|
|||||||
}
|
}
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
// This is Radarr
|
// This is Radarr
|
||||||
log.Printf("Radarr detected")
|
repairLogger.Info().Msg("Radarr detected")
|
||||||
a.Type = Radarr
|
a.Type = Radarr
|
||||||
return GetMovies(a, tvId)
|
return GetMovies(a, tvId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var repairLogger *zerolog.Logger
|
||||||
repairLogger zerolog.Logger = common.NewLogger("repair", "info", os.Stdout)
|
|
||||||
)
|
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) {
|
func (a *Arr) SearchMissing(id int) {
|
||||||
var payload interface{}
|
var payload interface{}
|
||||||
@@ -37,41 +43,40 @@ func (a *Arr) SearchMissing(id int) {
|
|||||||
MovieId: id,
|
MovieId: id,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
repairLogger.Info().Msgf("Unknown arr type: %s", a.Type)
|
getLogger().Info().Msgf("Unknown arr type: %s", a.Type)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.Request(http.MethodPost, "api/v3/command", payload)
|
resp, err := a.Request(http.MethodPost, "api/v3/command", payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
repairLogger.Info().Msgf("Failed to search missing: %v", err)
|
getLogger().Info().Msgf("Failed to search missing: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'; !statusOk {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Arr) Repair(tmdbId string) error {
|
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)
|
media, err := a.GetMedia(tmdbId)
|
||||||
if err != nil {
|
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
|
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)
|
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
|
// Automatic search for missing files
|
||||||
for _, m := range brokenMedia {
|
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)
|
a.SearchMissing(m.Id)
|
||||||
}
|
}
|
||||||
repairLogger.Info().Msgf("Search missing completed for %s", a.Name)
|
getLogger().Info().Msgf("Repair completed for %s", a.Name)
|
||||||
repairLogger.Info().Msgf("Repair completed for %s", a.Name)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,11 @@ func (a *Arr) processMedia(media []Content) []Content {
|
|||||||
if len(media) <= 1 {
|
if len(media) <= 1 {
|
||||||
var brokenMedia []Content
|
var brokenMedia []Content
|
||||||
for _, m := range media {
|
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) {
|
if a.checkMediaFiles(m) {
|
||||||
brokenMedia = append(brokenMedia, m)
|
brokenMedia = append(brokenMedia, m)
|
||||||
}
|
}
|
||||||
@@ -101,6 +111,12 @@ func (a *Arr) processMedia(media []Content) []Content {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for m := range jobs {
|
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) {
|
if a.checkMediaFilesParallel(m) {
|
||||||
results <- m
|
results <- m
|
||||||
}
|
}
|
||||||
@@ -146,24 +162,25 @@ func (a *Arr) checkMediaFilesParallel(m Content) bool {
|
|||||||
go func() {
|
go func() {
|
||||||
defer fileWg.Done()
|
defer fileWg.Done()
|
||||||
for f := range fileJobs {
|
for f := range fileJobs {
|
||||||
repairLogger.Debug().Msgf("Checking file: %s", f.Path)
|
getLogger().Debug().Msgf("Checking file: %s", f.Path)
|
||||||
isBroken := false
|
isBroken := false
|
||||||
|
|
||||||
if fileIsSymlinked(f.Path) {
|
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) {
|
if !fileIsCorrectSymlink(f.Path) {
|
||||||
repairLogger.Debug().Msgf("File is broken: %s", f.Path)
|
getLogger().Debug().Msgf("File is broken: %s", f.Path)
|
||||||
isBroken = true
|
isBroken = true
|
||||||
if err := a.DeleteFile(f.Id); err != nil {
|
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 {
|
} 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) {
|
if !fileIsReadable(f.Path) {
|
||||||
repairLogger.Debug().Msgf("File is broken: %s", f.Path)
|
getLogger().Debug().Msgf("File is broken: %s", f.Path)
|
||||||
isBroken = true
|
isBroken = true
|
||||||
if err := a.DeleteFile(f.Id); err != nil {
|
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) {
|
if !fileIsCorrectSymlink(f.Path) {
|
||||||
isBroken = true
|
isBroken = true
|
||||||
if err := a.DeleteFile(f.Id); err != nil {
|
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 {
|
} else {
|
||||||
if !fileIsReadable(f.Path) {
|
if !fileIsReadable(f.Path) {
|
||||||
isBroken = true
|
isBroken = true
|
||||||
if err := a.DeleteFile(f.Id); err != nil {
|
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
|
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 {
|
func fileIsSymlinked(file string) bool {
|
||||||
info, err := os.Lstat(file)
|
info, err := os.Lstat(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
function getStateColor(state) {
|
function getStateColor(state) {
|
||||||
const stateColors = {
|
const stateColors = {
|
||||||
'downloading': 'bg-primary',
|
'downloading': 'bg-primary',
|
||||||
'pausedUP': 'bg-success',
|
'pausedup': 'bg-success',
|
||||||
'error': 'bg-danger',
|
'error': 'bg-danger',
|
||||||
};
|
};
|
||||||
return stateColors[state?.toLowerCase()] || 'bg-secondary';
|
return stateColors[state?.toLowerCase()] || 'bg-secondary';
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Repairing...';
|
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 {
|
try {
|
||||||
const response = await fetch('/internal/repair', {
|
const response = await fetch('/internal/repair', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
arr: document.getElementById('arrSelect').value,
|
arr: document.getElementById('arrSelect').value,
|
||||||
mediaIds: document.getElementById('mediaIds').value.split(',').map(id => id.trim()),
|
mediaIds: mediaIds,
|
||||||
async: document.getElementById('isAsync').checked
|
async: document.getElementById('isAsync').checked
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -77,7 +77,6 @@
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
alert('Repair process initiated successfully!');
|
alert('Repair process initiated successfully!');
|
||||||
document.getElementById('mediaIds').value = '';
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error starting repair: ${error.message}`);
|
alert(`Error starting repair: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ type ContentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RepairRequest struct {
|
type RepairRequest struct {
|
||||||
ArrName string `json:"arr"`
|
ArrName string `json:"arr"`
|
||||||
TVIds string `json:"tvIds"`
|
MediaIds []string `json:"mediaIds"`
|
||||||
Async bool `json:"async"`
|
Async bool `json:"async"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed templates/*
|
//go:embed templates/*
|
||||||
@@ -174,10 +174,6 @@ func (u *uiHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tvids := []string{""}
|
|
||||||
if req.TVIds != "" {
|
|
||||||
tvids = strings.Split(req.TVIds, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
_arr := u.qbit.Arrs.Get(req.ArrName)
|
_arr := u.qbit.Arrs.Get(req.ArrName)
|
||||||
arrs := make([]*arr.Arr, 0)
|
arrs := make([]*arr.Arr, 0)
|
||||||
@@ -192,9 +188,14 @@ func (u *uiHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mediaIds := req.MediaIds
|
||||||
|
if len(mediaIds) == 0 {
|
||||||
|
mediaIds = []string{""}
|
||||||
|
}
|
||||||
|
|
||||||
if req.Async {
|
if req.Async {
|
||||||
for _, a := range arrs {
|
for _, a := range arrs {
|
||||||
for _, tvId := range tvids {
|
for _, tvId := range mediaIds {
|
||||||
go func() {
|
go func() {
|
||||||
err := a.Repair(tvId)
|
err := a.Repair(tvId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -209,7 +210,7 @@ func (u *uiHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, a := range arrs {
|
for _, a := range arrs {
|
||||||
for _, tvId := range tvids {
|
for _, tvId := range mediaIds {
|
||||||
if err := a.Repair(tvId); err != nil {
|
if err := a.Repair(tvId); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user