Add torrent streamer, UI overhaul, switch to GPL

Major updates: adds a libtorrent-based streaming service and refactors UI/behaviour.

Key changes:
- Add streaming service (src/kadr/services/streamer.py): HTTP server + sequential download streamer using libtorrent, StreamLog, buffering/ready states and local streaming URL.
- Rework Detail view (src/kadr/views/detail.py): new hero/backdrop layout, poster sizing, metadata chips, tagline, overview, cast placeholder and a "Watch" action that navigates to a Torrents view; loads full TMDB details/credits asynchronously.
- UI styling (src/kadr/data/style.css): many new classes for detail/hero/cast styling.
- Downloads manager improvements (src/kadr/services/downloads.py): temp dir for torrents, cleanup of stale files, history size cap, unified client launch with error handling.
- TMDB client refactor (src/kadr/services/tmdb.py): unified _fetch helper, added movie_credits/tv_credits endpoints.
- Utils improvements (src/kadr/utils.py): thread-safe LRU image cache, image download concurrency, and slight clipboard command ordering tweak.
- Jackett servers list updated (src/kadr/services/jackett.py): reorder/add servers.
- Project metadata & licensing: switch LICENSE to GPL-3.0-or-later, update pyproject.toml and application about dialog to GPL, adjust package/version in pyproject and __init__.
- README revamped (README.md): reorganized, added badges, Russian text, install/usage sections.
- Added new view/widget files (player/torrents/mpv_widget) and other UI tweaks across widgets/window.

These changes add streaming playback capability, tighten resource handling, and refresh the detail UI while changing the project license to GPL-3.0-or-later.
This commit is contained in:
2026-04-01 22:00:37 +10:00
parent 3508a7c5dd
commit 32fe518259
18 changed files with 1807 additions and 354 deletions
+33 -4
View File
@@ -16,6 +16,8 @@ DOWNLOAD_CLIENTS = [
{'id': 'ktorrent', 'name': 'KTorrent', 'command': 'ktorrent'},
]
_HISTORY_MAX = 100
class DownloadManager:
def __init__(self, settings=None):
@@ -27,19 +29,45 @@ class DownloadManager:
self._data_file = os.path.join(self._data_dir, 'downloads.json')
self._history = self._load_history()
self._lock = threading.Lock()
self._tmp_dir = os.path.join(tempfile.gettempdir(), 'kadr_torrents')
self._cleanup_temp()
def _load_history(self):
try:
with open(self._data_file) as f:
return json.load(f)
data = json.load(f)
return data[-_HISTORY_MAX:] if len(data) > _HISTORY_MAX else data
except (FileNotFoundError, json.JSONDecodeError):
return []
def _save_history(self):
os.makedirs(self._data_dir, exist_ok=True)
self._history = self._history[-_HISTORY_MAX:]
with open(self._data_file, 'w') as f:
json.dump(self._history, f, indent=2, ensure_ascii=False)
def _cleanup_temp(self):
"""Remove stale temp torrent files older than 1 hour."""
if not os.path.isdir(self._tmp_dir):
return
cutoff = time.time() - 3600
for name in os.listdir(self._tmp_dir):
path = os.path.join(self._tmp_dir, name)
try:
if os.path.getmtime(path) < cutoff:
os.remove(path)
except OSError:
pass
def _launch_client(self, client, arg):
"""Launch torrent client and raise on failure."""
try:
subprocess.Popen([client['command'], arg])
except OSError as e:
raise RuntimeError(
f'Не удалось запустить {client["name"]}: {e}'
)
def available_clients(self):
return [c for c in DOWNLOAD_CLIENTS if shutil.which(c['command'])]
@@ -64,7 +92,8 @@ class DownloadManager:
'Торрент-клиент не найден. '
'Установите qBittorrent, Transmission или другой клиент.'
)
subprocess.Popen([client['command'], magnet_uri])
self._launch_client(client, magnet_uri)
entry = {
'name': name,
@@ -94,14 +123,14 @@ class DownloadManager:
if not safe_name.lower().endswith('.torrent'):
safe_name += '.torrent'
tmp_dir = os.path.join(tempfile.gettempdir(), 'kadr_torrents')
tmp_dir = self._tmp_dir
os.makedirs(tmp_dir, exist_ok=True)
path = os.path.join(tmp_dir, safe_name)
with open(path, 'wb') as f:
f.write(resp.content)
subprocess.Popen([client['command'], path])
self._launch_client(client, path)
entry = {
'name': name,