qBitrr2 5.8.1__py3-none-any.whl → 5.8.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qBitrr/arss.py +30 -17
- qBitrr/bundled_data.py +2 -2
- qBitrr/database.py +71 -0
- qBitrr/static/assets/ProcessesView.js +1 -1
- qBitrr/static/assets/ProcessesView.js.map +1 -1
- qBitrr/webui.py +69 -21
- {qbitrr2-5.8.1.dist-info → qbitrr2-5.8.4.dist-info}/METADATA +4 -22
- {qbitrr2-5.8.1.dist-info → qbitrr2-5.8.4.dist-info}/RECORD +12 -12
- {qbitrr2-5.8.1.dist-info → qbitrr2-5.8.4.dist-info}/WHEEL +0 -0
- {qbitrr2-5.8.1.dist-info → qbitrr2-5.8.4.dist-info}/entry_points.txt +0 -0
- {qbitrr2-5.8.1.dist-info → qbitrr2-5.8.4.dist-info}/licenses/LICENSE +0 -0
- {qbitrr2-5.8.1.dist-info → qbitrr2-5.8.4.dist-info}/top_level.txt +0 -0
qBitrr/arss.py
CHANGED
|
@@ -1434,7 +1434,9 @@ class Arr:
|
|
|
1434
1434
|
):
|
|
1435
1435
|
continue
|
|
1436
1436
|
if self.persistent_queue:
|
|
1437
|
-
self.persistent_queue.insert(
|
|
1437
|
+
self.persistent_queue.insert(
|
|
1438
|
+
EntryId=series_id, ArrInstance=self._name
|
|
1439
|
+
).on_conflict_ignore()
|
|
1438
1440
|
else:
|
|
1439
1441
|
for object_id in object_ids:
|
|
1440
1442
|
episode_found = False
|
|
@@ -1503,7 +1505,7 @@ class Arr:
|
|
|
1503
1505
|
continue
|
|
1504
1506
|
if self.persistent_queue:
|
|
1505
1507
|
self.persistent_queue.insert(
|
|
1506
|
-
EntryId=object_id
|
|
1508
|
+
EntryId=object_id, ArrInstance=self._name
|
|
1507
1509
|
).on_conflict_ignore()
|
|
1508
1510
|
elif self.type == "radarr":
|
|
1509
1511
|
self.logger.trace("Requeue cache entry: %s", object_id)
|
|
@@ -1554,7 +1556,9 @@ class Arr:
|
|
|
1554
1556
|
):
|
|
1555
1557
|
continue
|
|
1556
1558
|
if self.persistent_queue:
|
|
1557
|
-
self.persistent_queue.insert(
|
|
1559
|
+
self.persistent_queue.insert(
|
|
1560
|
+
EntryId=object_id, ArrInstance=self._name
|
|
1561
|
+
).on_conflict_ignore()
|
|
1558
1562
|
elif self.type == "lidarr":
|
|
1559
1563
|
self.logger.trace("Requeue cache entry: %s", object_id)
|
|
1560
1564
|
album_found = False
|
|
@@ -1604,7 +1608,9 @@ class Arr:
|
|
|
1604
1608
|
):
|
|
1605
1609
|
continue
|
|
1606
1610
|
if self.persistent_queue:
|
|
1607
|
-
self.persistent_queue.insert(
|
|
1611
|
+
self.persistent_queue.insert(
|
|
1612
|
+
EntryId=object_id, ArrInstance=self._name
|
|
1613
|
+
).on_conflict_ignore()
|
|
1608
1614
|
|
|
1609
1615
|
def _process_errored(self) -> None:
|
|
1610
1616
|
# Recheck all torrents marked for rechecking.
|
|
@@ -1750,11 +1756,8 @@ class Arr:
|
|
|
1750
1756
|
try:
|
|
1751
1757
|
commands = self.client.get_command()
|
|
1752
1758
|
for command in commands:
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
and command["status"] != "completed"
|
|
1756
|
-
and "Missing" not in command["name"]
|
|
1757
|
-
):
|
|
1759
|
+
# Count all active search commands (including MissingEpisodeSearch)
|
|
1760
|
+
if command["name"].endswith("Search") and command["status"] != "completed":
|
|
1758
1761
|
search_commands = search_commands + 1
|
|
1759
1762
|
break
|
|
1760
1763
|
except (
|
|
@@ -3146,6 +3149,7 @@ class Arr:
|
|
|
3146
3149
|
CustomFormatScore=customFormat,
|
|
3147
3150
|
CustomFormatMet=customFormatMet,
|
|
3148
3151
|
Reason=reason,
|
|
3152
|
+
ArrInstance=self._name,
|
|
3149
3153
|
).on_conflict(conflict_target=[self.model_file.EntryId], update=to_update)
|
|
3150
3154
|
db_commands.execute()
|
|
3151
3155
|
else:
|
|
@@ -3325,6 +3329,7 @@ class Arr:
|
|
|
3325
3329
|
MinCustomFormatScore=minCustomFormat,
|
|
3326
3330
|
QualityProfileId=quality_profile_id,
|
|
3327
3331
|
QualityProfileName=qualityProfileName,
|
|
3332
|
+
ArrInstance=self._name,
|
|
3328
3333
|
).on_conflict(
|
|
3329
3334
|
conflict_target=[self.series_file_model.EntryId], update=to_update
|
|
3330
3335
|
)
|
|
@@ -3565,6 +3570,7 @@ class Arr:
|
|
|
3565
3570
|
Reason=reason,
|
|
3566
3571
|
QualityProfileId=qualityProfileId,
|
|
3567
3572
|
QualityProfileName=qualityProfileName,
|
|
3573
|
+
ArrInstance=self._name,
|
|
3568
3574
|
).on_conflict(conflict_target=[self.model_file.EntryId], update=to_update)
|
|
3569
3575
|
db_commands.execute()
|
|
3570
3576
|
else:
|
|
@@ -3866,6 +3872,7 @@ class Arr:
|
|
|
3866
3872
|
Reason=reason,
|
|
3867
3873
|
QualityProfileId=qualityProfileId,
|
|
3868
3874
|
QualityProfileName=qualityProfileName,
|
|
3875
|
+
ArrInstance=self._name,
|
|
3869
3876
|
).on_conflict(conflict_target=[self.model_file.EntryId], update=to_update)
|
|
3870
3877
|
db_commands.execute()
|
|
3871
3878
|
|
|
@@ -3902,6 +3909,7 @@ class Arr:
|
|
|
3902
3909
|
HasFile=track.get("hasFile", False),
|
|
3903
3910
|
TrackFileId=track.get("trackFileId", 0),
|
|
3904
3911
|
Monitored=track_monitored,
|
|
3912
|
+
ArrInstance=self._name,
|
|
3905
3913
|
).execute()
|
|
3906
3914
|
track_insert_count += 1
|
|
3907
3915
|
|
|
@@ -4045,6 +4053,7 @@ class Arr:
|
|
|
4045
4053
|
Monitored=Monitored,
|
|
4046
4054
|
Upgrade=False,
|
|
4047
4055
|
MinCustomFormatScore=minCustomFormat,
|
|
4056
|
+
ArrInstance=self._name,
|
|
4048
4057
|
).on_conflict(
|
|
4049
4058
|
conflict_target=[self.artists_file_model.EntryId], update=to_update
|
|
4050
4059
|
)
|
|
@@ -4342,10 +4351,10 @@ class Arr:
|
|
|
4342
4351
|
)
|
|
4343
4352
|
return False
|
|
4344
4353
|
self.persistent_queue.insert(
|
|
4345
|
-
EntryId=file_model.EntryId
|
|
4354
|
+
EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4346
4355
|
).on_conflict_ignore().execute()
|
|
4347
4356
|
self.model_queue.insert(
|
|
4348
|
-
Completed=False, EntryId=file_model.EntryId
|
|
4357
|
+
Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4349
4358
|
).on_conflict_replace().execute()
|
|
4350
4359
|
if file_model.EntryId not in self.queue_file_ids:
|
|
4351
4360
|
while True:
|
|
@@ -4414,10 +4423,10 @@ class Arr:
|
|
|
4414
4423
|
)
|
|
4415
4424
|
return False
|
|
4416
4425
|
self.persistent_queue.insert(
|
|
4417
|
-
EntryId=file_model.EntryId
|
|
4426
|
+
EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4418
4427
|
).on_conflict_ignore().execute()
|
|
4419
4428
|
self.model_queue.insert(
|
|
4420
|
-
Completed=False, EntryId=file_model.EntryId
|
|
4429
|
+
Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4421
4430
|
).on_conflict_replace().execute()
|
|
4422
4431
|
while True:
|
|
4423
4432
|
try:
|
|
@@ -4485,10 +4494,12 @@ class Arr:
|
|
|
4485
4494
|
file_model.EntryId,
|
|
4486
4495
|
)
|
|
4487
4496
|
return False
|
|
4488
|
-
self.persistent_queue.insert(
|
|
4497
|
+
self.persistent_queue.insert(
|
|
4498
|
+
EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4499
|
+
).on_conflict_ignore().execute()
|
|
4489
4500
|
|
|
4490
4501
|
self.model_queue.insert(
|
|
4491
|
-
Completed=False, EntryId=file_model.EntryId
|
|
4502
|
+
Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4492
4503
|
).on_conflict_replace().execute()
|
|
4493
4504
|
if file_model.EntryId:
|
|
4494
4505
|
while True:
|
|
@@ -4569,10 +4580,12 @@ class Arr:
|
|
|
4569
4580
|
file_model.EntryId,
|
|
4570
4581
|
)
|
|
4571
4582
|
return False
|
|
4572
|
-
self.persistent_queue.insert(
|
|
4583
|
+
self.persistent_queue.insert(
|
|
4584
|
+
EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4585
|
+
).on_conflict_ignore().execute()
|
|
4573
4586
|
|
|
4574
4587
|
self.model_queue.insert(
|
|
4575
|
-
Completed=False, EntryId=file_model.EntryId
|
|
4588
|
+
Completed=False, EntryId=file_model.EntryId, ArrInstance=self._name
|
|
4576
4589
|
).on_conflict_replace().execute()
|
|
4577
4590
|
if file_model.EntryId:
|
|
4578
4591
|
while True:
|
qBitrr/bundled_data.py
CHANGED
qBitrr/database.py
CHANGED
|
@@ -74,6 +74,77 @@ def get_database() -> SqliteDatabase:
|
|
|
74
74
|
# Create all tables
|
|
75
75
|
_db.create_tables(models, safe=True)
|
|
76
76
|
|
|
77
|
+
# Run migrations
|
|
78
|
+
_migrate_arrinstance_field(models)
|
|
79
|
+
_create_arrinstance_indexes(_db, models)
|
|
80
|
+
|
|
77
81
|
logger.info("Initialized single database: %s", db_path)
|
|
78
82
|
|
|
79
83
|
return _db
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _migrate_arrinstance_field(models: list) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Migration: Remove records with empty ArrInstance field.
|
|
89
|
+
|
|
90
|
+
After database consolidation, old records don't have ArrInstance set.
|
|
91
|
+
Since we can't reliably determine which instance they belong to,
|
|
92
|
+
we delete them and let the application repopulate with correct values.
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
deleted_count = 0
|
|
96
|
+
for model in models:
|
|
97
|
+
# Check if model has ArrInstance field
|
|
98
|
+
if hasattr(model, "ArrInstance"):
|
|
99
|
+
# Delete records where ArrInstance is NULL or empty string
|
|
100
|
+
query = model.delete().where(
|
|
101
|
+
(model.ArrInstance.is_null()) | (model.ArrInstance == "")
|
|
102
|
+
)
|
|
103
|
+
count = query.execute()
|
|
104
|
+
if count > 0:
|
|
105
|
+
logger.info(
|
|
106
|
+
"Migrated %s: deleted %d records with empty ArrInstance",
|
|
107
|
+
model.__name__,
|
|
108
|
+
count,
|
|
109
|
+
)
|
|
110
|
+
deleted_count += count
|
|
111
|
+
|
|
112
|
+
if deleted_count > 0:
|
|
113
|
+
logger.warning(
|
|
114
|
+
"Database migration: Removed %d old records without ArrInstance. "
|
|
115
|
+
"qBitrr will repopulate data from Arr instances.",
|
|
116
|
+
deleted_count,
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error("Error during ArrInstance migration: %s", e)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _create_arrinstance_indexes(db: SqliteDatabase, models: list) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Create database indexes on ArrInstance field for performance.
|
|
125
|
+
|
|
126
|
+
Indexes improve query performance when filtering by ArrInstance,
|
|
127
|
+
which is done on every WebUI page load.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
cursor = db.cursor()
|
|
131
|
+
for model in models:
|
|
132
|
+
if hasattr(model, "ArrInstance"):
|
|
133
|
+
table_name = model._meta.table_name
|
|
134
|
+
index_name = f"idx_arrinstance_{table_name}"
|
|
135
|
+
|
|
136
|
+
# Check if index already exists
|
|
137
|
+
cursor.execute(
|
|
138
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
|
139
|
+
(index_name,),
|
|
140
|
+
)
|
|
141
|
+
if cursor.fetchone():
|
|
142
|
+
continue # Index already exists
|
|
143
|
+
|
|
144
|
+
# Create index
|
|
145
|
+
cursor.execute(f"CREATE INDEX {index_name} ON {table_name}(ArrInstance)")
|
|
146
|
+
logger.info("Created index: %s on %s.ArrInstance", index_name, table_name)
|
|
147
|
+
|
|
148
|
+
db.commit()
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error("Error creating ArrInstance indexes: %s", e)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e,I as P,C as V,u as W,g as J,a as Q,r as K,b as Y,c as Z,R as M}from"./app.js";import{r as i}from"./table.js";import{u as ee}from"./useInterval.js";import"./vendor.js";function se({title:r,message:t,confirmLabel:p="Confirm",cancelLabel:f="Cancel",onConfirm:y,onCancel:x,danger:_=!1}){return e.jsx("div",{className:"modal-backdrop",onClick:x,children:e.jsxs("div",{className:"modal",style:{maxWidth:"500px"},onClick:b=>b.stopPropagation(),children:[e.jsxs("div",{className:"modal-header",children:[e.jsx("h2",{children:r}),e.jsx("button",{className:"btn ghost",onClick:x,children:e.jsx(P,{src:V})})]}),e.jsx("div",{className:"modal-body",children:e.jsx("p",{style:{margin:0,lineHeight:1.6},children:t})}),e.jsxs("div",{className:"modal-footer",children:[e.jsx("button",{className:"btn ghost",onClick:x,children:f}),e.jsx("button",{className:`btn ${_?"danger":"primary"}`,onClick:y,children:p})]})]})})}const re="/static/assets/build.svg",te=/\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\b/i,ae=/\bS\d{1,3}E\d{1,3}\b/i,ne=/\bSeason\s+\d+\b/i;function ce(r){const t=r.trim();if(!t
|
|
1
|
+
import{j as e,I as P,C as V,u as W,g as J,a as Q,r as K,b as Y,c as Z,R as M}from"./app.js";import{r as i}from"./table.js";import{u as ee}from"./useInterval.js";import"./vendor.js";function se({title:r,message:t,confirmLabel:p="Confirm",cancelLabel:f="Cancel",onConfirm:y,onCancel:x,danger:_=!1}){return e.jsx("div",{className:"modal-backdrop",onClick:x,children:e.jsxs("div",{className:"modal",style:{maxWidth:"500px"},onClick:b=>b.stopPropagation(),children:[e.jsxs("div",{className:"modal-header",children:[e.jsx("h2",{children:r}),e.jsx("button",{className:"btn ghost",onClick:x,children:e.jsx(P,{src:V})})]}),e.jsx("div",{className:"modal-body",children:e.jsx("p",{style:{margin:0,lineHeight:1.6},children:t})}),e.jsxs("div",{className:"modal-footer",children:[e.jsx("button",{className:"btn ghost",onClick:x,children:f}),e.jsx("button",{className:`btn ${_?"danger":"primary"}`,onClick:y,children:p})]})]})})}const re="/static/assets/build.svg",te=/\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\b/i,ae=/\bS\d{1,3}E\d{1,3}\b/i,ne=/\bSeason\s+\d+\b/i;function ce(r){const t=r.trim();if(!t)return"";if(/^\d+\s+queued item/i.test(t))return t;const p=t.replace(/\s+/g," "),f=p.match(/^(?<title>.+?)\s+(?<year>(?:19|20)\d{2})(?:\s+(?<rest>.*))?$/);if(f){const y=f.groups?.rest??"",x=ae.test(y)||ne.test(y);if(y&&!x&&te.test(y)){const b=(f.groups?.title??"").replace(/[-_.]/g," ").replace(/\s{2,}/g," ").trim(),R=f.groups?.year??"";if(b)return R?`${b} (${R})`:b}}return p}function oe(r,t){return r.category===t.category&&r.name===t.name&&r.kind===t.kind&&r.pid===t.pid&&r.alive===t.alive&&(r.rebuilding??!1)===(t.rebuilding??!1)&&(r.searchSummary??"")===(t.searchSummary??"")&&(r.searchTimestamp??"")===(t.searchTimestamp??"")&&(r.queueCount??null)===(t.queueCount??null)&&(r.categoryCount??null)===(t.categoryCount??null)&&(r.metricType??"")===(t.metricType??"")}function le(r,t){if(r===t)return!0;if(r.length!==t.length)return!1;for(let p=0;p<r.length;p+=1)if(!oe(r[p],t[p]))return!1;return!0}function ie(r,t){return r?1e3:null}function ge({active:r}){const[t,p]=i.useState([]),[f,y]=i.useState(!1),[x,_]=i.useState(!1),[b,R]=i.useState(!1),[I,D]=i.useState(null),[S,E]=i.useState(null),{push:u}=W(),F=i.useRef(!1),m=i.useCallback(async(a=!0)=>{if(!F.current){F.current=!0,a&&y(!0);try{const[c,g]=await Promise.all([J(),Q()]),j=(c.processes??[]).map(d=>{if(typeof d.searchSummary=="string"){const N=ce(d.searchSummary);return{...d,searchSummary:N}}return d});p(d=>le(d,j)?d:j),D(g)}catch(c){u(c instanceof Error?c.message:"Failed to load processes list","error")}finally{F.current=!1,a&&y(!1)}}},[u]);i.useEffect(()=>{m()},[m]),i.useEffect(()=>{r&&m()},[r,m]);const G=i.useMemo(()=>ie(r),[r,t]);ee(()=>{m(!1)},G);const z=i.useCallback(async(a,c)=>{try{await K(a,c),u(`Restarted ${a}:${c}`,"success"),m()}catch(g){u(g instanceof Error?g.message:`Failed to restart ${a}:${c}`,"error")}},[m,u]),B=i.useCallback(async()=>{E({title:"Restart All Processes",message:"Are you sure you want to restart all processes? This will temporarily interrupt all operations.",onConfirm:async()=>{E(null),_(!0);try{await Y(),u("Restarted all processes","success"),m()}catch(a){u(a instanceof Error?a.message:"Failed to restart all","error")}finally{_(!1)}}})},[m,u]),X=i.useCallback(async()=>{E({title:"Rebuild Arrs",message:"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.",onConfirm:async()=>{E(null),R(!0);try{await Z(),u("Requested Arr rebuild","success"),m()}catch(a){u(a instanceof Error?a.message:"Failed to rebuild Arrs","error")}finally{R(!1)}}})},[m,u]),H=i.useMemo(()=>{const a=new Map,c=n=>{const o=(n.category??"").toLowerCase(),l=(n.name??"").toLowerCase();return o.includes("radarr")||l.includes("radarr")?"Radarr":o.includes("sonarr")||l.includes("sonarr")?"Sonarr":o.includes("lidarr")||l.includes("lidarr")?"Lidarr":o.includes("qbit")||o.includes("qbittorrent")||l.includes("qbit")||l.includes("qbittorrent")?"qBittorrent":"Other"},g=I?.arrs??[],j=g.some(n=>n.type==="radarr"),d=g.some(n=>n.type==="sonarr"),N=g.some(n=>n.type==="lidarr");t.forEach(n=>{const o=c(n);if(o==="Radarr"&&!j||o==="Sonarr"&&!d||o==="Lidarr"&&!N)return;a.has(o)||a.set(o,new Map);const l=a.get(o),h=n.name||n.category||`${n.category}:${n.kind}`;l.has(h)||l.set(h,[]),l.get(h).push(n)});const v=["Radarr","Sonarr","Lidarr","qBittorrent","Other"],k=Array.from(a.entries()).map(([n,o])=>{const l=Array.from(o.entries()).map(([h,C])=>({name:h,items:C.sort((w,T)=>w.kind.localeCompare(T.kind))})).sort((h,C)=>h.name.localeCompare(C.name));return{app:n,instances:l}}).filter(n=>n.instances.length);return k.sort((n,o)=>{const l=h=>{const C=v.indexOf(h);return C===-1?Number.MAX_SAFE_INTEGER:C};return l(n.app)-l(o.app)||n.app.localeCompare(o.app)}),k},[t,I]),U=i.useCallback(async a=>{try{await Promise.all(a.map(c=>K(c.category,c.kind))),u(`Restarted ${a[0]?.name??"group"}`,"success"),m()}catch(c){u(c instanceof Error?c.message:"Failed to restart process group","error")}},[m,u]),O=H.map(({app:a,instances:c})=>{const g=c.map(({name:j,items:d})=>{const N=d.filter(s=>s.alive).length,v=d.length,k=v===0?"":N===v?"status-indicator--ok":N===0?"status-indicator--bad":"",n=["status-indicator"];k&&n.push(k);const o=v===0?"No processes":N===v?"All running":N===0?"Stopped":`${N}/${v} running`,l=v===1?"1 process":`${v} processes`,h=j==="FreeSpaceManager"?"Free Space Manager":j,w=Array.from(new Set(d.map(s=>s.kind))).filter(s=>{const A=s.toLowerCase();return A!=="search"&&A!=="torrent"}),T=s=>s&&s.charAt(0).toUpperCase()+s.slice(1);return e.jsxs("div",{className:"process-card",children:[e.jsxs("div",{className:"process-card__header",children:[e.jsxs("div",{className:"process-card__title",children:[e.jsx("div",{className:"process-card__name",children:h}),e.jsx("div",{className:"process-card__summary",children:l}),w.length?e.jsx("div",{className:"process-card__badges",children:w.map(s=>e.jsx("span",{className:"process-card__badge",children:T(s)},`${j}:${s}:badge`))}):null]}),e.jsx("div",{className:n.join(" "),title:o})]}),e.jsx("div",{className:"process-card__list",children:d.map(s=>e.jsxs("div",{className:"process-chip",children:[e.jsxs("div",{className:"process-chip__top",children:[e.jsx("div",{className:"process-chip__name",children:T(s.kind)}),e.jsx("div",{className:`status-pill__dot ${s.alive?"text-success":"text-danger"}`})]}),e.jsx("div",{className:"process-chip__detail",children:(()=>{if(s.rebuilding)return"Rebuilding";const A=s.kind.toLowerCase();if(A==="search")return(s.searchSummary??"")||"No searches recorded";if(A==="torrent"){const $=s.metricType?.toLowerCase(),L=typeof s.categoryCount=="number"?s.categoryCount:null,q=typeof s.queueCount=="number"?s.queueCount:null;return $?$==="category"&&L!==null?`Torrent count ${L}`:$==="free-space"&&q!==null?`Torrent count ${q}`:"Torrent count unavailable":`Torrents in queue ${q!==null?q:"?"} / total ${L!==null?L:"?"}`}return""})()}),e.jsx("div",{className:"process-chip__actions",children:e.jsx("button",{className:"btn small",onClick:()=>z(s.category,s.kind),children:"Restart"})})]},`${s.category}:${s.kind}`))}),e.jsx("div",{className:"process-card__footer",children:e.jsx("button",{className:"btn small outline",onClick:()=>{U(d)},children:"Restart All"})})]},j)});return{app:a,cards:g}});return e.jsxs(e.Fragment,{children:[e.jsxs("section",{className:"card",children:[e.jsx("div",{className:"card-header",children:"Processes"}),e.jsxs("div",{className:"card-body stack",children:[e.jsx("div",{className:"row",children:e.jsxs("div",{className:"col inline",children:[e.jsxs("button",{className:"btn ghost",onClick:()=>{m()},disabled:f,children:[f&&e.jsx("span",{className:"spinner"}),e.jsx(P,{src:M}),f?"Refreshing...":"Refresh"]}),e.jsxs("button",{className:"btn",onClick:()=>{B()},disabled:x,children:[x&&e.jsx("span",{className:"spinner"}),e.jsx(P,{src:M}),x?"Restarting...":"Restart All"]}),e.jsxs("button",{className:"btn",onClick:()=>{X()},disabled:b,children:[b&&e.jsx("span",{className:"spinner"}),e.jsx(P,{src:re}),b?"Rebuilding...":"Rebuild Arrs"]})]})}),O.length?O.map(({app:a,cards:c})=>e.jsxs("div",{className:"process-section",children:[e.jsx("div",{className:"process-section__title",children:a}),e.jsx("div",{className:"process-grid",children:c})]},a)):e.jsx("div",{className:"empty-state",children:"No processes available."})]})]}),S&&e.jsx(se,{title:S.title,message:S.message,confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!0,onConfirm:S.onConfirm,onCancel:()=>E(null)})]})}export{ge as ProcessesView};
|
|
2
2
|
//# sourceMappingURL=ProcessesView.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProcessesView.js","sources":["../../../webui/src/components/ConfirmDialog.tsx","../../../webui/src/icons/build.svg","../../../webui/src/pages/ProcessesView.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { IconImage } from \"./IconImage\";\nimport CloseIcon from \"../icons/close.svg\";\n\ninterface ConfirmDialogProps {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n onConfirm: () => void;\n onCancel: () => void;\n danger?: boolean;\n}\n\nexport function ConfirmDialog({\n title,\n message,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n danger = false,\n}: ConfirmDialogProps): JSX.Element {\n return (\n <div className=\"modal-backdrop\" onClick={onCancel}>\n <div\n className=\"modal\"\n style={{ maxWidth: '500px' }}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"modal-header\">\n <h2>{title}</h2>\n <button className=\"btn ghost\" onClick={onCancel}>\n <IconImage src={CloseIcon} />\n </button>\n </div>\n <div className=\"modal-body\">\n <p style={{ margin: 0, lineHeight: 1.6 }}>{message}</p>\n </div>\n <div className=\"modal-footer\">\n <button className=\"btn ghost\" onClick={onCancel}>\n {cancelLabel}\n </button>\n <button\n className={`btn ${danger ? 'danger' : 'primary'}`}\n onClick={onConfirm}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </div>\n );\n}\n","export default \"__VITE_ASSET__DznMzWc1__\"","import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport {\n getProcesses,\n getStatus,\n rebuildArrs,\n restartAllProcesses,\n restartProcess,\n} from \"../api/client\";\nimport type { ProcessInfo, StatusResponse } from \"../api/types\";\nimport { useToast } from \"../context/ToastContext\";\nimport { useInterval } from \"../hooks/useInterval\";\nimport { IconImage } from \"../components/IconImage\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\n\nimport RefreshIcon from \"../icons/refresh-arrow.svg\";\nimport RestartIcon from \"../icons/refresh-arrow.svg\";\nimport ToolsIcon from \"../icons/build.svg\";\n\nconst RELEASE_TOKEN_REGEX =\n /\\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\\b/i;\nconst EPISODE_TOKEN_REGEX = /\\bS\\d{1,3}E\\d{1,3}\\b/i;\nconst SEASON_TOKEN_REGEX = /\\bSeason\\s+\\d+\\b/i;\n\nfunction sanitizeSearchSummary(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return \"\";\n if (/^\\d+\\s+queued item/i.test(trimmed)) return \"\";\n const normalized = trimmed.replace(/\\s+/g, \" \");\n const releaseMatch = normalized.match(\n /^(?<title>.+?)\\s+(?<year>(?:19|20)\\d{2})(?:\\s+(?<rest>.*))?$/\n );\n\n if (releaseMatch) {\n const rest = releaseMatch.groups?.rest ?? \"\";\n const looksLikeEpisode =\n EPISODE_TOKEN_REGEX.test(rest) || SEASON_TOKEN_REGEX.test(rest);\n if (rest && !looksLikeEpisode && RELEASE_TOKEN_REGEX.test(rest)) {\n const rawTitle = releaseMatch.groups?.title ?? \"\";\n const cleanedTitle = rawTitle\n .replace(/[-_.]/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n const year = releaseMatch.groups?.year ?? \"\";\n if (cleanedTitle) {\n return year ? `${cleanedTitle} (${year})` : cleanedTitle;\n }\n }\n }\n\n return normalized;\n}\n\nfunction isProcessEqual(a: ProcessInfo, b: ProcessInfo): boolean {\n return (\n a.category === b.category &&\n a.name === b.name &&\n a.kind === b.kind &&\n a.pid === b.pid &&\n a.alive === b.alive &&\n (a.rebuilding ?? false) === (b.rebuilding ?? false) &&\n (a.searchSummary ?? \"\") === (b.searchSummary ?? \"\") &&\n (a.searchTimestamp ?? \"\") === (b.searchTimestamp ?? \"\") &&\n (a.queueCount ?? null) === (b.queueCount ?? null) &&\n (a.categoryCount ?? null) === (b.categoryCount ?? null) &&\n (a.metricType ?? \"\") === (b.metricType ?? \"\")\n );\n}\n\nfunction areProcessListsEqual(a: ProcessInfo[], b: ProcessInfo[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (!isProcessEqual(a[index], b[index])) {\n return false;\n }\n }\n return true;\n}\n\nfunction getRefreshDelay(active: boolean, processes: ProcessInfo[]): number | null {\n if (!active) return null;\n // Refresh every 1 second when active\n return 1000;\n}\n\ninterface ProcessesViewProps {\n active: boolean;\n}\n\nexport function ProcessesView({ active }: ProcessesViewProps): JSX.Element {\n const [processes, setProcesses] = useState<ProcessInfo[]>([]);\n const [loading, setLoading] = useState(false);\n const [restartingAll, setRestartingAll] = useState(false);\n const [rebuildingArrs, setRebuildingArrs] = useState(false);\n const [statusData, setStatusData] = useState<StatusResponse | null>(null);\n const [confirmAction, setConfirmAction] = useState<{\n title: string;\n message: string;\n onConfirm: () => void;\n } | null>(null);\n const { push } = useToast();\n const isFetching = useRef(false);\n\n const load = useCallback(async (showLoading = true) => {\n if (isFetching.current) {\n return;\n }\n isFetching.current = true;\n if (showLoading) {\n setLoading(true);\n }\n try {\n const [processData, status] = await Promise.all([\n getProcesses(),\n getStatus(),\n ]);\n const next = (processData.processes ?? []).map((process) => {\n if (typeof process.searchSummary === \"string\") {\n const sanitized = sanitizeSearchSummary(process.searchSummary);\n return {\n ...process,\n searchSummary: sanitized,\n };\n }\n return process;\n });\n setProcesses((prev) =>\n areProcessListsEqual(prev, next) ? prev : next\n );\n setStatusData(status);\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to load processes list\",\n \"error\"\n );\n } finally {\n isFetching.current = false;\n if (showLoading) {\n setLoading(false);\n }\n }\n }, [push]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (active) {\n void load();\n }\n }, [active, load]);\n\n const refreshDelay = useMemo(\n () => getRefreshDelay(active, processes),\n [active, processes]\n );\n\n useInterval(() => {\n void load(false); // Auto-refresh without showing loading spinner\n }, refreshDelay);\n\n const handleRestart = useCallback(\n async (category: string, kind: string) => {\n try {\n await restartProcess(category, kind);\n push(`Restarted ${category}:${kind}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : `Failed to restart ${category}:${kind}`,\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const handleRestartAll = useCallback(async () => {\n setConfirmAction({\n title: \"Restart All Processes\",\n message: \"Are you sure you want to restart all processes? This will temporarily interrupt all operations.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRestartingAll(true);\n try {\n await restartAllProcesses();\n push(\"Restarted all processes\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to restart all\",\n \"error\"\n );\n } finally {\n setRestartingAll(false);\n }\n }\n });\n }, [load, push]);\n\n const handleRebuildArrs = useCallback(async () => {\n setConfirmAction({\n title: \"Rebuild Arrs\",\n message: \"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRebuildingArrs(true);\n try {\n await rebuildArrs();\n push(\"Requested Arr rebuild\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to rebuild Arrs\",\n \"error\"\n );\n } finally {\n setRebuildingArrs(false);\n }\n }\n });\n }, [load, push]);\n\n const groupedProcesses = useMemo(() => {\n interface Instance {\n name: string;\n items: ProcessInfo[];\n }\n interface AppGroup {\n app: string;\n instances: Instance[];\n }\n const appBuckets = new Map<string, Map<string, ProcessInfo[]>>();\n\n const classifyApp = (proc: ProcessInfo): string => {\n const category = (proc.category ?? \"\").toLowerCase();\n const name = (proc.name ?? \"\").toLowerCase();\n if (category.includes(\"radarr\") || name.includes(\"radarr\")) return \"Radarr\";\n if (category.includes(\"sonarr\") || name.includes(\"sonarr\")) return \"Sonarr\";\n if (category.includes(\"lidarr\") || name.includes(\"lidarr\")) return \"Lidarr\";\n if (\n category.includes(\"qbit\") ||\n category.includes(\"qbittorrent\") ||\n name.includes(\"qbit\") ||\n name.includes(\"qbittorrent\")\n ) {\n return \"qBittorrent\";\n }\n return \"Other\";\n };\n\n // Check which Arr types are configured\n const arrs = statusData?.arrs ?? [];\n const hasRadarr = arrs.some((arr) => arr.type === \"radarr\");\n const hasSonarr = arrs.some((arr) => arr.type === \"sonarr\");\n const hasLidarr = arrs.some((arr) => arr.type === \"lidarr\");\n\n processes.forEach((proc) => {\n const app = classifyApp(proc);\n\n // Skip Arr processes if that Arr type is not configured\n if (app === \"Radarr\" && !hasRadarr) return;\n if (app === \"Sonarr\" && !hasSonarr) return;\n if (app === \"Lidarr\" && !hasLidarr) return;\n\n if (!appBuckets.has(app)) appBuckets.set(app, new Map());\n const instances = appBuckets.get(app)!;\n const instanceKey =\n proc.name || proc.category || `${proc.category}:${proc.kind}`;\n if (!instances.has(instanceKey)) instances.set(instanceKey, []);\n instances.get(instanceKey)!.push(proc);\n });\n\n const appOrder = [\"Radarr\", \"Sonarr\", \"Lidarr\", \"qBittorrent\", \"Other\"];\n\n const result: AppGroup[] = Array.from(appBuckets.entries())\n .map(([app, instances]) => {\n const sortedInstances = Array.from(instances.entries())\n .map(([name, items]) => ({\n name,\n items: items.sort((a, b) => a.kind.localeCompare(b.kind)),\n }))\n .sort((a, b) => a.name.localeCompare(b.name));\n return { app, instances: sortedInstances };\n })\n .filter((group) => group.instances.length);\n\n result.sort((a, b) => {\n const order = (label: string) => {\n const index = appOrder.indexOf(label);\n return index === -1 ? Number.MAX_SAFE_INTEGER : index;\n };\n return order(a.app) - order(b.app) || a.app.localeCompare(b.app);\n });\n\n return result;\n }, [processes, statusData]);\n\n const handleRestartGroup = useCallback(\n async (items: ProcessInfo[]) => {\n try {\n await Promise.all(\n items.map((item) => restartProcess(item.category, item.kind))\n );\n push(`Restarted ${items[0]?.name ?? \"group\"}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to restart process group\",\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const cardsByApp = groupedProcesses.map(({ app, instances }) => {\n const cards = instances.map(({ name, items }) => {\n const runningCount = items.filter((item) => item.alive).length;\n const totalCount = items.length;\n const tone =\n totalCount === 0\n ? \"\"\n : runningCount === totalCount\n ? \"status-indicator--ok\"\n : runningCount === 0\n ? \"status-indicator--bad\"\n : \"\";\n const statusClass = [\"status-indicator\"];\n if (tone) statusClass.push(tone);\n const statusLabel =\n totalCount === 0\n ? \"No processes\"\n : runningCount === totalCount\n ? \"All running\"\n : runningCount === 0\n ? \"Stopped\"\n : `${runningCount}/${totalCount} running`;\n const summaryLabel = totalCount === 1 ? \"1 process\" : `${totalCount} processes`;\n const displayName = name === \"FreeSpaceManager\" ? \"Free Space Manager\" : name;\n const uniqueKinds = Array.from(new Set(items.map((item) => item.kind)));\n const filteredKinds = uniqueKinds.filter((kind) => {\n const lower = kind.toLowerCase();\n return lower !== \"search\" && lower !== \"torrent\";\n });\n const formatKind = (kind: string) =>\n kind ? kind.charAt(0).toUpperCase() + kind.slice(1) : kind;\n\n return (\n <div className=\"process-card\" key={name}>\n <div className=\"process-card__header\">\n <div className=\"process-card__title\">\n <div className=\"process-card__name\">{displayName}</div>\n <div className=\"process-card__summary\">{summaryLabel}</div>\n {filteredKinds.length ? (\n <div className=\"process-card__badges\">\n {filteredKinds.map((kind) => (\n <span key={`${name}:${kind}:badge`} className=\"process-card__badge\">\n {formatKind(kind)}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n <div className={statusClass.join(\" \")} title={statusLabel} />\n </div>\n <div className=\"process-card__list\">\n {items.map((item) => (\n <div className=\"process-chip\" key={`${item.category}:${item.kind}`}>\n <div className=\"process-chip__top\">\n <div className=\"process-chip__name\">{formatKind(item.kind)}</div>\n <div className={`status-pill__dot ${item.alive ? \"text-success\" : \"text-danger\"}`} />\n </div>\n <div className=\"process-chip__detail\">\n {(() => {\n if (item.rebuilding) {\n return \"Rebuilding\";\n }\n const kindLower = item.kind.toLowerCase();\n if (kindLower === \"search\") {\n const summary = item.searchSummary ?? \"\";\n return summary || \"No searches recorded\";\n }\n if (kindLower === \"torrent\") {\n const metricType = item.metricType?.toLowerCase();\n const categoryTotal =\n typeof item.categoryCount === \"number\" ? item.categoryCount : null;\n const queueTotal =\n typeof item.queueCount === \"number\" ? item.queueCount : null;\n\n if (!metricType) {\n const queueLabel = queueTotal !== null ? queueTotal : \"?\";\n const categoryLabel = categoryTotal !== null ? categoryTotal : \"?\";\n return `Torrents in queue ${queueLabel} / total ${categoryLabel}`;\n }\n\n if (metricType === \"category\" && categoryTotal !== null) {\n return `Torrent count ${categoryTotal}`;\n }\n\n if (metricType === \"free-space\" && queueTotal !== null) {\n return `Torrent count ${queueTotal}`;\n }\n\n return \"Torrent count unavailable\";\n }\n return \"\";\n })()}\n </div>\n <div className=\"process-chip__actions\">\n <button\n className=\"btn small\"\n onClick={() => handleRestart(item.category, item.kind)}\n >\n Restart\n </button>\n </div>\n </div>\n ))}\n </div>\n <div className=\"process-card__footer\">\n <button\n className=\"btn small outline\"\n onClick={() => void handleRestartGroup(items)}\n >\n Restart All\n </button>\n </div>\n </div>\n );\n });\n return { app, cards };\n });\n\n return (\n <>\n <section className=\"card\">\n <div className=\"card-header\">Processes</div>\n <div className=\"card-body stack\">\n <div className=\"row\">\n <div className=\"col inline\">\n <button className=\"btn ghost\" onClick={() => void load()} disabled={loading}>\n {loading && <span className=\"spinner\" />}\n <IconImage src={RefreshIcon} />\n {loading ? 'Refreshing...' : 'Refresh'}\n </button>\n <button className=\"btn\" onClick={() => void handleRestartAll()} disabled={restartingAll}>\n {restartingAll && <span className=\"spinner\" />}\n <IconImage src={RestartIcon} />\n {restartingAll ? 'Restarting...' : 'Restart All'}\n </button>\n <button className=\"btn\" onClick={() => void handleRebuildArrs()} disabled={rebuildingArrs}>\n {rebuildingArrs && <span className=\"spinner\" />}\n <IconImage src={ToolsIcon} />\n {rebuildingArrs ? 'Rebuilding...' : 'Rebuild Arrs'}\n </button>\n </div>\n </div>\n {cardsByApp.length ? (\n cardsByApp.map(({ app, cards }) => (\n <div className=\"process-section\" key={app}>\n <div className=\"process-section__title\">{app}</div>\n <div className=\"process-grid\">{cards}</div>\n </div>\n ))\n ) : (\n <div className=\"empty-state\">No processes available.</div>\n )}\n </div>\n </section>\n {confirmAction && (\n <ConfirmDialog\n title={confirmAction.title}\n message={confirmAction.message}\n confirmLabel=\"Confirm\"\n cancelLabel=\"Cancel\"\n danger={true}\n onConfirm={confirmAction.onConfirm}\n onCancel={() => setConfirmAction(null)}\n />\n )}\n </>\n );\n}\n"],"names":["ConfirmDialog","title","message","confirmLabel","cancelLabel","onConfirm","onCancel","danger","jsx","jsxs","e","IconImage","CloseIcon","ToolsIcon","RELEASE_TOKEN_REGEX","EPISODE_TOKEN_REGEX","SEASON_TOKEN_REGEX","sanitizeSearchSummary","raw","trimmed","normalized","releaseMatch","rest","looksLikeEpisode","cleanedTitle","year","isProcessEqual","a","b","areProcessListsEqual","index","getRefreshDelay","active","processes","ProcessesView","setProcesses","useState","loading","setLoading","restartingAll","setRestartingAll","rebuildingArrs","setRebuildingArrs","statusData","setStatusData","confirmAction","setConfirmAction","push","useToast","isFetching","useRef","load","useCallback","showLoading","processData","status","getProcesses","getStatus","next","process","sanitized","prev","error","useEffect","refreshDelay","useMemo","useInterval","handleRestart","category","kind","restartProcess","handleRestartAll","restartAllProcesses","handleRebuildArrs","rebuildArrs","groupedProcesses","appBuckets","classifyApp","proc","name","arrs","hasRadarr","arr","hasSonarr","hasLidarr","app","instances","instanceKey","appOrder","result","sortedInstances","items","group","order","label","handleRestartGroup","item","cardsByApp","cards","runningCount","totalCount","tone","statusClass","statusLabel","summaryLabel","displayName","filteredKinds","lower","formatKind","kindLower","metricType","categoryTotal","queueTotal","Fragment","RefreshIcon","RestartIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProcessesView.js","sources":["../../../webui/src/components/ConfirmDialog.tsx","../../../webui/src/icons/build.svg","../../../webui/src/pages/ProcessesView.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { IconImage } from \"./IconImage\";\nimport CloseIcon from \"../icons/close.svg\";\n\ninterface ConfirmDialogProps {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n onConfirm: () => void;\n onCancel: () => void;\n danger?: boolean;\n}\n\nexport function ConfirmDialog({\n title,\n message,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n danger = false,\n}: ConfirmDialogProps): JSX.Element {\n return (\n <div className=\"modal-backdrop\" onClick={onCancel}>\n <div\n className=\"modal\"\n style={{ maxWidth: '500px' }}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"modal-header\">\n <h2>{title}</h2>\n <button className=\"btn ghost\" onClick={onCancel}>\n <IconImage src={CloseIcon} />\n </button>\n </div>\n <div className=\"modal-body\">\n <p style={{ margin: 0, lineHeight: 1.6 }}>{message}</p>\n </div>\n <div className=\"modal-footer\">\n <button className=\"btn ghost\" onClick={onCancel}>\n {cancelLabel}\n </button>\n <button\n className={`btn ${danger ? 'danger' : 'primary'}`}\n onClick={onConfirm}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </div>\n );\n}\n","export default \"__VITE_ASSET__DznMzWc1__\"","import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport {\n getProcesses,\n getStatus,\n rebuildArrs,\n restartAllProcesses,\n restartProcess,\n} from \"../api/client\";\nimport type { ProcessInfo, StatusResponse } from \"../api/types\";\nimport { useToast } from \"../context/ToastContext\";\nimport { useInterval } from \"../hooks/useInterval\";\nimport { IconImage } from \"../components/IconImage\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\n\nimport RefreshIcon from \"../icons/refresh-arrow.svg\";\nimport RestartIcon from \"../icons/refresh-arrow.svg\";\nimport ToolsIcon from \"../icons/build.svg\";\n\nconst RELEASE_TOKEN_REGEX =\n /\\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\\b/i;\nconst EPISODE_TOKEN_REGEX = /\\bS\\d{1,3}E\\d{1,3}\\b/i;\nconst SEASON_TOKEN_REGEX = /\\bSeason\\s+\\d+\\b/i;\n\nfunction sanitizeSearchSummary(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return \"\";\n\n // Keep \"X queued items\" messages as-is (don't filter them out)\n if (/^\\d+\\s+queued item/i.test(trimmed)) {\n return trimmed;\n }\n\n const normalized = trimmed.replace(/\\s+/g, \" \");\n const releaseMatch = normalized.match(\n /^(?<title>.+?)\\s+(?<year>(?:19|20)\\d{2})(?:\\s+(?<rest>.*))?$/\n );\n\n if (releaseMatch) {\n const rest = releaseMatch.groups?.rest ?? \"\";\n const looksLikeEpisode =\n EPISODE_TOKEN_REGEX.test(rest) || SEASON_TOKEN_REGEX.test(rest);\n if (rest && !looksLikeEpisode && RELEASE_TOKEN_REGEX.test(rest)) {\n const rawTitle = releaseMatch.groups?.title ?? \"\";\n const cleanedTitle = rawTitle\n .replace(/[-_.]/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n const year = releaseMatch.groups?.year ?? \"\";\n if (cleanedTitle) {\n return year ? `${cleanedTitle} (${year})` : cleanedTitle;\n }\n }\n }\n\n return normalized;\n}\n\nfunction isProcessEqual(a: ProcessInfo, b: ProcessInfo): boolean {\n return (\n a.category === b.category &&\n a.name === b.name &&\n a.kind === b.kind &&\n a.pid === b.pid &&\n a.alive === b.alive &&\n (a.rebuilding ?? false) === (b.rebuilding ?? false) &&\n (a.searchSummary ?? \"\") === (b.searchSummary ?? \"\") &&\n (a.searchTimestamp ?? \"\") === (b.searchTimestamp ?? \"\") &&\n (a.queueCount ?? null) === (b.queueCount ?? null) &&\n (a.categoryCount ?? null) === (b.categoryCount ?? null) &&\n (a.metricType ?? \"\") === (b.metricType ?? \"\")\n );\n}\n\nfunction areProcessListsEqual(a: ProcessInfo[], b: ProcessInfo[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (!isProcessEqual(a[index], b[index])) {\n return false;\n }\n }\n return true;\n}\n\nfunction getRefreshDelay(active: boolean, processes: ProcessInfo[]): number | null {\n if (!active) return null;\n // Refresh every 1 second when active\n return 1000;\n}\n\ninterface ProcessesViewProps {\n active: boolean;\n}\n\nexport function ProcessesView({ active }: ProcessesViewProps): JSX.Element {\n const [processes, setProcesses] = useState<ProcessInfo[]>([]);\n const [loading, setLoading] = useState(false);\n const [restartingAll, setRestartingAll] = useState(false);\n const [rebuildingArrs, setRebuildingArrs] = useState(false);\n const [statusData, setStatusData] = useState<StatusResponse | null>(null);\n const [confirmAction, setConfirmAction] = useState<{\n title: string;\n message: string;\n onConfirm: () => void;\n } | null>(null);\n const { push } = useToast();\n const isFetching = useRef(false);\n\n const load = useCallback(async (showLoading = true) => {\n if (isFetching.current) {\n return;\n }\n isFetching.current = true;\n if (showLoading) {\n setLoading(true);\n }\n try {\n const [processData, status] = await Promise.all([\n getProcesses(),\n getStatus(),\n ]);\n const next = (processData.processes ?? []).map((process) => {\n if (typeof process.searchSummary === \"string\") {\n const sanitized = sanitizeSearchSummary(process.searchSummary);\n return {\n ...process,\n searchSummary: sanitized,\n };\n }\n return process;\n });\n setProcesses((prev) =>\n areProcessListsEqual(prev, next) ? prev : next\n );\n setStatusData(status);\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to load processes list\",\n \"error\"\n );\n } finally {\n isFetching.current = false;\n if (showLoading) {\n setLoading(false);\n }\n }\n }, [push]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (active) {\n void load();\n }\n }, [active, load]);\n\n const refreshDelay = useMemo(\n () => getRefreshDelay(active, processes),\n [active, processes]\n );\n\n useInterval(() => {\n void load(false); // Auto-refresh without showing loading spinner\n }, refreshDelay);\n\n const handleRestart = useCallback(\n async (category: string, kind: string) => {\n try {\n await restartProcess(category, kind);\n push(`Restarted ${category}:${kind}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : `Failed to restart ${category}:${kind}`,\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const handleRestartAll = useCallback(async () => {\n setConfirmAction({\n title: \"Restart All Processes\",\n message: \"Are you sure you want to restart all processes? This will temporarily interrupt all operations.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRestartingAll(true);\n try {\n await restartAllProcesses();\n push(\"Restarted all processes\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to restart all\",\n \"error\"\n );\n } finally {\n setRestartingAll(false);\n }\n }\n });\n }, [load, push]);\n\n const handleRebuildArrs = useCallback(async () => {\n setConfirmAction({\n title: \"Rebuild Arrs\",\n message: \"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRebuildingArrs(true);\n try {\n await rebuildArrs();\n push(\"Requested Arr rebuild\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to rebuild Arrs\",\n \"error\"\n );\n } finally {\n setRebuildingArrs(false);\n }\n }\n });\n }, [load, push]);\n\n const groupedProcesses = useMemo(() => {\n interface Instance {\n name: string;\n items: ProcessInfo[];\n }\n interface AppGroup {\n app: string;\n instances: Instance[];\n }\n const appBuckets = new Map<string, Map<string, ProcessInfo[]>>();\n\n const classifyApp = (proc: ProcessInfo): string => {\n const category = (proc.category ?? \"\").toLowerCase();\n const name = (proc.name ?? \"\").toLowerCase();\n if (category.includes(\"radarr\") || name.includes(\"radarr\")) return \"Radarr\";\n if (category.includes(\"sonarr\") || name.includes(\"sonarr\")) return \"Sonarr\";\n if (category.includes(\"lidarr\") || name.includes(\"lidarr\")) return \"Lidarr\";\n if (\n category.includes(\"qbit\") ||\n category.includes(\"qbittorrent\") ||\n name.includes(\"qbit\") ||\n name.includes(\"qbittorrent\")\n ) {\n return \"qBittorrent\";\n }\n return \"Other\";\n };\n\n // Check which Arr types are configured\n const arrs = statusData?.arrs ?? [];\n const hasRadarr = arrs.some((arr) => arr.type === \"radarr\");\n const hasSonarr = arrs.some((arr) => arr.type === \"sonarr\");\n const hasLidarr = arrs.some((arr) => arr.type === \"lidarr\");\n\n processes.forEach((proc) => {\n const app = classifyApp(proc);\n\n // Skip Arr processes if that Arr type is not configured\n if (app === \"Radarr\" && !hasRadarr) return;\n if (app === \"Sonarr\" && !hasSonarr) return;\n if (app === \"Lidarr\" && !hasLidarr) return;\n\n if (!appBuckets.has(app)) appBuckets.set(app, new Map());\n const instances = appBuckets.get(app)!;\n const instanceKey =\n proc.name || proc.category || `${proc.category}:${proc.kind}`;\n if (!instances.has(instanceKey)) instances.set(instanceKey, []);\n instances.get(instanceKey)!.push(proc);\n });\n\n const appOrder = [\"Radarr\", \"Sonarr\", \"Lidarr\", \"qBittorrent\", \"Other\"];\n\n const result: AppGroup[] = Array.from(appBuckets.entries())\n .map(([app, instances]) => {\n const sortedInstances = Array.from(instances.entries())\n .map(([name, items]) => ({\n name,\n items: items.sort((a, b) => a.kind.localeCompare(b.kind)),\n }))\n .sort((a, b) => a.name.localeCompare(b.name));\n return { app, instances: sortedInstances };\n })\n .filter((group) => group.instances.length);\n\n result.sort((a, b) => {\n const order = (label: string) => {\n const index = appOrder.indexOf(label);\n return index === -1 ? Number.MAX_SAFE_INTEGER : index;\n };\n return order(a.app) - order(b.app) || a.app.localeCompare(b.app);\n });\n\n return result;\n }, [processes, statusData]);\n\n const handleRestartGroup = useCallback(\n async (items: ProcessInfo[]) => {\n try {\n await Promise.all(\n items.map((item) => restartProcess(item.category, item.kind))\n );\n push(`Restarted ${items[0]?.name ?? \"group\"}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to restart process group\",\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const cardsByApp = groupedProcesses.map(({ app, instances }) => {\n const cards = instances.map(({ name, items }) => {\n const runningCount = items.filter((item) => item.alive).length;\n const totalCount = items.length;\n const tone =\n totalCount === 0\n ? \"\"\n : runningCount === totalCount\n ? \"status-indicator--ok\"\n : runningCount === 0\n ? \"status-indicator--bad\"\n : \"\";\n const statusClass = [\"status-indicator\"];\n if (tone) statusClass.push(tone);\n const statusLabel =\n totalCount === 0\n ? \"No processes\"\n : runningCount === totalCount\n ? \"All running\"\n : runningCount === 0\n ? \"Stopped\"\n : `${runningCount}/${totalCount} running`;\n const summaryLabel = totalCount === 1 ? \"1 process\" : `${totalCount} processes`;\n const displayName = name === \"FreeSpaceManager\" ? \"Free Space Manager\" : name;\n const uniqueKinds = Array.from(new Set(items.map((item) => item.kind)));\n const filteredKinds = uniqueKinds.filter((kind) => {\n const lower = kind.toLowerCase();\n return lower !== \"search\" && lower !== \"torrent\";\n });\n const formatKind = (kind: string) =>\n kind ? kind.charAt(0).toUpperCase() + kind.slice(1) : kind;\n\n return (\n <div className=\"process-card\" key={name}>\n <div className=\"process-card__header\">\n <div className=\"process-card__title\">\n <div className=\"process-card__name\">{displayName}</div>\n <div className=\"process-card__summary\">{summaryLabel}</div>\n {filteredKinds.length ? (\n <div className=\"process-card__badges\">\n {filteredKinds.map((kind) => (\n <span key={`${name}:${kind}:badge`} className=\"process-card__badge\">\n {formatKind(kind)}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n <div className={statusClass.join(\" \")} title={statusLabel} />\n </div>\n <div className=\"process-card__list\">\n {items.map((item) => (\n <div className=\"process-chip\" key={`${item.category}:${item.kind}`}>\n <div className=\"process-chip__top\">\n <div className=\"process-chip__name\">{formatKind(item.kind)}</div>\n <div className={`status-pill__dot ${item.alive ? \"text-success\" : \"text-danger\"}`} />\n </div>\n <div className=\"process-chip__detail\">\n {(() => {\n if (item.rebuilding) {\n return \"Rebuilding\";\n }\n const kindLower = item.kind.toLowerCase();\n if (kindLower === \"search\") {\n const summary = item.searchSummary ?? \"\";\n return summary || \"No searches recorded\";\n }\n if (kindLower === \"torrent\") {\n const metricType = item.metricType?.toLowerCase();\n const categoryTotal =\n typeof item.categoryCount === \"number\" ? item.categoryCount : null;\n const queueTotal =\n typeof item.queueCount === \"number\" ? item.queueCount : null;\n\n if (!metricType) {\n const queueLabel = queueTotal !== null ? queueTotal : \"?\";\n const categoryLabel = categoryTotal !== null ? categoryTotal : \"?\";\n return `Torrents in queue ${queueLabel} / total ${categoryLabel}`;\n }\n\n if (metricType === \"category\" && categoryTotal !== null) {\n return `Torrent count ${categoryTotal}`;\n }\n\n if (metricType === \"free-space\" && queueTotal !== null) {\n return `Torrent count ${queueTotal}`;\n }\n\n return \"Torrent count unavailable\";\n }\n return \"\";\n })()}\n </div>\n <div className=\"process-chip__actions\">\n <button\n className=\"btn small\"\n onClick={() => handleRestart(item.category, item.kind)}\n >\n Restart\n </button>\n </div>\n </div>\n ))}\n </div>\n <div className=\"process-card__footer\">\n <button\n className=\"btn small outline\"\n onClick={() => void handleRestartGroup(items)}\n >\n Restart All\n </button>\n </div>\n </div>\n );\n });\n return { app, cards };\n });\n\n return (\n <>\n <section className=\"card\">\n <div className=\"card-header\">Processes</div>\n <div className=\"card-body stack\">\n <div className=\"row\">\n <div className=\"col inline\">\n <button className=\"btn ghost\" onClick={() => void load()} disabled={loading}>\n {loading && <span className=\"spinner\" />}\n <IconImage src={RefreshIcon} />\n {loading ? 'Refreshing...' : 'Refresh'}\n </button>\n <button className=\"btn\" onClick={() => void handleRestartAll()} disabled={restartingAll}>\n {restartingAll && <span className=\"spinner\" />}\n <IconImage src={RestartIcon} />\n {restartingAll ? 'Restarting...' : 'Restart All'}\n </button>\n <button className=\"btn\" onClick={() => void handleRebuildArrs()} disabled={rebuildingArrs}>\n {rebuildingArrs && <span className=\"spinner\" />}\n <IconImage src={ToolsIcon} />\n {rebuildingArrs ? 'Rebuilding...' : 'Rebuild Arrs'}\n </button>\n </div>\n </div>\n {cardsByApp.length ? (\n cardsByApp.map(({ app, cards }) => (\n <div className=\"process-section\" key={app}>\n <div className=\"process-section__title\">{app}</div>\n <div className=\"process-grid\">{cards}</div>\n </div>\n ))\n ) : (\n <div className=\"empty-state\">No processes available.</div>\n )}\n </div>\n </section>\n {confirmAction && (\n <ConfirmDialog\n title={confirmAction.title}\n message={confirmAction.message}\n confirmLabel=\"Confirm\"\n cancelLabel=\"Cancel\"\n danger={true}\n onConfirm={confirmAction.onConfirm}\n onCancel={() => setConfirmAction(null)}\n />\n )}\n </>\n );\n}\n"],"names":["ConfirmDialog","title","message","confirmLabel","cancelLabel","onConfirm","onCancel","danger","jsx","jsxs","e","IconImage","CloseIcon","ToolsIcon","RELEASE_TOKEN_REGEX","EPISODE_TOKEN_REGEX","SEASON_TOKEN_REGEX","sanitizeSearchSummary","raw","trimmed","normalized","releaseMatch","rest","looksLikeEpisode","cleanedTitle","year","isProcessEqual","a","b","areProcessListsEqual","index","getRefreshDelay","active","processes","ProcessesView","setProcesses","useState","loading","setLoading","restartingAll","setRestartingAll","rebuildingArrs","setRebuildingArrs","statusData","setStatusData","confirmAction","setConfirmAction","push","useToast","isFetching","useRef","load","useCallback","showLoading","processData","status","getProcesses","getStatus","next","process","sanitized","prev","error","useEffect","refreshDelay","useMemo","useInterval","handleRestart","category","kind","restartProcess","handleRestartAll","restartAllProcesses","handleRebuildArrs","rebuildArrs","groupedProcesses","appBuckets","classifyApp","proc","name","arrs","hasRadarr","arr","hasSonarr","hasLidarr","app","instances","instanceKey","appOrder","result","sortedInstances","items","group","order","label","handleRestartGroup","item","cardsByApp","cards","runningCount","totalCount","tone","statusClass","statusLabel","summaryLabel","displayName","filteredKinds","lower","formatKind","kindLower","metricType","categoryTotal","queueTotal","Fragment","RefreshIcon","RestartIcon"],"mappings":"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"}
|
qBitrr/webui.py
CHANGED
|
@@ -394,20 +394,25 @@ class WebUI:
|
|
|
394
394
|
}
|
|
395
395
|
page = max(page, 0)
|
|
396
396
|
page_size = max(page_size, 1)
|
|
397
|
+
arr_instance = getattr(arr, "_name", "")
|
|
397
398
|
with db.connection_context():
|
|
398
|
-
|
|
399
|
+
# Filter by ArrInstance
|
|
400
|
+
base_query = model.select().where(model.ArrInstance == arr_instance)
|
|
399
401
|
|
|
400
402
|
# Calculate counts
|
|
401
403
|
monitored_count = (
|
|
402
404
|
model.select(fn.COUNT(model.EntryId))
|
|
403
|
-
.where(
|
|
405
|
+
.where(
|
|
406
|
+
(model.ArrInstance == arr_instance) & (model.Monitored == True)
|
|
407
|
+
) # noqa: E712
|
|
404
408
|
.scalar()
|
|
405
409
|
or 0
|
|
406
410
|
)
|
|
407
411
|
available_count = (
|
|
408
412
|
model.select(fn.COUNT(model.EntryId))
|
|
409
413
|
.where(
|
|
410
|
-
(model.
|
|
414
|
+
(model.ArrInstance == arr_instance)
|
|
415
|
+
& (model.Monitored == True) # noqa: E712
|
|
411
416
|
& (model.MovieFileId.is_null(False))
|
|
412
417
|
& (model.MovieFileId != 0)
|
|
413
418
|
)
|
|
@@ -417,13 +422,17 @@ class WebUI:
|
|
|
417
422
|
missing_count = max(monitored_count - available_count, 0)
|
|
418
423
|
quality_met_count = (
|
|
419
424
|
model.select(fn.COUNT(model.EntryId))
|
|
420
|
-
.where(
|
|
425
|
+
.where(
|
|
426
|
+
(model.ArrInstance == arr_instance) & (model.QualityMet == True)
|
|
427
|
+
) # noqa: E712
|
|
421
428
|
.scalar()
|
|
422
429
|
or 0
|
|
423
430
|
)
|
|
424
431
|
request_count = (
|
|
425
432
|
model.select(fn.COUNT(model.EntryId))
|
|
426
|
-
.where(
|
|
433
|
+
.where(
|
|
434
|
+
(model.ArrInstance == arr_instance) & (model.IsRequest == True)
|
|
435
|
+
) # noqa: E712
|
|
427
436
|
.scalar()
|
|
428
437
|
or 0
|
|
429
438
|
)
|
|
@@ -544,24 +553,29 @@ class WebUI:
|
|
|
544
553
|
}
|
|
545
554
|
page = max(page, 0)
|
|
546
555
|
page_size = max(page_size, 1)
|
|
556
|
+
arr_instance = getattr(arr, "_name", "")
|
|
547
557
|
|
|
548
558
|
# Quality profiles are now stored in the database
|
|
549
559
|
# No need to fetch from API
|
|
550
560
|
|
|
551
561
|
with db.connection_context():
|
|
552
|
-
|
|
562
|
+
# Filter by ArrInstance
|
|
563
|
+
base_query = model.select().where(model.ArrInstance == arr_instance)
|
|
553
564
|
|
|
554
565
|
# Calculate counts
|
|
555
566
|
monitored_count = (
|
|
556
567
|
model.select(fn.COUNT(model.EntryId))
|
|
557
|
-
.where(
|
|
568
|
+
.where(
|
|
569
|
+
(model.ArrInstance == arr_instance) & (model.Monitored == True)
|
|
570
|
+
) # noqa: E712
|
|
558
571
|
.scalar()
|
|
559
572
|
or 0
|
|
560
573
|
)
|
|
561
574
|
available_count = (
|
|
562
575
|
model.select(fn.COUNT(model.EntryId))
|
|
563
576
|
.where(
|
|
564
|
-
(model.
|
|
577
|
+
(model.ArrInstance == arr_instance)
|
|
578
|
+
& (model.Monitored == True) # noqa: E712
|
|
565
579
|
& (model.AlbumFileId.is_null(False))
|
|
566
580
|
& (model.AlbumFileId != 0)
|
|
567
581
|
)
|
|
@@ -571,13 +585,17 @@ class WebUI:
|
|
|
571
585
|
missing_count = max(monitored_count - available_count, 0)
|
|
572
586
|
quality_met_count = (
|
|
573
587
|
model.select(fn.COUNT(model.EntryId))
|
|
574
|
-
.where(
|
|
588
|
+
.where(
|
|
589
|
+
(model.ArrInstance == arr_instance) & (model.QualityMet == True)
|
|
590
|
+
) # noqa: E712
|
|
575
591
|
.scalar()
|
|
576
592
|
or 0
|
|
577
593
|
)
|
|
578
594
|
request_count = (
|
|
579
595
|
model.select(fn.COUNT(model.EntryId))
|
|
580
|
-
.where(
|
|
596
|
+
.where(
|
|
597
|
+
(model.ArrInstance == arr_instance) & (model.IsRequest == True)
|
|
598
|
+
) # noqa: E712
|
|
581
599
|
.scalar()
|
|
582
600
|
or 0
|
|
583
601
|
)
|
|
@@ -772,8 +790,11 @@ class WebUI:
|
|
|
772
790
|
"tracks": [],
|
|
773
791
|
}
|
|
774
792
|
|
|
793
|
+
arr_instance = getattr(arr, "_name", "")
|
|
794
|
+
|
|
775
795
|
try:
|
|
776
796
|
# Join tracks with albums to get artist/album info
|
|
797
|
+
# Filter by ArrInstance on both models
|
|
777
798
|
query = (
|
|
778
799
|
track_model.select(
|
|
779
800
|
track_model,
|
|
@@ -782,7 +803,10 @@ class WebUI:
|
|
|
782
803
|
album_model.ArtistId,
|
|
783
804
|
)
|
|
784
805
|
.join(album_model, on=(track_model.AlbumId == album_model.EntryId))
|
|
785
|
-
.where(
|
|
806
|
+
.where(
|
|
807
|
+
(track_model.ArrInstance == arr_instance)
|
|
808
|
+
& (album_model.ArrInstance == arr_instance)
|
|
809
|
+
)
|
|
786
810
|
)
|
|
787
811
|
|
|
788
812
|
# Apply filters
|
|
@@ -797,17 +821,25 @@ class WebUI:
|
|
|
797
821
|
| (album_model.ArtistTitle.contains(search))
|
|
798
822
|
)
|
|
799
823
|
|
|
800
|
-
# Get counts
|
|
824
|
+
# Get counts with ArrInstance filter
|
|
801
825
|
available_count = (
|
|
802
826
|
track_model.select()
|
|
803
827
|
.join(album_model, on=(track_model.AlbumId == album_model.EntryId))
|
|
804
|
-
.where(
|
|
828
|
+
.where(
|
|
829
|
+
(track_model.ArrInstance == arr_instance)
|
|
830
|
+
& (album_model.ArrInstance == arr_instance)
|
|
831
|
+
& (track_model.HasFile == True)
|
|
832
|
+
)
|
|
805
833
|
.count()
|
|
806
834
|
)
|
|
807
835
|
monitored_count = (
|
|
808
836
|
track_model.select()
|
|
809
837
|
.join(album_model, on=(track_model.AlbumId == album_model.EntryId))
|
|
810
|
-
.where(
|
|
838
|
+
.where(
|
|
839
|
+
(track_model.ArrInstance == arr_instance)
|
|
840
|
+
& (album_model.ArrInstance == arr_instance)
|
|
841
|
+
& (track_model.Monitored == True)
|
|
842
|
+
)
|
|
811
843
|
.count()
|
|
812
844
|
)
|
|
813
845
|
missing_count = (
|
|
@@ -894,6 +926,7 @@ class WebUI:
|
|
|
894
926
|
page = max(page, 0)
|
|
895
927
|
page_size = max(page_size, 1)
|
|
896
928
|
resolved_page = page
|
|
929
|
+
arr_instance = getattr(arr, "_name", "")
|
|
897
930
|
missing_condition = episodes_model.EpisodeFileId.is_null(True) | (
|
|
898
931
|
episodes_model.EpisodeFileId == 0
|
|
899
932
|
)
|
|
@@ -901,14 +934,18 @@ class WebUI:
|
|
|
901
934
|
with db.connection_context():
|
|
902
935
|
monitored_count = (
|
|
903
936
|
episodes_model.select(fn.COUNT(episodes_model.EntryId))
|
|
904
|
-
.where(
|
|
937
|
+
.where(
|
|
938
|
+
(episodes_model.ArrInstance == arr_instance)
|
|
939
|
+
& (episodes_model.Monitored == True) # noqa: E712
|
|
940
|
+
)
|
|
905
941
|
.scalar()
|
|
906
942
|
or 0
|
|
907
943
|
)
|
|
908
944
|
available_count = (
|
|
909
945
|
episodes_model.select(fn.COUNT(episodes_model.EntryId))
|
|
910
946
|
.where(
|
|
911
|
-
(episodes_model.
|
|
947
|
+
(episodes_model.ArrInstance == arr_instance)
|
|
948
|
+
& (episodes_model.Monitored == True) # noqa: E712
|
|
912
949
|
& (episodes_model.EpisodeFileId.is_null(False))
|
|
913
950
|
& (episodes_model.EpisodeFileId != 0)
|
|
914
951
|
)
|
|
@@ -921,7 +958,11 @@ class WebUI:
|
|
|
921
958
|
missing_series_ids = [
|
|
922
959
|
row.SeriesId
|
|
923
960
|
for row in episodes_model.select(episodes_model.SeriesId)
|
|
924
|
-
.where(
|
|
961
|
+
.where(
|
|
962
|
+
(episodes_model.ArrInstance == arr_instance)
|
|
963
|
+
& (episodes_model.Monitored == True) # noqa: E712
|
|
964
|
+
& missing_condition
|
|
965
|
+
)
|
|
925
966
|
.distinct()
|
|
926
967
|
if getattr(row, "SeriesId", None) is not None
|
|
927
968
|
]
|
|
@@ -941,7 +982,9 @@ class WebUI:
|
|
|
941
982
|
total_series = 0
|
|
942
983
|
|
|
943
984
|
if series_model is not None:
|
|
944
|
-
series_query = series_model.select()
|
|
985
|
+
series_query = series_model.select().where(
|
|
986
|
+
series_model.ArrInstance == arr_instance
|
|
987
|
+
)
|
|
945
988
|
if search:
|
|
946
989
|
series_query = series_query.where(series_model.Title.contains(search))
|
|
947
990
|
if missing_only and missing_series_ids:
|
|
@@ -959,7 +1002,8 @@ class WebUI:
|
|
|
959
1002
|
)
|
|
960
1003
|
for series in series_rows:
|
|
961
1004
|
episodes_query = episodes_model.select().where(
|
|
962
|
-
episodes_model.
|
|
1005
|
+
(episodes_model.ArrInstance == arr_instance)
|
|
1006
|
+
& (episodes_model.SeriesId == series.EntryId)
|
|
963
1007
|
)
|
|
964
1008
|
if missing_only:
|
|
965
1009
|
episodes_query = episodes_query.where(missing_condition)
|
|
@@ -1060,7 +1104,9 @@ class WebUI:
|
|
|
1060
1104
|
|
|
1061
1105
|
if not payload:
|
|
1062
1106
|
# Fallback: construct series payload from episode data (episode mode)
|
|
1063
|
-
base_episode_query = episodes_model.select()
|
|
1107
|
+
base_episode_query = episodes_model.select().where(
|
|
1108
|
+
episodes_model.ArrInstance == arr_instance
|
|
1109
|
+
)
|
|
1064
1110
|
if search:
|
|
1065
1111
|
search_filters = []
|
|
1066
1112
|
if hasattr(episodes_model, "SeriesTitle"):
|
|
@@ -1129,7 +1175,9 @@ class WebUI:
|
|
|
1129
1175
|
episode_conditions.append(episodes_model.SeriesId == series_id)
|
|
1130
1176
|
if series_title is not None:
|
|
1131
1177
|
episode_conditions.append(episodes_model.SeriesTitle == series_title)
|
|
1132
|
-
episodes_query = episodes_model.select()
|
|
1178
|
+
episodes_query = episodes_model.select().where(
|
|
1179
|
+
episodes_model.ArrInstance == arr_instance
|
|
1180
|
+
)
|
|
1133
1181
|
if episode_conditions:
|
|
1134
1182
|
condition = episode_conditions[0]
|
|
1135
1183
|
for extra in episode_conditions[1:]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qBitrr2
|
|
3
|
-
Version: 5.8.
|
|
3
|
+
Version: 5.8.4
|
|
4
4
|
Summary: Intelligent automation for qBittorrent and *Arr apps (Radarr/Sonarr/Lidarr) - health monitoring, instant imports, quality upgrades, request integration
|
|
5
5
|
Home-page: https://github.com/Feramance/qBitrr
|
|
6
6
|
Author: Feramance
|
|
@@ -169,24 +169,6 @@ services:
|
|
|
169
169
|
|
|
170
170
|
Access the WebUI at `http://<host>:6969/ui` after startup.
|
|
171
171
|
|
|
172
|
-
## 🆕 What's New in v5.8.0
|
|
173
|
-
|
|
174
|
-
### Single Consolidated Database
|
|
175
|
-
qBitrr now uses a **single `qbitrr.db` file** for all Arr instances, replacing the previous per-instance database approach.
|
|
176
|
-
|
|
177
|
-
**Benefits:**
|
|
178
|
-
- ✅ Single file to backup instead of 9+ separate databases
|
|
179
|
-
- ✅ 78% code reduction in database initialization
|
|
180
|
-
- ✅ Better performance with shared connection pool
|
|
181
|
-
- ✅ Simplified database management
|
|
182
|
-
|
|
183
|
-
**Migration:**
|
|
184
|
-
- Automatic on first upgrade (5-30 minutes re-sync from Arr APIs)
|
|
185
|
-
- Old databases deleted automatically
|
|
186
|
-
- No manual intervention required
|
|
187
|
-
|
|
188
|
-
[Full Migration Guide →](https://feramance.github.io/qBitrr/getting-started/migration/)
|
|
189
|
-
|
|
190
172
|
## ✨ Key Features
|
|
191
173
|
|
|
192
174
|
- **🚀 Multi-qBittorrent Support (v3.0+)** – Manage torrents across multiple qBittorrent instances for load balancing, redundancy, and VPN isolation
|
|
@@ -254,8 +236,8 @@ See [Configuration Guide](https://feramance.github.io/qBitrr/configuration/) and
|
|
|
254
236
|
- **PyPI Package:** https://pypi.org/project/qBitrr2/
|
|
255
237
|
- **Docker Hub:** https://hub.docker.com/r/feramance/qbitrr
|
|
256
238
|
- **Example Config:** [config.example.toml](config.example.toml)
|
|
257
|
-
- **API Documentation:** [
|
|
258
|
-
- **Systemd Setup:** [
|
|
239
|
+
- **API Documentation:** [docs/reference/api.md](docs/reference/api.md)
|
|
240
|
+
- **Systemd Setup:** [docs/getting-started/installation/systemd.md](docs/getting-started/installation/systemd.md)
|
|
259
241
|
|
|
260
242
|
## 🐛 Issues & Support
|
|
261
243
|
|
|
@@ -266,7 +248,7 @@ See [Configuration Guide](https://feramance.github.io/qBitrr/configuration/) and
|
|
|
266
248
|
|
|
267
249
|
## 🤝 Contributing
|
|
268
250
|
|
|
269
|
-
Contributions welcome! See [
|
|
251
|
+
Contributions welcome! See [docs/development/contributing.md](docs/development/contributing.md) for coding guidelines and development setup.
|
|
270
252
|
|
|
271
253
|
**Development setup:**
|
|
272
254
|
```bash
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
qBitrr/__init__.py,sha256=smiPIV7d2lMJ_KTtFdAVlxLEBobFTheILdgry1iqpjQ,405
|
|
2
|
-
qBitrr/arss.py,sha256=
|
|
2
|
+
qBitrr/arss.py,sha256=oEnOKXASFYCtrmSqUejSA3AlyGKZpZRa9Ni6B3DUTBE,357493
|
|
3
3
|
qBitrr/auto_update.py,sha256=3mqlKKGnaWR5dNI58LN5WbHOMFRDWBsOs0zIC79QzQk,13359
|
|
4
|
-
qBitrr/bundled_data.py,sha256=
|
|
4
|
+
qBitrr/bundled_data.py,sha256=Ua82Ic829ivWX0PesmPIEVCPX_Nxlmllzlw9hhINq04,221
|
|
5
5
|
qBitrr/config.py,sha256=BtvzQCQnRHEGzNykEa-WsiWKPjwXCb7wMx994z3rWMs,6306
|
|
6
6
|
qBitrr/config_version.py,sha256=eWfc7DuthsDGwv2WJAZ7uB7srSqxaUD3Oj9kac3HbFE,4208
|
|
7
|
-
qBitrr/database.py,sha256=
|
|
7
|
+
qBitrr/database.py,sha256=DgPiIxT1MRAhZ5anM5IzjvzEMB5fGDQmktsLEf2dN28,4602
|
|
8
8
|
qBitrr/db_lock.py,sha256=ZM6TQagBOkcS7h9BDGsdxhCo5aJ5SMvtKSM_X7HLu8M,15375
|
|
9
9
|
qBitrr/db_recovery.py,sha256=4kS9mWKrBYE9135m7cxvCOriSxz6lz7Dw4MT6gz81SM,6438
|
|
10
10
|
qBitrr/env_config.py,sha256=299u_uEoyxlM_ceTD0Z_i41JdYjSHmqO6FKe7qGFgTM,2866
|
|
@@ -18,7 +18,7 @@ qBitrr/search_activity_store.py,sha256=JuJ0PQxSPBguacyqus2I4_g3qEYReOhZbghOjJOoE
|
|
|
18
18
|
qBitrr/tables.py,sha256=hAHYmpSjdbXyOXABUDXmQKFgf_TQLrgYlCQPZEZWN8o,6187
|
|
19
19
|
qBitrr/utils.py,sha256=T10win016yHwMMJlJ4yuPTRUI9m-AS_a_MouiAJAtC8,8190
|
|
20
20
|
qBitrr/versioning.py,sha256=vjQ55rMInuEte9PXQo29FLxF0ff643Ttm3_ri5ghXPM,4542
|
|
21
|
-
qBitrr/webui.py,sha256=
|
|
21
|
+
qBitrr/webui.py,sha256=c-L-cSzYxoZomZllchR4zg4j6vebPsSUdyRSCdrOTR4,138111
|
|
22
22
|
qBitrr/static/favicon-16x16.png,sha256=LrsZjRNNIivd0M-6OEghkY0bCYQNz_88kh-A6DDUpC4,1063
|
|
23
23
|
qBitrr/static/favicon-32x32.png,sha256=n3S3d7McOUokBxHEItYH2lEPmrgU7z0z652jeRrFwVQ,3398
|
|
24
24
|
qBitrr/static/favicon-48x48.png,sha256=r-co_lnPpXOLIptGm8FNZ4kM7QQkanwz_7vRxUAeqi0,6808
|
|
@@ -37,8 +37,8 @@ qBitrr/static/assets/ConfigView.js,sha256=X9-ovxOiExIMFDaDqZ-ajJJJSGzQsObNjgOLFK
|
|
|
37
37
|
qBitrr/static/assets/ConfigView.js.map,sha256=puLlBaPUmBXEUH9YWazhIshZE2DbH6LYMQMnumswwss,290042
|
|
38
38
|
qBitrr/static/assets/LogsView.js,sha256=W76pJiNn9PzdSiHNYXvQ9KajOcneyewdnxWjudao7L0,119172
|
|
39
39
|
qBitrr/static/assets/LogsView.js.map,sha256=yzdMUlhQ4NJMy3AGA5IX_gUu59l-Mrkr6o2pisLPnPU,473243
|
|
40
|
-
qBitrr/static/assets/ProcessesView.js,sha256=
|
|
41
|
-
qBitrr/static/assets/ProcessesView.js.map,sha256=
|
|
40
|
+
qBitrr/static/assets/ProcessesView.js,sha256=VLkE3Dn794Vd7FDQn_q_N3EKFEYIe6QYc-MyoyKI2z8,8704
|
|
41
|
+
qBitrr/static/assets/ProcessesView.js.map,sha256=H8i3o9QeUOGeV6X_txHLYffUWCa6amVRq1OsYoi_SSk,31280
|
|
42
42
|
qBitrr/static/assets/app.css,sha256=0anadTB1R43p4Brukr7S-rkzlHLzUS5R0GWyHtF8HG4,55598
|
|
43
43
|
qBitrr/static/assets/app.js,sha256=Ph7kHJB53tWx0v6CJxU7nzd2-RhQmje6kQYNfIwnHDM,209061
|
|
44
44
|
qBitrr/static/assets/app.js.map,sha256=7ZQI8jxfyWR2Rdw5QTmznnMAW5o36zIz3XNsOQZEVGQ,948745
|
|
@@ -65,9 +65,9 @@ qBitrr/static/assets/useInterval.js.map,sha256=3KvsE2Do5LepkgpP-0XBHeGOprIQiDIdK
|
|
|
65
65
|
qBitrr/static/assets/vendor.js,sha256=uDL8OA1Nbl4ZlnDVLXOmaKPBVqK6pBsoDiRriXdDk04,11357
|
|
66
66
|
qBitrr/static/assets/vendor.js.map,sha256=Nj3zAKcjMhDma3lqUKGqQvwozZmbx4SEMBKBLXTi9io,42253
|
|
67
67
|
qBitrr/static/assets/visibility.svg,sha256=258DiSuUKs2SH9dhhDDU88JQfLAlNQMvOGewc9uaTK4,9767
|
|
68
|
-
qbitrr2-5.8.
|
|
69
|
-
qbitrr2-5.8.
|
|
70
|
-
qbitrr2-5.8.
|
|
71
|
-
qbitrr2-5.8.
|
|
72
|
-
qbitrr2-5.8.
|
|
73
|
-
qbitrr2-5.8.
|
|
68
|
+
qbitrr2-5.8.4.dist-info/licenses/LICENSE,sha256=P978aVGi7dPbKz8lfvdiryOS5IjTAU7AA47XhBhVBlI,1066
|
|
69
|
+
qbitrr2-5.8.4.dist-info/METADATA,sha256=sAjKN_jywgJUEUcpdb-X2tZUxXnJ8S58xENXLvDkz6w,11549
|
|
70
|
+
qbitrr2-5.8.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
71
|
+
qbitrr2-5.8.4.dist-info/entry_points.txt,sha256=MIR-l5s31VBs9qlv3HiAaMdpOOyy0MNGfM7Ib1-fKeQ,43
|
|
72
|
+
qbitrr2-5.8.4.dist-info/top_level.txt,sha256=jIINodarzsPcQeTf-vvK8-_g7cQ8CvxEg41ms14K97g,7
|
|
73
|
+
qbitrr2-5.8.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|