qBitrr2 3.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qBitrr/__init__.py +15 -0
- qBitrr/arr_tables.py +138 -0
- qBitrr/arss.py +3918 -0
- qBitrr/bundled_data.py +6 -0
- qBitrr/config.py +172 -0
- qBitrr/env_config.py +60 -0
- qBitrr/errors.py +41 -0
- qBitrr/ffprobe.py +105 -0
- qBitrr/gen_config.py +773 -0
- qBitrr/home_path.py +23 -0
- qBitrr/logger.py +146 -0
- qBitrr/main.py +204 -0
- qBitrr/tables.py +52 -0
- qBitrr/utils.py +192 -0
- qBitrr2-3.7.1.dist-info/LICENSE +21 -0
- qBitrr2-3.7.1.dist-info/METADATA +224 -0
- qBitrr2-3.7.1.dist-info/RECORD +20 -0
- qBitrr2-3.7.1.dist-info/WHEEL +5 -0
- qBitrr2-3.7.1.dist-info/entry_points.txt +2 -0
- qBitrr2-3.7.1.dist-info/top_level.txt +1 -0
qBitrr/gen_config.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from tomlkit import comment, document, nl, parse, table
|
|
8
|
+
from tomlkit.items import Table
|
|
9
|
+
from tomlkit.toml_document import TOMLDocument
|
|
10
|
+
|
|
11
|
+
from qBitrr.env_config import ENVIRO_CONFIG
|
|
12
|
+
from qBitrr.home_path import APPDATA_FOLDER, HOME_PATH
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_doc() -> TOMLDocument:
|
|
18
|
+
config = document()
|
|
19
|
+
config.add(
|
|
20
|
+
comment(
|
|
21
|
+
"This is a config file for the qBitrr Script - "
|
|
22
|
+
'Make sure to change all entries of "CHANGE_ME".'
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
config.add(comment('This is a config file should be moved to "' f'{HOME_PATH}".'))
|
|
26
|
+
config.add(nl())
|
|
27
|
+
_add_settings_section(config)
|
|
28
|
+
_add_qbit_section(config)
|
|
29
|
+
_add_category_sections(config)
|
|
30
|
+
return config
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _add_settings_section(config: TOMLDocument):
|
|
34
|
+
settings = table()
|
|
35
|
+
settings.add(
|
|
36
|
+
comment("Level of logging; One of CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, TRACE")
|
|
37
|
+
)
|
|
38
|
+
settings.add("ConsoleLevel", ENVIRO_CONFIG.settings.console_level or "INFO")
|
|
39
|
+
settings.add(nl())
|
|
40
|
+
settings.add(
|
|
41
|
+
comment(
|
|
42
|
+
"Folder where your completed downloads are put into. "
|
|
43
|
+
"Can be found in qBitTorrent -> Options -> Downloads -> Default Save Path"
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
settings.add(
|
|
47
|
+
"CompletedDownloadFolder", ENVIRO_CONFIG.settings.completed_download_folder or "CHANGE_ME"
|
|
48
|
+
)
|
|
49
|
+
settings.add(nl())
|
|
50
|
+
settings.add(
|
|
51
|
+
comment("Time to sleep for if there is no internet (in seconds: 600 = 10 Minutes)")
|
|
52
|
+
)
|
|
53
|
+
settings.add("NoInternetSleepTimer", ENVIRO_CONFIG.settings.no_internet_sleep_timer or 15)
|
|
54
|
+
settings.add(nl())
|
|
55
|
+
settings.add(
|
|
56
|
+
comment("Time to sleep between reprocessing torrents (in seconds: 600 = 10 Minutes)")
|
|
57
|
+
)
|
|
58
|
+
settings.add("LoopSleepTimer", ENVIRO_CONFIG.settings.loop_sleep_timer or 5)
|
|
59
|
+
settings.add(nl())
|
|
60
|
+
settings.add(comment("Add torrents to this category to mark them as failed"))
|
|
61
|
+
settings.add("FailedCategory", ENVIRO_CONFIG.settings.failed_category or "failed")
|
|
62
|
+
settings.add(nl())
|
|
63
|
+
settings.add(comment("Add torrents to this category to trigger them to be rechecked properly"))
|
|
64
|
+
settings.add("RecheckCategory", ENVIRO_CONFIG.settings.recheck_category or "recheck")
|
|
65
|
+
settings.add(nl())
|
|
66
|
+
settings.add(
|
|
67
|
+
comment("Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes)")
|
|
68
|
+
)
|
|
69
|
+
settings.add(comment("Only applicable to Re-check and failed categories"))
|
|
70
|
+
settings.add(
|
|
71
|
+
"IgnoreTorrentsYoungerThan", ENVIRO_CONFIG.settings.ignore_torrents_younger_than or 180
|
|
72
|
+
)
|
|
73
|
+
settings.add(nl())
|
|
74
|
+
settings.add(comment("URL to be pinged to check if you have a valid internet connection"))
|
|
75
|
+
settings.add(
|
|
76
|
+
comment(
|
|
77
|
+
"These will be pinged a **LOT** make sure the service is okay "
|
|
78
|
+
"with you sending all the continuous pings."
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
settings.add(
|
|
82
|
+
"PingURLS", ENVIRO_CONFIG.settings.ping_urls or ["one.one.one.one", "dns.google.com"]
|
|
83
|
+
)
|
|
84
|
+
settings.add(nl())
|
|
85
|
+
settings.add(
|
|
86
|
+
comment(
|
|
87
|
+
"FFprobe auto updates, binaries are downloaded from https://ffbinaries.com/downloads"
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
settings.add(comment("If this is disabled and you want ffprobe to work"))
|
|
91
|
+
settings.add(
|
|
92
|
+
comment(
|
|
93
|
+
"Ensure that you add the ffprobe binary to the folder"
|
|
94
|
+
f"\"{APPDATA_FOLDER.joinpath('ffprobe.exe')}\""
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
settings.add(
|
|
98
|
+
comment(
|
|
99
|
+
"If no `ffprobe` binary is found in the folder above all "
|
|
100
|
+
"ffprobe functionality will be disabled."
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
settings.add(
|
|
104
|
+
comment(
|
|
105
|
+
"By default this will always be on even if config does not have these key - "
|
|
106
|
+
"to disable you need to explicitly set it to `False`"
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
settings.add(
|
|
110
|
+
"FFprobeAutoUpdate",
|
|
111
|
+
True if ENVIRO_CONFIG.settings.ping_urls is None else ENVIRO_CONFIG.settings.ping_urls,
|
|
112
|
+
)
|
|
113
|
+
config.add("Settings", settings)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _add_qbit_section(config: TOMLDocument):
|
|
117
|
+
qbit = table()
|
|
118
|
+
qbit.add(
|
|
119
|
+
comment(
|
|
120
|
+
"If this is enable qBitrr can run in a headless "
|
|
121
|
+
"mode where it will only process searches."
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
qbit.add(comment("If media search is enabled in their individual categories"))
|
|
125
|
+
qbit.add(
|
|
126
|
+
comment(
|
|
127
|
+
"This is useful if you use for example Sabnzbd/NZBGet for downloading content "
|
|
128
|
+
"but still want the faster media searches provided by qbit"
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
qbit.add(
|
|
132
|
+
"Disabled", False if ENVIRO_CONFIG.qbit.disabled is None else ENVIRO_CONFIG.qbit.disabled
|
|
133
|
+
)
|
|
134
|
+
qbit.add(nl())
|
|
135
|
+
qbit.add(comment('qBit WebUI Port - Can be found in Options > Web UI (called "IP Address")'))
|
|
136
|
+
qbit.add("Host", ENVIRO_CONFIG.qbit.host or "CHANGE_ME")
|
|
137
|
+
qbit.add(nl())
|
|
138
|
+
qbit.add(
|
|
139
|
+
comment(
|
|
140
|
+
'qBit WebUI Port - Can be found in Options > Web UI (called "Port" '
|
|
141
|
+
"on top right corner of the window)"
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
qbit.add("Port", ENVIRO_CONFIG.qbit.port or 8080)
|
|
145
|
+
qbit.add(nl())
|
|
146
|
+
qbit.add(
|
|
147
|
+
comment("qBit WebUI Authentication - Can be found in Options > Web UI > Authentication")
|
|
148
|
+
)
|
|
149
|
+
qbit.add("UserName", ENVIRO_CONFIG.qbit.username or "CHANGE_ME")
|
|
150
|
+
qbit.add(nl())
|
|
151
|
+
qbit.add(
|
|
152
|
+
comment(
|
|
153
|
+
'If you set "Bypass authentication on localhost or whitelisted IPs" remove this field.'
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
qbit.add("Password", ENVIRO_CONFIG.qbit.password or "CHANGE_ME")
|
|
157
|
+
qbit.add(nl())
|
|
158
|
+
config.add("qBit", qbit)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _add_category_sections(config: TOMLDocument):
|
|
162
|
+
for c in ["Sonarr-TV", "Sonarr-Anime", "Radarr-1080", "Radarr-4K"]:
|
|
163
|
+
_gen_default_cat(c, config)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _gen_default_cat(category: str, config: TOMLDocument):
|
|
167
|
+
cat_default = table()
|
|
168
|
+
cat_default.add(comment("Toggle whether to manage the Servarr instance torrents."))
|
|
169
|
+
cat_default.add("Managed", True)
|
|
170
|
+
cat_default.add(nl())
|
|
171
|
+
cat_default.add(
|
|
172
|
+
comment(
|
|
173
|
+
"The URL used to access Servarr interface "
|
|
174
|
+
"(if you use a domain enter the domain without a port)"
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
cat_default.add("URI", "CHANGE_ME")
|
|
178
|
+
cat_default.add(nl())
|
|
179
|
+
cat_default.add(comment("The Servarr API Key, Can be found it Settings > General > Security"))
|
|
180
|
+
cat_default.add("APIKey", "CHANGE_ME")
|
|
181
|
+
cat_default.add(nl())
|
|
182
|
+
cat_default.add(
|
|
183
|
+
comment(
|
|
184
|
+
"Category applied by Servarr to torrents in qBitTorrent, "
|
|
185
|
+
"can be found in Settings > Download Clients > qBit > Category"
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
cat_default.add("Category", category.lower())
|
|
189
|
+
cat_default.add(nl())
|
|
190
|
+
cat_default.add(
|
|
191
|
+
comment("Toggle whether to send a query to Servarr to search any failed torrents")
|
|
192
|
+
)
|
|
193
|
+
cat_default.add("ReSearch", True)
|
|
194
|
+
cat_default.add(nl())
|
|
195
|
+
cat_default.add(comment("The Servarr's Import Mode(one of Move, Copy or Hardlink)"))
|
|
196
|
+
cat_default.add("importMode", "Hardlink")
|
|
197
|
+
cat_default.add(nl())
|
|
198
|
+
cat_default.add(comment("Timer to call RSSSync (In minutes) - Set to 0 to disable"))
|
|
199
|
+
cat_default.add("RssSyncTimer", 1)
|
|
200
|
+
cat_default.add(nl())
|
|
201
|
+
cat_default.add(
|
|
202
|
+
comment(
|
|
203
|
+
"Timer to call RefreshDownloads to update the queue. (In minutes) - "
|
|
204
|
+
"Set to 0 to disable"
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
cat_default.add("RefreshDownloadsTimer", 1)
|
|
208
|
+
cat_default.add(nl())
|
|
209
|
+
|
|
210
|
+
messages = []
|
|
211
|
+
cat_default.add(
|
|
212
|
+
comment("Error messages shown my the Arr instance which should be considered failures.")
|
|
213
|
+
)
|
|
214
|
+
cat_default.add(
|
|
215
|
+
comment(
|
|
216
|
+
"This entry should be a list, "
|
|
217
|
+
"leave it empty if you want to disable this error handling."
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
cat_default.add(
|
|
221
|
+
comment(
|
|
222
|
+
"If enabled qBitrr will remove the failed files and "
|
|
223
|
+
"tell the Arr instance the download failed"
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if "radarr" in category.lower():
|
|
228
|
+
messages.extend(
|
|
229
|
+
[
|
|
230
|
+
"Not a preferred word upgrade for existing movie file(s)",
|
|
231
|
+
"Not an upgrade for existing movie file(s)",
|
|
232
|
+
"Unable to determine if file is a sample",
|
|
233
|
+
]
|
|
234
|
+
)
|
|
235
|
+
elif "sonarr" in category.lower():
|
|
236
|
+
messages.extend(
|
|
237
|
+
[
|
|
238
|
+
"Not a preferred word upgrade for existing episode file(s)",
|
|
239
|
+
"Not an upgrade for existing episode file(s)",
|
|
240
|
+
"Unable to determine if file is a sample",
|
|
241
|
+
]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
cat_default.add("ArrErrorCodesToBlocklist", list(set(messages)))
|
|
245
|
+
cat_default.add(nl())
|
|
246
|
+
|
|
247
|
+
_gen_default_search_table(category, cat_default)
|
|
248
|
+
_gen_default_torrent_table(category, cat_default)
|
|
249
|
+
config.add(category, cat_default)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _gen_default_torrent_table(category: str, cat_default: Table):
|
|
253
|
+
torrent_table = table()
|
|
254
|
+
torrent_table.add(comment("Set it to regex matches to respect/ignore case."))
|
|
255
|
+
torrent_table.add("CaseSensitiveMatches", False)
|
|
256
|
+
torrent_table.add(nl())
|
|
257
|
+
torrent_table.add(
|
|
258
|
+
comment(
|
|
259
|
+
"These regex values will match any folder where the full name matches "
|
|
260
|
+
"the specified values here, comma separated strings."
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
torrent_table.add(
|
|
264
|
+
comment("These regex need to be escaped, that's why you see so many backslashes.")
|
|
265
|
+
)
|
|
266
|
+
if "anime" in category.lower():
|
|
267
|
+
torrent_table.add(
|
|
268
|
+
"FolderExclusionRegex",
|
|
269
|
+
[
|
|
270
|
+
r"\bextras?\b",
|
|
271
|
+
r"\bfeaturettes?\b",
|
|
272
|
+
r"\bsamples?\b",
|
|
273
|
+
r"\bscreens?\b",
|
|
274
|
+
r"\bnc(ed|op)?(\\d+)?\b",
|
|
275
|
+
],
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
torrent_table.add(
|
|
279
|
+
"FolderExclusionRegex",
|
|
280
|
+
[
|
|
281
|
+
r"\bextras?\b",
|
|
282
|
+
r"\bfeaturettes?\b",
|
|
283
|
+
r"\bsamples?\b",
|
|
284
|
+
r"\bscreens?\b",
|
|
285
|
+
r"\bspecials?\b",
|
|
286
|
+
r"\bova\b",
|
|
287
|
+
r"\bnc(ed|op)?(\\d+)?\b",
|
|
288
|
+
],
|
|
289
|
+
)
|
|
290
|
+
torrent_table.add(nl())
|
|
291
|
+
torrent_table.add(
|
|
292
|
+
comment(
|
|
293
|
+
"These regex values will match any folder where the full name matches "
|
|
294
|
+
"the specified values here, comma separated strings."
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
torrent_table.add(
|
|
298
|
+
comment("These regex need to be escaped, that's why you see so many backslashes.")
|
|
299
|
+
)
|
|
300
|
+
torrent_table.add(
|
|
301
|
+
"FileNameExclusionRegex",
|
|
302
|
+
[
|
|
303
|
+
r"\bncop\\d+?\b",
|
|
304
|
+
r"\bnced\\d+?\b",
|
|
305
|
+
r"\bsample\b",
|
|
306
|
+
r"brarbg.com\b",
|
|
307
|
+
r"\btrailer\b",
|
|
308
|
+
r"music video",
|
|
309
|
+
r"comandotorrents.com",
|
|
310
|
+
],
|
|
311
|
+
)
|
|
312
|
+
torrent_table.add(nl())
|
|
313
|
+
torrent_table.add(
|
|
314
|
+
comment(
|
|
315
|
+
"Only files with these extensions will be allowed to be downloaded, "
|
|
316
|
+
"comma separated strings, leave it empty to allow all extensions"
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
torrent_table.add(
|
|
320
|
+
"FileExtensionAllowlist", [".mp4", ".mkv", ".sub", ".ass", ".srt", ".!qB", ".parts"]
|
|
321
|
+
)
|
|
322
|
+
torrent_table.add(nl())
|
|
323
|
+
torrent_table.add(comment("Auto delete files that can't be playable (i.e .exe, .png)"))
|
|
324
|
+
torrent_table.add("AutoDelete", False)
|
|
325
|
+
torrent_table.add(nl())
|
|
326
|
+
torrent_table.add(
|
|
327
|
+
comment("Ignore Torrents which are younger than this value (in seconds: 600 = 10 Minutes)")
|
|
328
|
+
)
|
|
329
|
+
torrent_table.add("IgnoreTorrentsYoungerThan", 180)
|
|
330
|
+
torrent_table.add(nl())
|
|
331
|
+
torrent_table.add(
|
|
332
|
+
comment("Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour)")
|
|
333
|
+
)
|
|
334
|
+
torrent_table.add(
|
|
335
|
+
comment(
|
|
336
|
+
"Note that if you set the MaximumETA on a tracker basis that value is "
|
|
337
|
+
"favoured over this value"
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
torrent_table.add("MaximumETA", 604800)
|
|
341
|
+
torrent_table.add(nl())
|
|
342
|
+
torrent_table.add(
|
|
343
|
+
comment(
|
|
344
|
+
"Do not delete torrents with higher completion percentage than this setting "
|
|
345
|
+
"(0.5 = 50%, 1.0 = 100%)"
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
torrent_table.add("MaximumDeletablePercentage", 0.99)
|
|
349
|
+
torrent_table.add(nl())
|
|
350
|
+
torrent_table.add(comment("Ignore slow torrents."))
|
|
351
|
+
torrent_table.add("DoNotRemoveSlow", True)
|
|
352
|
+
torrent_table.add(nl())
|
|
353
|
+
_gen_default_seeding_table(category, torrent_table)
|
|
354
|
+
_gen_default_tracker_tables(category, torrent_table)
|
|
355
|
+
|
|
356
|
+
cat_default.add("Torrent", torrent_table)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _gen_default_seeding_table(category: str, torrent_table: Table):
|
|
360
|
+
seeding_table = table()
|
|
361
|
+
seeding_table.add(comment("Set the maximum allowed download rate for torrents"))
|
|
362
|
+
seeding_table.add(comment("Set this value to -1 to disabled it"))
|
|
363
|
+
seeding_table.add(
|
|
364
|
+
comment(
|
|
365
|
+
"Note that if you set the DownloadRateLimit on a tracker basis that value is "
|
|
366
|
+
"avoured over this value"
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
seeding_table.add("DownloadRateLimitPerTorrent", -1)
|
|
370
|
+
seeding_table.add(nl())
|
|
371
|
+
seeding_table.add(comment("Set the maximum allowed upload rate for torrents"))
|
|
372
|
+
seeding_table.add(comment("Set this value to -1 to disabled it"))
|
|
373
|
+
seeding_table.add(
|
|
374
|
+
comment(
|
|
375
|
+
"Note that if you set the UploadRateLimit on a tracker basis that value is "
|
|
376
|
+
"favoured over this value"
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
seeding_table.add("UploadRateLimitPerTorrent", -1)
|
|
380
|
+
seeding_table.add(nl())
|
|
381
|
+
seeding_table.add(comment("Set the maximum allowed upload ratio for torrents"))
|
|
382
|
+
seeding_table.add(comment("Set this value to -1 to disabled it"))
|
|
383
|
+
seeding_table.add(
|
|
384
|
+
comment(
|
|
385
|
+
"Note that if you set the MaxUploadRatio on a tracker basis that value is "
|
|
386
|
+
"favoured over this value"
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
seeding_table.add("MaxUploadRatio", -1)
|
|
390
|
+
seeding_table.add(nl())
|
|
391
|
+
seeding_table.add(comment("Set the maximum seeding time in seconds for torrents"))
|
|
392
|
+
seeding_table.add(comment("Set this value to -1 to disabled it"))
|
|
393
|
+
seeding_table.add(
|
|
394
|
+
comment(
|
|
395
|
+
"Note that if you set the MaxSeedingTime on a tracker basis that value is "
|
|
396
|
+
"favoured over this value"
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
seeding_table.add("MaxSeedingTime", -1)
|
|
400
|
+
seeding_table.add(nl())
|
|
401
|
+
seeding_table.add(
|
|
402
|
+
comment(
|
|
403
|
+
"Remove torrent condition (-1=Do not remove, 1=Remove on MaxUploadRatio, 2=Remove on MaxSeedingTime, 3=Remove on MaxUploadRatio or MaxSeedingTime, 4=Remove on MaxUploadRatio and MaxSeedingTime)"
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
seeding_table.add("RemoveTorrent", -1)
|
|
407
|
+
seeding_table.add(nl())
|
|
408
|
+
seeding_table.add(comment("Enable if you want to remove dead trackers"))
|
|
409
|
+
seeding_table.add("RemoveDeadTrackers", False)
|
|
410
|
+
seeding_table.add(nl())
|
|
411
|
+
seeding_table.add(
|
|
412
|
+
comment(
|
|
413
|
+
'If "RemoveDeadTrackers" is set to true then remove trackers with the '
|
|
414
|
+
"following messages"
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
seeding_table.add(
|
|
418
|
+
"RemoveTrackerWithMessage",
|
|
419
|
+
[
|
|
420
|
+
"skipping tracker announce (unreachable)",
|
|
421
|
+
"No such host is known",
|
|
422
|
+
"unsupported URL protocol",
|
|
423
|
+
"info hash is not authorized with this tracker",
|
|
424
|
+
],
|
|
425
|
+
)
|
|
426
|
+
seeding_table.add(nl())
|
|
427
|
+
|
|
428
|
+
torrent_table.add("SeedingMode", seeding_table)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _gen_default_tracker_tables(category: str, torrent_table: Table):
|
|
432
|
+
tracker_table_list = []
|
|
433
|
+
tracker_list = []
|
|
434
|
+
if "anime" in category.lower():
|
|
435
|
+
tracker_list.append(("Nyaa", "http://nyaa.tracker.wf:7777/announce", ["qBitrr-anime"], 10))
|
|
436
|
+
elif "radarr" in category.lower():
|
|
437
|
+
t = ["qBitrr-Rarbg", "Movies and TV"]
|
|
438
|
+
t2 = []
|
|
439
|
+
if "4k" in category.lower():
|
|
440
|
+
t.append("4K")
|
|
441
|
+
t2.append("4K")
|
|
442
|
+
tracker_list.append(("Rarbg-2810", "udp://9.rarbg.com:2810/announce", t, 1))
|
|
443
|
+
tracker_list.append(("Rarbg-2740", "udp://9.rarbg.to:2740/announce", t2, 2))
|
|
444
|
+
|
|
445
|
+
for name, url, tags, priority in tracker_list:
|
|
446
|
+
tracker_table = table()
|
|
447
|
+
tracker_table.add(
|
|
448
|
+
comment(
|
|
449
|
+
"This is only for your own benefit, it is not currently used anywhere, "
|
|
450
|
+
"but one day it may be."
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
tracker_table.add("Name", name)
|
|
454
|
+
tracker_table.add(nl())
|
|
455
|
+
tracker_table.add(
|
|
456
|
+
comment("This is used when multiple trackers are in one single torrent.")
|
|
457
|
+
)
|
|
458
|
+
tracker_table.add(
|
|
459
|
+
comment(
|
|
460
|
+
"the tracker with the highest priority will have all its settings applied to "
|
|
461
|
+
"the torrent."
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
tracker_table.add("Priority", priority)
|
|
465
|
+
tracker_table.add(nl())
|
|
466
|
+
tracker_table.add(comment("The tracker URI used by qBit."))
|
|
467
|
+
tracker_table.add("URI", url)
|
|
468
|
+
tracker_table.add(nl())
|
|
469
|
+
tracker_table.add(
|
|
470
|
+
comment(
|
|
471
|
+
"Maximum allowed remaining ETA for torrent completion (in seconds: 3600 = 1 Hour)."
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
tracker_table.add("MaximumETA", 18000)
|
|
475
|
+
tracker_table.add(nl())
|
|
476
|
+
|
|
477
|
+
tracker_table.add(comment("Set the maximum allowed download rate for torrents"))
|
|
478
|
+
tracker_table.add(comment("Set this value to -1 to disabled it"))
|
|
479
|
+
tracker_table.add("DownloadRateLimit", -1)
|
|
480
|
+
tracker_table.add(nl())
|
|
481
|
+
tracker_table.add(comment("Set the maximum allowed upload rate for torrents"))
|
|
482
|
+
tracker_table.add(comment("Set this value to -1 to disabled it"))
|
|
483
|
+
tracker_table.add("UploadRateLimit", -1)
|
|
484
|
+
tracker_table.add(nl())
|
|
485
|
+
tracker_table.add(comment("Set the maximum allowed download rate for torrents"))
|
|
486
|
+
tracker_table.add(comment("Set this value to -1 to disabled it"))
|
|
487
|
+
tracker_table.add("MaxUploadRatio", -1)
|
|
488
|
+
tracker_table.add(nl())
|
|
489
|
+
tracker_table.add(comment("Set the maximum allowed download rate for torrents"))
|
|
490
|
+
tracker_table.add(comment("Set this value to -1 to disabled it"))
|
|
491
|
+
tracker_table.add("MaxSeedingTime", -1)
|
|
492
|
+
tracker_table.add(nl())
|
|
493
|
+
|
|
494
|
+
tracker_table.add(comment("Add this tracker from any torrent that does not contains it."))
|
|
495
|
+
tracker_table.add(comment("This setting does not respect priority."))
|
|
496
|
+
tracker_table.add(comment("Meaning it always be applies."))
|
|
497
|
+
tracker_table.add("AddTrackerIfMissing", False)
|
|
498
|
+
tracker_table.add(nl())
|
|
499
|
+
tracker_table.add(comment("Remove this tracker from any torrent that contains it."))
|
|
500
|
+
tracker_table.add(comment("This setting does not respect priority."))
|
|
501
|
+
tracker_table.add(comment("Meaning it always be applies."))
|
|
502
|
+
tracker_table.add("RemoveIfExists", False)
|
|
503
|
+
tracker_table.add(nl())
|
|
504
|
+
tracker_table.add(comment("Enable Super Seeding setting for torrents with this tracker."))
|
|
505
|
+
tracker_table.add("SuperSeedMode", False)
|
|
506
|
+
tracker_table.add(nl())
|
|
507
|
+
if tags:
|
|
508
|
+
tracker_table.add(comment("Adds these tags to any torrents containing this tracker."))
|
|
509
|
+
tracker_table.add(comment("This setting does not respect priority."))
|
|
510
|
+
tracker_table.add(comment("Meaning it always be applies."))
|
|
511
|
+
tracker_table.add("AddTags", tags)
|
|
512
|
+
tracker_table.add(nl())
|
|
513
|
+
|
|
514
|
+
tracker_table_list.append(tracker_table)
|
|
515
|
+
torrent_table.add(
|
|
516
|
+
comment("You can have multiple trackers set here or none just add more subsections.")
|
|
517
|
+
)
|
|
518
|
+
torrent_table.add("Trackers", tracker_table_list)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _gen_default_search_table(category: str, cat_default: Table):
|
|
522
|
+
search_table = table()
|
|
523
|
+
search_table.add(
|
|
524
|
+
comment(
|
|
525
|
+
"All these settings depends on SearchMissing being True and access to the Servarr "
|
|
526
|
+
"database file."
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
search_table.add(nl())
|
|
530
|
+
search_table.add(comment("Should search for Missing files?"))
|
|
531
|
+
search_table.add("SearchMissing", True)
|
|
532
|
+
search_table.add(nl())
|
|
533
|
+
search_table.add(comment("Should search for specials episodes? (Season 00)"))
|
|
534
|
+
search_table.add("AlsoSearchSpecials", False)
|
|
535
|
+
search_table.add(nl())
|
|
536
|
+
search_table.add(
|
|
537
|
+
comment(
|
|
538
|
+
"Maximum allowed Searches at any one points (I wouldn't recommend settings "
|
|
539
|
+
"this too high)"
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
if "sonarr" in category.lower():
|
|
543
|
+
search_table.add(comment("Sonarr has a hardcoded cap of 3 simultaneous tasks"))
|
|
544
|
+
elif "radarr" in category.lower():
|
|
545
|
+
search_table.add(
|
|
546
|
+
comment(
|
|
547
|
+
"Radarr has a default of 3 simultaneous tasks, which can be increased up to "
|
|
548
|
+
"10 tasks"
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
search_table.add(
|
|
552
|
+
comment(
|
|
553
|
+
'If you set the environment variable of "THREAD_LIMIT" to a number between and '
|
|
554
|
+
"including 2-10"
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
search_table.add(
|
|
558
|
+
comment(
|
|
559
|
+
"Radarr devs have stated that this is an unsupported feature so you will "
|
|
560
|
+
"not get any support for doing so from them."
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
search_table.add(
|
|
564
|
+
comment(
|
|
565
|
+
"That being said I've been daily driving 10 simultaneous tasks for quite a "
|
|
566
|
+
"while now with no issues."
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
search_table.add("SearchLimit", 5)
|
|
570
|
+
search_table.add(nl())
|
|
571
|
+
search_table.add(comment("Servarr Datapath file path"))
|
|
572
|
+
search_table.add(comment("This is required for any of the search functionality to work"))
|
|
573
|
+
search_table.add(
|
|
574
|
+
comment(
|
|
575
|
+
'The only exception for this is the "ReSearch" setting as that is done via an '
|
|
576
|
+
"API call."
|
|
577
|
+
)
|
|
578
|
+
)
|
|
579
|
+
if "sonarr" in category.lower():
|
|
580
|
+
search_table.add("DatabaseFile", "CHANGE_ME/sonarr.db")
|
|
581
|
+
elif "radarr" in category.lower():
|
|
582
|
+
search_table.add("DatabaseFile", "CHANGE_ME/radarr.db")
|
|
583
|
+
search_table.add(nl())
|
|
584
|
+
search_table.add(comment("It will order searches by the year the EPISODE was first aired"))
|
|
585
|
+
search_table.add("SearchByYear", True)
|
|
586
|
+
search_table.add(nl())
|
|
587
|
+
search_table.add(comment("Reverse search order (Start searching oldest to newest)"))
|
|
588
|
+
search_table.add("SearchInReverse", False)
|
|
589
|
+
search_table.add(nl())
|
|
590
|
+
search_table.add(comment("Delay between request searches in seconds"))
|
|
591
|
+
search_table.add("SearchRequestsEvery", 300)
|
|
592
|
+
search_table.add(nl())
|
|
593
|
+
search_table.add(
|
|
594
|
+
comment(
|
|
595
|
+
"Search movies which already have a file in the database in hopes of finding a "
|
|
596
|
+
"better quality version."
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
search_table.add("DoUpgradeSearch", False)
|
|
600
|
+
search_table.add(nl())
|
|
601
|
+
search_table.add(comment("Do a quality unmet search for existing entries."))
|
|
602
|
+
search_table.add("QualityUnmetSearch", False)
|
|
603
|
+
search_table.add(nl())
|
|
604
|
+
search_table.add(
|
|
605
|
+
comment(
|
|
606
|
+
"Once you have search all files on your specified year range restart the loop and "
|
|
607
|
+
"search again."
|
|
608
|
+
)
|
|
609
|
+
)
|
|
610
|
+
search_table.add("SearchAgainOnSearchCompletion", True)
|
|
611
|
+
search_table.add(nl())
|
|
612
|
+
|
|
613
|
+
if "sonarr" in category.lower():
|
|
614
|
+
search_table.add(comment("Search by series instead of by episode"))
|
|
615
|
+
search_table.add("SearchBySeries", True)
|
|
616
|
+
search_table.add(nl())
|
|
617
|
+
|
|
618
|
+
search_table.add(
|
|
619
|
+
comment(
|
|
620
|
+
"Prioritize Today's releases (Similar effect as RSS Sync, where it searches "
|
|
621
|
+
"today's release episodes first, only works on Sonarr)."
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
search_table.add("PrioritizeTodaysReleases", True)
|
|
625
|
+
search_table.add(nl())
|
|
626
|
+
_gen_default_ombi_table(category, search_table)
|
|
627
|
+
_gen_default_overseerr_table(category, search_table)
|
|
628
|
+
cat_default.add("EntrySearch", search_table)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _gen_default_ombi_table(category: str, search_table: Table):
|
|
632
|
+
ombi_table = table()
|
|
633
|
+
ombi_table.add(
|
|
634
|
+
comment("Search Ombi for pending requests (Will only work if 'SearchMissing' is enabled.)")
|
|
635
|
+
)
|
|
636
|
+
ombi_table.add("SearchOmbiRequests", False)
|
|
637
|
+
ombi_table.add(nl())
|
|
638
|
+
ombi_table.add(
|
|
639
|
+
comment(
|
|
640
|
+
"Ombi URI (Note that this has to be the instance of Ombi which manage the Arr "
|
|
641
|
+
"instance request (If you have multiple Ombi instances)"
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
ombi_table.add("OmbiURI", "CHANGE_ME")
|
|
645
|
+
ombi_table.add(nl())
|
|
646
|
+
ombi_table.add(comment("Ombi's API Key"))
|
|
647
|
+
ombi_table.add("OmbiAPIKey", "CHANGE_ME")
|
|
648
|
+
ombi_table.add(nl())
|
|
649
|
+
ombi_table.add(comment("Only process approved requests"))
|
|
650
|
+
ombi_table.add("ApprovedOnly", True)
|
|
651
|
+
ombi_table.add(nl())
|
|
652
|
+
|
|
653
|
+
search_table.add("Ombi", ombi_table)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _gen_default_overseerr_table(category: str, search_table: Table):
|
|
657
|
+
overseerr_table = table()
|
|
658
|
+
overseerr_table.add(
|
|
659
|
+
comment(
|
|
660
|
+
"Search Overseerr for pending requests (Will only work if 'SearchMissing' is enabled.)"
|
|
661
|
+
)
|
|
662
|
+
)
|
|
663
|
+
overseerr_table.add(comment("If this and Ombi are both enable, Ombi will be ignored"))
|
|
664
|
+
overseerr_table.add("SearchOverseerrRequests", False)
|
|
665
|
+
overseerr_table.add(nl())
|
|
666
|
+
overseerr_table.add(comment("Overseerr's URI"))
|
|
667
|
+
overseerr_table.add("OverseerrURI", "CHANGE_ME")
|
|
668
|
+
overseerr_table.add(nl())
|
|
669
|
+
overseerr_table.add(comment("Overseerr's API Key"))
|
|
670
|
+
overseerr_table.add("OverseerrAPIKey", "CHANGE_ME")
|
|
671
|
+
overseerr_table.add(nl())
|
|
672
|
+
overseerr_table.add(comment("Only process approved requests"))
|
|
673
|
+
overseerr_table.add("ApprovedOnly", True)
|
|
674
|
+
overseerr_table.add(nl())
|
|
675
|
+
overseerr_table.add(comment("Only for 4K Instances"))
|
|
676
|
+
if "radarr-4k" in category.lower():
|
|
677
|
+
overseerr_table.add("Is4K", True)
|
|
678
|
+
else:
|
|
679
|
+
overseerr_table.add("Is4K", False)
|
|
680
|
+
overseerr_table.add(nl())
|
|
681
|
+
search_table.add("Overseerr", overseerr_table)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class MyConfig:
|
|
685
|
+
# Original code taken from https://github.com/SemenovAV/toml_config
|
|
686
|
+
# Licence is MIT, can be located at
|
|
687
|
+
# https://github.com/SemenovAV/toml_config/blob/master/LICENSE.txt
|
|
688
|
+
|
|
689
|
+
path: pathlib.Path
|
|
690
|
+
config: TOMLDocument
|
|
691
|
+
defaults_config: TOMLDocument
|
|
692
|
+
|
|
693
|
+
def __init__(self, path: pathlib.Path | str, config: TOMLDocument | None = None):
|
|
694
|
+
self.path = pathlib.Path(path)
|
|
695
|
+
self._giving_data = bool(config)
|
|
696
|
+
self.config = config or document()
|
|
697
|
+
self.defaults_config = generate_doc()
|
|
698
|
+
self.err = None
|
|
699
|
+
self.state = True
|
|
700
|
+
self.load()
|
|
701
|
+
|
|
702
|
+
def __str__(self):
|
|
703
|
+
return self.config.as_string()
|
|
704
|
+
|
|
705
|
+
def load(self) -> MyConfig:
|
|
706
|
+
if self.state:
|
|
707
|
+
try:
|
|
708
|
+
if self._giving_data:
|
|
709
|
+
return self
|
|
710
|
+
with self.path.open() as file:
|
|
711
|
+
self.config = parse(file.read())
|
|
712
|
+
return self
|
|
713
|
+
except OSError as err:
|
|
714
|
+
self.state = False
|
|
715
|
+
self.err = err
|
|
716
|
+
except TypeError as err:
|
|
717
|
+
self.state = False
|
|
718
|
+
self.err = err
|
|
719
|
+
return self
|
|
720
|
+
|
|
721
|
+
def save(self) -> MyConfig:
|
|
722
|
+
if self.state:
|
|
723
|
+
try:
|
|
724
|
+
with open(self.path, "w", encoding="utf8") as file:
|
|
725
|
+
file.write(self.config.as_string())
|
|
726
|
+
return self
|
|
727
|
+
except OSError as err:
|
|
728
|
+
self.state = False
|
|
729
|
+
self.err = err
|
|
730
|
+
raise ValueError(
|
|
731
|
+
f"Possible permissions while attempting to read the config file.\n{err}"
|
|
732
|
+
)
|
|
733
|
+
except TypeError as err:
|
|
734
|
+
self.state = False
|
|
735
|
+
self.err = err
|
|
736
|
+
raise ValueError(f"While attempting to read the config file.\n{err}")
|
|
737
|
+
return self
|
|
738
|
+
|
|
739
|
+
def get(self, section: str, fallback: Any = None) -> T:
|
|
740
|
+
return self._deep_get(section, default=fallback)
|
|
741
|
+
|
|
742
|
+
def get_or_raise(self, section: str) -> T:
|
|
743
|
+
if (r := self._deep_get(section, default=KeyError)) is KeyError:
|
|
744
|
+
raise KeyError(f"{section} does not exist")
|
|
745
|
+
return r
|
|
746
|
+
|
|
747
|
+
def sections(self):
|
|
748
|
+
return self.config.keys()
|
|
749
|
+
|
|
750
|
+
def _deep_get(self, keys, default=...):
|
|
751
|
+
values = reduce(
|
|
752
|
+
lambda d, key: d.get(key, ...) if isinstance(d, dict) else ...,
|
|
753
|
+
keys.split("."),
|
|
754
|
+
self.config,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
return values if values is not ... else default
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _write_config_file(docker=False) -> pathlib.Path:
|
|
761
|
+
doc = generate_doc()
|
|
762
|
+
if docker:
|
|
763
|
+
file_name = "config.rename_me.toml"
|
|
764
|
+
else:
|
|
765
|
+
file_name = "config.toml"
|
|
766
|
+
CONFIG_FILE = HOME_PATH.joinpath(file_name)
|
|
767
|
+
if CONFIG_FILE.exists() and not docker:
|
|
768
|
+
print(f"{CONFIG_FILE} already exists, File is not being replaced.")
|
|
769
|
+
CONFIG_FILE = pathlib.Path.cwd().joinpath("config_new.toml")
|
|
770
|
+
config = MyConfig(CONFIG_FILE, config=doc)
|
|
771
|
+
config.save()
|
|
772
|
+
print(f'New config file has been saved to "{CONFIG_FILE}"')
|
|
773
|
+
return CONFIG_FILE
|