-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmaster_script
411 lines (361 loc) · 16.9 KB
/
master_script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
#!/usr/bin/env python3
import os
import json
import logging
import requests
from requests.exceptions import RequestException
import asyncio
import argparse
import subprocess
import sys
from packaging import version
from tqdm import tqdm
# Required dependencies and their minimum versions
required_system_dependencies = {"python3": "3.6", "pip3": "20.0"}
required_python_packages = {"requests": "2.22.0", "packaging": "20.0", "tqdm": "4.0.0"}
# Set up logging
logging.basicConfig(
format='%(asctime)s [%(levelname)s]: %(message)s',
level=logging.INFO,
handlers=[logging.StreamHandler()]
)
def check_system_dependencies():
missing_dependencies = []
for dep, min_version in required_system_dependencies.items():
result = subprocess.run(["which", dep], capture_output=True, text=True)
if result.returncode != 0:
missing_dependencies.append(dep)
else:
version_result = subprocess.run([dep, "--version"], capture_output=True, text=True)
installed_version = version_result.stdout.split()[-1].strip(')').strip('(').strip(',')
if version.parse(installed_version) < version.parse(min_version):
missing_dependencies.append(dep)
return missing_dependencies
def check_python_packages():
missing_packages = []
for package, min_version in required_python_packages.items():
try:
pkg = __import__(package)
installed_version = pkg.__version__
if version.parse(installed_version) < version.parse(min_version):
missing_packages.append(package)
except ImportError:
missing_packages.append(package)
return missing_packages
def install_system_dependencies(missing_dependencies):
package_managers = {
"apt-get": "sudo apt-get install -y",
"yum": "sudo yum install -y",
"dnf": "sudo dnf install -y",
"pacman": "sudo pacman -S --noconfirm",
"brew": "brew install"
}
detected_package_manager = None
for pm in package_managers.keys():
if subprocess.run(["which", pm], capture_output=True, text=True).returncode == 0:
detected_package_manager = pm
break
if not detected_package_manager:
logging.error("Unsupported package manager. Please install the required dependencies manually.")
return False
install_commands = []
if "pip3" in missing_dependencies and detected_package_manager in ["yum", "dnf"]:
missing_dependencies.remove("pip3")
install_commands.append(f"{package_managers[detected_package_manager]} python3-pip")
if missing_dependencies:
install_commands.append(f"{package_managers[detected_package_manager]} {' '.join(missing_dependencies)}")
try:
with tqdm(total=100, desc="Installing system dependencies") as pbar:
for command in install_commands:
subprocess.run(command, shell=True, check=True)
pbar.update(100)
logging.info("System dependencies installed successfully.")
return True
except subprocess.CalledProcessError as e:
logging.error(f"Failed to install system dependencies: {e}")
return False
def install_python_packages(missing_packages):
try:
with tqdm(total=100, desc="Installing Python packages") as pbar:
subprocess.run([sys.executable, "-m", "pip", "install", *missing_packages], check=True)
pbar.update(100)
logging.info("Python packages installed successfully.")
return True
except subprocess.CalledProcessError as e:
logging.error(f"Failed to install Python packages: {e}")
return False
# Function to make API requests with error handling and retries
async def make_api_request(url, api_key, params=None, retries=3):
headers = {'X-Api-Key': api_key}
for attempt in range(retries):
try:
response = await asyncio.get_event_loop().run_in_executor(None, lambda: requests.get(url, params=params, headers=headers))
response.raise_for_status()
return response.json()
except RequestException as e:
logging.error(f'Error making API request to {url}: {e}. Attempt {attempt + 1} of {retries}')
await asyncio.sleep(2 ** attempt) # Exponential backoff
return None
# Function to make API delete with error handling and retries
async def make_api_delete(url, api_key, params=None, retries=3):
headers = {'X-Api-Key': api_key}
for attempt in range(retries):
try:
response = await asyncio.get_event_loop().run_in_executor(None, lambda: requests.delete(url, params=params, headers=headers))
response.raise_for_status()
return response.json()
except RequestException as e:
logging.error(f'Error making API delete request to {url}: {e}. Attempt {attempt + 1} of {retries}')
await asyncio.sleep(2 ** attempt) # Exponential backoff
return None
# Function to get total records for pagination
async def count_records(api_url, api_key):
the_url = f'{api_url}/queue'
the_queue = await make_api_request(the_url, api_key)
if the_queue is not None and 'records' in the_queue:
return the_queue['totalRecords']
return 0
# Function to remove stalled downloads with pagination
async def remove_stalled_downloads(api_url, api_key, program):
logging.info(f'Checking {program.capitalize()} queue...')
page_size = 100 # Fetch 100 records per page
# Get total number of records
total_records = await count_records(api_url, api_key)
if total_records == 0:
logging.warning(f'{program.capitalize()} queue is empty or unavailable.')
return
for page in range(1, (total_records // page_size) + 2):
queue_url = f'{api_url}/queue'
queue = await make_api_request(queue_url, api_key, {'page': page, 'pageSize': page_size})
if queue is not None and 'records' in queue:
for item in queue['records']:
if 'title' in item and 'status' in item and 'trackedDownloadStatus' in item:
logging.info(f'Checking the status of {item["title"]}')
if item['status'] == 'warning' and item['errorMessage'] == 'The download is stalled with no connections':
logging.info(f'Removing stalled {program.capitalize()} download: {item["title"]}')
await make_api_delete(f'{api_url}/queue/{item["id"]}', api_key, {'removeFromClient': 'true', 'blocklist': 'true'})
else:
logging.warning(f'Skipping item in {program.capitalize()} queue due to missing or invalid keys')
else:
logging.warning(f'{program.capitalize()} queue is None or missing "records" key')
# Function to get program information from the user
def get_program_info():
while True:
program = input("Enter the *arr program (e.g., Sonarr, Radarr, Lidarr, Readarr, Prowlarr, Whisparr): ").strip().lower()
if program not in ['sonarr', 'radarr', 'lidarr', 'readarr', 'prowlarr', 'whisparr']:
print("Invalid program name. Please try again.")
continue
hostname = input(f"Enter the hostname/IP for {program}: ").strip()
api_key = input(f"Enter the API key for {program}: ").strip()
if not hostname or not api_key:
print("Hostname and API key cannot be empty. Please try again.")
continue
return {
"program": program,
"hostname": hostname,
"api_key": api_key
}
# Function to generate the cleaner script
def generate_script(programs, api_timeout):
script_content = f"""#!/usr/bin/env python3
import os
import asyncio
import logging
import requests
from requests.exceptions import RequestException
import json
# Set up logging
logging.basicConfig(
format='%(asctime)s [%(levelname)s]: %(message)s',
level=logging.INFO,
handlers=[logging.StreamHandler()]
)
# API timeout for API requests in seconds
API_TIMEOUT = {api_timeout}
"""
for program in programs:
prog_upper = program['program'].upper()
script_content += f"""# {program['program'].capitalize()} API endpoint
{prog_upper}_API_URL = "{program['hostname']}/api/v1"
{prog_upper}_API_KEY = "{program['api_key']}"
"""
script_content += """
# Function to make API requests with error handling and retries
async def make_api_request(url, api_key, params=None, retries=3):
headers = {'X-Api-Key': api_key}
for attempt in range(retries):
try:
response = await asyncio.get_event_loop().run_in_executor(None, lambda: requests.get(url, params=params, headers=headers))
response.raise_for_status()
return response.json()
except RequestException as e:
logging.error(f'Error making API request to {url}: {e}. Attempt {attempt + 1} of {retries}')
await asyncio.sleep(2 ** attempt) # Exponential backoff
return None
# Function to make API delete with error handling and retries
async def make_api_delete(url, api_key, params=None, retries=3):
headers = {'X-Api-Key': api_key}
for attempt in range(retries):
try:
response = await asyncio.get_event_loop().run_in_executor(None, lambda: requests.delete(url, params=params, headers=headers))
response.raise_for_status()
return response.json()
except RequestException as e:
logging.error(f'Error making API delete request to {url}: {e}. Attempt {attempt + 1} of {retries}')
await asyncio.sleep(2 ** attempt) # Exponential backoff
return None
# Function to get total records for pagination
async def count_records(api_url, api_key):
the_url = f'{api_url}/queue'
the_queue = await make_api_request(the_url, api_key)
if the_queue is not None and 'records' in the_queue:
return the_queue['totalRecords']
return 0
"""
for program in programs:
prog_upper = program['program'].upper()
script_content += f"""
# Function to remove stalled {program['program'].capitalize()} downloads with pagination
async def remove_stalled_{program['program']}_downloads():
logging.info('Checking {program['program'].capitalize()} queue...')
page_size = 100 # Fetch 100 records per page
# Get total number of records
total_records = await count_records({prog_upper}_API_URL, {prog_upper}_API_KEY)
if total_records == 0:
logging.warning('{program['program'].capitalize()} queue is empty or unavailable.')
return
for page in range(1, (total_records // page_size) + 2):
queue_url = f'{{{prog_upper}_API_URL}}/queue'
queue = await make_api_request(queue_url, {prog_upper}_API_KEY, {{'page': page, 'pageSize': page_size}})
if queue is not None and 'records' in queue:
for item in queue['records']:
if 'title' in item and 'status' in item and 'trackedDownloadStatus' in item:
logging.info(f'Checking the status of {{item["title"]}}')
if item['status'] == 'warning' and item['errorMessage'] == 'The download is stalled with no connections':
logging.info(f'Removing stalled {program['program'].capitalize()} download: {{item["title"]}}')
await make_api_delete(f'{{{prog_upper}_API_URL}}/queue/{{item["id"]}}', {prog_upper}_API_KEY, {{'removeFromClient': 'true', 'blocklist': 'true'}})
else:
logging.warning(f'Skipping item in {program['program'].capitalize()} queue due to missing or invalid keys')
else:
logging.warning('{program['program'].capitalize()} queue is None or missing "records" key')
"""
script_content += """
# Main function
async def main():
while True:
"""
for program in programs:
script_content += f" await remove_stalled_{program['program']}_downloads()\n"
script_content += """
logging.info('Finished running media-tools script. Sleeping for 10 minutes.')
await asyncio.sleep(API_TIMEOUT)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
"""
with open("queue_cleaner.py", "w") as script_file:
script_file.write(script_content)
print("Generated queue_cleaner.py script")
def generate_service(username, script_path, interpreter, init_system):
service_content = f"""[Unit]
Description=Stalled Cleaner Service
After=network.target
[Service]
Type=simple
User={username}
WorkingDirectory={script_path}
ExecStart={interpreter} {script_path}/queue_cleaner.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
"""
try:
if init_system == 'systemd':
service_path = "/etc/systemd/system/stalled_cleaner.service"
with open(service_path, "w") as service_file:
service_file.write(service_content)
os.system("systemctl daemon-reload")
os.system(f"systemctl enable stalled_cleaner.service --now")
elif init_system == 'init':
service_path = "/etc/init.d/stalled_cleaner"
with open(service_path, "w") as service_file:
service_file.write(service_content)
os.system(f"chmod +x {service_path}")
os.system(f"update-rc.d stalled_cleaner defaults")
os.system(f"service stalled_cleaner start")
elif init_system == 'upstart':
service_path = "/etc/init/stalled_cleaner.conf"
service_content = f"""description "Stalled Cleaner Service"
start on filesystem or runlevel [2345]
stop on runlevel [!2345]
respawn
exec {interpreter} {script_path}/queue_cleaner.py
"""
with open(service_path, "w") as service_file:
service_file.write(service_content)
os.system(f"initctl reload-configuration")
os.system(f"initctl start stalled_cleaner")
else:
logging.error("Unsupported init system")
return False
if os.path.exists(service_path):
logging.info(f"Service created and enabled at {service_path}")
return True
else:
logging.error(f"Failed to create service file at {service_path}")
return False
except Exception as e:
logging.error(f"Error creating service: {e}")
return False
def select_init_system():
init_systems = ['systemd', 'init', 'upstart']
print("Select the init system:")
for i, init in enumerate(init_systems, 1):
print(f"{i}. {init}")
choice = int(input("Enter the number of your choice: "))
if 1 <= choice <= len(init_systems):
return init_systems[choice - 1]
else:
print("Invalid choice. Please try again.")
return select_init_system()
def main():
parser = argparse.ArgumentParser(description="Generate a queue cleaner script for *arr programs.")
parser.add_argument('--api_timeout', type=int, default=600, help='API timeout in seconds (default: 600)')
args = parser.parse_args()
missing_system_dependencies = check_system_dependencies()
missing_python_packages = check_python_packages()
if missing_system_dependencies or missing_python_packages:
if missing_system_dependencies:
print(f"Missing system dependencies: {', '.join(missing_system_dependencies)}")
if missing_python_packages:
print(f"Missing Python packages: {', '.join(missing_python_packages)}")
install_deps = input("Do you want to install the missing dependencies? (yes/no): ").strip().lower()
if install_deps == 'yes':
if missing_system_dependencies and not install_system_dependencies(missing_system_dependencies):
print("Failed to install system dependencies. Exiting.")
sys.exit(1)
if missing_python_packages and not install_python_packages(missing_python_packages):
print("Failed to install Python packages. Exiting.")
sys.exit(1)
else:
print("Missing dependencies not installed. Exiting.")
sys.exit(1)
programs = []
while True:
programs.append(get_program_info())
add_more = input("Would you like to add another *arr program to the script? (yes/no): ").strip().lower()
if add_more != 'yes':
break
api_timeout = int(input("Enter the API timeout in seconds (default is 600): ").strip() or 600)
generate_script(programs, api_timeout)
create_service = input("Would you like to add and enable this script as a service? (yes/no): ").strip().lower()
if create_service == 'yes':
username = input("Enter the username for the service: ").strip()
script_path = os.path.abspath(os.path.dirname(__file__))
interpreter = input("Enter the full path to the Python interpreter (e.g., /usr/bin/python3): ").strip()
init_system = select_init_system()
if not generate_service(username, script_path, interpreter, init_system):
print("Failed to create the service. Please check the logs for more details.")
if __name__ == "__main__":
main()