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.
Files changed (53) hide show
  1. config/__init__.py +4 -0
  2. config/api_data.py +28 -0
  3. config/constants.py +34 -0
  4. config/host_data.py +47 -0
  5. config/itt.py +154 -0
  6. config/logger.py +37 -0
  7. config/settings.py +212 -0
  8. config/sis.py +137 -0
  9. config/tags.py +395 -0
  10. config/trackers.py +47 -0
  11. external/__init__.py +0 -0
  12. external/async_http_client_service.py +48 -0
  13. external/websocket.py +41 -0
  14. models/__init__.py +0 -0
  15. models/interfaces.py +39 -0
  16. models/keywords.py +13 -0
  17. models/media.py +597 -0
  18. models/media_info.py +142 -0
  19. models/movie.py +287 -0
  20. models/tv.py +266 -0
  21. models/tvdb_search.py +102 -0
  22. models/videos.py +26 -0
  23. repositories/__init__.py +0 -0
  24. repositories/db_online.py +82 -0
  25. repositories/interfaces.py +59 -0
  26. repositories/job_repos.py +166 -0
  27. repositories/media_info_factory.py +28 -0
  28. services/__init__.py +0 -0
  29. services/auto_async_service.py +237 -0
  30. services/create_torrent_service.py +94 -0
  31. services/interfaces.py +72 -0
  32. services/itt_tracker_helper.py +463 -0
  33. services/itt_tracker_service.py +85 -0
  34. services/lifespan_service.py +58 -0
  35. services/media_service.py +114 -0
  36. services/tags_service.py +389 -0
  37. services/tmdb.py +246 -0
  38. services/torrent_client_service.py +92 -0
  39. services/torrent_service.py +107 -0
  40. services/tvdb.py +65 -0
  41. services/utility.py +433 -0
  42. services/video_service.py +356 -0
  43. unit3dwebup-0.0.14.dist-info/METADATA +191 -0
  44. unit3dwebup-0.0.14.dist-info/RECORD +53 -0
  45. unit3dwebup-0.0.14.dist-info/WHEEL +5 -0
  46. unit3dwebup-0.0.14.dist-info/entry_points.txt +2 -0
  47. unit3dwebup-0.0.14.dist-info/top_level.txt +6 -0
  48. use_case/__init__.py +0 -0
  49. use_case/make_torrent_usecase.py +67 -0
  50. use_case/process_all_usecase.py +43 -0
  51. use_case/scan_media_usecase.py +133 -0
  52. use_case/seed_usecase.py +117 -0
  53. 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
+ ]