Skip to content

Commit

Permalink
Added the option to repair missing files and the option to repair unm…
Browse files Browse the repository at this point in the history
…onitored as well
  • Loading branch information
westsurname authored Jul 16, 2024
1 parent 482e8c4 commit 28922e6
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 67 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,38 @@
python3 python_watcher.py
```
## Repair
### Usage
The repair script can be run with the following command:
```bash
python3 repair.py
```
The script accepts the following arguments:
- `--dry-run`: Perform a dry run without making any changes.
- `--no-confirm`: Execute without confirmation prompts.
- `--repair-interval`: Optional interval in smart format (e.g., '1h2m3s') to wait between repairing each media file.
- `--run-interval`: Optional interval in smart format (e.g., '1w2d3h4m5s') to run the repair process.
- `--mode`: Choose repair mode: `symlink` or `file`. `symlink` to repair broken symlinks and `file` to repair missing files. (default: 'symlink').
- `--include-unmonitored`: Include unmonitored media in the repair process.
### Warning
This script can potentially delete and re-download a large number of files. It is recommended to use the `--dry-run` flag first to see what actions the script will take.
### Example
Here's an example of how you might use this script:

```bash
python3 repair.py --mode file --repair-interval 30m --run-interval 1d --dry-run
```

In this example, the script will run in 'file' mode, waiting 30 minutes between each repair and running once a day. It will perform a dry run, printing actions without executing them.


## Import Torrent Folder

### Usage
Expand Down
7 changes: 3 additions & 4 deletions blackhole.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,16 +323,15 @@ def print(*values: object):
print(f"Failing")

torrentHash = torrent.getHash()
history = arr.getHistory(blackhole['historyPageSize'])['records']
items = [item for item in history if item['data'].get('torrentInfoHash', '').casefold() == torrentHash.casefold() or cleanFileName(item['sourceTitle'].casefold()) == torrent.file.fileInfo.filenameWithoutExt.casefold()]

history = arr.getHistory(blackhole['historyPageSize'])
items = [item for item in history if (item.torrentInfoHash and item.torrentInfoHash.casefold() == torrentHash.casefold()) or cleanFileName(item.sourceTitle.casefold()) == torrent.file.fileInfo.filenameWithoutExt.casefold()]
if not items:
message = "No history items found to mark as failed. Arr will not attempt to grab an alternative."
print(message)
discordError(message, torrent.file.fileInfo.filenameWithoutExt)
for item in items:
# TODO: See if we can fail without blacklisting as cached items constantly changes
arr.failHistoryItem(item['id'])
arr.failHistoryItem(item.id)
print(f"Failed")

def getFiles(isRadarr):
Expand Down
111 changes: 58 additions & 53 deletions repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,50 @@
from shared.arr import Sonarr, Radarr
from shared.discord import discordUpdate
from shared.shared import repair, realdebrid, torbox, intersperse
from datetime import datetime

def parse_interval(interval_str):
def parseInterval(intervalStr):
"""Parse a smart interval string (e.g., '1w2d3h4m5s') into seconds."""
if not interval_str:
if not intervalStr:
return 0
total_seconds = 0
time_dict = {'w': 604800, 'd': 86400, 'h': 3600, 'm': 60, 's': 1}
current_number = ''
for char in interval_str:
totalSeconds = 0
timeDict = {'w': 604800, 'd': 86400, 'h': 3600, 'm': 60, 's': 1}
currentNumber = ''
for char in intervalStr:
if char.isdigit():
current_number += char
elif char in time_dict and current_number:
total_seconds += int(current_number) * time_dict[char]
current_number = ''
return total_seconds

currentNumber += char
elif char in timeDict and currentNumber:
totalSeconds += int(currentNumber) * timeDict[char]
currentNumber = ''
return totalSeconds
# Parse arguments for dry run, no confirm options, and optional intervals
parser = argparse.ArgumentParser(description='Repair broken symlinks and manage media files.')
parser = argparse.ArgumentParser(description='Repair broken symlinks or missing files.')
parser.add_argument('--dry-run', action='store_true', help='Perform a dry run without making any changes.')
parser.add_argument('--no-confirm', action='store_true', help='Execute without confirmation prompts.')
parser.add_argument('--repair-interval', type=str, default=repair['repairInterval'], help='Optional interval in smart format (e.g. 1h2m3s) to wait between repairing each media file.')
parser.add_argument('--run-interval', type=str, default=repair['runInterval'], help='Optional interval in smart format (e.g. 1w2d3h4m5s) to run the repair process.')
parser.add_argument('--mode', type=str, choices=['symlink', 'file'], default='symlink', help='Choose repair mode: `symlink` or `file`. `symlink` to repair broken symlinks and `file` to repair missing files.')
parser.add_argument('--include-unmonitored', action='store_true', help='Include unmonitored media in the repair process')
args = parser.parse_args()

_print = print

def print(*values: object):
_print(f"[{datetime.now()}] [{args.mode}]", *values)

if not args.repair_interval and not args.run_interval:
print("Running repair once")
else:
print(f"Running repair{' once every ' + args.run_interval if args.run_interval else ''}{', and waiting ' + args.repair_interval + ' between each repair.' if args.repair_interval else '.'}")

try:
repair_interval_seconds = parse_interval(args.repair_interval)
repairIntervalSeconds = parseInterval(args.repair_interval)
except Exception as e:
print(f"Invalid interval format for repair interval: {args.repair_interval}")
exit(1)

try:
run_interval_seconds = parse_interval(args.run_interval)
runIntervalSeconds = parseInterval(args.run_interval)
except Exception as e:
print(f"Invalid interval format for run interval: {args.run_interval}")
exit(1)
Expand All @@ -50,49 +57,46 @@ def main():
print("Collecting media...")
sonarr = Sonarr()
radarr = Radarr()
sonarrMedia = [(sonarr, media) for media in sonarr.getAll() if media.anyMonitoredChildren]
radarrMedia = [(radarr, media) for media in radarr.getAll() if media.anyMonitoredChildren]
sonarrMedia = [(sonarr, media) for media in sonarr.getAll() if args.mode == 'file' or media.anyMonitoredChildren]
radarrMedia = [(radarr, media) for media in radarr.getAll() if args.include_unmonitored or media.anyMonitoredChildren]
print("Finished collecting media.")

for arr, media in intersperse(sonarrMedia, radarrMedia):
files = {}
for file in arr.getFiles(media):
if file.parentId in files:
files[file.parentId].append(file)
else:
files[file.parentId] = [file]
for childId in media.monitoredChildrenIds:
realPaths = []
brokenSymlinks = []
getItems = lambda media, childId: arr.getFiles(media=media, childId=childId) if args.mode == 'symlink' else arr.getHistory(media=media, childId=childId, includeGrandchildDetails=True)
childrenIds = media.childrenIds if args.include_unmonitored else media.monitoredChildrenIds

childFiles = files.get(childId, [])
for childFile in childFiles:
for childId in childrenIds:
brokenItems = []
childItems = list(getItems(media=media, childId=childId))

fullPath = childFile.path
realPath = os.path.realpath(fullPath)
realPaths.append(realPath)

if os.path.islink(fullPath):
destinationPath = os.readlink(fullPath)

if ((realdebrid['enabled'] and destinationPath.startswith(realdebrid['mountTorrentsPath']) and not os.path.exists(destinationPath)) or
(torbox['enabled'] and destinationPath.startswith(torbox['mountTorrentsPath']) and not os.path.exists(realPath))):
brokenSymlinks.append(realPath)

# If not full season just repair individual episodes?
if brokenSymlinks:
for item in childItems:
if args.mode == 'symlink':
fullPath = item.path
if os.path.islink(fullPath):
destinationPath = os.readlink(fullPath)
if ((realdebrid['enabled'] and destinationPath.startswith(realdebrid['mountTorrentsPath']) and not os.path.exists(destinationPath)) or
(torbox['enabled'] and destinationPath.startswith(torbox['mountTorrentsPath']) and not os.path.exists(os.path.realpath(fullPath)))):
brokenItems.append(os.path.realpath(fullPath))
else: # file mode
if item.reason == 'MissingFromDisk' and item.parentId not in media.fullyAvailableChildrenIds:
brokenItems.append(item.sourceTitle)

if brokenItems:
print("Title:", media.title)
print("Movie ID/Season Number:", childId)
print("Broken symlinks:")
[print(brokenSymlink) for brokenSymlink in brokenSymlinks]
print("Broken items:")
[print(item) for item in brokenItems]
print()
if args.dry_run or args.no_confirm or input("Do you want to delete and re-grab? (y/n): ").lower() == 'y':
discordUpdate(f"Repairing... {media.title} - {childId}")
print("Deleting files:")
[print(childFile.path) for childFile in childFiles]
if not args.dry_run:
results = arr.deleteFiles(childFiles)
print("Remonitoring")
discordUpdate(f"[{args.mode}] Repairing {media.title}: {childId}")
if args.mode == 'symlink':
print("Deleting files:")
[print(item.path) for item in childItems]
if not args.dry_run:
results = arr.deleteFiles(childItems)
if not args.dry_run:
print("Re-monitoring")
media = arr.get(media.id)
media.setChildMonitored(childId, False)
arr.put(media)
Expand All @@ -102,12 +106,13 @@ def main():
results = arr.automaticSearch(media, childId)
print(results)

if repair_interval_seconds > 0:
time.sleep(repair_interval_seconds)
if repairIntervalSeconds > 0:
time.sleep(repairIntervalSeconds)
else:
print("Skipping")
print()
else:
elif args.mode == 'symlink':
realPaths = [os.path.realpath(item.path) for item in childItems]
parentFolders = set(os.path.dirname(path) for path in realPaths)
if childId in media.fullyAvailableChildrenIds and len(parentFolders) > 1:
print("Title:", media.title)
Expand All @@ -116,9 +121,9 @@ def main():
[print(parentFolder) for parentFolder in parentFolders]
print()

if run_interval_seconds > 0:
if runIntervalSeconds > 0:
while True:
main()
time.sleep(run_interval_seconds)
time.sleep(runIntervalSeconds)
else:
main()
Loading

0 comments on commit 28922e6

Please sign in to comment.