Files
Kadr/src/kadr/services/tmdb.py
T
Cheviiot 32fe518259 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.
2026-04-01 22:00:37 +10:00

102 lines
3.2 KiB
Python

import requests
import urllib.parse
TMDB_API_KEY = '4ef0d7355d9ffb5151e987764708ce96'
TMDB_PROXIES = {
'direct': {
'name': 'TMDB Direct',
'api_url': 'https://api.themoviedb.org',
'image_url': 'https://image.tmdb.org',
},
'apn_render': {
'name': 'APN Render',
'api_url': 'https://apn-latest.onrender.com/https://api.themoviedb.org',
'image_url': 'https://apn-latest.onrender.com/https://image.tmdb.org',
},
}
class TMDBService:
def __init__(self, settings):
self.settings = settings
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Origin': 'https://kadr.app',
})
self._healthy_proxy = None
def _resolve_proxy_id(self):
"""Return the proxy_id to use: pinned or first healthy."""
chosen = self.settings.get('tmdb_proxy', 'auto')
if chosen != 'auto':
return chosen
if self._healthy_proxy:
return self._healthy_proxy
for pid in TMDB_PROXIES:
if self.check_health(pid):
self._healthy_proxy = pid
return pid
return 'direct'
@property
def _proxy(self):
pid = self._resolve_proxy_id()
return TMDB_PROXIES.get(pid, TMDB_PROXIES['direct'])
def _api_url(self, path, **params):
params['api_key'] = TMDB_API_KEY
params.setdefault('language', 'ru-RU')
qs = urllib.parse.urlencode(params)
return f"{self._proxy['api_url']}/3{path}?{qs}"
def image_url(self, path, size='w342'):
if not path:
return None
return f"{self._proxy['image_url']}/t/p/{size}{path}"
def _fetch(self, path, timeout=10, **params):
url = self._api_url(path, **params)
resp = self.session.get(url, timeout=timeout)
resp.raise_for_status()
return resp.json()
def search_movies(self, query, page=1):
return self._fetch('/search/movie', query=query, page=page)
def search_tv(self, query, page=1):
return self._fetch('/search/tv', query=query, page=page)
def popular_movies(self, page=1):
return self._fetch('/movie/popular', page=page)
def popular_tv(self, page=1):
return self._fetch('/tv/popular', page=page)
def movie_details(self, movie_id):
return self._fetch(f'/movie/{int(movie_id)}')
def tv_details(self, tv_id):
return self._fetch(f'/tv/{int(tv_id)}')
def movie_credits(self, movie_id):
return self._fetch(f'/movie/{int(movie_id)}/credits')
def tv_credits(self, tv_id):
return self._fetch(f'/tv/{int(tv_id)}/credits')
def genres(self, media_type='movie'):
return self._fetch(f'/genre/{media_type}/list').get('genres', [])
def check_health(self, proxy_id):
proxy = TMDB_PROXIES.get(proxy_id)
if not proxy:
return False
try:
url = f"{proxy['api_url']}/3/movie/popular?api_key={TMDB_API_KEY}&language=ru-RU&page=1"
resp = self.session.get(url, timeout=5)
return resp.status_code == 200
except Exception:
return False