qBitrr2 5.5.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qBitrr/__init__.py +14 -0
- qBitrr/arss.py +7100 -0
- qBitrr/auto_update.py +382 -0
- qBitrr/bundled_data.py +7 -0
- qBitrr/config.py +192 -0
- qBitrr/config_version.py +144 -0
- qBitrr/db_lock.py +400 -0
- qBitrr/db_recovery.py +202 -0
- qBitrr/env_config.py +73 -0
- qBitrr/errors.py +41 -0
- qBitrr/ffprobe.py +105 -0
- qBitrr/gen_config.py +1331 -0
- qBitrr/home_path.py +23 -0
- qBitrr/logger.py +235 -0
- qBitrr/main.py +790 -0
- qBitrr/search_activity_store.py +92 -0
- qBitrr/static/assets/ArrView.js +2 -0
- qBitrr/static/assets/ArrView.js.map +1 -0
- qBitrr/static/assets/ConfigView.js +4 -0
- qBitrr/static/assets/ConfigView.js.map +1 -0
- qBitrr/static/assets/LogsView.js +2 -0
- qBitrr/static/assets/LogsView.js.map +1 -0
- qBitrr/static/assets/ProcessesView.js +2 -0
- qBitrr/static/assets/ProcessesView.js.map +1 -0
- qBitrr/static/assets/app.css +1 -0
- qBitrr/static/assets/app.js +11 -0
- qBitrr/static/assets/app.js.map +1 -0
- qBitrr/static/assets/build.svg +3 -0
- qBitrr/static/assets/check-mark.svg +5 -0
- qBitrr/static/assets/close.svg +4 -0
- qBitrr/static/assets/download.svg +5 -0
- qBitrr/static/assets/gear.svg +5 -0
- qBitrr/static/assets/live-streaming.svg +8 -0
- qBitrr/static/assets/log.svg +3 -0
- qBitrr/static/assets/logo.svg +48 -0
- qBitrr/static/assets/plus.svg +4 -0
- qBitrr/static/assets/process.svg +15 -0
- qBitrr/static/assets/react-select.esm.js +7 -0
- qBitrr/static/assets/react-select.esm.js.map +1 -0
- qBitrr/static/assets/refresh-arrow.svg +3 -0
- qBitrr/static/assets/table.js +5 -0
- qBitrr/static/assets/table.js.map +1 -0
- qBitrr/static/assets/trash.svg +8 -0
- qBitrr/static/assets/up-arrow.svg +3 -0
- qBitrr/static/assets/useInterval.js +2 -0
- qBitrr/static/assets/useInterval.js.map +1 -0
- qBitrr/static/assets/vendor.js +2 -0
- qBitrr/static/assets/vendor.js.map +1 -0
- qBitrr/static/assets/visibility.svg +9 -0
- qBitrr/static/index.html +33 -0
- qBitrr/static/logov2-clean.svg +48 -0
- qBitrr/static/manifest.json +23 -0
- qBitrr/static/sw.js +87 -0
- qBitrr/static/vite.svg +1 -0
- qBitrr/tables.py +143 -0
- qBitrr/utils.py +274 -0
- qBitrr/versioning.py +136 -0
- qBitrr/webui.py +3114 -0
- qbitrr2-5.5.5.dist-info/METADATA +1191 -0
- qbitrr2-5.5.5.dist-info/RECORD +64 -0
- qbitrr2-5.5.5.dist-info/WHEEL +5 -0
- qbitrr2-5.5.5.dist-info/entry_points.txt +2 -0
- qbitrr2-5.5.5.dist-info/licenses/LICENSE +21 -0
- qbitrr2-5.5.5.dist-info/top_level.txt +1 -0
qBitrr/db_recovery.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""SQLite database recovery utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import sqlite3
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("qBitrr.DBRecovery")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatabaseRecoveryError(Exception):
|
|
14
|
+
"""Raised when database recovery fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def checkpoint_wal(db_path: Path, logger_override=None) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Force checkpoint of WAL file to main database.
|
|
20
|
+
|
|
21
|
+
This operation flushes all Write-Ahead Log entries to the main database
|
|
22
|
+
file, which can resolve certain types of corruption and reduce the risk
|
|
23
|
+
of data loss.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
db_path: Path to SQLite database file
|
|
27
|
+
logger_override: Optional logger instance to use instead of module logger
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if successful, False otherwise
|
|
31
|
+
"""
|
|
32
|
+
log = logger_override or logger
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
log.info("Starting WAL checkpoint for database: %s", db_path)
|
|
36
|
+
conn = sqlite3.connect(str(db_path), timeout=10.0)
|
|
37
|
+
cursor = conn.cursor()
|
|
38
|
+
|
|
39
|
+
# Force WAL checkpoint with TRUNCATE mode
|
|
40
|
+
# This checkpoints all frames and truncates the WAL file
|
|
41
|
+
cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
42
|
+
result = cursor.fetchone()
|
|
43
|
+
|
|
44
|
+
conn.close()
|
|
45
|
+
|
|
46
|
+
# Result is (busy, log_pages, checkpointed_pages)
|
|
47
|
+
# If busy=0, checkpoint was fully successful
|
|
48
|
+
if result and result[0] == 0:
|
|
49
|
+
log.info(
|
|
50
|
+
"WAL checkpoint successful: %s frames checkpointed, %s pages in log",
|
|
51
|
+
result[2],
|
|
52
|
+
result[1],
|
|
53
|
+
)
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
log.warning(
|
|
57
|
+
"WAL checkpoint partially successful: result=%s (database may be busy)",
|
|
58
|
+
result,
|
|
59
|
+
)
|
|
60
|
+
return True # Still consider partial success as success
|
|
61
|
+
|
|
62
|
+
except sqlite3.OperationalError as e:
|
|
63
|
+
log.error("WAL checkpoint failed (OperationalError): %s", e)
|
|
64
|
+
return False
|
|
65
|
+
except Exception as e:
|
|
66
|
+
log.error("WAL checkpoint failed (unexpected error): %s", e)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def repair_database(db_path: Path, backup: bool = True, logger_override=None) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Attempt to repair corrupted SQLite database via dump/restore.
|
|
73
|
+
|
|
74
|
+
This operation:
|
|
75
|
+
1. Creates a backup of the corrupted database
|
|
76
|
+
2. Dumps all recoverable data to a temporary database
|
|
77
|
+
3. Replaces the original with the repaired copy
|
|
78
|
+
4. Verifies integrity of the repaired database
|
|
79
|
+
|
|
80
|
+
WARNING: Some data may be lost if corruption is severe.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
db_path: Path to SQLite database file
|
|
84
|
+
backup: Whether to create backup before repair (default: True)
|
|
85
|
+
logger_override: Optional logger instance to use instead of module logger
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if repair successful, False otherwise
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
DatabaseRecoveryError: If repair fails critically
|
|
92
|
+
"""
|
|
93
|
+
log = logger_override or logger
|
|
94
|
+
|
|
95
|
+
backup_path = db_path.with_suffix(".db.backup")
|
|
96
|
+
temp_path = db_path.with_suffix(".db.temp")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Step 1: Backup original
|
|
100
|
+
if backup:
|
|
101
|
+
log.info("Creating backup: %s", backup_path)
|
|
102
|
+
shutil.copy2(db_path, backup_path)
|
|
103
|
+
|
|
104
|
+
# Step 2: Dump recoverable data
|
|
105
|
+
log.info("Dumping recoverable data from corrupted database...")
|
|
106
|
+
source_conn = sqlite3.connect(str(db_path))
|
|
107
|
+
|
|
108
|
+
temp_conn = sqlite3.connect(str(temp_path))
|
|
109
|
+
|
|
110
|
+
# Dump schema and data
|
|
111
|
+
skipped_rows = 0
|
|
112
|
+
for line in source_conn.iterdump():
|
|
113
|
+
try:
|
|
114
|
+
temp_conn.execute(line)
|
|
115
|
+
except sqlite3.Error as e:
|
|
116
|
+
# Log but continue - recover what we can
|
|
117
|
+
skipped_rows += 1
|
|
118
|
+
log.debug("Skipping corrupted row during dump: %s", e)
|
|
119
|
+
|
|
120
|
+
if skipped_rows > 0:
|
|
121
|
+
log.warning("Skipped %s corrupted rows during dump", skipped_rows)
|
|
122
|
+
|
|
123
|
+
temp_conn.commit()
|
|
124
|
+
temp_conn.close()
|
|
125
|
+
source_conn.close()
|
|
126
|
+
|
|
127
|
+
# Step 3: Replace original with repaired copy
|
|
128
|
+
log.info("Replacing database with repaired version...")
|
|
129
|
+
db_path.unlink()
|
|
130
|
+
shutil.move(str(temp_path), str(db_path))
|
|
131
|
+
|
|
132
|
+
# Step 4: Verify integrity
|
|
133
|
+
log.info("Verifying integrity of repaired database...")
|
|
134
|
+
verify_conn = sqlite3.connect(str(db_path))
|
|
135
|
+
cursor = verify_conn.cursor()
|
|
136
|
+
cursor.execute("PRAGMA integrity_check")
|
|
137
|
+
result = cursor.fetchone()[0]
|
|
138
|
+
verify_conn.close()
|
|
139
|
+
|
|
140
|
+
if result != "ok":
|
|
141
|
+
raise DatabaseRecoveryError(f"Repair verification failed: {result}")
|
|
142
|
+
|
|
143
|
+
log.info("Database repair successful!")
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
log.error("Database repair failed: %s", e)
|
|
148
|
+
|
|
149
|
+
# Attempt to restore backup
|
|
150
|
+
if backup and backup_path.exists():
|
|
151
|
+
log.warning("Restoring from backup...")
|
|
152
|
+
try:
|
|
153
|
+
shutil.copy2(backup_path, db_path)
|
|
154
|
+
log.info("Backup restored successfully")
|
|
155
|
+
except Exception as restore_error:
|
|
156
|
+
log.error("Failed to restore backup: %s", restore_error)
|
|
157
|
+
|
|
158
|
+
# Cleanup temp files
|
|
159
|
+
if temp_path.exists():
|
|
160
|
+
try:
|
|
161
|
+
temp_path.unlink()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass # Best effort cleanup
|
|
164
|
+
|
|
165
|
+
raise DatabaseRecoveryError(f"Repair failed: {e}") from e
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def vacuum_database(db_path: Path, logger_override=None) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Run VACUUM to reclaim space and optimize database.
|
|
171
|
+
|
|
172
|
+
VACUUM rebuilds the database file, repacking it into a minimal amount of
|
|
173
|
+
disk space. This can help resolve some types of corruption and improve
|
|
174
|
+
performance.
|
|
175
|
+
|
|
176
|
+
Note: VACUUM requires free disk space approximately 2x the database size.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
db_path: Path to SQLite database file
|
|
180
|
+
logger_override: Optional logger instance to use instead of module logger
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if successful, False otherwise
|
|
184
|
+
"""
|
|
185
|
+
log = logger_override or logger
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
log.info("Running VACUUM on database: %s", db_path)
|
|
189
|
+
conn = sqlite3.connect(str(db_path), timeout=30.0)
|
|
190
|
+
|
|
191
|
+
conn.execute("VACUUM")
|
|
192
|
+
conn.close()
|
|
193
|
+
|
|
194
|
+
log.info("VACUUM completed successfully")
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
except sqlite3.OperationalError as e:
|
|
198
|
+
log.error("VACUUM failed (OperationalError): %s", e)
|
|
199
|
+
return False
|
|
200
|
+
except Exception as e:
|
|
201
|
+
log.error("VACUUM failed (unexpected error): %s", e)
|
|
202
|
+
return False
|
qBitrr/env_config.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import environ
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _strtobool(value: str) -> int:
|
|
7
|
+
"""Return 1 for truthy strings and 0 for falsy strings, mirroring distutils.util.strtobool."""
|
|
8
|
+
if value is None:
|
|
9
|
+
raise ValueError("Boolean value must be a string")
|
|
10
|
+
normalized = value.strip().lower()
|
|
11
|
+
if normalized in {"y", "yes", "t", "true", "on", "1"}:
|
|
12
|
+
return 1
|
|
13
|
+
if normalized in {"n", "no", "f", "false", "off", "0"}:
|
|
14
|
+
return 0
|
|
15
|
+
raise ValueError(f"Invalid truth value {value!r}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Converter:
|
|
19
|
+
@staticmethod
|
|
20
|
+
def int(value: str | None) -> int | None:
|
|
21
|
+
return None if value is None else int(value)
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def list(value: str | None, delimiter=",", converter=str) -> list | None:
|
|
25
|
+
return None if value is None else list(map(converter, value.split(delimiter)))
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def bool(value: str | None) -> bool | None:
|
|
29
|
+
return None if value is None else _strtobool(value) == 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@environ.config(prefix="QBITRR", frozen=True)
|
|
33
|
+
class AppConfig:
|
|
34
|
+
@environ.config(prefix="OVERRIDES", frozen=True)
|
|
35
|
+
class Overrides:
|
|
36
|
+
search_only = environ.var(None, converter=Converter.bool)
|
|
37
|
+
processing_only = environ.var(None, converter=Converter.bool)
|
|
38
|
+
data_path = environ.var(None)
|
|
39
|
+
|
|
40
|
+
@environ.config(prefix="SETTINGS", frozen=True)
|
|
41
|
+
class Settings:
|
|
42
|
+
console_level = environ.var(None)
|
|
43
|
+
logging = environ.var(None, converter=Converter.bool)
|
|
44
|
+
completed_download_folder = environ.var(None)
|
|
45
|
+
free_space = environ.var(None)
|
|
46
|
+
free_space_folder = environ.var(None)
|
|
47
|
+
no_internet_sleep_timer = environ.var(None, converter=Converter.int)
|
|
48
|
+
loop_sleep_timer = environ.var(None, converter=Converter.int)
|
|
49
|
+
search_loop_delay = environ.var(None, converter=Converter.int)
|
|
50
|
+
auto_pause_resume = environ.var(None, converter=Converter.bool)
|
|
51
|
+
failed_category = environ.var(None)
|
|
52
|
+
recheck_category = environ.var(None)
|
|
53
|
+
tagless = environ.var(None, converter=Converter.bool)
|
|
54
|
+
ignore_torrents_younger_than = environ.var(None, converter=Converter.int)
|
|
55
|
+
ping_urls = environ.var(None, converter=Converter.list)
|
|
56
|
+
ffprobe_auto_update = environ.var(None, converter=Converter.bool)
|
|
57
|
+
auto_update_enabled = environ.var(None, converter=Converter.bool)
|
|
58
|
+
auto_update_cron = environ.var(None)
|
|
59
|
+
|
|
60
|
+
@environ.config(prefix="QBIT", frozen=True)
|
|
61
|
+
class qBit:
|
|
62
|
+
disabled = environ.var(None, converter=Converter.bool)
|
|
63
|
+
host = environ.var(None)
|
|
64
|
+
port = environ.var(None, converter=Converter.int)
|
|
65
|
+
username = environ.var(None)
|
|
66
|
+
password = environ.var(None)
|
|
67
|
+
|
|
68
|
+
overrides: Overrides = environ.group(Overrides)
|
|
69
|
+
settings: Settings = environ.group(Settings)
|
|
70
|
+
qbit: qBit = environ.group(qBit)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
ENVIRO_CONFIG: AppConfig = environ.to_config(AppConfig)
|
qBitrr/errors.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class qBitManagerError(Exception):
|
|
2
|
+
"""Base Exception"""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UnhandledError(qBitManagerError):
|
|
6
|
+
"""Use to raise when there an unhandled edge case"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigException(qBitManagerError):
|
|
10
|
+
"""Base Exception for Config related exceptions"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ArrManagerException(qBitManagerError):
|
|
14
|
+
"""Base Exception for Arr related Exceptions"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SkipException(qBitManagerError):
|
|
18
|
+
"""Dummy error to skip actions"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RequireConfigValue(qBitManagerError):
|
|
22
|
+
"""Exception raised when a config value requires a value."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config_class: str, config_key: str):
|
|
25
|
+
self.message = f"Config key '{config_key}' in '{config_class}' requires a value."
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NoConnectionrException(qBitManagerError):
|
|
29
|
+
def __init__(self, message: str, type: str = "delay"):
|
|
30
|
+
self.message = message
|
|
31
|
+
self.type = type
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DelayLoopException(qBitManagerError):
|
|
35
|
+
def __init__(self, length: int, type: str):
|
|
36
|
+
self.type = type
|
|
37
|
+
self.length = length
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RestartLoopException(ArrManagerException):
|
|
41
|
+
"""Exception to trigger a loop restart"""
|
qBitrr/ffprobe.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
import zipfile
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from qBitrr.config import FF_PROBE, FF_VERSION, FFPROBE_AUTO_UPDATE
|
|
12
|
+
from qBitrr.logger import run_logs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FFprobeDownloader:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.api = "https://ffbinaries.com/api/v1/version/latest"
|
|
18
|
+
self.version_file = FF_VERSION
|
|
19
|
+
self.logger = logging.getLogger("qBitrr.FFprobe")
|
|
20
|
+
run_logs(self.logger)
|
|
21
|
+
self.platform = platform.system()
|
|
22
|
+
if self.platform == "Windows":
|
|
23
|
+
self.probe_path = FF_PROBE.with_suffix(".exe")
|
|
24
|
+
else:
|
|
25
|
+
self.probe_path = FF_PROBE
|
|
26
|
+
|
|
27
|
+
def get_upstream_version(self) -> dict:
|
|
28
|
+
with requests.Session() as session:
|
|
29
|
+
with session.get(self.api) as response:
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
self.logger.warning("Failed to retrieve ffprobe version from API.")
|
|
32
|
+
return {}
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def get_current_version(self):
|
|
36
|
+
try:
|
|
37
|
+
with self.version_file.open(mode="r") as file:
|
|
38
|
+
data = json.load(file)
|
|
39
|
+
return data.get("version")
|
|
40
|
+
except Exception: # If file can't be found or read or parsed
|
|
41
|
+
self.logger.warning("Failed to retrieve current ffprobe version.")
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
def update(self):
|
|
45
|
+
if not FFPROBE_AUTO_UPDATE:
|
|
46
|
+
return
|
|
47
|
+
current_version = self.get_current_version()
|
|
48
|
+
upstream_data = self.get_upstream_version()
|
|
49
|
+
upstream_version = upstream_data.get("version")
|
|
50
|
+
if upstream_version is None:
|
|
51
|
+
self.logger.debug(
|
|
52
|
+
"Failed to retrieve ffprobe version from API.'upstream_version' is None"
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
probe_file_exists = self.probe_path.exists()
|
|
56
|
+
if current_version == upstream_version and probe_file_exists:
|
|
57
|
+
self.logger.debug("Current FFprobe is up to date.")
|
|
58
|
+
return
|
|
59
|
+
arch_key = self.get_arch()
|
|
60
|
+
urls = upstream_data.get("bin", {}).get(arch_key)
|
|
61
|
+
if urls is None:
|
|
62
|
+
self.logger.debug("Failed to retrieve ffprobe version from API.'urls' is None")
|
|
63
|
+
return
|
|
64
|
+
ffprobe_url = urls.get("ffprobe")
|
|
65
|
+
self.logger.debug("Downloading newer FFprobe: %s", ffprobe_url)
|
|
66
|
+
self.download_and_extract(ffprobe_url)
|
|
67
|
+
self.logger.debug("Updating local version of FFprobe: %s", upstream_version)
|
|
68
|
+
self.version_file.write_text(json.dumps({"version": upstream_version}))
|
|
69
|
+
try:
|
|
70
|
+
os.chmod(self.probe_path, 0o777)
|
|
71
|
+
self.logger.debug("Successfully changed permissions for ffprobe")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self.logger.debug("Failed to change permissions for ffprobe, %s", e)
|
|
74
|
+
|
|
75
|
+
def download_and_extract(self, ffprobe_url):
|
|
76
|
+
r = requests.get(ffprobe_url)
|
|
77
|
+
z = zipfile.ZipFile(io.BytesIO(r.content))
|
|
78
|
+
self.logger.debug("Extracting downloaded FFprobe to: %s", FF_PROBE.parent)
|
|
79
|
+
z.extract(member=self.probe_path.name, path=FF_PROBE.parent)
|
|
80
|
+
|
|
81
|
+
def get_arch(self):
|
|
82
|
+
part1 = None
|
|
83
|
+
is_64bits = sys.maxsize > 2**32
|
|
84
|
+
part2 = "64" if is_64bits else "32"
|
|
85
|
+
if self.platform == "Windows":
|
|
86
|
+
part1 = "windows-"
|
|
87
|
+
elif self.platform == "Linux":
|
|
88
|
+
part1 = "linux-"
|
|
89
|
+
machine = platform.machine()
|
|
90
|
+
if machine == "armv6l":
|
|
91
|
+
part2 = "armhf"
|
|
92
|
+
elif ("arm" in machine and is_64bits) or machine == "aarch64":
|
|
93
|
+
part2 = "arm64"
|
|
94
|
+
# Else just 32/64, Not armel - because just no
|
|
95
|
+
elif self.platform == "Darwin":
|
|
96
|
+
part1 = "osx-"
|
|
97
|
+
part2 = "64"
|
|
98
|
+
if part1 is None:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"You are running in an unsupported platform, "
|
|
101
|
+
"if you expect this to be supported please open an issue on GitHub "
|
|
102
|
+
"https://github.com/Feramance/qBitrr."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return part1 + part2
|