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
@@ -0,0 +1,237 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ import asyncio
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from repositories.media_info_factory import MediaFileFactory
9
+ from services.utility import ManageTitles
10
+ from services.tags_service import SearchTags
11
+ from config.constants import MediaStatus
12
+ from config.logger import get_logger
13
+ from models.media import Media
14
+
15
+ from fastapi import FastAPI
16
+
17
+
18
+ class AsyncMediaManager:
19
+ """
20
+ Process files and folder
21
+ Every file or folder is converted in a Media Object
22
+ Every Media object receives additional values
23
+ """
24
+
25
+ def __init__(self, path: str, app: FastAPI, job_id_list: str, force_category: int = None):
26
+ """
27
+ :param path: The user path
28
+ :param force_category: force category to movie or series
29
+ """
30
+ self.path = os.path.normpath(path)
31
+ self.job_id_list = job_id_list
32
+ self.app = app
33
+ self.mode = 'auto'
34
+ self.force_category = force_category
35
+ self.is_dir = os.path.isdir(self.path)
36
+ self.media_list: list[Media] = []
37
+ self.sem = asyncio.Semaphore(60)
38
+ self.logger = get_logger(self.__class__.__name__)
39
+
40
+ @staticmethod
41
+ def scan_folder(path: Path):
42
+ with os.scandir(path) as it:
43
+ for entry in it:
44
+ if entry.is_file() and ManageTitles.filter_ext(entry.name):
45
+ yield entry
46
+
47
+ async def process_all(self) -> list[Media]:
48
+ """
49
+ start method to process all files
50
+
51
+ :returns:
52
+ list[str]: a list of media objects
53
+ """
54
+ if os.path.isfile(self.path):
55
+ self.mode = "man"
56
+
57
+ if self.mode in ["man", "folder"]:
58
+ self.media_list = await self.upload()
59
+ else: # modalità 'auto'
60
+ self.media_list = await self.scan()
61
+
62
+ tasks = [self.process_media(media) for media in self.media_list]
63
+
64
+ results = await asyncio.gather(*tasks)
65
+ return [m for m in results if m]
66
+
67
+ async def scan(self) -> list[Media]:
68
+ """
69
+ process all files or folders ( 1 subfolder only) ( auto)
70
+
71
+ :returns:
72
+ list[str]: a list of media objects
73
+ """
74
+ if not os.path.exists(self.path):
75
+ self.logger.warning(f"Path not found {self.path}")
76
+ return []
77
+
78
+ if not self.is_dir:
79
+ self.logger.warning(f"We can't scan a file {self.path}")
80
+ return []
81
+
82
+ entries = await asyncio.to_thread(os.listdir, self.path)
83
+ if not entries:
84
+ self.logger.warning(f"Folder {self.path} is empty")
85
+
86
+ files = [Path(self.path) / e for e in entries if
87
+ os.path.isfile(Path(self.path) / e) and ManageTitles.filter_ext(e)]
88
+ subfolders = [Path(self.path) / e for e in entries if os.path.isdir(Path(self.path) / e)]
89
+
90
+ async def safe_create(path):
91
+ async with self.sem:
92
+ return await self._create_media_list([path])
93
+
94
+ tasks = [safe_create(p) for p in files + subfolders]
95
+ media_list = await asyncio.gather(*tasks)
96
+ # Clean the list, remove None object
97
+ return [m for sublist in media_list for m in sublist if m]
98
+
99
+ async def upload(self) -> list[Media]:
100
+ """
101
+ process only one file or folder
102
+
103
+ :returns:
104
+ list[str]: a list of media objects
105
+ """
106
+
107
+ if self.is_dir:
108
+ files = await asyncio.to_thread(os.listdir, self.path)
109
+ video_files = [Path(self.path) / f for f in files if ManageTitles.filter_ext(f)]
110
+
111
+ if self.mode == "man":
112
+ return await self._create_media_list(video_files)
113
+ if self.mode == "folder":
114
+ return await self._create_media_list([self.path])
115
+ return []
116
+ else:
117
+ return await self._create_media_list([self.path])
118
+
119
+ async def _create_media_list(self, paths: list[str]) -> list[Media]:
120
+ """
121
+ create a Media object for each single file or single folder encountered
122
+ files inside subfolders are not converted in Media objects
123
+ :param paths: list of file path
124
+ :return: list[Media]: a list of media objects based on user path
125
+ """
126
+
127
+ return [
128
+ Media(folder=self.path, subfolder=os.path.basename(_path),
129
+ torrent_archive_path=self.app.state.torrent_archive_path)
130
+ for _path in paths
131
+ if _path is not None
132
+ ]
133
+
134
+ async def process_media(self, media: Media) -> Media | None:
135
+ """
136
+ start method to process media object ( file or folder)
137
+
138
+ :param media: single Media object
139
+ :return:
140
+ """
141
+ path = media.torrent_path
142
+ if self.force_category:
143
+ media.category = self.force_category
144
+
145
+ if os.path.isdir(path):
146
+ success = await self.process_folder(media)
147
+ elif os.path.isfile(path):
148
+ success = await self.process_file(media)
149
+ else:
150
+ self.logger.warning("Process Media return None")
151
+ return None
152
+
153
+ # Add a Mediainfo object
154
+ if success:
155
+ if media.file_name:
156
+ media.mediafile = await MediaFileFactory.from_path(media.file_name)
157
+ assert media.mediafile is not None, "MediaFileFactory ha restituito None"
158
+ await self.app.state.ws_manager.broadcast({
159
+ "type": "log",
160
+ "level": "success",
161
+ "message": f"Process media -> {media.display_name}",
162
+ })
163
+ ######################################################
164
+ search_tags = SearchTags(filename=media.title,
165
+ title=media.guess_filename.guessit_title, # guess.get("title", None),
166
+ year=media.guess_filename.guessit_year, # guess.get("year", ""),
167
+ season=media.guess_season,
168
+ episode=media.guess_episode,
169
+ releaser_sign=media.releaser_sign,
170
+ # config_settings.user_preferences.RELEASER_SIGN,
171
+ tags_position=media.tag_position,
172
+ tags_list=media.tags_list,
173
+ signs_list=media.signs_list,
174
+ ban_list=media.ban_list,
175
+ media=media,
176
+ )
177
+ media.display_name = search_tags.process()
178
+ #######################################################
179
+ return media
180
+
181
+ return None
182
+
183
+ async def process_file(self, media: Media) -> bool:
184
+ """
185
+ process media object file
186
+
187
+ :param media: single Media object
188
+ :return: success or failure
189
+ """
190
+ media.status = MediaStatus.INDEXED
191
+ media.job_id_list = self.job_id_list
192
+ # Convert PosixPath to str
193
+ media.file_name = str(media.torrent_path)
194
+ media.display_name, _ = os.path.splitext(os.path.basename(media.file_name))
195
+ media.display_name = ManageTitles.clean_text(media.display_name)
196
+ media.torrent_name = os.path.basename(media.file_name)
197
+ media.doc_description = media.file_name
198
+
199
+ # Read file size value
200
+ async with self.sem:
201
+ media.size = await asyncio.to_thread(lambda: os.stat(media.file_name).st_size)
202
+
203
+ # Convert PosixPath to str and build info
204
+ media.metainfo = json.dumps([{"length": media.size, "path": [media.file_name]}], indent=4)
205
+ return True
206
+
207
+ async def process_folder(self, media: Media) -> bool:
208
+ """
209
+ process media object folder
210
+
211
+ :param media: single Media object
212
+ :return: success or failure
213
+ """
214
+ # Read the folder and create a list of files
215
+ # TODO forse ha poco senso asyncio.to_thread qui
216
+ entries = await asyncio.to_thread(os.listdir, media.torrent_path)
217
+ files = [my_file for my_file in entries if ManageTitles.filter_ext(my_file)]
218
+
219
+ if not files:
220
+ return False
221
+
222
+ files.sort()
223
+ media.status = MediaStatus.INDEXED
224
+ media.job_id_list = self.job_id_list
225
+ media.file_name = Path(media.torrent_path) / files[0]
226
+ media.display_name = ManageTitles.clean_text(Path(media.torrent_path).name)
227
+ media.torrent_name = Path(media.torrent_path).name
228
+ media.doc_description = "\n".join(files)
229
+
230
+ entries = await asyncio.to_thread(lambda: list(self.scan_folder(media.torrent_path)))
231
+ sizes = [e.stat().st_size for e in entries]
232
+ files = [e.name for e in entries]
233
+
234
+ # Build metainfo
235
+ media.size = sum(sizes)
236
+ media.metainfo = json.dumps([{"length": s, "path": [f]} for f, s in zip(files, sizes)], indent=4)
237
+ return True
@@ -0,0 +1,94 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ from multiprocessing import Pool, cpu_count, Manager
4
+ from pathlib import Path
5
+
6
+ from services.interfaces import TorrentServiceInterface
7
+ from config.api_data import trackers_api_data
8
+ from config.constants import MediaStatus
9
+ from config.settings import get_settings
10
+ from models.media import Media
11
+
12
+ from fastapi import FastAPI
13
+ import torf
14
+
15
+ settings = get_settings()
16
+
17
+
18
+ def worker(args) -> dict:
19
+ torrent_path, torrent_name, torrent_archive, trackers_list, progress_queue, job_id = args
20
+ archive = torrent_archive or '.'
21
+ os.makedirs(archive, exist_ok=True)
22
+ torrent_filepath: Path = (Path(archive) / trackers_list[0] / f"{torrent_name}.torrent")
23
+
24
+ announces = [
25
+ trackers_api_data[t.upper()]['announce']
26
+ for t in (trackers_list or []) if t.upper() in trackers_api_data
27
+ ]
28
+
29
+ mytorr = torf.Torrent(path=torrent_path, trackers=announces)
30
+ mytorr.comment = settings.prefs.TORRENT_COMMENT
31
+ mytorr.name = torrent_name
32
+ mytorr.created_by = ""
33
+ mytorr.private = True
34
+ mytorr.source = trackers_list[0] if trackers_list else None
35
+ mytorr.segments = 16 * 1024 * 1024
36
+
37
+ # The callback
38
+ def callback_progress(torrent, filepath, pieces_done, pieces_total):
39
+ progress = pieces_done / pieces_total * 100
40
+ progress_queue.put({'torrent': torrent_name, 'progress': progress, 'job_id': job_id})
41
+
42
+ mytorr.generate(callback=callback_progress, interval=1)
43
+ mytorr.write(torrent_filepath)
44
+ return {'status': '200', 'message': torrent_filepath, 'job_id': job_id}
45
+
46
+
47
+ class MyTorrentService(TorrentServiceInterface):
48
+ def __init__(self, app: FastAPI):
49
+ self.app = app
50
+
51
+ def start(self, media_list: list[Media], batch_size=16, workers: int = 4, progress_queue=None):
52
+ """
53
+ :param media_list: list of Media objects to process
54
+ :param batch_size: numer of item per batch
55
+ :param workers: number of workers
56
+ :param progress_queue: a safe queue where put the progress
57
+ :return:
58
+ """
59
+ results = []
60
+ # Sort by Size ( start with the smallest)
61
+ media_list.sort(key=lambda m: m.size)
62
+
63
+ # Create one or more batch list and call the function
64
+ for i in range(0, len(media_list), batch_size):
65
+ batch = media_list[i:i + batch_size]
66
+ batch_results = self._create_batch(batch, trackers=['ITT'], workers=workers, progress_queue=progress_queue)
67
+ results.extend(batch_results)
68
+ return results
69
+
70
+ def _create_batch(self, media_list: list[Media], trackers: list[str] = None, workers: int = None,
71
+ progress_queue=None):
72
+
73
+ with Manager() as manager:
74
+ if progress_queue is None:
75
+ progress_queue = manager.Queue()
76
+
77
+ # Create a list of task only for Media with description and video status positive
78
+ archive_path = self.app.state.torrent_archive_path or '.'
79
+ jobs = [
80
+ (m.torrent_path, m.torrent_name, archive_path, trackers, progress_queue, m.job_id)
81
+ for m in media_list if m.status not in (MediaStatus.DESCRIPTION_ERROR, MediaStatus.VIDEO_ERROR)
82
+ ]
83
+
84
+ # Go
85
+ workers = workers or cpu_count()
86
+ with Pool(workers) as pool:
87
+ results = pool.map(worker, jobs)
88
+
89
+ # Wait for the end
90
+ while not progress_queue.empty():
91
+ progress_update = progress_queue.get()
92
+ print(f"Torrent {progress_update['torrent']}: {progress_update['progress']:.2f}%")
93
+
94
+ return results
services/interfaces.py ADDED
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Iterable
5
+ from fastapi import FastAPI
6
+ from models.media import Media
7
+
8
+
9
+ class VideoServiceInterface(ABC):
10
+ @abstractmethod
11
+ async def generate(self) -> None:
12
+ """Extract screenshot"""
13
+ ...
14
+
15
+
16
+ class DescriptionBuilderInterface(ABC):
17
+ @abstractmethod
18
+ async def description(self) -> None:
19
+ """Build a description for the tracker"""
20
+ ...
21
+
22
+ @abstractmethod
23
+ async def close(self) -> None: ...
24
+
25
+
26
+ class TorrentServiceInterface(ABC):
27
+
28
+ @abstractmethod
29
+ def _create_batch(self, media_list: Iterable[Media], trackers: list[str] = None,
30
+ workers: int | None = None) -> list[str]:
31
+ """Process groups of media and create for each a torrent file"""
32
+ ...
33
+
34
+ @abstractmethod
35
+ def start(self, media_list: list[Media], batch_size=16, workers: int = 2):
36
+ """ Build a list of batches """
37
+ ...
38
+
39
+
40
+ class TrackerServiceInterface(ABC):
41
+
42
+ @abstractmethod
43
+ async def prepare_payload(self, media: Media) -> dict:
44
+ """ Build the payload for the tracker """
45
+ pass
46
+
47
+ @abstractmethod
48
+ async def upload(self, media: Media) -> dict:
49
+ """Upload the torrent and return a result"""
50
+ pass
51
+
52
+ @abstractmethod
53
+ async def search(self, query: str) -> dict:
54
+ """ Search for a title """
55
+ pass
56
+
57
+
58
+ class TorrentClientServiceInterface(ABC):
59
+ """
60
+ Interface for torrent clients
61
+ """
62
+ @abstractmethod
63
+ async def login(self) -> bool: ...
64
+
65
+ @abstractmethod
66
+ async def add_torrents(self, torrent_paths: list[str], save_path: str, app: FastAPI) -> bool: ...
67
+
68
+ @abstractmethod
69
+ async def list_torrents(self) -> list: ...
70
+
71
+ @abstractmethod
72
+ async def remove_torrent(self, torrent_id: str) -> bool: ...