Skip to content

Commit

Permalink
Merge pull request #26 from westsurname/dev
Browse files Browse the repository at this point in the history
Misc Updates
  • Loading branch information
westsurname authored Jul 23, 2024
2 parents 4a5553a + 794471c commit de2e36a
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 89 deletions.
4 changes: 2 additions & 2 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ PLEX_SERVER_TV_SHOW_LIBRARY_ID=<plex_server_movie_library_id>
# OVERSEERR - WATCHLIST, PLEX AUTHENTICATION, PLEX REQUEST, RECLAIM SPACE #
#-------------------------------------------------------------------------#

OVERSEERR_HOST="http://overseerr:5055"
OVERSEERR_API_KEY=
OVERSEERR_HOST=<overseerr_host>
OVERSEERR_API_KEY=<overseerr_api_key>

#------------------------------------------------------------------------------------#
# SONARR - BLACKHOLE, REPAIR, IMPORT TORRENT FOLDER, RECLAIM SPACE, ADD NEXT EPISODE #
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.blackhole
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9-slim

# Metadata labels
LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.plex_authentication
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9-slim

# Metadata labels
LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.plex_request
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9-slim

# Metadata labels
LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.scripts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9-slim

# Metadata labels
LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.watchlist
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim
FROM python:3.9-slim

# Metadata labels
LABEL org.opencontainers.image.source="https://github.com/westsurname/scripts"
Expand Down
22 changes: 12 additions & 10 deletions blackhole.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import re
import requests
import asyncio
import uuid
from datetime import datetime
# import urllib
from shared.discord import discordError, discordUpdate
Expand Down Expand Up @@ -47,11 +48,12 @@ def __init__(self, isTorrentOrMagnet, isDotTorrentFile) -> None:
def __init__(self, filename, isRadarr) -> None:
print('filename:', filename)
baseBath = getPath(isRadarr)
uniqueId = str(uuid.uuid4())[:8] # Generate a unique identifier
isDotTorrentFile = filename.casefold().endswith('.torrent')
isTorrentOrMagnet = isDotTorrentFile or filename.casefold().endswith('.magnet')
filenameWithoutExt, _ = os.path.splitext(filename)
filenameWithoutExt, ext = os.path.splitext(filename)
filePath = os.path.join(baseBath, filename)
filePathProcessing = os.path.join(baseBath, 'processing', filename)
filePathProcessing = os.path.join(baseBath, 'processing', f"{filenameWithoutExt}_{uniqueId}{ext}")
folderPathCompleted = os.path.join(baseBath, 'completed', filenameWithoutExt)

self.fileInfo = self.FileInfo(filename, filenameWithoutExt, filePath, filePathProcessing, folderPathCompleted)
Expand Down Expand Up @@ -287,14 +289,13 @@ async def is_accessible(path, timeout=10):
if torbox['enabled']:
torrentConstructors.append(TorboxTorrent if file.torrentInfo.isDotTorrentFile else TorboxMagnet)

onlyLargestFile = isRadarr or bool(re.search(r'S[\d]{2}E[\d]{2}', file.fileInfo.filename))
onlyLargestFile = isRadarr or bool(re.search(r'S[\d]{2}E[\d]{2}(?![\W_][\d]{2}[\W_])', file.fileInfo.filename))
if not blackhole['failIfNotCached']:
torrents = [constructor(f, fileData, file, blackhole['failIfNotCached'], onlyLargestFile) for constructor in torrentConstructors]
results = await asyncio.gather(*(processTorrent(torrent, file, arr) for torrent in torrents))

if not any(results):
for torrent in torrents:
fail(torrent, arr)
await asyncio.gather(*(fail(torrent, arr) for torrent in torrents))
else:
for i, constructor in enumerate(torrentConstructors):
isLast = (i == len(torrentConstructors) - 1)
Expand All @@ -303,7 +304,7 @@ async def is_accessible(path, timeout=10):
if await processTorrent(torrent, file, arr):
break
elif isLast:
fail(torrent, arr)
await fail(torrent, arr)

os.remove(file.fileInfo.filePathProcessing)
except:
Expand All @@ -314,7 +315,7 @@ async def is_accessible(path, timeout=10):

discordError(f"Error processing {file.fileInfo.filenameWithoutExt}", e)

def fail(torrent: TorrentBase, arr: Arr):
async def fail(torrent: TorrentBase, arr: Arr):
_print = globals()['print']

def print(*values: object):
Expand All @@ -323,15 +324,16 @@ def print(*values: object):
print(f"Failing")

torrentHash = torrent.getHash()
history = arr.getHistory(blackhole['historyPageSize'])
history = await asyncio.to_thread(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:
else:
# TODO: See if we can fail without blacklisting as cached items constantly changes
arr.failHistoryItem(item.id)
failTasks = [asyncio.to_thread(arr.failHistoryItem, item.id) for item in items]
await asyncio.gather(*failTasks)
print(f"Failed")

def getFiles(isRadarr):
Expand Down
116 changes: 63 additions & 53 deletions repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time
import shared.debrid # Run validation
from shared.arr import Sonarr, Radarr
from shared.discord import discordUpdate
from shared.discord import discordUpdate, discordError
from shared.shared import repair, realdebrid, torbox, intersperse
from datetime import datetime

Expand Down Expand Up @@ -62,68 +62,78 @@ def main():
print("Finished collecting media.")

for arr, media in intersperse(sonarrMedia, radarrMedia):
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
try:
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

for childId in childrenIds:
brokenItems = []
childItems = list(getItems(media=media, childId=childId))
for childId in childrenIds:
brokenItems = []
childItems = list(getItems(media=media, childId=childId))

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 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':
if not args.dry_run:
discordUpdate(f"[{args.mode}] Repairing {media.title}: {childId}")
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 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':
if not args.dry_run:
discordUpdate(f"[{args.mode}] Repairing {media.title}: {childId}")
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)
media.setChildMonitored(childId, True)
arr.put(media)
print("Searching for new files")
results = arr.automaticSearch(media, childId)
print(results)

if repairIntervalSeconds > 0:
time.sleep(repairIntervalSeconds)
else:
print("Skipping")
print()
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)
print("Movie ID/Season Number:", childId)
print("Inconsistent folders:")
[print(parentFolder) for parentFolder in parentFolders]
print("Re-monitoring")
media = arr.get(media.id)
media.setChildMonitored(childId, False)
arr.put(media)
media.setChildMonitored(childId, True)
arr.put(media)
print("Searching for new files")
results = arr.automaticSearch(media, childId)
print(results)

if repairIntervalSeconds > 0:
time.sleep(repairIntervalSeconds)
else:
print("Skipping")
print()
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)
print("Movie ID/Season Number:", childId)
print("Inconsistent folders:")
[print(parentFolder) for parentFolder in parentFolders]
print()
except Exception as e:
print(f"An error occurred while processing {media.title}: {str(e)}")
discordError(f"[{args.mode}] An error occurred while processing {media.title}", str(e))

print("Repair complete")
discordUpdate(f"[{args.mode}] Repair complete")

if runIntervalSeconds > 0:
while True:
main()
time.sleep(runIntervalSeconds)
try:
main()
time.sleep(runIntervalSeconds)
except Exception as e:
print(f"An error occurred in the main loop: {str(e)}")
discordError(f"[{args.mode}] An error occurred in the main loop", str(e))
time.sleep(runIntervalSeconds) # Still wait before retrying
else:
main()
40 changes: 21 additions & 19 deletions shared/arr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Type, List
import requests
from shared.shared import sonarr, radarr, checkRequiredEnvs
from shared.requests import retryRequest

def validateSonarrHost():
url = f"{sonarr['host']}/login"
Expand Down Expand Up @@ -40,6 +41,7 @@ def validateRadarrApiKey():
return False

return True

requiredEnvs = {
'Sonarr host': (sonarr['host'], validateSonarrHost),
'Sonarr API key': (sonarr['apiKey'], validateSonarrApiKey, True),
Expand Down Expand Up @@ -253,20 +255,20 @@ def __init__(self, host: str, apiKey: str, endpoint: str, fileEndpoint: str, chi
self.historyConstructor = historyConstructor

def get(self, id: int):
get = requests.get(f"{self.host}/api/v3/{self.endpoint}/{id}?apiKey={self.apiKey}")
return self.constructor(get.json())
response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/{self.endpoint}/{id}?apiKey={self.apiKey}"))
return self.constructor(response.json())

def getAll(self):
get = requests.get(f"{self.host}/api/v3/{self.endpoint}?apiKey={self.apiKey}")
return map(self.constructor, get.json())
response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/{self.endpoint}?apiKey={self.apiKey}"))
return map(self.constructor, response.json())

def put(self, media: Media):
put = requests.put(f"{self.host}/api/v3/{self.endpoint}/{media.id}?apiKey={self.apiKey}&moveFiles=true", json=media.json)
retryRequest(lambda: requests.put(f"{self.host}/api/v3/{self.endpoint}/{media.id}?apiKey={self.apiKey}&moveFiles=true", json=media.json))

def getFiles(self, media: Media, childId: int=None):
get = requests.get(f"{self.host}/api/v3/{self.fileEndpoint}?apiKey={self.apiKey}&{self.endpoint}Id={media.id}")
response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/{self.fileEndpoint}?apiKey={self.apiKey}&{self.endpoint}Id={media.id}"))

files = map(self.fileConstructor, get.json())
files = map(self.fileConstructor, response.json())

if childId != None and childId != media.id:
files = filter(lambda file: file.parentId == childId, files)
Expand All @@ -275,38 +277,38 @@ def getFiles(self, media: Media, childId: int=None):

def deleteFiles(self, files: List[MediaFile]):
fileIds = [file.id for file in files]
delete = requests.delete(f"{self.host}/api/v3/{self.fileEndpoint}/bulk?apiKey={self.apiKey}", json={f"{self.fileEndpoint}ids": fileIds})

return delete.json()
response = retryRequest(lambda: requests.delete(f"{self.host}/api/v3/{self.fileEndpoint}/bulk?apiKey={self.apiKey}", json={f"{self.fileEndpoint}ids": fileIds}))
return response.json()

def getHistory(self, pageSize: int=None, includeGrandchildDetails: bool=False, media: Media=None, childId: int=None):
endpoint = f"/{self.endpoint}" if media else ''
pageSizeParam = f"pageSize={pageSize}&" if pageSize else ''
includeGrandchildDetailsParam = f"include{self.grandchildName}=true&" if includeGrandchildDetails else ''
idParam = f"{self.endpoint}Id={media.id}&" if media else ''
childIdParam = f"{self.childIdName}={childId}&" if media and childId != None and childId != media.id else ''
historyRequest = requests.get(f"{self.host}/api/v3/history{endpoint}?{pageSizeParam}{includeGrandchildDetailsParam}{idParam}{childIdParam}apiKey={self.apiKey}")
response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/history{endpoint}?{pageSizeParam}{includeGrandchildDetailsParam}{idParam}{childIdParam}apiKey={self.apiKey}"))

history = historyRequest.json()
history = response.json()

return map(self.historyConstructor, history['records'] if isinstance(history, dict) else history)

def failHistoryItem(self, historyId: int):
failRequest = requests.post(f"{self.host}/api/v3/history/failed/{historyId}?apiKey={self.apiKey}")
retryRequest(lambda: requests.post(f"{self.host}/api/v3/history/failed/{historyId}?apiKey={self.apiKey}"))

def refreshMonitoredDownloads(self):
commandRequest = requests.post(f"{self.host}/api/v3/command?apiKey={self.apiKey}", json={'name': 'RefreshMonitoredDownloads'}, headers={'Content-Type': 'application/json'})
retryRequest(lambda: requests.post(f"{self.host}/api/v3/command?apiKey={self.apiKey}", json={'name': 'RefreshMonitoredDownloads'}, headers={'Content-Type': 'application/json'}))

def interactiveSearch(self, media: Media, childId: int):
search = requests.get(f"{self.host}/api/v3/release?apiKey={self.apiKey}&{self.endpoint}Id={media.id}{f'&{self.childIdName}={childId}' if childId != media.id else ''}")
return search.json()
response = retryRequest(lambda: requests.get(f"{self.host}/api/v3/release?apiKey={self.apiKey}&{self.endpoint}Id={media.id}{f'&{self.childIdName}={childId}' if childId != media.id else ''}"))
return response.json()

def automaticSearch(self, media: Media, childId: int):
search = requests.post(
response = retryRequest(lambda: requests.post(
f"{self.host}/api/v3/command?apiKey={self.apiKey}",
json=self._automaticSearchJson(media, childId),
)
return search.json()
))
return response.json()

def _automaticSearchJson(self, media: Media, childId: int):
pass
Expand Down

0 comments on commit de2e36a

Please sign in to comment.