Compare commits

..

311 Commits
v2.0.5 ... dev

Author SHA1 Message Date
Flaminel
d9341ff441 fixed readme 2025-06-27 15:26:37 +03:00
Flaminel
555be79a7a fixed docs favicon 2025-06-27 15:13:15 +03:00
Flaminel
66be8aaa32 removed flmorg references 2025-06-27 15:07:55 +03:00
Flaminel
737b683285 fixed release workflow 2025-06-27 14:56:41 +03:00
Flaminel
c0e94733cd removed test directory 2025-06-27 14:52:31 +03:00
Flaminel
5843ec1d15 changed docs #8 2025-06-27 14:51:06 +03:00
Flaminel
51c968fb2d changed docs #7 2025-06-27 14:29:00 +03:00
Flaminel
ea50a7a535 changed docs #6 2025-06-27 14:20:24 +03:00
Flaminel
b1477704b6 changed docs #5 2025-06-27 13:21:14 +03:00
Flaminel
04d287e1e0 hidden username for deluge 2025-06-26 23:03:21 +03:00
Flaminel
59a5eea684 fixed docs tag location 2025-06-26 22:47:15 +03:00
Flaminel
2078eab054 changed docs #4 2025-06-26 22:35:56 +03:00
Flaminel
3278a24931 changed docs #3 2025-06-26 20:00:22 +03:00
Flaminel
f9b83cf3b2 changed docs #2 2025-06-26 19:37:34 +03:00
Flaminel
54997b184c changed docs #1 2025-06-26 16:27:08 +03:00
Flaminel
2e7d89e1c5 fixed frontend error handling 2025-06-26 12:13:38 +03:00
Flaminel
68c9d41f5f fixed ignored downloads description 2025-06-25 20:17:00 +03:00
Flaminel
a9503bbca3 fixed blocklist reload time 2025-06-25 20:14:45 +03:00
Flaminel
07553131a0 try fix macos installers #4 2025-06-25 16:58:40 +03:00
Flaminel
b21bab2a8a try fix macos installers #3 2025-06-25 15:35:29 +03:00
Flaminel
c9e033c9d1 fixed port configuration 2025-06-25 13:35:02 +03:00
Flaminel
a26fd1da5d removed usenet type 2025-06-24 21:28:38 +03:00
Flaminel
d904240aa8 try fix macos installers #2 2025-06-24 20:56:32 +03:00
Flaminel
4f066c3f2a try fix macos installers 2025-06-24 20:40:37 +03:00
Flaminel
84bee5f63a fixed download cleaner reset 2025-06-24 20:40:23 +03:00
Flaminel
8d7b8e5848 fixed cache strikes not being reset when dry run is disabled 2025-06-24 19:27:34 +03:00
Flaminel
8115205158 fixed frontend name 2025-06-24 19:08:26 +03:00
Flaminel
ada2b9fa4a try fix installers and removed default configuration file 2025-06-23 23:15:40 +03:00
Flaminel
222c21a85e revert base path validator 2025-06-23 21:01:42 +03:00
Flaminel
d6cea762eb fixed macos installer 2025-06-23 19:57:44 +03:00
Flaminel
f9d096d76a fixed base path validator 2025-06-23 19:57:38 +03:00
Flaminel
ee89570cad fixed executable build 2025-06-23 19:44:03 +03:00
Flaminel
42145e58eb fixed naming 2025-06-23 19:22:51 +03:00
Flaminel
0cda75157e fixed config for installers and icons 2025-06-23 19:09:57 +03:00
Flaminel
4ef223b0cc removed debug symbols from app build 2025-06-23 18:48:00 +03:00
Flaminel
0df1fd0d19 fixed release workflows 2025-06-23 18:36:44 +03:00
Flaminel
7409ddeb33 removed windsurf 2025-06-23 17:51:10 +03:00
Flaminel
f46702f073 fixed missing port when not in docker 2025-06-23 17:51:01 +03:00
Flaminel
9f22db9cad updated input description 2025-06-23 15:21:35 +03:00
Flaminel
bbd2052086 fixed queue cleaner input states 2025-06-23 15:20:08 +03:00
Flaminel
8436f93727 fixed content blocker input states 2025-06-23 15:20:01 +03:00
Flaminel
8c020d5aa5 fixed download cleaner input states 2025-06-23 15:17:29 +03:00
Flaminel
cd36a32b91 fixed column value type 2025-06-23 01:51:20 +03:00
Flaminel
9e43ce76f6 changed settings page layout 2025-06-22 22:10:41 +03:00
Flaminel
a068b93458 fixed blocklist input width 2025-06-22 22:09:12 +03:00
Flaminel
96dc8bec51 fixed unlinked categories validation 2025-06-22 22:03:42 +03:00
Flaminel
910633a413 added download cleaner pop-up 2025-06-22 21:51:02 +03:00
Flaminel
823b9fec3f added general settings pop-up 2025-06-22 21:50:48 +03:00
Flaminel
b99cbd0d6e reorganized content blocker settings 2025-06-22 12:06:25 +03:00
Flaminel
534a8a9c66 reorganized download cleaner settings 2025-06-22 12:06:16 +03:00
Flaminel
0473d14462 fixed setting message 2025-06-22 04:55:56 +03:00
Flaminel
febb9c4432 fixed some settings 2025-06-22 04:53:33 +03:00
Flaminel
0933b99cea fixed encryption key seeding 2025-06-22 02:12:18 +03:00
Flaminel
20483fab9a fix unlink categories not being sent 2025-06-22 01:53:35 +03:00
Flaminel
60f96589cd fixed general setting HTTPS description 2025-06-22 00:45:23 +03:00
Flaminel
eec0cb9605 fixed general settings dropdowns 2025-06-22 00:08:20 +03:00
Flaminel
20fe4c5b3f fixed cleanup settings 2025-06-21 23:51:47 +03:00
Flaminel
8d7b207181 fixed queue cleaner and download cleaner layout 2025-06-21 20:29:13 +03:00
Flaminel
8f34bdc780 fixed port configuration and logs 2025-06-21 20:20:20 +03:00
Flaminel
a61a10af8f updated support section 2025-06-21 19:52:47 +03:00
Flaminel
17c37e8eef reorganized sidebar 2025-06-21 19:43:02 +03:00
Flaminel
bfbd2c16dd fixed support component icons; removed p-card 2025-06-21 16:40:20 +03:00
Flaminel
eef21ac734 added huntarr link 2025-06-21 16:28:01 +03:00
Flaminel
978b1c9ade added makefile 2025-06-21 16:16:29 +03:00
Flaminel
5ea1361832 removed enabled per arr config and added enabled per arr instance 2025-06-21 16:16:21 +03:00
Flaminel
861c74e452 fixed entrypoint to create user and group if they don't exist 2025-06-21 02:12:49 +03:00
Flaminel
6f437de698 added support card 2025-06-21 02:12:12 +03:00
Flaminel
2cdbc938fd fixed qbit label name 2025-06-20 23:13:41 +03:00
Flaminel
26d98f815f added support for PUID, PGID and UMASK 2025-06-20 23:06:25 +03:00
Flaminel
1a7e86aca4 removed https redirection 2025-06-20 21:12:59 +03:00
Flaminel
4b2aa6c4f6 Merge branch 'main' into dev 2025-06-20 15:00:40 +03:00
Flaminel
2dcd495da7 fixed default base path 2025-06-20 14:57:20 +03:00
Flaminel
38b7d1d4bb fixed content blocker 2025-06-20 14:39:00 +03:00
Flaminel
c109b15ec1 added content blocker back 2025-06-20 12:06:48 +03:00
Flaminel
1cc749243c fixed base path and port 2025-06-20 01:32:05 +03:00
Flaminel
a7c8f37bed fixed job enabling failing 2025-06-19 00:08:06 +03:00
Flaminel
65d0d5188d gitignore 2025-06-18 23:53:49 +03:00
Flaminel
15b0e4218d fixed settings page 2025-06-18 23:38:42 +03:00
Flaminel
de7e7d244b fixed initial docker build 2025-06-18 23:31:49 +03:00
Flaminel
7a8cdbb354 fixed frontend build 2025-06-18 23:02:52 +03:00
Flaminel
4c5e4e95d9 try fix Dockerfile 2025-06-18 21:59:14 +03:00
Flaminel
5881c24ff3 moved frontend 2025-06-18 21:59:04 +03:00
Flaminel
b323235227 moved backend 2025-06-18 21:58:48 +03:00
Flaminel
ee0f915aaf fixed some icons 2025-06-18 18:17:21 +03:00
Flaminel
8883a2ca3f fixed notifications page layout 2025-06-18 18:00:16 +03:00
Flaminel
bbfde4bb17 added notifications endpoint 2025-06-18 17:48:50 +03:00
Flaminel
4d8d3ea732 fixed mobile menu bar 2025-06-17 18:49:47 +03:00
Flaminel
e9718c3a66 fixed tooltip positions 2025-06-17 18:47:34 +03:00
Flaminel
3e8fb01f44 fixed cron validations 2025-06-17 18:44:49 +03:00
Flaminel
e5d7d8ed12 reorganized project 2025-06-17 18:21:18 +03:00
Flaminel
d76216665b try #1 for deployment 2025-06-17 14:41:55 +03:00
Flaminel
1255d0a50a fixed serialization options 2025-06-17 10:36:24 +03:00
Flaminel
cb53ee09da removed commented code 2025-06-16 23:59:18 +03:00
Flaminel
f2622b129d removed TODO comment 2025-06-16 23:58:19 +03:00
Flaminel
7ac72cbece fixed download cleaner processing 2025-06-16 23:57:51 +03:00
Flaminel
b7902ca7ba fixed queue cleaner processing 2025-06-16 23:35:01 +03:00
Flaminel
84dd43a85a removed dry run attribute 2025-06-16 23:14:10 +03:00
Flaminel
fe54813abf fixed download clients not being invalidated on config change 2025-06-16 23:11:00 +03:00
Flaminel
7b2af6dd5d try fix download clients factory 2025-06-16 22:40:12 +03:00
Flaminel
cacd62058f fixed singletons using datacontext 2025-06-16 22:23:40 +03:00
Flaminel
0a8d1450dd try fix download client factory 2025-06-16 21:38:27 +03:00
Flaminel
f651663fd3 fixed health checks and download service factory 2025-06-16 20:17:06 +03:00
Flaminel
b4548573ee fixed download client UI 2025-06-16 18:26:16 +03:00
Flaminel
f39e8eca46 fixed download services 2025-06-16 18:26:09 +03:00
Flaminel
ba1ced3b84 some more layout fixes 2025-06-16 17:37:49 +03:00
Flaminel
ef8aa4c002 fixed download button context menu 2025-06-16 17:28:05 +03:00
Flaminel
cd2b66a525 fix events and logs layout 2025-06-16 17:21:56 +03:00
Flaminel
b4dad7ebc9 fixed download clients layout 2025-06-16 14:42:32 +03:00
Flaminel
9411ab815e fixed lidarr layout 2025-06-15 23:51:23 +03:00
Flaminel
9ebf49f0b8 fixed mobile menu 2025-06-15 23:51:17 +03:00
Flaminel
369a75b4c5 fixed radarr layout 2025-06-15 23:38:15 +03:00
Flaminel
1f55929ab1 fixed sonarr disabled states 2025-06-15 23:12:53 +03:00
Flaminel
4558fa462f fixed sonarr layout 2025-06-15 23:08:37 +03:00
Flaminel
f23c0e0186 changed heart icon color 2025-06-15 23:01:43 +03:00
Flaminel
d5d661543f try fix sonarr layout 2025-06-15 22:55:58 +03:00
Flaminel
53163774a5 separated settings pages 2025-06-15 22:31:34 +03:00
Flaminel
00dfd63797 try fix sonarr 2025-06-15 22:23:52 +03:00
Flaminel
d5e3e9954d try fix arrs again 2025-06-15 21:46:01 +03:00
Flaminel
bf37668dcb combine arr configs #1 2025-06-15 21:15:50 +03:00
Flaminel
62eee94497 removed search type from sonarr config 2025-06-15 19:46:25 +03:00
Flaminel
91274dac4b try fix arr #3 2025-06-15 19:30:11 +03:00
Flaminel
689adb13d8 try fix arr #2 2025-06-15 18:54:33 +03:00
Flaminel
1194db6c1e try fix arr #1 2025-06-15 18:54:28 +03:00
Flaminel
5f412c2e6a try fix config controller 2025-06-15 11:27:16 +03:00
Flaminel
bd9f24eb1a fixed general settings 2025-06-15 03:30:25 +03:00
Flaminel
458688770a try fix download client #4 2025-06-15 03:06:24 +03:00
Flaminel
cf208754a3 try fix download client #3 2025-06-15 03:01:53 +03:00
Flaminel
4b5f4dc447 try fix download client #2 2025-06-15 02:45:49 +03:00
Flaminel
3c2e36eb9e try fix download client UI 2025-06-15 02:26:37 +03:00
Flaminel
95d39c40ed fixed initial db state 2025-06-15 01:21:58 +03:00
Flaminel
9e393eebcc try switch to db 2025-06-15 00:42:20 +03:00
Flaminel
033b50519b db config checkpoint 2025-06-14 01:20:37 +03:00
Flaminel
0be5f48717 fixed db naming 2025-06-13 18:52:51 +03:00
Flaminel
9353a55cff added config db 2025-06-13 18:16:03 +03:00
Flaminel
0cb737a7e1 try fix config controller 2025-06-13 16:42:22 +03:00
Flaminel
6a0641ef63 try fix config update 2025-06-13 15:46:00 +03:00
Flaminel
26bfa5adb2 try fix again 2025-06-13 14:29:01 +03:00
Flaminel
96130501c3 try fix download client settings 2025-06-13 14:18:17 +03:00
Flaminel
fef7efb7dc added download client config UI 2025-06-13 14:09:04 +03:00
Flaminel
857a6a88b4 added lidarr UI 2025-06-12 23:18:13 +03:00
Flaminel
b85842d3f0 added radarr config UI; fixed some messages 2025-06-12 22:57:14 +03:00
Flaminel
4800ec66d5 fixed some buttons 2025-06-12 22:28:50 +03:00
Flaminel
225e80cdbe try add sonarr config #2 2025-06-11 17:39:53 +03:00
Flaminel
64a24051d7 try add sonarr config #1 2025-06-11 16:01:55 +03:00
Flaminel
d73cb46006 try fix download cleaner again 2025-06-11 00:28:29 +03:00
Flaminel
0ab5c01ebc added toggle for unlinked settings 2025-06-10 22:31:42 +03:00
Flaminel
f2303023b4 try fix download cleaner #2 2025-06-10 21:48:26 +03:00
Flaminel
7b67ab6967 try fix download cleaner settings #1 2025-06-10 20:25:53 +03:00
Flaminel
3ae1f9c81d added download cleaner settings 2025-06-10 19:19:16 +03:00
Flaminel
3b676a8c60 try fix settings notifications and stuff 2025-06-10 09:42:49 +03:00
Flaminel
0f45b298b8 fixed dropdown 2025-06-09 12:52:34 +03:00
Flaminel
0260e43d79 try basic/advanced scheduling mode 2025-06-09 12:50:26 +03:00
Flaminel
a85687f687 fixed some other stuff 2025-06-09 03:28:20 +03:00
Flaminel
ee24856504 added new event type and fixed more stuff 2025-06-09 02:40:41 +03:00
Flaminel
9b337134c9 fixed queue settings validations 2025-06-09 02:21:34 +03:00
Flaminel
64d4abf25b fixed setting log level 2025-06-09 02:21:20 +03:00
Flaminel
a4423a28e9 some more fucking fixing 2025-06-09 01:47:22 +03:00
Flaminel
fc26f40fb3 fixed some stuff on queue cleaner 2025-06-09 00:59:42 +03:00
Flaminel
e2e775c073 fix some job stuff 2025-06-09 00:52:53 +03:00
Flaminel
ca13171b82 try fix settings 2025-06-08 23:59:55 +03:00
Flaminel
ee71eab6fa more fixes 2025-06-08 23:10:35 +03:00
Flaminel
565b6b3fde fixed general settings layout 2025-06-08 23:03:02 +03:00
Flaminel
91e7e92cc6 fixed api path 2025-06-08 22:37:16 +03:00
Flaminel
c8626c220e fixed missing certificate validation option 2025-06-08 22:35:28 +03:00
Flaminel
c9dc917401 try fix general settings #1 2025-06-08 22:25:30 +03:00
Flaminel
20e1df722e added general settings 2025-06-08 21:38:16 +03:00
Flaminel
06a2b82881 added loading/error screen for settings 2025-06-08 18:21:05 +03:00
Flaminel
1afbf5c573 fixed content blocker checkbox state 2025-06-08 02:45:15 +03:00
Flaminel
e5586fc774 fixed settings dirty state management and added leave page popup 2025-06-08 02:37:24 +03:00
Flaminel
565d6a78a7 fixed queue cleaner settings button 2025-06-08 01:53:01 +03:00
Flaminel
9cac4fdf8c fixed too many OnFileChanged being trigger when starting the app 2025-06-08 01:45:16 +03:00
Flaminel
d28389641a fixed trace and debug logs icon 2025-06-08 01:44:37 +03:00
Flaminel
46c48d7c00 try fix config manager 2025-06-07 02:15:36 +03:00
Flaminel
d2bcf8ac89 removed content blocker config file 2025-06-07 02:14:07 +03:00
Flaminel
8463b8b786 removed ignored downloads path and config 2025-06-07 02:05:51 +03:00
Flaminel
fd74455951 removed fallback code 2025-06-07 00:41:56 +03:00
Flaminel
34314de284 fixed accordion toggle 2025-06-07 00:36:27 +03:00
Flaminel
195d361364 fixed patterns input 2025-06-07 00:11:46 +03:00
Flaminel
cc07466c2b try change to accordion panel 2025-06-06 23:39:35 +03:00
Flaminel
30aaac5e67 fixed dropdowns and schedule 2025-06-06 23:12:34 +03:00
Flaminel
44a6c37530 fixed small stuff 2025-06-06 22:53:29 +03:00
Flaminel
9526f133a1 changed some info logs 2025-06-06 22:45:19 +03:00
Flaminel
ddbfee33d1 fixed queue cleaner config retrieval 2025-06-06 22:44:08 +03:00
Flaminel
12ab97825b fixed queue cleaner UI settings 2025-06-06 22:20:56 +03:00
Flaminel
bcf093148c fixed UI queue cleaner config 2025-06-06 21:26:51 +03:00
Flaminel
d2cc8517c1 try fix blocklist provider to be dynamic 2025-06-06 21:11:15 +03:00
Flaminel
cae4e323a5 try remove content blocker 2025-06-06 20:46:38 +03:00
Flaminel
f6b0014ec6 fixed some configuration stuff 2025-06-02 12:59:33 +03:00
Flaminel
5d0a48e7cd fixed download cleaner job on api update 2025-06-01 18:49:59 +03:00
Flaminel
d177790d6f fixed queue cleaner job scheduling on API call 2025-06-01 18:37:58 +03:00
Flaminel
d7e28fc6e7 change logs layout 2025-06-01 18:12:16 +03:00
Flaminel
14f16cbe70 fixed events layout 2025-06-01 02:37:15 +03:00
Flaminel
8b8dc672d1 updated some names 2025-06-01 02:36:59 +03:00
Flaminel
d80d897acb test 2025-05-31 21:38:45 +03:00
Flaminel
d9d1f86897 try fix #4 2025-05-31 21:38:41 +03:00
Flaminel
2de3cccac7 try fix #3 2025-05-31 20:39:30 +03:00
Flaminel
b2bb48a260 try fix #2 2025-05-31 00:54:25 +03:00
Flaminel
d078ea288c try fix settings enablement 2025-05-30 23:18:36 +03:00
Flaminel
84d984082c created separate component for settings 2025-05-30 19:13:02 +03:00
Flaminel
b289b2ee39 added queue cleaner settings 2025-05-30 18:36:38 +03:00
Flaminel
97473b47fd updated config controller 2025-05-30 16:27:20 +03:00
Flaminel
a38d370925 added global exception handler 2025-05-30 16:02:56 +03:00
Flaminel
1e3a4cb220 added config DTOs 2025-05-30 15:19:04 +03:00
Flaminel
62e31a2497 changed db file name 2025-05-30 04:14:28 +03:00
Flaminel
c58f6080f0 added encryption for sensitive configs 2025-05-29 23:01:42 +03:00
Flaminel
7cfd69b1f7 fixed search on events 2025-05-29 20:14:07 +03:00
Flaminel
86c356c3a9 added missing event search; fixed connection status for events page 2025-05-29 04:31:54 +03:00
Flaminel
36cfd3b4e5 fixed events viewer 2025-05-29 03:25:05 +03:00
Flaminel
c6f34432b7 removed auto scroll on events 2025-05-29 02:55:24 +03:00
Flaminel
97fe7138c2 fixed event timestamp 2025-05-29 01:57:12 +03:00
Flaminel
2b83e1a334 events api #1 2025-05-28 22:39:15 +03:00
Flaminel
599f8959a9 fixed some logs 2025-05-28 21:41:38 +03:00
Flaminel
f6bcd29ea0 removed old hubs 2025-05-28 19:31:02 +03:00
Flaminel
62fc39251b added unified signalr hub 2025-05-28 19:01:54 +03:00
Flaminel
f5fe9405cd removed some stuff from LoggingController 2025-05-28 15:12:59 +03:00
Flaminel
e4bb0ac04c added glow on logo and heart 2025-05-28 15:12:45 +03:00
Flaminel
fdab02a937 fixed some things; renamed stuff 2025-05-28 11:34:18 +03:00
Flaminel
b7546a7015 fixed dashboard streams not being populated at first 2025-05-28 02:17:52 +03:00
Flaminel
7482f963e3 removed weird left border on event and log entries 2025-05-28 01:21:59 +03:00
Flaminel
90b0ea8306 fixed connection status styling on dashboard 2025-05-28 01:18:36 +03:00
Flaminel
25ae542e87 fixed timeline colors 2025-05-28 01:06:15 +03:00
Flaminel
e96d76091b fixes to layout and styling 2025-05-27 23:03:21 +03:00
Flaminel
28b6c10917 fixed sidebar items 2025-05-27 20:31:32 +03:00
Flaminel
60884198ff removed icons; fixed buttons 2025-05-27 20:30:58 +03:00
Flaminel
367a499155 removed title from topbar 2025-05-27 20:04:18 +03:00
Flaminel
414ad299b6 removed dashboard refresh button 2025-05-27 18:56:44 +03:00
Flaminel
b3bc071943 added dashboard cards 2025-05-27 18:45:53 +03:00
Flaminel
7bd69046ea removed notification publisher definitions 2025-05-27 15:25:13 +03:00
Flaminel
7cfe1333d2 fixed config path 2025-05-27 15:21:18 +03:00
Flaminel
4bb54517c1 changed failed import naming 2025-05-27 14:37:39 +03:00
Flaminel
607bebaf0f fixed event architecture 2025-05-27 14:30:59 +03:00
Flaminel
0da1ef518a events #5 2025-05-27 03:03:06 +03:00
Flaminel
a660480a7c events #4 2025-05-27 02:21:34 +03:00
Flaminel
e8a7373b0d events #3 2025-05-27 01:32:02 +03:00
Flaminel
0785fe9a12 events #2 2025-05-26 23:16:22 +03:00
Flaminel
153c490198 added events #1 2025-05-26 23:08:02 +03:00
Flaminel
4f8d2c57d7 fixed toggle switch 2025-05-25 23:34:56 +03:00
Flaminel
0ba1979016 update theming 2025-05-25 23:16:44 +03:00
Flaminel
7ec9dfe68d remove collapsed sidebar 2025-05-25 22:50:37 +03:00
Flaminel
8b7e39fa86 try sidebar #2 2025-05-25 19:23:00 +03:00
Flaminel
2143c74767 try sidebar 2025-05-25 02:33:21 +03:00
Flaminel
9e596cfe17 topbar 2025-05-25 02:13:24 +03:00
Flaminel
dd95c40bec fixed logs 2025-05-24 01:28:07 +03:00
Flaminel
df37aee2a2 try #3 2025-05-24 00:54:45 +03:00
Flaminel
cb57b06abb try fix collapse #2 2025-05-24 00:48:26 +03:00
Flaminel
ac924d1294 try fix collapse animation 2025-05-24 00:38:43 +03:00
Flaminel
ba4379417e fixed nav bar icon alignment 2025-05-24 00:31:36 +03:00
Flaminel
ff565895ce fixed download logs 2025-05-24 00:12:54 +03:00
Flaminel
1f5e8e5711 fixed mobile side bar 2025-05-24 00:04:12 +03:00
Flaminel
e284ea2c13 changing main layout and styling 2025-05-23 23:49:45 +03:00
Flaminel
d5dab0a726 styling again 2025-05-23 23:13:31 +03:00
Flaminel
8b36e363ac style adjustments 2025-05-23 22:11:01 +03:00
Flaminel
d3c5f7a210 debounce 2025-05-23 22:00:58 +03:00
Flaminel
a97b898d33 #37 2025-05-23 20:58:22 +03:00
Flaminel
aa3ec6c49c theme 2025-05-23 20:18:04 +03:00
Flaminel
2a55ad456c rules 2025-05-23 19:59:15 +03:00
Flaminel
856bd9a93d #36 2025-05-23 17:27:25 +03:00
Flaminel
64518ad071 #35 2025-05-23 17:15:15 +03:00
Flaminel
100528ab06 fix #34 2025-05-22 19:18:47 +03:00
Flaminel
a8e188aa01 #33 2025-05-22 17:03:09 +03:00
Flaminel
91bd85708c #32 2025-05-22 15:13:25 +03:00
Flaminel
0bd4e77e9d #31 2025-05-21 20:49:40 +03:00
Flaminel
9bd46d7255 fix #30 2025-05-20 16:28:54 +03:00
Flaminel
1604e56a89 huntarr 2025-05-20 13:36:10 +03:00
Flaminel
6c9b60dff5 #29 2025-05-20 13:35:28 +03:00
Flaminel
ee02666dc1 #28 2025-05-20 13:21:32 +03:00
Flaminel
21e1fde1c8 #27 2025-05-20 12:32:42 +03:00
Flaminel
8721bc411e #26 2025-05-20 10:35:30 +03:00
Flaminel
3d0ab5f4a6 #25 2025-05-19 23:11:16 +03:00
Flaminel
0e4535d7a6 add ui 2025-05-19 22:16:52 +03:00
Flaminel
7a1e019c76 fix #24 2025-05-19 19:40:39 +03:00
Flaminel
701a7dc417 fix #23 2025-05-19 19:30:12 +03:00
Flaminel
eb0f782f53 #22 2025-05-19 15:32:57 +03:00
Flaminel
a1bd278652 #21 2025-05-19 13:40:59 +03:00
Flaminel
9409346732 fix #20 2025-05-19 12:38:52 +03:00
Flaminel
3d9b286206 #19 2025-05-19 12:35:58 +03:00
Flaminel
c675924be7 fix 2025-05-19 12:08:58 +03:00
Flaminel
2bd8f69aff fix 2025-05-18 02:39:20 +03:00
Flaminel
ef9868be4d fix 2025-05-18 02:17:45 +03:00
Flaminel
4a394928bb #17 2025-05-17 22:14:00 +03:00
Flaminel
60d3ddb3d2 fix #16 2025-05-17 21:33:02 +03:00
Flaminel
0173598519 #15 2025-05-17 21:30:09 +03:00
Flaminel
96b9a54b64 fix #14 2025-05-17 21:03:14 +03:00
Flaminel
212aeccaf3 fix env 2025-05-17 20:14:30 +03:00
Flaminel
d2eb9e50e0 fix #13 2025-05-17 20:12:53 +03:00
Flaminel
1b47921ac5 fix #12 2025-05-17 19:16:59 +03:00
Flaminel
d8d6e31395 fix #11 2025-05-16 21:52:28 +03:00
Flaminel
10be7d4a73 fix #10 2025-05-16 21:15:35 +03:00
Flaminel
e98ecfcb2a fix #9 2025-05-16 20:48:52 +03:00
Flaminel
bc1da2113c fix #8 2025-05-16 20:05:21 +03:00
Flaminel
46ef6123cc fix #7 2025-05-16 19:28:52 +03:00
Flaminel
f2027f77a9 #12 2025-05-16 19:16:32 +03:00
Flaminel
3c2bb7a289 #11 2025-05-16 18:53:01 +03:00
Flaminel
f9fd118f88 #10 2025-05-16 18:25:47 +03:00
Flaminel
4d79307d24 #9 2025-05-16 18:06:18 +03:00
Flaminel
a52fd9198d fix #6 2025-05-16 17:22:59 +03:00
Flaminel
4cb36e6727 #8 2025-05-16 16:24:38 +03:00
Flaminel
a7c2b698d6 rules #2 2025-05-16 16:24:23 +03:00
Flaminel
57326b2f8e fix #5 2025-05-15 22:09:42 +03:00
Flaminel
374f0f72a7 fix #4 2025-05-15 20:29:27 +03:00
Flaminel
06f514c3ea #7 2025-05-15 20:25:21 +03:00
Flaminel
f386bf700c fix #3 2025-05-15 20:15:20 +03:00
Flaminel
b6950f545f #6 2025-05-15 19:34:12 +03:00
Flaminel
6eb457ed8f fix #2 2025-05-15 18:18:41 +03:00
Flaminel
0e99a510a8 #5 2025-05-15 18:15:42 +03:00
Flaminel
b4316a4f0d #4 2025-05-15 17:46:25 +03:00
Flaminel
fa3aebde9a #3 2025-05-15 16:33:39 +03:00
Flaminel
a68a4c733f fix #1 2025-05-15 16:18:09 +03:00
Flaminel
0fc7352db6 #2 2025-05-14 22:49:39 +03:00
Flaminel
69788d55d2 #1 2025-05-14 22:42:52 +03:00
Flaminel
461e935128 initial windsurf 2025-05-14 22:42:24 +03:00
56 changed files with 318 additions and 1632 deletions

View File

@@ -14,7 +14,7 @@ body:
options:
- label: Reviewed the documentation.
required: true
- label: Ensured I am using ghcr.io/cleanuparr/cleanuparr docker repository.
- label: Ensured I am using ghcr.io/Cleanuparr/Cleanuparr docker repository.
required: true
- label: Ensured I am using the latest version.
required: true

View File

@@ -14,7 +14,7 @@ body:
options:
- label: Reviewed the documentation.
required: true
- label: Ensured I am using ghcr.io/cleanuparr/cleanuparr docker repository.
- label: Ensured I am using ghcr.io/Cleanuparr/Cleanuparr docker repository.
required: true
- label: Ensured I am using the latest version.
required: true

View File

@@ -53,10 +53,11 @@ jobs:
githubTags=""
if [ -n "$latestDockerTag" ]; then
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$latestDockerTag"
githubTags="$githubTags,ghcr.io/cleanuparr:$latestDockerTag"
fi
if [ -n "$versionDockerTag" ]; then
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$versionDockerTag"
githubTags="$githubTags,ghcr.io/cleanuparr:$versionDockerTag"
fi
# set env vars
@@ -112,7 +113,7 @@ jobs:
version=${{ env.versionDockerTag }}
build-args: |
VERSION=${{ env.version }}
PACKAGES_USERNAME=${{ secrets.PACKAGES_USERNAME }}
PACKAGES_USERNAME=${{ env.PACKAGES_USERNAME }}
PACKAGES_PAT=${{ env.PACKAGES_PAT }}
outputs: |
type=image

View File

@@ -101,6 +101,28 @@ jobs:
- name: Build osx-arm64
run: dotnet publish code/backend/${{ env.executableName }}/${{ env.executableName }}.csproj -c Release --runtime osx-arm64 --self-contained -o artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64 /p:PublishSingleFile=true /p:Version=${{ env.appVersion }} /p:DebugSymbols=false
- name: Create sample configuration files
run: |
# Create a sample appsettings.json for each platform
cat > sample-config.json << 'EOF'
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
EOF
# Copy to each build directory
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-win-amd64/appsettings.json
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-amd64/appsettings.json
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-linux-arm64/appsettings.json
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-amd64/appsettings.json
cp sample-config.json artifacts/${{ env.githubRepositoryName }}-${{ env.appVersion }}-osx-arm64/appsettings.json
- name: Zip win-x64
run: |
cd ./artifacts

View File

@@ -55,7 +55,7 @@ jobs:
# Build portable executables
build-executables:
needs: validate
uses: ./.github/workflows/build-executable.yml
uses: ./.github/workflows/build_executable.yml
secrets: inherit
# Build Windows installer
@@ -110,8 +110,8 @@ jobs:
tag_name: ${{ needs.validate.outputs.release_version }}
token: ${{ env.REPO_READONLY_PAT }}
make_latest: true
target_commitish: main
generate_release_notes: true
prerelease: ${{ contains(needs.validate.outputs.app_version, '-') }}
files: |
./artifacts/**/*.zip
./artifacts/**/*.pkg

View File

@@ -12,67 +12,34 @@ Cleanuparr was created primarily to address malicious files, such as `*.lnk` or
> **Features:**
> - Strike system to mark bad downloads.
> - Remove and block downloads that reached a maximum number of strikes.
> - Remove and block downloads that are **failing to be imported** by the arrs.
> - Remove and block downloads that are **stalled** or in **metadata downloading** state.
> - Remove and block downloads that have a **low download speed** or **high estimated completion time**.
> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Content Blocker**.
> - Remove and block downloads that are **failing to be imported** by the arrs. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/import-failed)
> - Remove and block downloads that are **stalled** or in **metadata downloading** state. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/stalled)
> - Remove and block downloads that have a **low download speed** or **high estimated completion time**. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/queue-cleaner/slow)
> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Content Blocker**. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/content-blocker/general)
> - Automatically trigger a search for downloads removed from the arrs.
> - Clean up downloads that have been **seeding** for a certain amount of time.
> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support).
> - Notify on strike or download removal.
> - Clean up downloads that have been **seeding** for a certain amount of time. [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/download-cleaner/seeding)
> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support). [configuration](https://cleanuparr.github.io/cleanuparr/docs/configuration/download-cleaner/hardlinks)
> - Notify on strike or download removal. [configuration](https://cleanuparr.github.io/cleanuparr/docs/category/notifications)
> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuparr.
## 🎯 Supported Applications
Cleanuparr supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment.
### *Arr Applications
- **Sonarr** (TV Shows)
- **Radarr** (Movies)
- **Lidarr** (Music)
## Quick Start
### Download Clients
- **qBittorrent**
- **Transmission**
- **Deluge**
> [!NOTE]
>
> 1. **Docker (Recommended)**
> Pull the Docker image from `ghcr.io/Cleanuparr/Cleanuparr:latest`.
>
> 2. **Unraid (for Unraid users)**
> Use the Unraid Community App.
>
> 3. **Manual Installation (if you're not using Docker)**
> Go to [Windows](#windows), [Linux](#linux) or [MacOS](#macos).
### Platforms
- **Docker** (Linux, Windows, macOS)
- **Windows** (Native installer)
- **macOS** (Intel & Apple Silicon)
- **Linux** (Portable executable)
- **Unraid** (Community Apps)
# Docs
## 🚀 Quick Start
```bash
docker run -d --name cleanuparr \
--restart unless-stopped \
-p 11011:11011 \
-v /path/to/config:/config \
-e PORT=11011 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Etc/UTC \
ghcr.io/cleanuparr/cleanuparr:latest
```
For Docker Compose, health checks, and other installation methods, see our [Complete Installation Guide](https://cleanuparr.github.io/Cleanuparr/docs/installation/detailed).
### 🌐 Access the Web Interface
After installation, open your browser and navigate to:
```
http://localhost:11011
```
**Next Steps:** Check out the [📖 Complete Documentation](https://cleanuparr.github.io/Cleanuparr/) for detailed configuration guides and setup instructions.
## 📖 Documentation & Support
- **📚 [Complete Documentation](https://cleanuparr.github.io/Cleanuparr/)** - Installation guides, configuration, and troubleshooting
- **⚙️ [Configuration Guide](https://cleanuparr.github.io/Cleanuparr/docs/category/configuration)** - Set up download clients, *arr apps, and features
- **🔧 [Setup Scenarios](https://cleanuparr.github.io/Cleanuparr/docs/category/setup-scenarios)** - Common use cases and examples
- **💬 [Discord Community](https://discord.gg/SCtMCgtsc4)** - Get help and discuss with other users
- **🔗 [GitHub Releases](https://github.com/Cleanuparr/Cleanuparr/releases)** - Download binaries and view changelog
Docs can be found [here](https://Cleanuparr.github.io/Cleanuparr/).
# <img style="vertical-align: middle;" width="24px" src="./Logo/256.png" alt="Cleanuparr"> <span style="vertical-align: middle;">Cleanuparr</span> <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/x.svg" height="24px" width="30px" style="vertical-align: middle;"> <span style="vertical-align: middle;">Huntarr</span> <img style="vertical-align: middle;" width="24px" src="https://github.com/plexguide/Huntarr.io/blob/main/frontend/static/logo/512.png?raw=true" alt Huntarr></img>

347
blacklist
View File

@@ -1,27 +1,12 @@
*(sample).*
*sample.avchd
*sample.avi
*sample.mkv
*sample.mov
*sample.mp4
*sample.webm
*sample.wmv
*.000
*.001
*.002
*.004
*.0xe
*.001
*.73k
*.73p
*.7z
*.7z.001
*.7z.002
*.89k
*.89z
*.8ck
*.a00
*.a01
*.a02
*.a7r
*.ac
*.acc
@@ -37,11 +22,8 @@
*.ahk
*.ai
*.aif
*.ain
*.air
*.alz
*.ana
*.apex
*.api
*.apk
*.app
@@ -49,28 +31,15 @@
*.applescript
*.application
*.appx
*.apz
*.ar
*.arc
*.archiver
*.arduboy
*.arh
*.ari
*.arj
*.ark
*.arscript
*.asb
*.asice
*.asp
*.aspx
*.aspx-exe
*.atmx
*.ayt
*.azw2
*.b1
*.b6z
*.b64
*.ba
*.ba_
*.bak
*.bas
@@ -78,48 +47,26 @@
*.bat
*.bdjo
*.bdmv
*.bdoc
*.beam
*.bh
*.bin
*.bmp
*.bms
*.bndl
*.bns
*.boo
*.bsa
*.btm
*.bundle
*.bz
*.bz2
*.bza
*.bzabw
*.bzip
*.bzip2
*.c
*.c00
*.c01
*.c02
*.c10
*.cab
*.caction
*.car
*.cb7
*.cba
*.cbr
*.cbt
*.cbz
*.cci
*.cda
*.cdb
*.cdz
*.cel
*.celx
*.cfs
*.cgi
*.cheat
*.chm
*.cit
*.ckpt
*.cla
*.class
@@ -129,15 +76,9 @@
*.coffee
*.com
*.command
*.comppkg.hauptwerk.rar
*.comppkg_hauptwerk_rar
*.conda
*.conf
*.config
*.cp9
*.cpgz
*.cpl
*.cpt
*.crt
*.cs
*.csh
@@ -145,27 +86,17 @@
*.csproj
*.css
*.csv
*.ctx
*.ctz
*.cue
*.cur
*.cxarchive
*.cyw
*.czip
*.daemon
*.daf
*.dar
*.dat
*.data-00000-of-00001
*.db
*.dd
*.deamon
*.deb
*.dek
*.dgc
*.dist
*.diz
*.dl_
*.dld
*.dll
*.dmc
@@ -182,27 +113,19 @@
*.dw
*.dword
*.dxl
*.dz
*.e_e
*.ear
*.ebacmd
*.ebm
*.ebs
*.ebs2
*.ecar
*.ecf
*.ecs
*.ecsbx
*.edz
*.efw
*.egg
*.eham
*.elf
*.elf-so
*.email
*.emu
*.epk
*.epi
*.es
*.esh
*.etc
@@ -218,62 +141,36 @@
*.exz
*.ezs
*.ezt
*.f
*.f3z
*.fas
*.fba
*.fcx
*.fky
*.flac
*.flatpak
*.flv
*.fp8
*.fpi
*.frs
*.fxp
*.fzpz
*.gadget
*.gar
*.gat
*.gca
*.gif
*.gifv
*.gm9
*.gmz
*.gpe
*.gpu
*.gs
*.gz
*.gz2
*.gza
*.gzi
*.gzip
*.h5
*.ha
*.ham
*.hbc
*.hbc2
*.hbe
*.hex
*.hki
*.hki1
*.hki2
*.hki3
*.hlp
*.hms
*.hpf
*.hpk
*.hpkg
*.hta
*.hta-psh
*.htaccess
*.htm
*.html
*.htmi
*.hyp
*.iadproj
*.icd
*.ice
*.icns
*.ico
*.idx
@@ -286,27 +183,17 @@
*.ins
*.ipa
*.ipf
*.ipg
*.ipk
*.ipsw
*.iqylink
*.ish
*.iso
*.isp
*.isu
*.isx
*.ita
*.ize
*.izh
*.izma ace
*.j
*.jar
*.jar.pack
*.java
*.jex
*.jgz
*.jhh
*.jic
*.jpeg
*.jpg
*.js
@@ -315,51 +202,27 @@
*.jse
*.jsf
*.json
*.jsonlz4
*.jsp
*.jsx
*.kextraction
*.kgb
*.kix
*.ksh
*.ksp
*.kwgt
*.kx
*.kz
*.layout
*.lbr
*.lck
*.ldb
*.lemon
*.lha
*.lhzd
*.lib
*.libzip
*.link
*.lnk
*.lo
*.lock
*.log
*.loop-vbs
*.lpkg
*.lqr
*.ls
*.lz
*.lz4
*.lzh
*.lzm
*.lzma
*.lzo
*.lzr
*.lzx
*.m3u
*.m4a
*.mac
*.macho
*.mamc
*.manifest
*.mar
*.mbz
*.mcr
*.md
*.mda
@@ -370,29 +233,22 @@
*.mdt
*.mel
*.mem
*.memo
*.meta
*.mgm
*.mhm
*.mht
*.mhtml
*.mid
*.mint
*.mio
*.mlappinstall
*.mlproj
*.mlx
*.mm
*.mobileconfig
*.model
*.moo
*.mou
*.movpkg
*.mozlz4
*.mp3
*.mpa
*.mpk
*.mpkg
*.mpls
*.mrc
*.mrp
@@ -411,79 +267,41 @@
*.msp
*.mst
*.msu
*.mxc
*.mxe
*.mzp
*.n
*.nar
*.ncl
*.net
*.nex
*.nexe
*.nfo
*.npk
*.nrg
*.num
*.nz
*.nzb.bz2
*.nzb.gz
*.nzbs
*.oar
*.ocx
*.odlgz
*.odt
*.opk
*.ore
*.osf
*.ost
*.osx
*.osx-app
*.otm
*.out
*.ova
*.oz
*.p
*.p01
*.p19
*.p7z
*.pa
*.pack.gz
*.package
*.pae
*.paf
*.pak
*.paq6
*.paq7
*.paq8
*.paq8f
*.paq8l
*.paq8p
*.par
*.par2
*.pax
*.pb
*.pbi
*.pcd
*.pcv
*.pdb
*.pdf
*.pea
*.perl
*.pet
*.pex
*.pf
*.phar
*.php
*.php5
*.pif
*.pim
*.pima
*.pit
*.piz
*.pkg
*.pkg.tar.xz
*.pkg.tar.zst
*.pkz
*.pl
*.plsc
*.plx
@@ -501,7 +319,6 @@
*.pptx
*.prc
*.prg
*.prs
*.ps
*.ps1
*.ps1xml
@@ -517,16 +334,9 @@
*.psh-reflection
*.psm1
*.pst
*.psz
*.pt
*.pup
*.puz
*.pvd
*.pvmp
*.pvmz
*.pwa
*.pwc
*.pxl
*.pxo
*.py
*.pyc
@@ -534,20 +344,8 @@
*.pyo
*.python
*.pyz
*.q
*.qda
*.qit
*.qpx
*.r0
*.r00
*.r01
*.r02
*.r03
*.r04
*.r1
*.r2
*.r21
*.r30
*.ram
*.rar
*.raw
@@ -558,35 +356,22 @@
*.reg
*.resources
*.resx
*.rev
*.rfs
*.rfu
*.rgs
*.rk
*.rm
*.rnc
*.rox
*.rp9
*.rpg
*.rpj
*.rpm
*.rss
*.ruby
*.run
*.rxe
*.rz
*.s00
*.s01
*.s02
*.s09
*.s2a
*.s7z
*.sample
*.sapk
*.sar
*.savedmodel
*.sbs
*.sbx
*.sca
*.scar
*.scb
@@ -596,85 +381,42 @@
*.scr
*.script
*.sct
*.sdc
*.sdn
*.sdoc
*.sdocx
*.sea
*.seed
*.sen
*.server
*.service
*.sfg
*.sfm
*.sfs
*.sfv
*.sfx
*.sh
*.shar
*.shb
*.shell
*.shk
*.shortcut
*.shr
*.shs
*.shtml
*.sifz
*.sipa
*.sit
*.sitx
*.sk
*.sldm
*.sln
*.smm
*.smpf
*.snap
*.snagitstamps
*.snappy
*.snb
*.snd
*.snz
*.spa
*.spd
*.spl
*.spm
*.spr
*.spt
*.sqf
*.sql
*.sqx
*.sqz
*.srec
*.srep
*.srt
*.ssm
*.stg
*.stkdoodlz
*.stproj
*.sts
*.sub
*.svg
*.swf
*.sy_
*.sys
*.tar
*.tar.bz2
*.tar.gz
*.tar.gz2
*.tar.lz
*.tar.lzma
*.tar.xz
*.tar.z
*.tar.zip
*.taz
*.tbl
*.tbz
*.tbz2
*.tcp
*.tcx
*.text
*.tf
*.tg
*.tgs
*.tgz
*.thm
*.thmx
@@ -683,35 +425,19 @@
*.tif
*.tiff
*.tipa
*.tlz
*.tlzma
*.tmp
*.tms
*.toast
*.torrent
*.tpk
*.tpsr
*.trs
*.txt
*.tx_
*.txz
*.tz
*.tzst
*.u3p
*.ubz
*.uc2
*.udf
*.ufdr
*.ufs.uzip
*.uha
*.upk
*.upx
*.url
*.uue
*.uvm
*.uw8
*.uzed
*.uzip
*.vb
*.vba
*.vba-exe
@@ -723,46 +449,26 @@
*.vbscript
*.vcd
*.vdo
*.vem
*.vexe
*.vfs
*.vhd
*.vhdx
*.vib
*.vip
*.vlx
*.vm
*.vmcz
*.vmdk
*.vms
*.vob
*.vocab
*.voca
*.vpk
*.vpm
*.vrpackage
*.vsi
*.vwi
*.vxp
*.wa
*.wacz
*.waff
*.war
*.wastickers
*.wav
*.wbk
*.wcm
*.wdz
*.webm
*.whl
*.wick
*.widget
*.wim
*.wiz
*.wlb
*.wma
*.workflow
*.wot
*.wpk
*.wpl
*.wpm
@@ -771,26 +477,14 @@
*.wsc
*.wsf
*.wsh
*.wux
*.x86
*.x86_64
*.xaml
*.xap
*.xapk
*.xar
*.xbap
*.xbe
*.xcf.bz2
*.xcf.gz
*.xcf.xz
*.xcfbz2
*.xcfgz
*.xcfxz
*.xex
*.xez
*.xfp
*.xig
*.xip
*.xla
*.xlam
*.xll
@@ -803,47 +497,24 @@
*.xltb
*.xltm
*.xlw
*.xmcdz
*.xml
*.xoj
*.xopp
*.xqt
*.xrt
*.xx
*.xys
*.xz
*.xzm
*.y
*.yc
*.ygh
*.yz1
*.z
*.z00
*.z01
*.z02
*.z03
*.z04
*.zabw
*.zap
*.zed
*.zfsendtotarget
*.zhelp
*.zi
*.zi_
*.zim
*.zip
*.zipx
*.zix
*.zl
*.zl9
*.zoo
*.zpaq
*.zpi
*.zsplit
*.zst
*.zw
*.zwi
*.zz
*sample.avchd
*sample.avi
*sample.mkv
*sample.mov
*sample.mp4
*sample.webm
*sample.wmv
Trailer.*
VOSTFR
api

View File

@@ -1,410 +1,53 @@
*.000
*.001
*.002
*.004
*.7z
*.7z.001
*.7z.002
*.a00
*.a01
*.a02
*.ace
*.ain
*.alz
*.ana
*.apex
*.apk
*.apz
*.ar
*.arc
*.archiver
*.arduboy
*.arh
*.ari
*.arj
*.ark
*.asice
*.ayt
*.b1
*.b6z
*.b64
*.ba
*.bat
*.bdoc
*.bh
*.bin
*.bmp
*.bndl
*.boo
*.bundle
*.bz
*.bz2
*.bza
*.bzabw
*.bzip
*.bzip2
*.c00
*.c01
*.c02
*.c10
*.car
*.cb7
*.cba
*.cbr
*.cbt
*.cbz
*.cdz
*.cit
*.cmd
*.com
*.comppkg.hauptwerk.rar
*.comppkg_hauptwerk_rar
*.conda
*.cp9
*.cpgz
*.cpt
*.ctx
*.ctz
*.cxarchive
*.czip
*.daf
*.dar
*.db
*.dd
*.deb
*.dgc
*.dist
*.diz
*.dl_
*.dll
*.dmg
*.dz
*.ecar
*.ecs
*.ecsbx
*.edz
*.efw
*.egg
*.epi
*.etc
*.exe
*.f
*.f3z
*.fcx
*.fp8
*.fzpz
*.gar
*.gca
*.gif
*.gmz
*.gz
*.gz2
*.gza
*.gzi
*.gzip
*.ha
*.hbc
*.hbc2
*.hbe
*.hki
*.hki1
*.hki2
*.hki3
*.hpk
*.hpkg
*.htm
*.html
*.htmi
*.hyp
*.iadproj
*.ice
*.ico
*.ini
*.ipg
*.ipk
*.ish
*.iso
*.isx
*.ita
*.ize
*.j
*.jar
*.jar.pack
*.jex
*.jgz
*.jhh
*.jic
*.jpg
*.js
*.jsonlz4
*.kextraction
*.kgb
*.ksp
*.kwgt
*.kz
*.layout
*.lbr
*.lemon
*.lha
*.lhzd
*.libzip
*.link
*.lnk
*.lpkg
*.lqr
*.lz
*.lz4
*.lzh
*.lzm
*.lzma
*.lzo
*.lzr
*.lzx
*.mar
*.mbz
*.md
*.memo
*.mint
*.mlproj
*.mou
*.movpkg
*.mozlz4
*.mpkg
*.msi
*.mxc
*.mzp
*.nar
*.nex
*.nfo
*.npk
*.nz
*.oar
*.odlgz
*.opk
*.osf
*.oz
*.p01
*.p19
*.p7z
*.pa
*.pack.gz
*.package
*.pae
*.pak
*.paq6
*.paq7
*.paq8
*.paq8f
*.paq8l
*.paq8p
*.par
*.par2
*.pax
*.pbi
*.pcv
*.pea
*.perl
*.pet
*.pf
*.php
*.pim
*.pima
*.pit
*.piz
*.pkg
*.pkg.tar.xz
*.pkg.tar.zst
*.pkz
*.pl
*.png
*.prs
*.ps1
*.psc1
*.psd1
*.psm1
*.psz
*.pup
*.puz
*.pvmp
*.pvmz
*.pwa
*.pxl
*.py
*.pyd
*.q
*.qda
*.r0
*.r00
*.r01
*.r02
*.r03
*.r04
*.r1
*.r2
*.r21
*.r30
*.rar
*.rb
*.readme
*.reg
*.rev
*.rk
*.rnc
*.rp9
*.rpm
*.rss
*.run
*.rz
*.s00
*.s01
*.s02
*.s09
*.s7z
*.sar
*.sbx
*.scr
*.sdc
*.sdn
*.sdoc
*.sdocx
*.sea
*.sen
*.sfg
*.sfm
*.sfs
*.sfx
*.sh
*.shar
*.shk
*.shr
*.sifz
*.sipa
*.sit
*.sitx
*.smpf
*.snagitstamps
*.snappy
*.snb
*.snz
*.spa
*.spd
*.spl
*.spm
*.spt
*.sql
*.sqf
*.sqx
*.sqz
*.srep
*.stg
*.stkdoodlz
*.stproj
*.sy_
*.tar.bz2
*.tar.gz
*.tar.gz2
*.tar.lz
*.tar.lzma
*.tar.xz
*.tar.z
*.tar.zip
*.taz
*.tbz
*.tbz2
*.tcx
*.text
*.tg
*.tgs
*.tgz
*.thumb
*.tlz
*.tlzma
*.torrent
*.tpsr
*.trs
*.txt
*.tx_
*.txz
*.tz
*.tzst
*.ubz
*.uc2
*.ufdr
*.ufs.uzip
*.uha
*.url
*.uue
*.uvm
*.uzed
*.uzip
*.vbs
*.vem
*.vfs
*.vib
*.vip
*.vmcz
*.vms
*.voca
*.vpk
*.vrpackage
*.vsi
*.vwi
*.wa
*.wacz
*.waff
*.war
*.wastickers
*.wdz
*.whl
*.wick
*.wlb
*.wot
*.wsf
*.wux
*.xapk
*.xar
*.xcf.bz2
*.xcf.gz
*.xcf.xz
*.xcfbz2
*.xcfgz
*.xcfxz
*.xez
*.xfp
*.xip
*.xml
*.xmcdz
*.xoj
*.xopp
*.xx
*.xz
*.xzm
*.y
*.yc
*.yz1
*.z
*.z00
*.z01
*.z02
*.z03
*.z04
*.zabw
*.zap
*.zed
*.zfsendtotarget
*.zhelp
*.zi
*.zi_
*.zim
*.zip
*.zipx
*.zix
*.zl
*.zoo
*.zpaq
*.zpi
*.zsplit
*.zst
*.zw
*.zwi
*.zz
*.zipx

View File

@@ -7,7 +7,6 @@ using Cleanuparr.Infrastructure.Helpers;
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
using Cleanuparr.Infrastructure.Logging;
using Cleanuparr.Infrastructure.Models;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Infrastructure.Utilities;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration;
@@ -17,6 +16,7 @@ using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Infrastructure.Services.Interfaces;
using Mapster;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

View File

@@ -1,125 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Cleanuparr.Api.Controllers;
/// <summary>
/// Health check endpoints for Docker and Kubernetes
/// </summary>
[ApiController]
[Route("[controller]")]
public class HealthController : ControllerBase
{
private readonly HealthCheckService _healthCheckService;
private readonly ILogger<HealthController> _logger;
public HealthController(HealthCheckService healthCheckService, ILogger<HealthController> logger)
{
_healthCheckService = healthCheckService;
_logger = logger;
}
/// <summary>
/// Basic liveness probe - checks if the application is running
/// Used by Docker HEALTHCHECK and Kubernetes liveness probes
/// </summary>
[HttpGet]
[Route("/health")]
public async Task<IActionResult> GetHealth()
{
try
{
var result = await _healthCheckService.CheckHealthAsync(
registration => registration.Tags.Contains("liveness"));
return result.Status == HealthStatus.Healthy
? Ok(new { status = "healthy", timestamp = DateTime.UtcNow })
: StatusCode(503, new { status = "unhealthy", timestamp = DateTime.UtcNow });
}
catch (Exception ex)
{
_logger.LogError(ex, "Health check failed");
return StatusCode(503, new { status = "unhealthy", error = "Health check failed", timestamp = DateTime.UtcNow });
}
}
/// <summary>
/// Readiness probe - checks if the application is ready to serve traffic
/// Used by Kubernetes readiness probes
/// </summary>
[HttpGet]
[Route("/health/ready")]
public async Task<IActionResult> GetReadiness()
{
try
{
var result = await _healthCheckService.CheckHealthAsync(
registration => registration.Tags.Contains("readiness"));
if (result.Status == HealthStatus.Healthy)
{
return Ok(new { status = "ready", timestamp = DateTime.UtcNow });
}
// For readiness, we consider degraded as not ready
return StatusCode(503, new {
status = "not_ready",
timestamp = DateTime.UtcNow,
details = result.Entries.Where(e => e.Value.Status != HealthStatus.Healthy)
.ToDictionary(e => e.Key, e => new {
status = e.Value.Status.ToString().ToLowerInvariant(),
description = e.Value.Description
})
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Readiness check failed");
return StatusCode(503, new { status = "not_ready", error = "Readiness check failed", timestamp = DateTime.UtcNow });
}
}
/// <summary>
/// Detailed health status - for monitoring and debugging
/// </summary>
[HttpGet]
[Route("/health/detailed")]
public async Task<IActionResult> GetDetailedHealth()
{
try
{
var result = await _healthCheckService.CheckHealthAsync();
var response = new
{
status = result.Status.ToString().ToLowerInvariant(),
timestamp = DateTime.UtcNow,
totalDuration = result.TotalDuration.TotalMilliseconds,
entries = result.Entries.ToDictionary(
e => e.Key,
e => new
{
status = e.Value.Status.ToString().ToLowerInvariant(),
description = e.Value.Description,
duration = e.Value.Duration.TotalMilliseconds,
tags = e.Value.Tags,
data = e.Value.Data,
exception = e.Value.Exception?.Message
})
};
return result.Status == HealthStatus.Healthy
? Ok(response)
: StatusCode(503, response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Detailed health check failed");
return StatusCode(503, new {
status = "unhealthy",
error = "Detailed health check failed",
timestamp = DateTime.UtcNow
});
}
}
}

View File

@@ -1,6 +1,6 @@
using Cleanuparr.Api.Models;
using Cleanuparr.Infrastructure.Models;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Infrastructure.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace Cleanuparr.Api.Controllers;

View File

@@ -83,17 +83,9 @@ public static class MainDI
/// </summary>
private static IServiceCollection AddHealthServices(this IServiceCollection services) =>
services
// Register the existing health check service for download clients
// Register the health check service
.AddSingleton<IHealthCheckService, HealthCheckService>()
// Register the background service for periodic health checks
.AddHostedService<HealthCheckBackgroundService>()
// Add ASP.NET Core health checks
.AddHealthChecks()
.AddCheck<ApplicationHealthCheck>("application", tags: ["liveness"])
.AddCheck<DatabaseHealthCheck>("database", tags: ["readiness"])
.AddCheck<FileSystemHealthCheck>("filesystem", tags: ["readiness"])
.AddCheck<DownloadClientsHealthCheck>("download_clients", tags: ["readiness"])
.Services;
.AddHostedService<HealthCheckBackgroundService>();
}

View File

@@ -12,9 +12,9 @@ using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.Security;
using Cleanuparr.Infrastructure.Interceptors;
using Cleanuparr.Infrastructure.Services;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Persistence;
using Infrastructure.Interceptors;
using Infrastructure.Services.Interfaces;
using Infrastructure.Verticals.Files;
namespace Cleanuparr.Api.DependencyInjection;

View File

@@ -1,28 +0,0 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text;
namespace Cleanuparr.Api;
/// <summary>
/// Custom health check response writers for different formats
/// </summary>
public static class HealthCheckResponseWriter
{
/// <summary>
/// Writes a minimal plain text response suitable for Docker health checks
/// </summary>
public static async Task WriteMinimalPlaintext(HttpContext context, HealthReport report)
{
context.Response.ContentType = "text/plain";
var status = report.Status switch
{
HealthStatus.Healthy => "healthy",
HealthStatus.Degraded => "degraded",
HealthStatus.Unhealthy => "unhealthy",
_ => "unknown"
};
await context.Response.WriteAsync(status, Encoding.UTF8);
}
}

View File

@@ -175,7 +175,7 @@ public class BackgroundJobManager : IHostedService
IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
.WithIdentity("ValidationTrigger")
.StartNow()
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
.WithCronSchedule(cronExpression)
.Build();
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
@@ -197,26 +197,26 @@ public class BackgroundJobManager : IHostedService
}
}
// Create main cron trigger with consistent naming (matches JobManagementService)
// Create cron trigger
var trigger = TriggerBuilder.Create()
.WithIdentity($"{typeName}-trigger")
.ForJob(jobKey)
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
.StartNow()
.Build();
// Schedule the main trigger
await _scheduler.ScheduleJob(trigger, cancellationToken);
// Trigger immediate execution for startup using a one-time trigger
// Create startup trigger to run immediately
var startupTrigger = TriggerBuilder.Create()
.WithIdentity($"{typeName}-startup-{DateTimeOffset.UtcNow.Ticks}")
.WithIdentity($"{typeName}-startup-trigger")
.ForJob(jobKey)
.StartNow()
.Build();
// Schedule job with both triggers
await _scheduler.ScheduleJob(trigger, cancellationToken);
await _scheduler.ScheduleJob(startupTrigger, cancellationToken);
_logger.LogInformation("Added trigger for job {name} with cron expression {CronExpression} and immediate startup execution",
_logger.LogInformation("Added triggers for job {name} with cron expression {CronExpression}",
typeName, cronExpression);
}

View File

@@ -4,8 +4,6 @@ using Cleanuparr.Api;
using Cleanuparr.Api.DependencyInjection;
using Cleanuparr.Infrastructure.Logging;
using Cleanuparr.Shared.Helpers;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
@@ -149,19 +147,6 @@ logConfig.WriteTo.Sink(signalRSink);
Log.Logger = logConfig.CreateLogger();
// Configure health check endpoints before the API configuration
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = registration => registration.Tags.Contains("liveness"),
ResponseWriter = HealthCheckResponseWriter.WriteMinimalPlaintext
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = registration => registration.Tags.Contains("readiness"),
ResponseWriter = HealthCheckResponseWriter.WriteMinimalPlaintext
});
app.ConfigureApi();
await app.RunAsync();

View File

@@ -16,7 +16,6 @@
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="MassTransit.Abstractions" Version="8.4.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />

View File

@@ -1,5 +1,6 @@
using Cleanuparr.Domain.Entities.Deluge.Response;
using Cleanuparr.Infrastructure.Services;
using Infrastructure.Services;
namespace Cleanuparr.Infrastructure.Extensions;

View File

@@ -1,4 +1,5 @@
using Cleanuparr.Infrastructure.Services;
using Infrastructure.Services;
using QBittorrent.Client;
namespace Cleanuparr.Infrastructure.Extensions;

View File

@@ -1,4 +1,5 @@
using Cleanuparr.Infrastructure.Services;
using Infrastructure.Services;
using Transmission.API.RPC.Entity;
namespace Cleanuparr.Infrastructure.Extensions;

View File

@@ -168,7 +168,9 @@ public sealed class DelugeClient
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
UriBuilder uriBuilder = new(_config.Url);
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/json";
uriBuilder.Path = string.IsNullOrEmpty(_config.UrlBase)
? $"{uriBuilder.Path.TrimEnd('/')}/json"
: $"{uriBuilder.Path.TrimEnd('/')}/{_config.UrlBase.TrimStart('/').TrimEnd('/')}/json";
var responseMessage = await _httpClient.PostAsync(uriBuilder.Uri, content);
responseMessage.EnsureSuccessStatusCode();

View File

@@ -238,8 +238,6 @@ public partial class QBitService
}
await _dryRunInterceptor.InterceptAsync(ChangeCategory, download.Hash, downloadCleanerConfig.UnlinkedTargetCategory);
await _eventPublisher.PublishCategoryChanged(download.Category, downloadCleanerConfig.UnlinkedTargetCategory, downloadCleanerConfig.UnlinkedUseTag);
if (downloadCleanerConfig.UnlinkedUseTag)
{
@@ -250,6 +248,8 @@ public partial class QBitService
_logger.LogInformation("category changed for {name}", download.Name);
download.Category = downloadCleanerConfig.UnlinkedTargetCategory;
}
await _eventPublisher.PublishCategoryChanged(download.Category, downloadCleanerConfig.UnlinkedTargetCategory, downloadCleanerConfig.UnlinkedUseTag);
}
}

View File

@@ -53,7 +53,9 @@ public partial class TransmissionService : DownloadService, ITransmissionService
)
{
UriBuilder uriBuilder = new(_downloadClientConfig.Url);
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/rpc";
uriBuilder.Path = string.IsNullOrEmpty(_downloadClientConfig.UrlBase)
? $"{uriBuilder.Path.TrimEnd('/')}/rpc"
: $"{uriBuilder.Path.TrimEnd('/')}/{_downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc";
_client = new Client(
_httpClient,
uriBuilder.Uri.ToString(),

View File

@@ -132,7 +132,7 @@ public abstract class GenericHandler : IHandler
.FirstAsync(x => x.Type == InstanceType.Lidarr));
ContextProvider.Set(nameof(QueueCleanerConfig), await _dataContext.QueueCleanerConfigs.AsNoTracking().FirstAsync());
ContextProvider.Set(nameof(ContentBlockerConfig), await _dataContext.ContentBlockerConfigs.AsNoTracking().FirstAsync());
ContextProvider.Set(nameof(DownloadCleanerConfig), await _dataContext.DownloadCleanerConfigs.Include(x => x.Categories).AsNoTracking().FirstAsync());
ContextProvider.Set(nameof(DownloadCleanerConfig), await _dataContext.DownloadCleanerConfigs.AsNoTracking().FirstAsync());
ContextProvider.Set(nameof(DownloadClientConfig), await _dataContext.DownloadClients.AsNoTracking()
.Where(x => x.Enabled)
.ToListAsync());

View File

@@ -1,16 +0,0 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Cleanuparr.Infrastructure.Health;
/// <summary>
/// Basic application health check that verifies the application is running
/// </summary>
public class ApplicationHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
// Basic liveness check - if we can execute this, the app is running
return Task.FromResult(HealthCheckResult.Healthy("Application is running"));
}
}

View File

@@ -1,50 +0,0 @@
using Cleanuparr.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Health;
/// <summary>
/// Health check that verifies database connectivity
/// </summary>
public class DatabaseHealthCheck : IHealthCheck
{
private readonly DataContext _dataContext;
private readonly ILogger<DatabaseHealthCheck> _logger;
public DatabaseHealthCheck(DataContext dataContext, ILogger<DatabaseHealthCheck> logger)
{
_dataContext = dataContext;
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
// Try to execute a simple query to verify database connectivity
var canConnect = await _dataContext.Database.CanConnectAsync(cancellationToken);
if (!canConnect)
{
return HealthCheckResult.Unhealthy("Cannot connect to database");
}
// Optionally check if database schema is up to date
var pendingMigrations = await _dataContext.Database.GetPendingMigrationsAsync(cancellationToken);
if (pendingMigrations.Any())
{
_logger.LogWarning("Database has pending migrations: {migrations}", string.Join(", ", pendingMigrations));
return HealthCheckResult.Degraded($"Database has {pendingMigrations.Count()} pending migrations");
}
return HealthCheckResult.Healthy("Database connection successful");
}
catch (Exception ex)
{
_logger.LogError(ex, "Database health check failed");
return HealthCheckResult.Unhealthy("Database health check failed", ex);
}
}
}

View File

@@ -1,60 +0,0 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Health;
/// <summary>
/// Health check that verifies download clients are healthy
/// </summary>
public class DownloadClientsHealthCheck : IHealthCheck
{
private readonly IHealthCheckService _healthCheckService;
private readonly ILogger<DownloadClientsHealthCheck> _logger;
public DownloadClientsHealthCheck(IHealthCheckService healthCheckService, ILogger<DownloadClientsHealthCheck> logger)
{
_healthCheckService = healthCheckService;
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
// Get current health status of all clients without triggering new checks
var allClientHealth = _healthCheckService.GetAllClientHealth();
if (!allClientHealth.Any())
{
// No clients configured - this might be ok depending on the deployment
return HealthCheckResult.Healthy("No download clients configured");
}
var healthyClients = allClientHealth.Values.Where(h => h.IsHealthy).ToList();
var unhealthyClients = allClientHealth.Values.Where(h => !h.IsHealthy).ToList();
var totalClients = allClientHealth.Count;
if (unhealthyClients.Any())
{
var unhealthyNames = string.Join(", ", unhealthyClients.Select(c => c.ClientName));
var message = $"{unhealthyClients.Count}/{totalClients} download clients unhealthy: {unhealthyNames}";
// If more than half are unhealthy, consider it unhealthy
if (unhealthyClients.Count > totalClients / 2)
{
return HealthCheckResult.Unhealthy(message);
}
// Otherwise, it's degraded
return HealthCheckResult.Degraded(message);
}
return HealthCheckResult.Healthy($"All {totalClients} download clients are healthy");
}
catch (Exception ex)
{
_logger.LogError(ex, "Download clients health check failed");
return HealthCheckResult.Unhealthy("Download clients health check failed", ex);
}
}
}

View File

@@ -1,76 +0,0 @@
using Cleanuparr.Shared.Helpers;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Health;
/// <summary>
/// Health check that verifies file system access to critical directories
/// </summary>
public class FileSystemHealthCheck : IHealthCheck
{
private readonly ILogger<FileSystemHealthCheck> _logger;
public FileSystemHealthCheck(ILogger<FileSystemHealthCheck> logger)
{
_logger = logger;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var issues = new List<string>();
// Check config directory access
var configPath = ConfigurationPathProvider.GetConfigPath();
if (!CheckDirectoryAccess(configPath, "config"))
{
issues.Add($"Cannot access config directory: {configPath}");
}
// Check current working directory
var currentDir = Directory.GetCurrentDirectory();
if (!CheckDirectoryAccess(currentDir, "working"))
{
issues.Add($"Cannot access working directory: {currentDir}");
}
if (issues.Any())
{
var message = $"File system issues detected: {string.Join(", ", issues)}";
return Task.FromResult(HealthCheckResult.Unhealthy(message));
}
return Task.FromResult(HealthCheckResult.Healthy("File system access verified"));
}
catch (Exception ex)
{
_logger.LogError(ex, "File system health check failed");
return Task.FromResult(HealthCheckResult.Unhealthy("File system health check failed", ex));
}
}
private bool CheckDirectoryAccess(string path, string description)
{
try
{
if (!Directory.Exists(path))
{
_logger.LogWarning("Directory does not exist: {path} ({description})", path, description);
return false;
}
// Try to enumerate directory contents
_ = Directory.GetFiles(path).Take(1).ToList();
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Cannot access {description} directory: {path}", description, path);
return false;
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Net;
using Cleanuparr.Infrastructure.Services;
using Infrastructure.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;

View File

@@ -9,3 +9,29 @@ public enum JobType
ContentBlocker,
DownloadCleaner
}
/// <summary>
/// Extension methods for JobType enum
/// </summary>
public static class JobTypeExtensions
{
/// <summary>
/// Converts a JobType enum to its string representation
/// </summary>
/// <param name="jobType">The job type to convert</param>
/// <returns>String representation of the job type</returns>
public static string ToJobName(this JobType jobType) => jobType.ToString();
/// <summary>
/// Parses a string to JobType enum
/// </summary>
/// <param name="jobName">The job name to parse</param>
/// <returns>JobType if successful, null if parsing failed</returns>
public static JobType? TryParseJobType(string jobName)
{
if (string.IsNullOrEmpty(jobName))
return null;
return Enum.TryParse<JobType>(jobName, true, out var result) ? result : null;
}
}

View File

@@ -1,7 +1,6 @@
using Cleanuparr.Infrastructure.Models;
using Quartz;
namespace Cleanuparr.Infrastructure.Services.Interfaces;
namespace Infrastructure.Services.Interfaces;
public interface IJobManagementService
{
@@ -9,9 +8,7 @@ public interface IJobManagementService
Task<bool> StopJob(JobType jobType);
Task<bool> PauseJob(JobType jobType);
Task<bool> ResumeJob(JobType jobType);
Task<bool> TriggerJobOnce(JobType jobType);
Task<IReadOnlyList<JobInfo>> GetAllJobs(IScheduler? scheduler = null);
Task<IReadOnlyList<JobInfo>> GetAllJobs();
Task<JobInfo> GetJob(JobType jobType);
Task<bool> UpdateJobSchedule(JobType jobType, JobSchedule schedule);
Task<ITrigger?> GetMainTrigger(JobType jobType);
}

View File

@@ -1,11 +1,10 @@
using System.Collections.Concurrent;
using Cleanuparr.Infrastructure.Models;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Infrastructure.Utilities;
using Infrastructure.Services.Interfaces;
using Microsoft.Extensions.Logging;
using Quartz;
using Quartz.Impl.Matchers;
using Quartz.Spi;
namespace Cleanuparr.Infrastructure.Services;
@@ -23,7 +22,7 @@ public class JobManagementService : IJobManagementService
public async Task<bool> StartJob(JobType jobType, JobSchedule? schedule = null, string? directCronExpression = null)
{
string jobName = jobType.ToString();
string jobName = jobType.ToJobName();
string? cronExpression = null;
// Validate and set the cron expression
@@ -60,48 +59,60 @@ public class JobManagementService : IJobManagementService
// Check if job exists, create it if it doesn't
if (!await scheduler.CheckExists(jobKey))
{
_logger.LogError("Job {name} does not exist in scheduler. " +
"Jobs should be created at startup by BackgroundJobManager.", jobName);
return false;
_logger.LogInformation("Job {name} does not exist, creating it", jobName);
// Create the job based on its type
if (!await CreateJobIfNotExists(scheduler, jobType, jobKey))
{
_logger.LogError("Failed to create job {name}", jobName);
return false;
}
}
// Store the job key for later use
_jobKeys.TryAdd(jobName, jobKey);
// Clean up all existing triggers for this job first
await CleanupAllTriggersForJob(scheduler, jobKey);
// If cron expression is provided, create and schedule the main trigger
// If cron expression is provided, update the trigger
if (!string.IsNullOrEmpty(cronExpression))
{
var triggerKey = new TriggerKey($"{jobName}-trigger");
var newTrigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.ForJob(jobKey)
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
.Build();
var triggerKey = new TriggerKey($"{jobName}Trigger");
var existingTrigger = await scheduler.GetTrigger(triggerKey);
await scheduler.ScheduleJob(newTrigger);
// Compute next fire time for logging
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes((IOperableTrigger)newTrigger, null, 1);
_logger.LogInformation("Job {name} scheduled with cron expression '{cronExpression}', next run at {nextRunTime}",
jobName, cronExpression, nextFireTimes.FirstOrDefault().LocalDateTime);
// Optionally trigger immediate execution for startup
// await TriggerJobImmediately(scheduler, jobKey, "startup");
if (existingTrigger != null)
{
var newTrigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.ForJob(jobKey)
.WithCronSchedule(cronExpression)
.Build();
await scheduler.RescheduleJob(triggerKey, newTrigger);
}
else
{
var trigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.ForJob(jobKey)
.WithCronSchedule(cronExpression)
.Build();
await scheduler.ScheduleJob(trigger);
}
}
else
{
// If no cron expression, create a one-time trigger to run now
var oneTimeTrigger = TriggerBuilder.Create()
.WithIdentity($"{jobName}-onetime-trigger")
.ForJob(jobKey)
.StartNow()
.Build();
await scheduler.ScheduleJob(oneTimeTrigger);
_logger.LogInformation("Job {name} scheduled for immediate one-time execution", jobName);
// If no trigger exists, create a simple one-time trigger
var triggers = await scheduler.GetTriggersOfJob(jobKey);
if (!triggers.Any())
{
var trigger = TriggerBuilder.Create()
.WithIdentity($"{jobName}Trigger")
.ForJob(jobKey)
.StartNow()
.Build();
await scheduler.ScheduleJob(trigger);
}
}
// Resume the job if it's paused
@@ -117,86 +128,21 @@ public class JobManagementService : IJobManagementService
}
/// <summary>
/// Cleans up all existing triggers for a job to ensure a clean state
/// Creates a job in the scheduler if it doesn't exist based on the job type.
/// Note: Since this is in the Infrastructure layer, we cannot directly reference Application layer job types.
/// Job creation is now handled at startup by BackgroundJobManager.
/// </summary>
private async Task CleanupAllTriggersForJob(IScheduler scheduler, JobKey jobKey)
private Task<bool> CreateJobIfNotExists(IScheduler scheduler, JobType jobType, JobKey jobKey)
{
try
{
var existingTriggers = await scheduler.GetTriggersOfJob(jobKey);
foreach (var trigger in existingTriggers)
{
await scheduler.UnscheduleJob(trigger.Key);
_logger.LogDebug("Removed existing trigger {triggerKey} for job {jobKey}",
trigger.Key.Name, jobKey.Name);
}
if (existingTriggers.Any())
{
_logger.LogDebug("Cleaned up {count} existing triggers for job {jobName}",
existingTriggers.Count, jobKey.Name);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error cleaning up triggers for job {jobName}", jobKey.Name);
}
_logger.LogError("Job {jobName} of type {jobType} does not exist in scheduler. " +
"Jobs should be created at startup by BackgroundJobManager, regardless of enabled status.",
jobKey.Name, jobType);
return Task.FromResult(false);
}
/// <summary>
/// Triggers a job immediately with a one-time trigger
/// </summary>
private async Task TriggerJobImmediately(IScheduler scheduler, JobKey jobKey, string reason)
{
try
{
var immediateTrigger = TriggerBuilder.Create()
.WithIdentity($"{jobKey.Name}-immediate-{reason}-{DateTimeOffset.UtcNow.Ticks}")
.ForJob(jobKey)
.StartNow()
.Build();
await scheduler.ScheduleJob(immediateTrigger);
_logger.LogDebug("Triggered job {jobName} immediately for reason: {reason}", jobKey.Name, reason);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to trigger job {jobName} immediately", jobKey.Name);
}
}
/// <summary>
/// Gets the main scheduled trigger for a job (excludes one-time triggers)
/// </summary>
public async Task<ITrigger?> GetMainTrigger(JobType jobType)
{
string jobName = jobType.ToString();
try
{
var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey(jobName);
if (!await scheduler.CheckExists(jobKey))
{
return null;
}
// Look for the main trigger (follows our naming convention)
var mainTriggerKey = new TriggerKey($"{jobName}-trigger");
return await scheduler.GetTrigger(mainTriggerKey);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting main trigger for job {jobName}", jobName);
return null;
}
}
public async Task<bool> StopJob(JobType jobType)
{
string jobName = jobType.ToString();
string jobName = jobType.ToJobName();
try
{
var scheduler = await _schedulerFactory.GetScheduler();
@@ -208,8 +154,12 @@ public class JobManagementService : IJobManagementService
return false;
}
// Clean up all triggers for this job (reuse our centralized method)
await CleanupAllTriggersForJob(scheduler, jobKey);
// Unschedule all triggers for this job
var triggers = await scheduler.GetTriggersOfJob(jobKey);
foreach (var trigger in triggers)
{
await scheduler.UnscheduleJob(trigger.Key);
}
_logger.LogInformation("Job {name} stopped successfully", jobName);
return true;
@@ -223,7 +173,7 @@ public class JobManagementService : IJobManagementService
public async Task<bool> PauseJob(JobType jobType)
{
string jobName = jobType.ToString();
string jobName = jobType.ToJobName();
try
{
var scheduler = await _schedulerFactory.GetScheduler();
@@ -248,7 +198,7 @@ public class JobManagementService : IJobManagementService
public async Task<bool> ResumeJob(JobType jobType)
{
string jobName = jobType.ToString();
string jobName = jobType.ToJobName();
try
{
var scheduler = await _schedulerFactory.GetScheduler();
@@ -271,11 +221,11 @@ public class JobManagementService : IJobManagementService
}
}
public async Task<IReadOnlyList<JobInfo>> GetAllJobs(IScheduler? scheduler = null)
public async Task<IReadOnlyList<JobInfo>> GetAllJobs()
{
try
{
scheduler ??= await _schedulerFactory.GetScheduler();
var scheduler = await _schedulerFactory.GetScheduler();
var result = new List<JobInfo>();
var jobGroups = await scheduler.GetJobGroupNames();
@@ -333,7 +283,7 @@ public class JobManagementService : IJobManagementService
public async Task<JobInfo> GetJob(JobType jobType)
{
string jobName = jobType.ToString();
string jobName = jobType.ToJobName();
try
{
var scheduler = await _schedulerFactory.GetScheduler();
@@ -389,37 +339,12 @@ public class JobManagementService : IJobManagementService
}
}
public async Task<bool> TriggerJobOnce(JobType jobType)
{
string jobName = jobType.ToString();
try
{
var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey(jobName);
if (!await scheduler.CheckExists(jobKey))
{
_logger.LogError("Job {name} does not exist", jobName);
return false;
}
await TriggerJobImmediately(scheduler, jobKey, "manual");
_logger.LogInformation("Job {name} triggered for one-time execution", jobName);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error triggering job {jobName}", jobName);
return false;
}
}
public async Task<bool> UpdateJobSchedule(JobType jobType, JobSchedule schedule)
{
if (schedule == null)
throw new ArgumentNullException(nameof(schedule));
string jobName = jobType.ToString();
string jobName = jobType.ToJobName();
string cronExpression = schedule.ToCronExpression();
try
@@ -433,18 +358,24 @@ public class JobManagementService : IJobManagementService
return false;
}
// Clean up all existing triggers for this job
await CleanupAllTriggersForJob(scheduler, jobKey);
// Create new trigger with consistent naming
var triggerKey = new TriggerKey($"{jobName}-trigger");
var triggerKey = new TriggerKey($"{jobName}Trigger");
var existingTrigger = await scheduler.GetTrigger(triggerKey);
var newTrigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.ForJob(jobKey)
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
.WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(10))
.WithCronSchedule(cronExpression)
.Build();
await scheduler.ScheduleJob(newTrigger);
if (existingTrigger != null)
{
await scheduler.RescheduleJob(triggerKey, newTrigger);
}
else
{
await scheduler.ScheduleJob(newTrigger);
}
_logger.LogInformation("Job {name} schedule updated successfully to {cronExpression}", jobName, cronExpression);
return true;

View File

@@ -35,7 +35,7 @@ public static class CronValidationHelper
IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
.WithIdentity("ValidationTrigger")
.StartNow()
.WithCronSchedule(cronExpression, x => x.WithMisfireHandlingInstructionDoNothing())
.WithCronSchedule(cronExpression)
.Build();
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);

View File

@@ -23,10 +23,10 @@ export class ApplicationPathService {
*/
getDocumentationBaseUrl(): string {
if (isDevMode()) {
return 'http://localhost:3000/Cleanuparr';
return 'http://localhost:3000';
}
return 'https://cleanuparr.github.io/Cleanuparr';
return 'https://cleanuparr.github.io/cleanuparr';
}
/**
@@ -59,7 +59,7 @@ export class ApplicationPathService {
*/
buildDocumentationUrl(section: string, fieldAnchor?: string): string {
const baseUrl = this.getDocumentationBaseUrl();
let url = `${baseUrl}/docs/configuration/${section}`;
let url = `${baseUrl}/cleanuparr/docs/configuration/${section}`;
if (fieldAnchor) {
url += `#${fieldAnchor}`;

View File

@@ -12,7 +12,7 @@
<!-- Sidebar Navigation -->
<nav class="nav-menu">
<!-- Project Sponsors Link -->
<a href="https://cleanuparr.github.io/Cleanuparr/support" class="nav-item sponsor-link" target="_blank" rel="noopener noreferrer">
<a href="https://cleanuparr.github.io/cleanuparr/support" class="nav-item sponsor-link" target="_blank" rel="noopener noreferrer">
<div class="nav-icon-wrapper heart-icon">
<i class="pi pi-heart"></i>
</div>

View File

@@ -7,6 +7,9 @@
<h2 class="card-title m-0">Content Blocker Configuration</h2>
<span class="card-subtitle">Configure automatic content filtering and blocking</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-ban text-xl"></i>
</div>
</div>
</ng-template>
@@ -137,16 +140,7 @@
<p-accordion [multiple]="false" [value]="activeAccordionIndices" styleClass="mt-3">
<!-- Sonarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="0">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Sonarr Settings
</p-accordion-header>
<p-accordion-header>Sonarr Settings</p-accordion-header>
<p-accordion-content>
<div formGroupName="sonarr">
<div class="field-row">
@@ -207,16 +201,7 @@
<!-- Radarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="1">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Radarr Settings
</p-accordion-header>
<p-accordion-header>Radarr Settings</p-accordion-header>
<p-accordion-content>
<div formGroupName="radarr">
<div class="field-row">
@@ -277,16 +262,7 @@
<!-- Lidarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="2">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Lidarr Settings
</p-accordion-header>
<p-accordion-header>Lidarr Settings</p-accordion-header>
<p-accordion-content>
<div formGroupName="lidarr">
<div class="field-row">

View File

@@ -7,6 +7,9 @@
<h2 class="card-title m-0">Download Cleaner Configuration</h2>
<span class="card-subtitle">Configure automatic download cleanup</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>
@@ -110,16 +113,7 @@
<p-accordion [multiple]="false" [value]="activeAccordionIndices" styleClass="mt-3">
<!-- Seeding Settings -->
<p-accordion-panel [disabled]="!downloadCleanerForm.get('enabled')?.value" [value]="0">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Seeding Settings
</p-accordion-header>
<p-accordion-header>Seeding Settings</p-accordion-header>
<p-accordion-content>
<!-- Delete Private Option -->
<div class="field-row">
@@ -236,16 +230,7 @@
<!-- Unlinked Download Settings -->
<p-accordion-panel [disabled]="!downloadCleanerForm.get('enabled')?.value" [value]="1">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Unlinked Download Settings
</p-accordion-header>
<p-accordion-header>Unlinked Download Settings</p-accordion-header>
<p-accordion-content>
<div class="field-row">
<label class="field-label">

View File

@@ -25,6 +25,9 @@
<h2 class="card-title m-0">Download Clients</h2>
<span class="card-subtitle">Manage download client instances</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-download text-xl"></i>
</div>
</div>
</ng-template>

View File

@@ -363,9 +363,8 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
const clientType = this.clientForm.get('type')?.value;
const hostControl = this.clientForm.get('host');
const usernameControl = this.clientForm.get('username');
const urlBaseControl = this.clientForm.get('urlBase');
if (!hostControl || !usernameControl || !urlBaseControl) return;
if (!hostControl || !usernameControl) return;
hostControl.setValidators([
Validators.required,
@@ -378,11 +377,6 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
usernameControl.clearValidators();
}
// Set default URL base for Transmission
if (clientType === DownloadClientType.Transmission) {
urlBaseControl.setValue('transmission');
}
// Update validation state
hostControl.updateValueAndValidity();
usernameControl.updateValueAndValidity();

View File

@@ -7,6 +7,9 @@
<h2 class="card-title m-0">General Configuration</h2>
<span class="card-subtitle">Configure general application settings</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>

View File

@@ -22,9 +22,12 @@
<ng-template pTemplate="header">
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
<div class="header-title-container">
<h2 class="card-title m-0">Lidarr Settings</h2>
<h2 class="card-title m-0">Global Settings</h2>
<span class="card-subtitle">Configure general Lidarr integration settings</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>
@@ -70,6 +73,9 @@
<h2 class="card-title m-0">Instances</h2>
<span class="card-subtitle">Manage Lidarr server instances</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>

View File

@@ -10,6 +10,9 @@
<h2 class="card-title m-0">Notification Configuration</h2>
<span class="card-subtitle">Configure notification settings for Notifiarr and Apprise</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-bell text-xl"></i>
</div>
</div>
</ng-template>
@@ -33,11 +36,11 @@
<!-- API Key -->
<div class="field-row">
<label class="field-label">
API Key
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('notifiarr.apiKey')"
title="View documentation for Notifiarr API key setup">
</i>
API Key
</label>
<div class="field-input">
<input type="text" pInputText formControlName="apiKey" inputId="notifiarrApiKey" placeholder="Enter Notifiarr API key" />
@@ -48,10 +51,6 @@
<!-- Channel ID -->
<div class="field-row">
<label class="field-label">
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('notifiarr.channelId')"
title="View documentation for Discord channel ID setup">
</i>
Channel ID
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('notifiarr.channelId')"
@@ -67,11 +66,11 @@
<!-- Event Triggers -->
<div class="field-row">
<label class="field-label">
Event Triggers
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('eventTriggers')"
title="View documentation for notification event types">
</i>
Event Triggers
</label>
<div class="field-input">
<div class="flex flex-column gap-2">
@@ -113,11 +112,11 @@
<!-- URL -->
<div class="field-row">
<label class="field-label">
URL
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('apprise.url')"
title="View documentation for Apprise server URL setup">
</i>
URL
</label>
<div class="field-input">
<input type="text" pInputText formControlName="url" inputId="appriseUrl" placeholder="Enter Apprise URL" />
@@ -128,11 +127,11 @@
<!-- Key -->
<div class="field-row">
<label class="field-label">
Key
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('apprise.key')"
title="View documentation for Apprise configuration key">
</i>
Key
</label>
<div class="field-input">
<input type="text" pInputText formControlName="key" inputId="appriseKey" placeholder="Enter key" />
@@ -143,11 +142,11 @@
<!-- Event Triggers -->
<div class="field-row">
<label class="field-label">
Event Triggers
<i class="pi pi-info-circle field-info-icon"
(click)="openFieldDocs('eventTriggers')"
title="View documentation for notification event types">
</i>
Event Triggers
</label>
<div class="field-input">
<div class="flex flex-column gap-2">

View File

@@ -7,6 +7,9 @@
<h2 class="card-title m-0">Queue Cleaner Configuration</h2>
<span class="card-subtitle">Configure automatic arr queue cleanup</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>
@@ -113,16 +116,7 @@
<p-accordion [multiple]="false" [value]="activeAccordionIndices" styleClass="mt-3">
<!-- Failed Import Settings -->
<p-accordion-panel [disabled]="!queueCleanerForm.get('enabled')?.value" [value]="0">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Failed Import Settings
</p-accordion-header>
<p-accordion-header>Failed Import Settings</p-accordion-header>
<p-accordion-content>
<div class="field-row" formGroupName="failedImport">
<label class="field-label">
@@ -202,16 +196,7 @@
<!-- Stalled Settings -->
<p-accordion-panel [disabled]="!queueCleanerForm.get('enabled')?.value" [value]="1">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Stalled Download Settings
</p-accordion-header>
<p-accordion-header>Stalled Download Settings</p-accordion-header>
<p-accordion-content>
<div class="field-row" formGroupName="stalled">
<label class="field-label">
@@ -285,16 +270,7 @@
<!-- Downloading Metadata Settings -->
<p-accordion-panel [disabled]="!queueCleanerForm.get('enabled')?.value" [value]="2">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Downloading Metadata Settings (qBittorrent only)
</p-accordion-header>
<p-accordion-header>Downloading Metadata Settings (qBittorrent only)</p-accordion-header>
<p-accordion-content>
<div class="field-row" formGroupName="stalled">
<label class="field-label">
@@ -326,16 +302,7 @@
<!-- Slow Download Settings -->
<p-accordion-panel [disabled]="!queueCleanerForm.get('enabled')?.value" [value]="3">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
<i class="pi pi-chevron-up"></i>
} @else {
<i class="pi pi-chevron-down"></i>
}
</ng-template>
Slow Download Settings
</p-accordion-header>
<p-accordion-header>Slow Download Settings</p-accordion-header>
<p-accordion-content>
<div class="field-row" formGroupName="slow">
<label class="field-label">

View File

@@ -22,9 +22,12 @@
<ng-template pTemplate="header">
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
<div class="header-title-container">
<h2 class="card-title m-0">Radarr Settings</h2>
<h2 class="card-title m-0">Global Settings</h2>
<span class="card-subtitle">Configure general Radarr integration settings</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>
@@ -70,6 +73,9 @@
<h2 class="card-title m-0">Instances</h2>
<span class="card-subtitle">Manage Radarr server instances</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>

View File

@@ -22,9 +22,12 @@
<ng-template pTemplate="header">
<div class="flex align-items-center justify-content-between p-3 border-bottom-1 surface-border">
<div class="header-title-container">
<h2 class="card-title m-0">Sonarr Settings</h2>
<h2 class="card-title m-0">Global Settings</h2>
<span class="card-subtitle">Configure general Sonarr integration settings</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>
@@ -70,6 +73,9 @@
<h2 class="card-title m-0">Instances</h2>
<span class="card-subtitle">Manage Sonarr server instances</span>
</div>
<div class="flex align-items-center gap-2">
<i class="pi pi-cog text-xl"></i>
</div>
</div>
</ng-template>

View File

@@ -25,7 +25,7 @@
</div>
<!-- Donation Link Placeholder -->
<a href="https://cleanuparr.github.io/Cleanuparr/support" class="support-link donate-link" target="_blank" rel="noopener noreferrer">
<a href="#" class="support-link donate-link" (click)="onDonateClick($event)">
<div class="support-icon donate-icon">
<i class="pi pi-heart-fill"></i>
</div>

View File

@@ -17,4 +17,10 @@ import { TagModule } from 'primeng/tag';
styleUrl: './support-section.component.scss'
})
export class SupportSectionComponent {
onDonateClick(event: Event): void {
event.preventDefault();
// TODO: Navigate to donation page when implemented
console.log('Donation functionality coming soon!');
}
}

View File

@@ -39,4 +39,3 @@ $ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@@ -25,6 +25,7 @@ These settings need a download client to be configured.
<ConfigSection
id="enable-content-blocker"
title="Enable Content Blocker"
description="When enabled, the Content Blocker will run according to the configured schedule to automatically block or remove downloads based on the configured blocklists."
icon="🔄"
>
@@ -35,6 +36,7 @@ When enabled, the Content Blocker will run according to the configured schedule
<ConfigSection
id="scheduling-mode"
title="Scheduling Mode"
description="Choose how to configure the Content Blocker schedule"
icon="📅"
>
@@ -47,6 +49,7 @@ Choose how to configure the Content Blocker schedule:
<ConfigSection
id="cron-expression"
title="Cron Expression"
description="Enter a valid Quartz.NET cron expression to control when the Content Blocker runs"
icon="⏲️"
>
@@ -62,6 +65,7 @@ Enter a valid Quartz.NET cron expression to control when the Content Blocker run
<ConfigSection
id="ignore-private"
title="Ignore Private"
description="When enabled, private torrents will be skipped from being processed during content blocking"
icon="🔒"
>
@@ -72,6 +76,7 @@ When enabled, private torrents will be skipped from being processed during conte
<ConfigSection
id="delete-private"
title="Delete Private"
description="When enabled, private torrents that match blocklist criteria will be deleted from the download client"
icon="🗑️"
>
@@ -99,6 +104,7 @@ Setting this to true means private torrents will be permanently deleted, potenti
<ConfigSection
id="enable-blocklist"
title="Enable Blocklist"
description="When enabled, the Content Blocker will use the configured blocklist to filter content"
icon="✅"
>
@@ -109,6 +115,7 @@ When enabled, the Content Blocker will use the configured blocklist to filter co
<ConfigSection
id="blocklist-path"
title="Blocklist Path"
description="Path to the blocklist file or URL. This can be a local file path or a remote URL that will be fetched automatically"
icon="📂"
>
@@ -132,6 +139,7 @@ regex:<ANY_REGEX> // regex that needs to be marked at the start of the line wi
<ConfigSection
id="blocklist-type"
title="Blocklist Type"
description="Controls how the blocklist is interpreted"
icon="🎭"
>

View File

@@ -26,6 +26,7 @@ These settings need a download client to be configured.
<ConfigSection
id="enable-download-cleaner"
title="Enable Download Cleaner"
description="When enabled, the Download Cleaner will run according to the configured schedule to automatically clean completed downloads from your download client."
icon="🔄"
>
@@ -36,6 +37,7 @@ When enabled, the Download Cleaner will run according to the configured schedule
<ConfigSection
id="scheduling-mode"
title="Scheduling Mode"
description="Choose how to configure the Download Cleaner schedule"
icon="📅"
>
@@ -48,6 +50,7 @@ Choose how to configure the Download Cleaner schedule:
<ConfigSection
id="cron-expression"
title="Cron Expression"
description="Enter a valid Quartz.NET cron expression to control when the Download Cleaner runs"
icon="⏲️"
>
@@ -76,6 +79,7 @@ Enter a valid Quartz.NET cron expression to control when the Download Cleaner ru
<ConfigSection
id="delete-private-torrents"
title="Delete Private Torrents"
description="When enabled, private torrents will be deleted from the download client when they meet the cleanup criteria"
icon="🗑️"
>
@@ -111,6 +115,7 @@ Both Max Ratio and Max Seed Time cannot be disabled (-1) at the same time. At le
<ConfigSection
id="category-name"
title="Category Name"
description="The name of the download client category to apply these rules to"
icon="🏷️"
>
@@ -126,6 +131,7 @@ The name of the download client category to apply these rules to. Must match the
<ConfigSection
id="max-ratio"
title="Max Ratio"
description="Maximum ratio to seed before considering the download for removal"
icon="📊"
>
@@ -136,6 +142,7 @@ Maximum ratio to seed before considering the download for removal. Set to `-1` t
<ConfigSection
id="min-seed-time"
title="Min Seed Time (hours)"
description="Minimum time in hours to seed before removing a download that has reached the max ratio"
icon="⏰"
>
@@ -146,6 +153,7 @@ Minimum time in hours to seed before removing a download that has reached the ma
<ConfigSection
id="max-seed-time"
title="Max Seed Time (hours)"
description="Maximum time in hours to seed before removing a download regardless of ratio"
icon="⏳"
>
@@ -169,6 +177,7 @@ Maximum time in hours to seed before removing a download regardless of ratio. Se
<ConfigSection
id="enable-unlinked-download-handling"
title="Enable Unlinked Download Handling"
description="Enable management of downloads that have no hardlinks remaining"
icon="🔍"
>
@@ -183,6 +192,7 @@ If you are using Docker, make sure to mount the downloads directory the same way
<ConfigSection
id="target-category"
title="Target Category"
description="Category to move unlinked downloads to"
icon="🎯"
>
@@ -193,6 +203,7 @@ Category to move unlinked downloads to.
<ConfigSection
id="use-tag"
title="Use Tag"
description="When enabled, uses a tag instead of category for marking unlinked downloads (qBittorrent only)"
icon="🏷️"
>
@@ -203,6 +214,7 @@ When enabled, uses a tag instead of category for marking unlinked downloads (qBi
<ConfigSection
id="ignored-root-directory"
title="Ignored Root Directory"
description="Root directory to ignore when checking for unlinked downloads"
icon="📁"
>
@@ -224,6 +236,7 @@ For the example above, the ignored root directory should be set to `/data/downlo
<ConfigSection
id="unlinked-categories"
title="Unlinked Categories"
description="Categories to check for unlinked downloads"
icon="📋"
>

View File

@@ -18,6 +18,7 @@ Configure download client connections for torrents and usenet. Cleanuparr suppor
<ConfigSection
id="enable-download-client"
title="Enable Download Client"
description="Controls whether this download client instance is active and will be used by Cleanuparr for operations."
icon="🔄"
>
@@ -36,6 +37,7 @@ Controls whether this download client instance is active and will be used by Cle
<ConfigSection
id="client-name"
title="Client Name"
description="A descriptive name to identify this download client instance in the Cleanuparr interface."
icon="🏷️"
>
@@ -46,6 +48,7 @@ A descriptive name to identify this download client instance in the Cleanuparr i
<ConfigSection
id="client-type"
title="Client Type"
description="Specifies which download client software this configuration connects to."
icon="📱"
>
@@ -65,6 +68,7 @@ Specifies which download client software this configuration connects to.
<ConfigSection
id="client-host"
title="Client Host"
description="The complete URL to access your download client's web interface."
icon="🖥️"
>
@@ -83,6 +87,7 @@ The complete URL to access your download client's web interface.
<ConfigSection
id="url-base-path"
title="URL Base Path"
description="URL path prefix if your download client runs behind a reverse proxy with a subpath."
icon="📂"
>
@@ -112,6 +117,7 @@ URL path prefix if your download client runs behind a reverse proxy with a subpa
<ConfigSection
id="username"
title="Username"
description="Username for download client authentication if required."
icon="👤"
>
@@ -122,6 +128,7 @@ Username for download client authentication if required.
<ConfigSection
id="password"
title="Password"
description="Password for download client authentication."
icon="🔑"
>

View File

@@ -1,195 +0,0 @@
---
sidebar_position: 8
---
import { Important, Warning } from '@site/src/components/Admonition';
import {
ConfigSection,
EnhancedImportant,
EnhancedWarning,
styles
} from '@site/src/components/documentation';
# Health Checks
Cleanuparr provides comprehensive health check endpoints that are compatible with Docker health checks and Kubernetes probes. This allows for better monitoring, automated failover, and container orchestration.
<div className={styles.documentationPage}>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
<span className={styles.sectionIcon}>🏥</span>
Health Check Endpoints
</h2>
<ConfigSection
id="health-liveness"
title="/health - Liveness Probe"
icon="💓"
>
**Purpose**: Basic liveness check to verify the application is running
**Use Case**: Docker HEALTHCHECK and Kubernetes liveness probes
**Response**: HTTP 200 (healthy) or 503 (unhealthy)
**Checks**: Application startup and basic functionality
```bash
curl http://localhost:11011/health
```
</ConfigSection>
<ConfigSection
id="health-readiness"
title="/health/ready - Readiness Probe"
icon="✅"
>
**Purpose**: Verify the application is ready to serve traffic
**Use Case**: Kubernetes readiness probes and load balancer health checks
**Response**: HTTP 200 (ready) or 503 (not ready)
**Checks**: Database connectivity, file system access, download client health
```bash
curl http://localhost:11011/health/ready
```
</ConfigSection>
<ConfigSection
id="health-detailed"
title="/health/detailed - Detailed Status"
icon="📊"
>
**Purpose**: Comprehensive health status for monitoring and debugging
**Use Case**: Monitoring systems and troubleshooting
**Response**: Detailed JSON with status of all components
**Checks**: All health checks with timing and detailed status
```bash
curl http://localhost:11011/health/detailed
```
</ConfigSection>
</div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
<span className={styles.sectionIcon}>🔍</span>
Health Check Components
</h2>
<ConfigSection
id="application-health"
title="Application Health"
icon="🚀"
>
- Verifies the application is running and responsive
- Basic functionality test
- Used for liveness probes
</ConfigSection>
<ConfigSection
id="database-health"
title="Database Health"
icon="🗄️"
>
- Tests database connectivity
- Checks for pending migrations
- Validates schema integrity
- Used for readiness probes
</ConfigSection>
<ConfigSection
id="filesystem-health"
title="File System Health"
icon="📁"
>
- Verifies access to configuration directories
- Validates working directory access
- Used for readiness probes
<EnhancedImportant>
File write tests are not performed during health checks to avoid creating temporary files on every check.
</EnhancedImportant>
</ConfigSection>
<ConfigSection
id="download-client-health"
title="Download Client Health"
icon="⬇️"
>
- Integrates with existing download client monitoring
- Reports status of all configured download clients
- Considers overall health based on client availability
- Used for readiness probes
</ConfigSection>
</div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
<span className={styles.sectionIcon}>🐳</span>
Environment Variables
</h2>
<ConfigSection
id="health-environment-variables"
title="Environment Variables"
icon="🌍"
>
Health check endpoints are affected by these environment variables:
- **`PORT`**: Application port (default: 11011)
- **`BASE_PATH`**: Base path for the application (affects health check URLs)
If you set `BASE_PATH=/cleanuparr`, health checks will be available at:
- `localhost:[PORT]/cleanuparr/health`
- `localhost:[PORT]/cleanuparr/health/ready`
- `localhost:[PORT]/cleanuparr/health/detailed`
</ConfigSection>
</div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
<span className={styles.sectionIcon}>🔧</span>
Troubleshooting
</h2>
<ConfigSection
id="health-common-issues"
title="Common Issues"
icon="⚠️"
>
**Health check timeout**
Increase timeout values if the application is slow to respond
**File system permission errors**
Check container user permissions and mount points
**Download client unavailability**
Review download client configurations and network connectivity
</ConfigSection>
</div>
</div>

View File

@@ -29,6 +29,7 @@ Configure notification services to receive alerts about Cleanuparr operations.
<ConfigSection
id="notifiarr-api-key"
title="Notifiarr API Key"
description="Your Notifiarr API key for authentication. This key is obtained from your Notifiarr dashboard."
icon="🔑"
>
@@ -43,6 +44,7 @@ Requires Notifiarr's [Passthrough](https://notifiarr.wiki/pages/integrations/pas
<ConfigSection
id="notifiarr-channel-id"
title="Notifiarr Channel ID"
description="The Discord channel ID where notifications will be sent. This determines the destination for your alerts."
icon="💬"
>
@@ -66,6 +68,7 @@ The Discord channel ID where notifications will be sent. This determines the des
<ConfigSection
id="apprise-url"
title="Apprise URL"
description="The Apprise server URL where notification requests will be sent."
icon="🌐"
>
@@ -76,6 +79,7 @@ The Apprise server URL where notification requests will be sent.
<ConfigSection
id="apprise-key"
title="Apprise Key"
description="The key that identifies your Apprise configuration. This corresponds to a configuration defined in your Apprise server."
icon="🔐"
>
@@ -95,6 +99,7 @@ The key that identifies your Apprise configuration. This corresponds to a config
<ConfigSection
id="event-failed-import-strike"
title="Failed Import Strike"
description="Triggered When: A download receives a strike for failed import."
icon="❌"
>
@@ -105,6 +110,7 @@ The key that identifies your Apprise configuration. This corresponds to a config
<ConfigSection
id="event-stalled-strike"
title="Stalled Strike"
description="Triggered When: A download receives a strike for being stalled."
icon="⏸️"
>
@@ -115,6 +121,7 @@ The key that identifies your Apprise configuration. This corresponds to a config
<ConfigSection
id="event-slow-strike"
title="Slow Strike"
description="Triggered When: A download receives a strike for slow speed."
icon="🐌"
>
@@ -125,6 +132,7 @@ The key that identifies your Apprise configuration. This corresponds to a config
<ConfigSection
id="event-queue-item-deleted"
title="Queue Item Deleted"
description="Triggered When: A download is removed from the queue."
icon="🗑️"
>
@@ -135,6 +143,7 @@ The key that identifies your Apprise configuration. This corresponds to a config
<ConfigSection
id="event-download-cleaned"
title="Download Cleaned"
description="Triggered When: Download Cleaner removes completed downloads."
icon="🧽"
>
@@ -145,6 +154,7 @@ The key that identifies your Apprise configuration. This corresponds to a config
<ConfigSection
id="event-category-changed"
title="Category Changed"
description="Triggered When: Download Cleaner changes a download's category."
icon="🏷️"
>

View File

@@ -37,10 +37,11 @@ Docker is the preferred installation method as it ensures all dependencies are c
- Docker (version 20.10 or newer)
- Docker Compose (optional, for compose method)
### Docker Run
### Docker Run Method
The simplest way to run Cleanuparr is with a single Docker command:
#### Option 1: GitHub Container Registry (Recommended)
```bash
docker run -d --name cleanuparr \
--restart unless-stopped \
@@ -55,7 +56,22 @@ docker run -d --name cleanuparr \
ghcr.io/cleanuparr:latest
```
### Docker Compose
#### Option 2: DockerHub
```bash
docker run -d --name cleanuparr \
--restart unless-stopped \
-p 11011:11011 \
-v /path/to/config:/config \
-e PORT=11011 \
-e BASE_PATH= \
-e PUID=1000 \
-e PGID=1000 \
-e UMASK=022 \
-e TZ=Etc/UTC \
cleanuparr/cleanuparr:latest
```
### Docker Compose Method
For easier management, create a `docker-compose.yml` file:
@@ -77,13 +93,6 @@ services:
- PGID=1000
- UMASK=022
- TZ=Etc/UTC
# Health check configuration
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:11011/health"]
interval: 30s # Check every 30 seconds
timeout: 10s # Allow up to 10 seconds for response
start_period: 30s # Wait 30 seconds before first check
retries: 3 # Mark unhealthy after 3 consecutive failures
```
### Environment Variables
@@ -264,6 +273,3 @@ To run Cleanuparr as a systemd service:
- **Web Interface**: `http://localhost:11011`
- **Base Path**: *(empty)* (for reverse proxy setups, change `BASE_PATH` environment variable)
- **Configuration Location**: Varies by platform and installation method
#### Health Checks
Cleanuparr provides comprehensive health check endpoints for monitoring and container orchestration. For detailed information about available endpoints and configuration, see the [Health Checks documentation](../configuration/health-checks/).

View File

@@ -52,7 +52,7 @@ services:
- **Linux**: Download and extract the portable executable
<Note>
For detailed installation instructions, security notes, troubleshooting, and all platform-specific options, see the [Complete Installation Guide](/docs/installation/detailed).
For detailed installation instructions, security notes, troubleshooting, and all platform-specific options, see the [Complete Installation Guide](/docs/installation/installation).
</Note>
## 🚀 After Installation

View File

@@ -8,7 +8,7 @@ const config: Config = {
favicon: 'img/favicon.ico',
url: 'https://cleanuparr.github.io',
baseUrl: '/Cleanuparr/',
baseUrl: '/cleanuparr/',
organizationName: 'Cleanuparr',
projectName: 'Cleanuparr',