qBitrr2 4.10.9__py3-none-any.whl → 5.4.5__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/arss.py +2165 -889
- qBitrr/auto_update.py +382 -0
- qBitrr/bundled_data.py +3 -2
- qBitrr/config.py +20 -3
- qBitrr/db_lock.py +79 -0
- qBitrr/env_config.py +19 -7
- qBitrr/gen_config.py +287 -26
- qBitrr/logger.py +87 -3
- qBitrr/main.py +453 -101
- qBitrr/search_activity_store.py +88 -0
- qBitrr/static/assets/ArrView.js +2 -0
- qBitrr/static/assets/ArrView.js.map +1 -0
- qBitrr/static/assets/ConfigView.js +4 -0
- qBitrr/static/assets/ConfigView.js.map +1 -0
- qBitrr/static/assets/LogsView.js +230 -0
- qBitrr/static/assets/LogsView.js.map +1 -0
- qBitrr/static/assets/ProcessesView.js +2 -0
- qBitrr/static/assets/ProcessesView.js.map +1 -0
- qBitrr/static/assets/app.css +1 -0
- qBitrr/static/assets/app.js +11 -0
- qBitrr/static/assets/app.js.map +1 -0
- qBitrr/static/assets/build.svg +3 -0
- qBitrr/static/assets/check-mark.svg +5 -0
- qBitrr/static/assets/close.svg +4 -0
- qBitrr/static/assets/download.svg +5 -0
- qBitrr/static/assets/gear.svg +5 -0
- qBitrr/static/assets/lidarr.svg +1 -0
- qBitrr/static/assets/live-streaming.svg +8 -0
- qBitrr/static/assets/log.svg +3 -0
- qBitrr/static/assets/plus.svg +4 -0
- qBitrr/static/assets/process.svg +15 -0
- qBitrr/static/assets/react-select.esm.js +14 -0
- qBitrr/static/assets/react-select.esm.js.map +1 -0
- qBitrr/static/assets/refresh-arrow.svg +3 -0
- qBitrr/static/assets/table.js +23 -0
- qBitrr/static/assets/table.js.map +1 -0
- qBitrr/static/assets/trash.svg +8 -0
- qBitrr/static/assets/up-arrow.svg +3 -0
- qBitrr/static/assets/useInterval.js +2 -0
- qBitrr/static/assets/useInterval.js.map +1 -0
- qBitrr/static/assets/vendor.js +33 -0
- qBitrr/static/assets/vendor.js.map +1 -0
- qBitrr/static/assets/visibility.svg +9 -0
- qBitrr/static/index.html +47 -0
- qBitrr/static/manifest.json +23 -0
- qBitrr/static/sw.js +105 -0
- qBitrr/static/vite.svg +1 -0
- qBitrr/tables.py +44 -0
- qBitrr/utils.py +82 -15
- qBitrr/versioning.py +136 -0
- qBitrr/webui.py +2612 -0
- qbitrr2-5.4.5.dist-info/METADATA +1116 -0
- qbitrr2-5.4.5.dist-info/RECORD +61 -0
- {qBitrr2-4.10.9.dist-info → qbitrr2-5.4.5.dist-info}/WHEEL +1 -1
- qBitrr2-4.10.9.dist-info/METADATA +0 -233
- qBitrr2-4.10.9.dist-info/RECORD +0 -19
- {qBitrr2-4.10.9.dist-info → qbitrr2-5.4.5.dist-info}/entry_points.txt +0 -0
- {qBitrr2-4.10.9.dist-info → qbitrr2-5.4.5.dist-info/licenses}/LICENSE +0 -0
- {qBitrr2-4.10.9.dist-info → qbitrr2-5.4.5.dist-info}/top_level.txt +0 -0
qBitrr/gen_config.py
CHANGED
|
@@ -14,6 +14,56 @@ from qBitrr.home_path import APPDATA_FOLDER, HOME_PATH
|
|
|
14
14
|
T = TypeVar("T")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _add_web_settings_section(config: TOMLDocument):
|
|
18
|
+
web_settings = table()
|
|
19
|
+
_gen_default_line(
|
|
20
|
+
web_settings,
|
|
21
|
+
"WebUI listen host (default 0.0.0.0)",
|
|
22
|
+
"Host",
|
|
23
|
+
"0.0.0.0",
|
|
24
|
+
)
|
|
25
|
+
_gen_default_line(
|
|
26
|
+
web_settings,
|
|
27
|
+
"WebUI listen port (default 6969)",
|
|
28
|
+
"Port",
|
|
29
|
+
6969,
|
|
30
|
+
)
|
|
31
|
+
_gen_default_line(
|
|
32
|
+
web_settings,
|
|
33
|
+
[
|
|
34
|
+
"Optional bearer token to secure WebUI/API.",
|
|
35
|
+
"Set a non-empty value to require Authorization: Bearer <token>.",
|
|
36
|
+
],
|
|
37
|
+
"Token",
|
|
38
|
+
"",
|
|
39
|
+
)
|
|
40
|
+
_gen_default_line(
|
|
41
|
+
web_settings,
|
|
42
|
+
"Enable live updates for Arr views",
|
|
43
|
+
"LiveArr",
|
|
44
|
+
True,
|
|
45
|
+
)
|
|
46
|
+
_gen_default_line(
|
|
47
|
+
web_settings,
|
|
48
|
+
"Group Sonarr episodes by series in views",
|
|
49
|
+
"GroupSonarr",
|
|
50
|
+
True,
|
|
51
|
+
)
|
|
52
|
+
_gen_default_line(
|
|
53
|
+
web_settings,
|
|
54
|
+
"Group Lidarr albums by artist in views",
|
|
55
|
+
"GroupLidarr",
|
|
56
|
+
True,
|
|
57
|
+
)
|
|
58
|
+
_gen_default_line(
|
|
59
|
+
web_settings,
|
|
60
|
+
"WebUI theme (Light or Dark)",
|
|
61
|
+
"Theme",
|
|
62
|
+
"Dark",
|
|
63
|
+
)
|
|
64
|
+
config.add("WebUI", web_settings)
|
|
65
|
+
|
|
66
|
+
|
|
17
67
|
def generate_doc() -> TOMLDocument:
|
|
18
68
|
config = document()
|
|
19
69
|
config.add(
|
|
@@ -25,6 +75,7 @@ def generate_doc() -> TOMLDocument:
|
|
|
25
75
|
config.add(comment('This is a config file should be moved to "' f'{HOME_PATH}".'))
|
|
26
76
|
config.add(nl())
|
|
27
77
|
_add_settings_section(config)
|
|
78
|
+
_add_web_settings_section(config)
|
|
28
79
|
_add_qbit_section(config)
|
|
29
80
|
_add_category_sections(config)
|
|
30
81
|
return config
|
|
@@ -129,6 +180,28 @@ def _add_settings_section(config: TOMLDocument):
|
|
|
129
180
|
"FFprobeAutoUpdate",
|
|
130
181
|
True if ENVIRO_CONFIG.settings.ping_urls is None else ENVIRO_CONFIG.settings.ping_urls,
|
|
131
182
|
)
|
|
183
|
+
_gen_default_line(
|
|
184
|
+
settings,
|
|
185
|
+
[
|
|
186
|
+
"Automatically attempt to update qBitrr on a schedule",
|
|
187
|
+
"Set to true to enable the auto-update worker.",
|
|
188
|
+
],
|
|
189
|
+
"AutoUpdateEnabled",
|
|
190
|
+
(
|
|
191
|
+
ENVIRO_CONFIG.settings.auto_update_enabled
|
|
192
|
+
if ENVIRO_CONFIG.settings.auto_update_enabled is not None
|
|
193
|
+
else False
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
_gen_default_line(
|
|
197
|
+
settings,
|
|
198
|
+
[
|
|
199
|
+
"Cron expression describing when to check for updates",
|
|
200
|
+
"Default is weekly Sunday at 03:00 (0 3 * * 0).",
|
|
201
|
+
],
|
|
202
|
+
"AutoUpdateCron",
|
|
203
|
+
ENVIRO_CONFIG.settings.auto_update_cron or "0 3 * * 0",
|
|
204
|
+
)
|
|
132
205
|
config.add("Settings", settings)
|
|
133
206
|
|
|
134
207
|
|
|
@@ -146,19 +219,19 @@ def _add_qbit_section(config: TOMLDocument):
|
|
|
146
219
|
)
|
|
147
220
|
_gen_default_line(
|
|
148
221
|
qbit,
|
|
149
|
-
'
|
|
222
|
+
'qbittorrent WebUI URL/IP - Can be found in Options > Web UI (called "IP Address")',
|
|
150
223
|
"Host",
|
|
151
224
|
ENVIRO_CONFIG.qbit.host or "CHANGE_ME",
|
|
152
225
|
)
|
|
153
226
|
_gen_default_line(
|
|
154
227
|
qbit,
|
|
155
|
-
'
|
|
228
|
+
'qbittorrent WebUI Port - Can be found in Options > Web UI (called "Port" on top right corner of the window)',
|
|
156
229
|
"Port",
|
|
157
230
|
ENVIRO_CONFIG.qbit.port or 8080,
|
|
158
231
|
)
|
|
159
232
|
_gen_default_line(
|
|
160
233
|
qbit,
|
|
161
|
-
"
|
|
234
|
+
"qbittorrent WebUI Authentication - Can be found in Options > Web UI > Authentication",
|
|
162
235
|
"UserName",
|
|
163
236
|
ENVIRO_CONFIG.qbit.username or "CHANGE_ME",
|
|
164
237
|
)
|
|
@@ -168,17 +241,11 @@ def _add_qbit_section(config: TOMLDocument):
|
|
|
168
241
|
"Password",
|
|
169
242
|
ENVIRO_CONFIG.qbit.password or "CHANGE_ME",
|
|
170
243
|
)
|
|
171
|
-
_gen_default_line(
|
|
172
|
-
qbit,
|
|
173
|
-
"Set to true to allow abittorrent v5 (Some API calls will not work as expected due to qbittorrent API issues not qBitrr)",
|
|
174
|
-
"v5",
|
|
175
|
-
ENVIRO_CONFIG.qbit.v5 or False,
|
|
176
|
-
)
|
|
177
244
|
config.add("qBit", qbit)
|
|
178
245
|
|
|
179
246
|
|
|
180
247
|
def _add_category_sections(config: TOMLDocument):
|
|
181
|
-
for c in ["Sonarr-TV", "Sonarr-Anime", "Radarr-1080", "Radarr-4K"]:
|
|
248
|
+
for c in ["Sonarr-TV", "Sonarr-Anime", "Radarr-1080", "Radarr-4K", "Lidarr-Music"]:
|
|
182
249
|
_gen_default_cat(c, config)
|
|
183
250
|
|
|
184
251
|
|
|
@@ -245,6 +312,14 @@ def _gen_default_cat(category: str, config: TOMLDocument):
|
|
|
245
312
|
"Unable to determine if file is a sample",
|
|
246
313
|
]
|
|
247
314
|
)
|
|
315
|
+
elif "lidarr" in category.lower():
|
|
316
|
+
messages.extend(
|
|
317
|
+
[
|
|
318
|
+
"Not a preferred word upgrade for existing track file(s)",
|
|
319
|
+
"Not an upgrade for existing track file(s)",
|
|
320
|
+
"Unable to determine if file is a sample",
|
|
321
|
+
]
|
|
322
|
+
)
|
|
248
323
|
_gen_default_line(
|
|
249
324
|
cat_default,
|
|
250
325
|
[
|
|
@@ -357,11 +432,11 @@ def _gen_default_torrent_table(category: str, cat_default: Table):
|
|
|
357
432
|
torrent_table,
|
|
358
433
|
"Maximum allowed time for allowed stalled torrents in minutes (-1 = Disabled, 0 = Infinite)",
|
|
359
434
|
"StalledDelay",
|
|
360
|
-
|
|
435
|
+
15,
|
|
361
436
|
)
|
|
362
437
|
_gen_default_line(
|
|
363
438
|
torrent_table,
|
|
364
|
-
"Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent",
|
|
439
|
+
"Re-search stalled torrents when StalledDelay is enabled and you want to re-search before removing the stalled torrent, or only after the torrent is removed.",
|
|
365
440
|
"ReSearchStalled",
|
|
366
441
|
False,
|
|
367
442
|
)
|
|
@@ -579,12 +654,14 @@ def _gen_default_search_table(category: str, cat_default: Table):
|
|
|
579
654
|
"SearchLimit",
|
|
580
655
|
5,
|
|
581
656
|
)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
657
|
+
# SearchByYear doesn't apply to Lidarr (music albums)
|
|
658
|
+
if "lidarr" not in category.lower():
|
|
659
|
+
_gen_default_line(
|
|
660
|
+
search_table,
|
|
661
|
+
"It will order searches by the year the EPISODE was first aired",
|
|
662
|
+
"SearchByYear",
|
|
663
|
+
True,
|
|
664
|
+
)
|
|
588
665
|
_gen_default_line(
|
|
589
666
|
search_table,
|
|
590
667
|
"Reverse search order (Start searching oldest to newest)",
|
|
@@ -627,6 +704,7 @@ def _gen_default_search_table(category: str, cat_default: Table):
|
|
|
627
704
|
True,
|
|
628
705
|
)
|
|
629
706
|
_gen_default_line(search_table, "Use Temp profile for missing", "UseTempForMissing", False)
|
|
707
|
+
_gen_default_line(search_table, "Don't change back to main profile", "KeepTempProfile", False)
|
|
630
708
|
_gen_default_line(
|
|
631
709
|
search_table,
|
|
632
710
|
"Main quality profile (To pair quality profiles, ensure they are in the same order as in the temp profiles)",
|
|
@@ -642,9 +720,13 @@ def _gen_default_search_table(category: str, cat_default: Table):
|
|
|
642
720
|
if "sonarr" in category.lower():
|
|
643
721
|
_gen_default_line(
|
|
644
722
|
search_table,
|
|
645
|
-
|
|
723
|
+
[
|
|
724
|
+
"Search mode: true (always series search), false (always episode search), or 'smart' (automatic)",
|
|
725
|
+
"Smart mode: uses series search for entire seasons/series, episode search for single episodes",
|
|
726
|
+
"(Series search ignores QualityUnmetSearch and CustomFormatUnmetSearch settings)",
|
|
727
|
+
],
|
|
646
728
|
"SearchBySeries",
|
|
647
|
-
|
|
729
|
+
"smart",
|
|
648
730
|
)
|
|
649
731
|
_gen_default_line(
|
|
650
732
|
search_table,
|
|
@@ -653,8 +735,10 @@ def _gen_default_search_table(category: str, cat_default: Table):
|
|
|
653
735
|
"PrioritizeTodaysReleases",
|
|
654
736
|
True,
|
|
655
737
|
)
|
|
656
|
-
|
|
657
|
-
|
|
738
|
+
# Ombi and Overseerr don't support music requests
|
|
739
|
+
if "lidarr" not in category.lower():
|
|
740
|
+
_gen_default_ombi_table(category, search_table)
|
|
741
|
+
_gen_default_overseerr_table(category, search_table)
|
|
658
742
|
cat_default.add("EntrySearch", search_table)
|
|
659
743
|
|
|
660
744
|
|
|
@@ -775,11 +859,188 @@ class MyConfig:
|
|
|
775
859
|
return values if values is not ... else default
|
|
776
860
|
|
|
777
861
|
|
|
778
|
-
def
|
|
862
|
+
def _migrate_webui_config(config: MyConfig) -> bool:
|
|
863
|
+
"""
|
|
864
|
+
Migrate WebUI configuration from old location (Settings section) to new location (WebUI section).
|
|
865
|
+
Returns True if any migration was performed, False otherwise.
|
|
866
|
+
"""
|
|
867
|
+
migrated = False
|
|
868
|
+
|
|
869
|
+
# Check if WebUI section exists, if not create it
|
|
870
|
+
if "WebUI" not in config.config:
|
|
871
|
+
config.config["WebUI"] = table()
|
|
872
|
+
|
|
873
|
+
webui_section = config.config.get("WebUI", {})
|
|
874
|
+
|
|
875
|
+
# Migrate Host from Settings to WebUI
|
|
876
|
+
if "Host" not in webui_section:
|
|
877
|
+
old_host = config.get("Settings.Host", fallback=None)
|
|
878
|
+
if old_host is not None:
|
|
879
|
+
webui_section["Host"] = old_host
|
|
880
|
+
migrated = True
|
|
881
|
+
print(f"Migrated WebUI Host from Settings to WebUI section: {old_host}")
|
|
882
|
+
|
|
883
|
+
# Migrate Port from Settings to WebUI
|
|
884
|
+
if "Port" not in webui_section:
|
|
885
|
+
old_port = config.get("Settings.Port", fallback=None)
|
|
886
|
+
if old_port is not None:
|
|
887
|
+
webui_section["Port"] = old_port
|
|
888
|
+
migrated = True
|
|
889
|
+
print(f"Migrated WebUI Port from Settings to WebUI section: {old_port}")
|
|
890
|
+
|
|
891
|
+
# Migrate Token from Settings to WebUI
|
|
892
|
+
if "Token" not in webui_section:
|
|
893
|
+
old_token = config.get("Settings.Token", fallback=None)
|
|
894
|
+
if old_token is not None:
|
|
895
|
+
webui_section["Token"] = old_token
|
|
896
|
+
migrated = True
|
|
897
|
+
print(f"Migrated WebUI Token from Settings to WebUI section")
|
|
898
|
+
|
|
899
|
+
return migrated
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _normalize_theme_value(value: Any) -> str:
|
|
903
|
+
"""
|
|
904
|
+
Normalize theme value to always be 'Light' or 'Dark' (case insensitive input).
|
|
905
|
+
"""
|
|
906
|
+
if value is None:
|
|
907
|
+
return "Dark"
|
|
908
|
+
value_str = str(value).strip().lower()
|
|
909
|
+
if value_str == "light":
|
|
910
|
+
return "Light"
|
|
911
|
+
elif value_str == "dark":
|
|
912
|
+
return "Dark"
|
|
913
|
+
else:
|
|
914
|
+
# Default to Dark if invalid value
|
|
915
|
+
return "Dark"
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _validate_and_fill_config(config: MyConfig) -> bool:
|
|
919
|
+
"""
|
|
920
|
+
Validate configuration and fill in missing values with defaults.
|
|
921
|
+
Returns True if any changes were made, False otherwise.
|
|
922
|
+
"""
|
|
923
|
+
changed = False
|
|
924
|
+
defaults = config.defaults_config
|
|
925
|
+
|
|
926
|
+
# Helper function to ensure a config section exists
|
|
927
|
+
def ensure_section(section_name: str) -> None:
|
|
928
|
+
"""Ensure a config section exists."""
|
|
929
|
+
if section_name not in config.config:
|
|
930
|
+
config.config[section_name] = table()
|
|
931
|
+
|
|
932
|
+
# Helper function to check and fill config values
|
|
933
|
+
def ensure_value(config_section: str, key: str, default_value: Any) -> bool:
|
|
934
|
+
"""Ensure a config value exists, setting to default if missing."""
|
|
935
|
+
ensure_section(config_section)
|
|
936
|
+
section = config.config[config_section]
|
|
937
|
+
|
|
938
|
+
if key not in section or section[key] is None:
|
|
939
|
+
# Get the value from defaults if available
|
|
940
|
+
default_section = defaults.get(config_section, {})
|
|
941
|
+
if default_section and key in default_section:
|
|
942
|
+
default = default_section[key]
|
|
943
|
+
else:
|
|
944
|
+
default = default_value
|
|
945
|
+
section[key] = default
|
|
946
|
+
return True
|
|
947
|
+
return False
|
|
948
|
+
|
|
949
|
+
# Validate Settings section
|
|
950
|
+
settings_defaults = [
|
|
951
|
+
("ConsoleLevel", "INFO"),
|
|
952
|
+
("Logging", True),
|
|
953
|
+
("CompletedDownloadFolder", "CHANGE_ME"),
|
|
954
|
+
("FreeSpace", "-1"),
|
|
955
|
+
("FreeSpaceFolder", "CHANGE_ME"),
|
|
956
|
+
("AutoPauseResume", True),
|
|
957
|
+
("NoInternetSleepTimer", 15),
|
|
958
|
+
("LoopSleepTimer", 5),
|
|
959
|
+
("SearchLoopDelay", -1),
|
|
960
|
+
("FailedCategory", "failed"),
|
|
961
|
+
("RecheckCategory", "recheck"),
|
|
962
|
+
("Tagless", False),
|
|
963
|
+
("IgnoreTorrentsYoungerThan", 600),
|
|
964
|
+
("PingURLS", ["one.one.one.one", "dns.google.com"]),
|
|
965
|
+
("FFprobeAutoUpdate", True),
|
|
966
|
+
("AutoUpdateEnabled", False),
|
|
967
|
+
("AutoUpdateCron", "0 3 * * 0"),
|
|
968
|
+
]
|
|
969
|
+
|
|
970
|
+
for key, default in settings_defaults:
|
|
971
|
+
if ensure_value("Settings", key, default):
|
|
972
|
+
changed = True
|
|
973
|
+
|
|
974
|
+
# Validate WebUI section
|
|
975
|
+
webui_defaults = [
|
|
976
|
+
("Host", "0.0.0.0"),
|
|
977
|
+
("Port", 6969),
|
|
978
|
+
("Token", ""),
|
|
979
|
+
("LiveArr", True),
|
|
980
|
+
("GroupSonarr", True),
|
|
981
|
+
("GroupLidarr", True),
|
|
982
|
+
("Theme", "Dark"),
|
|
983
|
+
]
|
|
984
|
+
|
|
985
|
+
for key, default in webui_defaults:
|
|
986
|
+
if ensure_value("WebUI", key, default):
|
|
987
|
+
changed = True
|
|
988
|
+
|
|
989
|
+
# Normalize Theme value to always be capitalized (Light or Dark)
|
|
990
|
+
ensure_section("WebUI")
|
|
991
|
+
webui_section = config.config["WebUI"]
|
|
992
|
+
if "Theme" in webui_section:
|
|
993
|
+
current_theme = webui_section["Theme"]
|
|
994
|
+
normalized_theme = _normalize_theme_value(current_theme)
|
|
995
|
+
if current_theme != normalized_theme:
|
|
996
|
+
webui_section["Theme"] = normalized_theme
|
|
997
|
+
changed = True
|
|
998
|
+
|
|
999
|
+
# Validate qBit section
|
|
1000
|
+
qbit_defaults = [
|
|
1001
|
+
("Disabled", False),
|
|
1002
|
+
("Host", "localhost"),
|
|
1003
|
+
("Port", 8105),
|
|
1004
|
+
("UserName", ""),
|
|
1005
|
+
("Password", ""),
|
|
1006
|
+
]
|
|
1007
|
+
|
|
1008
|
+
for key, default in qbit_defaults:
|
|
1009
|
+
if ensure_value("qBit", key, default):
|
|
1010
|
+
changed = True
|
|
1011
|
+
|
|
1012
|
+
return changed
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def apply_config_migrations(config: MyConfig) -> None:
|
|
1016
|
+
"""
|
|
1017
|
+
Apply all configuration migrations and validations.
|
|
1018
|
+
Saves the config if any changes were made.
|
|
1019
|
+
"""
|
|
1020
|
+
changes_made = False
|
|
1021
|
+
|
|
1022
|
+
# Apply migrations
|
|
1023
|
+
if _migrate_webui_config(config):
|
|
1024
|
+
changes_made = True
|
|
1025
|
+
|
|
1026
|
+
# Validate and fill config
|
|
1027
|
+
if _validate_and_fill_config(config):
|
|
1028
|
+
changes_made = True
|
|
1029
|
+
|
|
1030
|
+
# Save if changes were made
|
|
1031
|
+
if changes_made:
|
|
1032
|
+
config.save()
|
|
1033
|
+
print("Configuration has been updated with migrations and defaults.")
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _write_config_file(docker: bool = False) -> pathlib.Path:
|
|
779
1037
|
doc = generate_doc()
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1038
|
+
config_file = HOME_PATH.joinpath("config.toml")
|
|
1039
|
+
if docker:
|
|
1040
|
+
if config_file.exists():
|
|
1041
|
+
print(f"{config_file} already exists, keeping current configuration.")
|
|
1042
|
+
return config_file
|
|
1043
|
+
elif config_file.exists():
|
|
783
1044
|
print(f"{config_file} already exists, File is not being replaced.")
|
|
784
1045
|
config_file = pathlib.Path.cwd().joinpath("config_new.toml")
|
|
785
1046
|
config = MyConfig(config_file, config=doc)
|
qBitrr/logger.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import pathlib
|
|
4
5
|
import time
|
|
5
6
|
from logging import Logger
|
|
6
7
|
|
|
@@ -35,7 +36,7 @@ SUCCESS = 25
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class VerboseLogger(Logger):
|
|
38
|
-
def
|
|
39
|
+
def __init__(self, *args, **kwargs):
|
|
39
40
|
super().__init__(*args, **kwargs)
|
|
40
41
|
if self.name.startswith("qBitrr"):
|
|
41
42
|
self.set_config_level()
|
|
@@ -83,10 +84,11 @@ logger = logging.getLogger("qBitrr.Misc")
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
HAS_RUN = False
|
|
87
|
+
ALL_LOGS_HANDLER = None # Global handler for unified All.log file
|
|
86
88
|
|
|
87
89
|
|
|
88
|
-
def run_logs(logger: Logger) -> None:
|
|
89
|
-
global HAS_RUN
|
|
90
|
+
def run_logs(logger: Logger, _name: str = None) -> None:
|
|
91
|
+
global HAS_RUN, ALL_LOGS_HANDLER
|
|
90
92
|
try:
|
|
91
93
|
configkeys = {f"qBitrr.{i}" for i in CONFIG.sections()}
|
|
92
94
|
key_length = max(len(max(configkeys, key=len)), 10)
|
|
@@ -118,6 +120,88 @@ def run_logs(logger: Logger) -> None:
|
|
|
118
120
|
},
|
|
119
121
|
reconfigure=True,
|
|
120
122
|
)
|
|
123
|
+
logger.propagate = False
|
|
124
|
+
if ENABLE_LOGS:
|
|
125
|
+
# Initialize unified All.log handler once (first time run_logs is called)
|
|
126
|
+
if ALL_LOGS_HANDLER is None:
|
|
127
|
+
logs_folder = HOME_PATH.joinpath("logs")
|
|
128
|
+
logs_folder.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
logs_folder.chmod(mode=0o777)
|
|
130
|
+
all_logfile = logs_folder.joinpath("All.log")
|
|
131
|
+
# Rotate old All.log if it exists
|
|
132
|
+
if all_logfile.exists():
|
|
133
|
+
all_logold = logs_folder.joinpath("All.log.old")
|
|
134
|
+
if all_logold.exists():
|
|
135
|
+
all_logold.unlink()
|
|
136
|
+
all_logfile.rename(all_logold)
|
|
137
|
+
# Create handler for All.log that all loggers will use
|
|
138
|
+
ALL_LOGS_HANDLER = logging.FileHandler(all_logfile)
|
|
139
|
+
ALL_LOGS_HANDLER.setFormatter(
|
|
140
|
+
coloredlogs.ColoredFormatter(
|
|
141
|
+
fmt="[%(asctime)-15s] " f"%(levelname)-8s: %(name)-{key_length}s: %(message)s",
|
|
142
|
+
level_styles={
|
|
143
|
+
"trace": {"color": "black", "bold": True},
|
|
144
|
+
"debug": {"color": "magenta", "bold": True},
|
|
145
|
+
"verbose": {"color": "blue", "bold": True},
|
|
146
|
+
"info": {"color": "white"},
|
|
147
|
+
"notice": {"color": "cyan"},
|
|
148
|
+
"hnotice": {"color": "cyan", "bold": True},
|
|
149
|
+
"warning": {"color": "yellow", "bold": True},
|
|
150
|
+
"success": {"color": "green", "bold": True},
|
|
151
|
+
"error": {"color": "red"},
|
|
152
|
+
"critical": {"color": "red", "bold": True},
|
|
153
|
+
},
|
|
154
|
+
field_styles={
|
|
155
|
+
"asctime": {"color": "green"},
|
|
156
|
+
"process": {"color": "magenta"},
|
|
157
|
+
"levelname": {"color": "red", "bold": True},
|
|
158
|
+
"name": {"color": "blue", "bold": True},
|
|
159
|
+
"thread": {"color": "cyan"},
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Add All.log handler to this logger (since propagate=False, we can't use root)
|
|
165
|
+
logger.addHandler(ALL_LOGS_HANDLER)
|
|
166
|
+
|
|
167
|
+
# Add individual component log file handler
|
|
168
|
+
if _name:
|
|
169
|
+
logs_folder = HOME_PATH.joinpath("logs")
|
|
170
|
+
logs_folder.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
logs_folder.chmod(mode=0o777)
|
|
172
|
+
logfile = logs_folder.joinpath(_name + ".log")
|
|
173
|
+
if pathlib.Path(logfile).is_file():
|
|
174
|
+
logold = logs_folder.joinpath(_name + ".log.old")
|
|
175
|
+
if pathlib.Path(logold).exists():
|
|
176
|
+
logold.unlink()
|
|
177
|
+
logfile.rename(logold)
|
|
178
|
+
fh = logging.FileHandler(logfile)
|
|
179
|
+
# Use ColoredFormatter for file output to include ANSI colors in log files
|
|
180
|
+
fh.setFormatter(
|
|
181
|
+
coloredlogs.ColoredFormatter(
|
|
182
|
+
fmt="[%(asctime)-15s] " f"%(levelname)-8s: %(name)-{key_length}s: %(message)s",
|
|
183
|
+
level_styles={
|
|
184
|
+
"trace": {"color": "black", "bold": True},
|
|
185
|
+
"debug": {"color": "magenta", "bold": True},
|
|
186
|
+
"verbose": {"color": "blue", "bold": True},
|
|
187
|
+
"info": {"color": "white"},
|
|
188
|
+
"notice": {"color": "cyan"},
|
|
189
|
+
"hnotice": {"color": "cyan", "bold": True},
|
|
190
|
+
"warning": {"color": "yellow", "bold": True},
|
|
191
|
+
"success": {"color": "green", "bold": True},
|
|
192
|
+
"error": {"color": "red"},
|
|
193
|
+
"critical": {"color": "red", "bold": True},
|
|
194
|
+
},
|
|
195
|
+
field_styles={
|
|
196
|
+
"asctime": {"color": "green"},
|
|
197
|
+
"process": {"color": "magenta"},
|
|
198
|
+
"levelname": {"color": "red", "bold": True},
|
|
199
|
+
"name": {"color": "blue", "bold": True},
|
|
200
|
+
"thread": {"color": "cyan"},
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
logger.addHandler(fh)
|
|
121
205
|
if HAS_RUN is False:
|
|
122
206
|
HAS_RUN = True
|
|
123
207
|
log_debugs(logger)
|