Unit3DwebUp 0.0.14__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.
- config/__init__.py +4 -0
- config/api_data.py +28 -0
- config/constants.py +34 -0
- config/host_data.py +47 -0
- config/itt.py +154 -0
- config/logger.py +37 -0
- config/settings.py +212 -0
- config/sis.py +137 -0
- config/tags.py +395 -0
- config/trackers.py +47 -0
- external/__init__.py +0 -0
- external/async_http_client_service.py +48 -0
- external/websocket.py +41 -0
- models/__init__.py +0 -0
- models/interfaces.py +39 -0
- models/keywords.py +13 -0
- models/media.py +597 -0
- models/media_info.py +142 -0
- models/movie.py +287 -0
- models/tv.py +266 -0
- models/tvdb_search.py +102 -0
- models/videos.py +26 -0
- repositories/__init__.py +0 -0
- repositories/db_online.py +82 -0
- repositories/interfaces.py +59 -0
- repositories/job_repos.py +166 -0
- repositories/media_info_factory.py +28 -0
- services/__init__.py +0 -0
- services/auto_async_service.py +237 -0
- services/create_torrent_service.py +94 -0
- services/interfaces.py +72 -0
- services/itt_tracker_helper.py +463 -0
- services/itt_tracker_service.py +85 -0
- services/lifespan_service.py +58 -0
- services/media_service.py +114 -0
- services/tags_service.py +389 -0
- services/tmdb.py +246 -0
- services/torrent_client_service.py +92 -0
- services/torrent_service.py +107 -0
- services/tvdb.py +65 -0
- services/utility.py +433 -0
- services/video_service.py +356 -0
- unit3dwebup-0.0.14.dist-info/METADATA +191 -0
- unit3dwebup-0.0.14.dist-info/RECORD +53 -0
- unit3dwebup-0.0.14.dist-info/WHEEL +5 -0
- unit3dwebup-0.0.14.dist-info/entry_points.txt +2 -0
- unit3dwebup-0.0.14.dist-info/top_level.txt +6 -0
- use_case/__init__.py +0 -0
- use_case/make_torrent_usecase.py +67 -0
- use_case/process_all_usecase.py +43 -0
- use_case/scan_media_usecase.py +133 -0
- use_case/seed_usecase.py +117 -0
- use_case/upload_usecase.py +77 -0
services/tmdb.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from models.movie import (Movie, AltTitle, Title, NowPlaying, MovieDetails, Genre,
|
|
6
|
+
ProductionCompany, SpokenLanguage, ProductionCountry)
|
|
7
|
+
from models.tv import (TVShowDetails, CreatedBy, Network, LastEpisodeToAir, Season,
|
|
8
|
+
OnTheAir, TvShow, DataResponse, Alternative)
|
|
9
|
+
from models.videos import Videos, Data
|
|
10
|
+
from models.keywords import Keyword
|
|
11
|
+
|
|
12
|
+
from external.async_http_client_service import AsyncHttpClient
|
|
13
|
+
|
|
14
|
+
from config.settings import get_settings
|
|
15
|
+
|
|
16
|
+
settings = get_settings()
|
|
17
|
+
BASE_URL = "https://api.themoviedb.org/3"
|
|
18
|
+
TMDB_APIKEY = settings.tracker.TMDB_APIKEY
|
|
19
|
+
T = TypeVar('T')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Endpoints TMDB
|
|
23
|
+
class TmdbEndpoints:
|
|
24
|
+
BASE_URL = "https://api.themoviedb.org/3"
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def movie_search(query: str) -> str:
|
|
28
|
+
return f"{TmdbEndpoints.BASE_URL}/search/movie"
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def tv_search(query: str) -> str:
|
|
32
|
+
return f"{TmdbEndpoints.BASE_URL}/search/tv"
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def alternative_movie(movie_id: int) -> str:
|
|
36
|
+
return f"{TmdbEndpoints.BASE_URL}/movie/{movie_id}/alternative_titles"
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def alternative_show(serie_id: int) -> str:
|
|
40
|
+
return f"{TmdbEndpoints.BASE_URL}/tv/{serie_id}/alternative_titles"
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def videos_movie(movie_id: int) -> str:
|
|
44
|
+
return f"{TmdbEndpoints.BASE_URL}/movie/{movie_id}/videos"
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def videos_show(serie_id: int) -> str:
|
|
48
|
+
return f"{TmdbEndpoints.BASE_URL}/tv/{serie_id}/videos"
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def movie_details(movie_id: int) -> str:
|
|
52
|
+
return f"{TmdbEndpoints.BASE_URL}/movie/{movie_id}"
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def tv_details(serie_id: int) -> str:
|
|
56
|
+
return f"{TmdbEndpoints.BASE_URL}/tv/{serie_id}"
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def movie_keywords(movie_id: int) -> str:
|
|
60
|
+
return f"{TmdbEndpoints.BASE_URL}/movie/{movie_id}/keywords"
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def tv_keywords(serie_id: int) -> str:
|
|
64
|
+
return f"{TmdbEndpoints.BASE_URL}/tv/{serie_id}/keywords"
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def movie_now_playing() -> str:
|
|
68
|
+
return f"{TmdbEndpoints.BASE_URL}/movie/now_playing"
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def tv_on_the_air() -> str:
|
|
72
|
+
return f"{TmdbEndpoints.BASE_URL}/tv/on_the_air"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# TMDB async
|
|
76
|
+
class TmdbAsyncAPI:
|
|
77
|
+
def __init__(self, session: aiohttp.ClientSession | None = None):
|
|
78
|
+
self.api_key = TMDB_APIKEY
|
|
79
|
+
self.language = "it-IT"
|
|
80
|
+
self.session = session or aiohttp.ClientSession()
|
|
81
|
+
self.http = AsyncHttpClient(self.session)
|
|
82
|
+
|
|
83
|
+
async def close(self):
|
|
84
|
+
await self.session.close()
|
|
85
|
+
|
|
86
|
+
async def search(self, query: str, category: str) -> list[T]:
|
|
87
|
+
"""
|
|
88
|
+
:param query: the title to search for
|
|
89
|
+
:param category: serie or movie
|
|
90
|
+
:return:
|
|
91
|
+
"""
|
|
92
|
+
url = TmdbEndpoints.movie_search(query) if category == "movie" else TmdbEndpoints.tv_search(query)
|
|
93
|
+
params = {"api_key": self.api_key, "query": query, "language": self.language}
|
|
94
|
+
|
|
95
|
+
data = await self.http.get(url, params=params)
|
|
96
|
+
|
|
97
|
+
results: list[T] = []
|
|
98
|
+
if data and "results" in data:
|
|
99
|
+
items = data["results"]
|
|
100
|
+
if category == "movie":
|
|
101
|
+
for item in items:
|
|
102
|
+
results.append(Movie(**item)) # converte JSON in dataclass Movie
|
|
103
|
+
else: # "tv"
|
|
104
|
+
for item in items:
|
|
105
|
+
results.append(TvShow(**item)) # converte JSON in dataclass TvShow
|
|
106
|
+
|
|
107
|
+
return results
|
|
108
|
+
|
|
109
|
+
async def alternative(self, movie_id: int, category: str) -> AltTitle | DataResponse | None:
|
|
110
|
+
"""
|
|
111
|
+
:param movie_id: the movie id
|
|
112
|
+
:param category: serie or movie
|
|
113
|
+
:return:
|
|
114
|
+
|
|
115
|
+
Get alternative title of a movie or serie
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
url = TmdbEndpoints.alternative_movie(movie_id) if category == "movie" else TmdbEndpoints.alternative_show(
|
|
119
|
+
movie_id)
|
|
120
|
+
params = {"api_key": self.api_key, "language": self.language}
|
|
121
|
+
|
|
122
|
+
data = await self.http.get(url, params=params)
|
|
123
|
+
|
|
124
|
+
if not data:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
if category == "movie":
|
|
128
|
+
titles = [title for t in data.get("titles", []) if (title := Title.from_data(t))]
|
|
129
|
+
|
|
130
|
+
return AltTitle(id=data.get("id", 0), titles=titles)
|
|
131
|
+
else: # series
|
|
132
|
+
results = [Alternative(**r) for r in data.get("results", [])]
|
|
133
|
+
return DataResponse(id=data.get("id", 0), results=results)
|
|
134
|
+
|
|
135
|
+
async def videos(self, movie_id: int, category: str) -> Data | None:
|
|
136
|
+
"""
|
|
137
|
+
:param movie_id: the movie id
|
|
138
|
+
:param category: serie or movie
|
|
139
|
+
:return:
|
|
140
|
+
|
|
141
|
+
Get trailers o associated video
|
|
142
|
+
"""
|
|
143
|
+
url = TmdbEndpoints.videos_movie(movie_id) if category == "movie" else TmdbEndpoints.videos_show(movie_id)
|
|
144
|
+
params = {"api_key": self.api_key, "language": self.language}
|
|
145
|
+
|
|
146
|
+
data = await self.http.get(url, params=params)
|
|
147
|
+
|
|
148
|
+
if not data:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
results = [Videos(**v) for v in data.get("results", [])]
|
|
152
|
+
return Data(id=data.get("id", 0), results=results)
|
|
153
|
+
|
|
154
|
+
async def details(self, video_id: int, category: str) -> MovieDetails | TVShowDetails | None:
|
|
155
|
+
"""
|
|
156
|
+
:param video_id: the video id
|
|
157
|
+
:param category: serie or movie
|
|
158
|
+
:return:
|
|
159
|
+
|
|
160
|
+
Get details about a movie or serie
|
|
161
|
+
"""
|
|
162
|
+
url = TmdbEndpoints.movie_details(video_id) if category == "movie" else TmdbEndpoints.tv_details(video_id)
|
|
163
|
+
params = {"api_key": self.api_key, "language": self.language}
|
|
164
|
+
|
|
165
|
+
data = await self.http.get(url, params=params)
|
|
166
|
+
|
|
167
|
+
if not data:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
if category == "movie":
|
|
171
|
+
data["genres"] = [Genre(**g) for g in data.get("genres", [])]
|
|
172
|
+
data["production_companies"] = [ProductionCompany(**c) for c in data.get("production_companies", [])]
|
|
173
|
+
data["production_countries"] = [ProductionCountry(**c) for c in data.get("production_countries", [])]
|
|
174
|
+
data["spoken_languages"] = [SpokenLanguage(**l) for l in data.get("spoken_languages", [])]
|
|
175
|
+
return MovieDetails(**data)
|
|
176
|
+
|
|
177
|
+
else: # tv
|
|
178
|
+
|
|
179
|
+
data["genres"] = [Genre(**g) for g in data.get("genres", [])]
|
|
180
|
+
data["created_by"] = [CreatedBy(**c) for c in data.get("created_by", [])]
|
|
181
|
+
data["networks"] = [Network(**n) for n in data.get("networks", [])]
|
|
182
|
+
data["production_companies"] = [ProductionCompany(**p) for p in data.get("production_companies", [])]
|
|
183
|
+
data["production_countries"] = [ProductionCountry(**c) for c in data.get("production_countries", [])]
|
|
184
|
+
data["spoken_languages"] = [SpokenLanguage(**l) for l in data.get("spoken_languages", [])]
|
|
185
|
+
|
|
186
|
+
if data.get("last_episode_to_air"):
|
|
187
|
+
data["last_episode_to_air"] = LastEpisodeToAir(**data["last_episode_to_air"])
|
|
188
|
+
|
|
189
|
+
if data.get("next_episode_to_air"):
|
|
190
|
+
data["next_episode_to_air"] = LastEpisodeToAir(**data["next_episode_to_air"])
|
|
191
|
+
|
|
192
|
+
data["seasons"] = [Season(**s) for s in data.get("seasons", [])]
|
|
193
|
+
return TVShowDetails(**data)
|
|
194
|
+
|
|
195
|
+
async def keywords(self, movie_id: int, category: str) -> list[Keyword] | None:
|
|
196
|
+
"""
|
|
197
|
+
:param movie_id: the movie id
|
|
198
|
+
:param category: serie or movie
|
|
199
|
+
:return:
|
|
200
|
+
|
|
201
|
+
Get the keywords associated with a movie or serie
|
|
202
|
+
"""
|
|
203
|
+
url = TmdbEndpoints.movie_keywords(movie_id) if category == "movie" else TmdbEndpoints.tv_keywords(movie_id)
|
|
204
|
+
params = {"api_key": self.api_key, "language": self.language}
|
|
205
|
+
data = await self.http.get(url, params=params)
|
|
206
|
+
if not data:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
# TMDB restituisce "keywords" per i film, "results" per TV
|
|
210
|
+
if category == "movie":
|
|
211
|
+
items = data.get("keywords", [])
|
|
212
|
+
else:
|
|
213
|
+
items = data.get("results", [])
|
|
214
|
+
|
|
215
|
+
return [Keyword(**k) for k in items]
|
|
216
|
+
|
|
217
|
+
async def playing(self, category: str) -> list[NowPlaying] | list[OnTheAir] | None:
|
|
218
|
+
"""
|
|
219
|
+
:param category: serie or movie
|
|
220
|
+
:return:
|
|
221
|
+
|
|
222
|
+
Get the last movie/series
|
|
223
|
+
"""
|
|
224
|
+
if category == "movie":
|
|
225
|
+
url = TmdbEndpoints.movie_now_playing()
|
|
226
|
+
elif category == "tv":
|
|
227
|
+
url = TmdbEndpoints.tv_on_the_air()
|
|
228
|
+
else:
|
|
229
|
+
raise ValueError(f"Invalid category: {category}")
|
|
230
|
+
|
|
231
|
+
params = {"api_key": self.api_key, "language": self.language}
|
|
232
|
+
|
|
233
|
+
data = await self.http.get(url, params=params)
|
|
234
|
+
|
|
235
|
+
if not data or "results" not in data:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
results = []
|
|
239
|
+
if category == "movie":
|
|
240
|
+
for item in data["results"]:
|
|
241
|
+
results.append(NowPlaying(**item))
|
|
242
|
+
else: # tv
|
|
243
|
+
for item in data["results"]:
|
|
244
|
+
results.append(OnTheAir(**item))
|
|
245
|
+
|
|
246
|
+
return results
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
logging.getLogger("urllib3.connectionpool").setLevel(logging.DEBUG)
|
|
8
|
+
|
|
9
|
+
from services.interfaces import TorrentClientServiceInterface
|
|
10
|
+
from config.settings import get_settings
|
|
11
|
+
from config.logger import get_logger
|
|
12
|
+
|
|
13
|
+
import qbittorrentapi
|
|
14
|
+
from qbittorrentapi import APIConnectionError
|
|
15
|
+
|
|
16
|
+
settings = get_settings()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QbittorrentClientService(TorrentClientServiceInterface):
|
|
20
|
+
"""
|
|
21
|
+
Qbittorrent client service
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.qbt_client = None
|
|
26
|
+
self._logged = False
|
|
27
|
+
self.logger = get_logger(self.__class__.__name__)
|
|
28
|
+
|
|
29
|
+
async def login(self) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
:return: True = success, False = fail
|
|
32
|
+
"""
|
|
33
|
+
if self._logged and self.qbt_client and self.qbt_client.is_logged_in:
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
conn_info = dict(
|
|
37
|
+
host=settings.torrent.QBIT_HOST,
|
|
38
|
+
port=settings.torrent.QBIT_PORT,
|
|
39
|
+
username=settings.torrent.QBIT_USER,
|
|
40
|
+
password=settings.torrent.QBIT_PASS,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.qbt_client = qbittorrentapi.Client(**conn_info)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
await asyncio.to_thread(self.qbt_client.auth_log_in)
|
|
47
|
+
self._logged = True
|
|
48
|
+
self.logger.info("Login successfully")
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
except qbittorrentapi.LoginFailed as e:
|
|
52
|
+
self.logger.error(e)
|
|
53
|
+
return False
|
|
54
|
+
except APIConnectionError as e:
|
|
55
|
+
self.logger.error(e)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
async def add_torrents(self, torrent_paths: list[str], save_path: str, app: FastAPI) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Add torrents to torrent client
|
|
61
|
+
|
|
62
|
+
:param torrent_paths: If Docker = true, torrent_path must refer the docker mounted path
|
|
63
|
+
:param save_path: The folder with data file, host path
|
|
64
|
+
:param app: app.state
|
|
65
|
+
:return: success or fail
|
|
66
|
+
"""
|
|
67
|
+
if not self.qbt_client or not self.qbt_client.is_logged_in:
|
|
68
|
+
await self.login()
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
await asyncio.to_thread(
|
|
72
|
+
self.qbt_client.torrents_add,
|
|
73
|
+
torrent_files=torrent_paths, # Docker
|
|
74
|
+
save_path=save_path, # Host
|
|
75
|
+
autoTMM=False,
|
|
76
|
+
paused=False,
|
|
77
|
+
is_skip_checking=True
|
|
78
|
+
)
|
|
79
|
+
except qbittorrentapi.exceptions.TorrentFileError as e:
|
|
80
|
+
logging.info(f"Add torrents {e}")
|
|
81
|
+
return False
|
|
82
|
+
# File is already seeding
|
|
83
|
+
except qbittorrentapi.exceptions.Conflict409Error as e:
|
|
84
|
+
logging.info(f"Add torrents {e}")
|
|
85
|
+
return False
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
async def list_torrents(self) -> list:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
async def remove_torrent(self, torrent_id: str) -> bool:
|
|
92
|
+
pass
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from services.create_torrent_service import MyTorrentService
|
|
5
|
+
from models.media import Media
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from multiprocessing import Manager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TorrentService:
|
|
12
|
+
"""
|
|
13
|
+
Accept a list of Media objects and create torrents in batch mode
|
|
14
|
+
TODO For the moment n° workers and batch_size are hardcoded
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, app: FastAPI):
|
|
18
|
+
"""
|
|
19
|
+
:param app: FastApi
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Process media
|
|
23
|
+
self.media_list: list[Media] | None = None
|
|
24
|
+
|
|
25
|
+
# FastApi instance app
|
|
26
|
+
self.app = app
|
|
27
|
+
|
|
28
|
+
# Torrent creation
|
|
29
|
+
self.torr_service = MyTorrentService(app=app)
|
|
30
|
+
|
|
31
|
+
# Use the queue to get the progress % during the torrent creation process
|
|
32
|
+
self.manager = Manager()
|
|
33
|
+
self.progress_queue = self.manager.Queue()
|
|
34
|
+
self.result_queue = self.manager.Queue()
|
|
35
|
+
|
|
36
|
+
def run_batch(self):
|
|
37
|
+
"""
|
|
38
|
+
Try to process group of files to avoid ssd saturation
|
|
39
|
+
:return:
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# TODO: ha senso spingere con i workers se il mio ssd lavoro già al 100%?
|
|
43
|
+
try:
|
|
44
|
+
results = self.torr_service.start(self.media_list, batch_size=16, workers=4,
|
|
45
|
+
progress_queue=self.progress_queue)
|
|
46
|
+
self.result_queue.put(results)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(e)
|
|
49
|
+
|
|
50
|
+
async def start(self, media_list: list[Media]):
|
|
51
|
+
"""
|
|
52
|
+
Start the torrent service
|
|
53
|
+
:return:
|
|
54
|
+
"""
|
|
55
|
+
self.media_list = media_list
|
|
56
|
+
|
|
57
|
+
# Run run_batch() in background
|
|
58
|
+
thread = Thread(target=self.run_batch)
|
|
59
|
+
thread.start()
|
|
60
|
+
|
|
61
|
+
# Keep looping while the thread is running or the queue is not empty
|
|
62
|
+
all_progress = {}
|
|
63
|
+
while thread.is_alive() or not self.progress_queue.empty():
|
|
64
|
+
while not self.progress_queue.empty():
|
|
65
|
+
# The worker fill the queue, here we consume it and forward the progress to the client
|
|
66
|
+
update = self.progress_queue.get()
|
|
67
|
+
|
|
68
|
+
# For each torrent send the current progress...
|
|
69
|
+
all_progress[update['torrent']] = update['progress']
|
|
70
|
+
await self.app.state.ws_manager.broadcast({
|
|
71
|
+
"type": "progress",
|
|
72
|
+
"level": "progress",
|
|
73
|
+
"job_id": update['job_id'],
|
|
74
|
+
"process": "Torrent",
|
|
75
|
+
"progress": update['progress'],
|
|
76
|
+
"message": f"[New torrent] {update['torrent']} - {round(update['progress'], 2)}",
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
print("log", "progress", update['job_id'], update['progress'],
|
|
80
|
+
f"[New torrent] {update['torrent']} - {round(update['progress'], 2)}")
|
|
81
|
+
|
|
82
|
+
await asyncio.sleep(0.1)
|
|
83
|
+
|
|
84
|
+
thread.join()
|
|
85
|
+
results = self.result_queue.get()
|
|
86
|
+
|
|
87
|
+
# Invio log finali
|
|
88
|
+
for torrent in results:
|
|
89
|
+
if torrent.get('status') == '200':
|
|
90
|
+
await self.app.state.ws_manager.broadcast({
|
|
91
|
+
"type": "progress",
|
|
92
|
+
"level": "progress",
|
|
93
|
+
"job_id": torrent['job_id'],
|
|
94
|
+
"process": "Completed",
|
|
95
|
+
"progress": 100.0,
|
|
96
|
+
"message": f"[New torrent] {torrent['message'].name} from {torrent['message'].parent}"
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if torrent.get('status') == '409':
|
|
100
|
+
await self.app.state.ws_manager.broadcast({
|
|
101
|
+
"type": "progress",
|
|
102
|
+
"level": "progress",
|
|
103
|
+
"job_id": torrent['job_id'],
|
|
104
|
+
"process": "Error",
|
|
105
|
+
"progress": 100.0,
|
|
106
|
+
"message": f"[Reloaded] {torrent['message'].name} from {torrent['message'].parent}"
|
|
107
|
+
})
|
services/tvdb.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from models.tvdb_search import TvdbSearchResult
|
|
5
|
+
from models.interfaces import MediaRepoInterface
|
|
6
|
+
|
|
7
|
+
from external.async_http_client_service import AsyncHttpClient
|
|
8
|
+
from config.settings import get_settings
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
settings = get_settings()
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TvdbAsyncAPI:
|
|
17
|
+
def __init__(self, session: aiohttp.ClientSession | None = None):
|
|
18
|
+
"""
|
|
19
|
+
:param session: an active aiohttp session
|
|
20
|
+
"""
|
|
21
|
+
# Use the apikey received from the website (apikey for project)
|
|
22
|
+
self.api_key = settings.tracker.TVDB_APIKEY
|
|
23
|
+
# get a new session
|
|
24
|
+
self.session = session or aiohttp.ClientSession()
|
|
25
|
+
# an http async client instance
|
|
26
|
+
self.http = AsyncHttpClient(self.session)
|
|
27
|
+
# the classe state for tvdb token
|
|
28
|
+
self.token = None
|
|
29
|
+
|
|
30
|
+
async def tvdb_login(self) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Pass the api_key to Tvdb wait for a new jwt token
|
|
33
|
+
:return: None
|
|
34
|
+
"""
|
|
35
|
+
if self.token:
|
|
36
|
+
return self.token
|
|
37
|
+
url = "https://api4.thetvdb.com/v4/login"
|
|
38
|
+
payload = {"apikey": self.api_key}
|
|
39
|
+
|
|
40
|
+
resp = await self.session.post(url, json=payload)
|
|
41
|
+
data = await resp.json()
|
|
42
|
+
self.token = data["data"]["token"]
|
|
43
|
+
return self.token
|
|
44
|
+
|
|
45
|
+
async def search(self, query: str, category: str = "series") -> list[MediaRepoInterface]:
|
|
46
|
+
"""
|
|
47
|
+
:param query: the searched title
|
|
48
|
+
:param category: restrict search to 'movies' or 'series'
|
|
49
|
+
:return: a TVDB repository implementing the MediaRepoInterface
|
|
50
|
+
"""
|
|
51
|
+
if not self.token:
|
|
52
|
+
self.token = await self.tvdb_login()
|
|
53
|
+
|
|
54
|
+
url = f"https://api4.thetvdb.com/v4/search?query={query}&type={category}"
|
|
55
|
+
headers = {"Authorization": f"Bearer {self.token}"}
|
|
56
|
+
|
|
57
|
+
resp = await self.session.get(url, headers=headers)
|
|
58
|
+
resp.raise_for_status()
|
|
59
|
+
payload = await resp.json()
|
|
60
|
+
|
|
61
|
+
# Build TvdbSearchResult objects
|
|
62
|
+
return [
|
|
63
|
+
TvdbSearchResult.from_dict(item)
|
|
64
|
+
for item in payload.get("data", [])
|
|
65
|
+
]
|