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,114 @@
1
+ # -*- coding: utf-8 -*-
2
+ from datetime import datetime
3
+
4
+ from models.movie import AltTitle
5
+ from models.tv import Alternative, DataResponse
6
+ from repositories.interfaces import MovieRepositoryInterface
7
+ from services.utility import ManageTitles
8
+ from config.settings import get_settings
9
+
10
+
11
+ # based on old code unit3dup 0.8.21
12
+ class MediaService:
13
+ def __init__(self, repository: MovieRepositoryInterface):
14
+ self.repo = repository
15
+ self.settings = get_settings()
16
+
17
+ async def fetch(self, media):
18
+ async def get_keyword() -> str | None:
19
+ keywords = await self.repo.keywords(movie_id=show.get_id(), category=media.category)
20
+ return " ".join([key.name for key in keywords]) if keywords else None
21
+
22
+ async def get_trailer() -> str | None:
23
+ if self.settings.prefs.SKIP_YOUTUBE:
24
+ return None
25
+ trailers = await self.repo.videos(movie_id=show.get_id(), category=media.category)
26
+ if trailers:
27
+ trailer = next((video.key for video in trailers.results if video.type.lower() == 'trailer' # .results
28
+ and video.site.lower() == 'youtube'), None)
29
+ return trailer if trailer else None
30
+ return None
31
+
32
+ results = await self.repo.search(query=media.guess_title, category=media.category)
33
+ for show in results:
34
+ if show.get_date() and media.guess_filename.guessit_year:
35
+ if not datetime.strptime(show.get_date(), '%Y-%m-%d').year == media.guess_filename.guessit_year:
36
+ continue
37
+
38
+ # Search for title
39
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(show.get_title())) > 95:
40
+ media.tmdb_id = show.get_id()
41
+ media.imdb_id_from_tvdb = show.get_imdb()
42
+ media.keyword = await get_keyword()
43
+ media.trailer = await get_trailer()
44
+ media.backdrop_path = show.get_poster_path()
45
+ return True
46
+
47
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(show.get_original())) > 95:
48
+ media.tmdb_id = show.get_id()
49
+ media.imdb_id_from_tvdb = show.get_imdb()
50
+ media.keyword = await get_keyword()
51
+ media.trailer = await get_trailer()
52
+ media.backdrop_path = show.get_poster_path()
53
+ return True
54
+
55
+ alt_list = []
56
+ for show in results:
57
+ # Search for alternative title TODO: rework dataclass
58
+ alternative = await self.repo.alternative(movie_id=show.get_id(), category=media.category)
59
+ if isinstance(alternative, DataResponse):
60
+ alt_list = [alt.title for alt in alternative.results]
61
+ if isinstance(alternative, AltTitle):
62
+ alt_list = [alt.title for alt in alternative.titles]
63
+ for title in alt_list:
64
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(title)) > 95:
65
+ media.tmdb_id = show.get_id()
66
+ media.keyword = await get_keyword()
67
+ media.trailer = await get_trailer()
68
+ media.backdrop_path = show.get_poster_path()
69
+ return True
70
+ return False
71
+
72
+
73
+ class MediaService2:
74
+ def __init__(self, repository: MovieRepositoryInterface):
75
+ self.repo = repository
76
+
77
+ async def fetch(self, media):
78
+ results = await self.repo.search(query=media.guess_title, category=media.category)
79
+
80
+ for show in results:
81
+ if show.get_date() and media.guess_filename.guessit_year:
82
+ if not datetime.strptime(show.get_date(), '%Y-%m-%d').year == media.guess_filename.guessit_year:
83
+ continue
84
+
85
+ # Search for title
86
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(show.get_title())) > 95:
87
+ media.imdb_id_from_tvdb = show.get_imdb()
88
+ media.tvdb_id = show.get_id()
89
+ return True
90
+
91
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(show.get_original())) > 95:
92
+ media.imdb_id_from_tvdb = show.get_imdb()
93
+ media.tvdb_id = show.get_id()
94
+ return True
95
+
96
+ # Search for alternative title ( translations field)
97
+ alternative = show.get_translations()
98
+ title_ita = alternative.get('ita', None)
99
+ title_eng = alternative.get('eng', None)
100
+
101
+ if title_ita:
102
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(title_ita)) > 95:
103
+ media.imdb_id_from_tvdb = show.get_imdb()
104
+ media.tvdb_id = show.get_id()
105
+ return True
106
+
107
+ # The result is not always in english
108
+ if title_eng:
109
+ if ManageTitles.fuzzyit(str1=media.guess_title, str2=ManageTitles.clean_text(title_eng)) > 95:
110
+ media.imdb_id_from_tvdb = show.get_imdb()
111
+ media.tvdb_id = show.get_id()
112
+ return True
113
+
114
+ return False
@@ -0,0 +1,389 @@
1
+ # -*- coding: utf-8 -*-
2
+ import re
3
+ from services.utility import ManageTitles
4
+ from config.logger import get_logger
5
+ from models.media import Media
6
+
7
+ # From hdr format
8
+ hdr_map = {
9
+ "DOLBY VISION": "DV",
10
+ "DOLBY VISION HDR": "DV HDR",
11
+ "DOLBY VISION HDR10": "DV HDR10",
12
+ "DOLBY VISION HDR10+": "DV HDR10+",
13
+ "HDR10PLUS": "HDR10+",
14
+ "HDRPLUS+": "HDR10+",
15
+ "HDR10+": "HDR10+",
16
+ "HDR10": "HDR10",
17
+ "BLU-RAY / HDR10": "HDR10",
18
+ "HDR10 / HDR10": "HDR10",
19
+ "HDR10 / HDR10+": "HDR10+",
20
+ "HDR10 / HDR10 / HDR10+": "HDR10+",
21
+ "SMPTE ST 2086": "HDR10",
22
+ "SMPTE ST 2094": "HDR10+",
23
+
24
+ "DOVI": "DV",
25
+ "HDR": "HDR",
26
+ }
27
+
28
+ audio_translate = {
29
+ "AC3": "DD",
30
+ "AAC LC": "AAC",
31
+ "AAC LC SBR": "HE-AAC",
32
+ "AC-3": "DD",
33
+ "EAC3": "DD+",
34
+ "E-AC3": "DD+",
35
+ "E-AC-3": "DD+",
36
+ "E-AC-3 JOC": "DD+",
37
+ "DTS": "DTS",
38
+ "DTS ES": "DTS-ES",
39
+ "DTS ES XLL": "DTS-HD MA",
40
+ "DTS XLL": "DTS-HD MA",
41
+ "MLP FBA 16-ch": "TrueHD",
42
+ "MPEG Audio": "MPEG",
43
+ }
44
+
45
+ video_translate = {
46
+ "AVC": "H.264",
47
+ "HEVC": "H.265",
48
+ "H265": "H.265",
49
+ "H264": "H.264",
50
+ }
51
+
52
+ video_encoder_translate = {
53
+ "X265": "x.265",
54
+ "X264": "x.264",
55
+ }
56
+
57
+
58
+ class SearchTags(object):
59
+ def __init__(self, filename, title: str, year: str | None, season: int | None, episode: int | None,
60
+ media: Media, tags_position: list, tags_list: dict, signs_list: dict, ban_list: dict,
61
+ releaser_sign: str):
62
+
63
+ self.tags_position = tags_position
64
+ self.releaser_sign = releaser_sign
65
+ self.mediafile = media.mediafile
66
+ self.filename = filename
67
+ self.episode = episode
68
+ self.season = season
69
+ self.title = title
70
+ self.year = year
71
+ self.tags_dict = {}
72
+ self.tags_position = tags_position
73
+ self.TAG_TYPES: dict = tags_list
74
+ self.SIGNS_LIST: dict = signs_list
75
+ self.BAN_LIST: dict = ban_list
76
+ self.logger = get_logger(self.__class__.__name__)
77
+
78
+ @staticmethod
79
+ def normalize_version_tag(tag: str) -> str:
80
+ tag_esc = re.escape(tag)
81
+ return tag_esc
82
+
83
+ @staticmethod
84
+ def normalize_part_tag(title: str) -> str | None:
85
+ """
86
+ Extract substring PartX
87
+ Try to remove noisy chars and return a normalized tag
88
+ Part1, Part 1, Part.1, [Part 1], Parte1, Pt1, Prt 2,
89
+ """
90
+ pattern = r'[\[\(]?\b(?:Part|Parte|Pt|Prt)[\s\.-]*?(\d+)\b[\]\)]?'
91
+ match = re.search(pattern, title, re.IGNORECASE)
92
+ if match:
93
+ part_number = match.group(1)
94
+ return f"Part {part_number}"
95
+ return None
96
+
97
+ @staticmethod
98
+ def normalize_platform_tag(tag: str) -> str:
99
+ tag_esc = re.escape(tag)
100
+ return tag_esc
101
+
102
+ @staticmethod
103
+ def normalize_sources(tag: str) -> str:
104
+ tag_esc = re.escape(tag)
105
+ return tag_esc
106
+
107
+ @staticmethod
108
+ def normalize_video_encoder(tag: str) -> str:
109
+ tag_esc = re.escape(tag)
110
+ tag_esc = re.sub(r'([A-Z])(\d+)', r'\1[._-]?\2', tag_esc)
111
+ return tag_esc
112
+
113
+ def build_title(self, dictionary: dict) -> str:
114
+ # /// Build the title
115
+ build = []
116
+ for k, v in dictionary.items():
117
+ if isinstance(v, list):
118
+ for item in v:
119
+ build.append(str(item))
120
+ else:
121
+ build.append(str(v))
122
+
123
+ refactored = ' '.join(build) + self.releaser_sign
124
+ return refactored
125
+
126
+ def process(self) -> str:
127
+ patterns = []
128
+
129
+ # Remove banned items from categories
130
+ self.tags_position = [x for x in self.tags_position if x not in self.BAN_LIST]
131
+
132
+ # loop sorted TAG_TYPES dictionary
133
+ for i, (tag, category) in enumerate(
134
+ sorted(self.TAG_TYPES.items(), key=lambda x: len(x[0]), reverse=True)
135
+ ):
136
+ if category == "version":
137
+ norm = self.normalize_version_tag(tag)
138
+ elif category == "platform":
139
+ norm = self.normalize_platform_tag(tag)
140
+ elif category == "source":
141
+ norm = self.normalize_sources(tag)
142
+ elif category == "video_encoder":
143
+ norm = self.normalize_video_encoder(tag)
144
+ else:
145
+ norm = re.escape(tag)
146
+ # Save a regex pattern for each category
147
+ patterns.append([norm, category])
148
+
149
+ # Run regex
150
+ for p, category in patterns:
151
+ regex = re.compile(r'(?<!\w)' + p + r'(?!\w)', re.IGNORECASE)
152
+ matches = regex.findall(self.filename)
153
+ if matches:
154
+ self.tags_dict.setdefault(category, []).append(matches[0])
155
+
156
+ # /// Tags with no categories
157
+ # Identify PartX
158
+ norm = self.normalize_part_tag(self.filename)
159
+ if norm:
160
+ # Skip if it is part of title es: "Wicked.Parte.2.2025.iTA" Title = Wicked Parte 2
161
+ if not any(t in self.title.lower() for t in ['part', 'parte']):
162
+ self.tags_dict.update({'part': norm})
163
+
164
+ # /// Read from mediainfo
165
+ updated_category = {}
166
+ for category in self.tags_position:
167
+ if category == "acodec":
168
+ updated_category = self.mediainfo_audio(category=category)
169
+
170
+ elif category == "vcodec":
171
+ updated_category = self.mediainfo_video(category=category)
172
+
173
+ elif category == "video_encoder":
174
+ if self.tags_dict.get('video_encoder', None):
175
+ self.tags_dict['video_encoder'][0] = self.tags_dict['video_encoder'][0].lower()
176
+
177
+ elif category == "hdr":
178
+ updated_category = self.mediainfo_hdr(category=category)
179
+ if not updated_category:
180
+ updated_category = {category: 'SDR'}
181
+
182
+ elif category == "uhd":
183
+ updated_category = self.mediainfo_uhd(category=category)
184
+
185
+
186
+ elif category == "subtitle":
187
+ if self.mediafile.subtitle_tracks:
188
+ updated_category = {'subtitle': "SUBS" if len(self.mediafile.subtitle_tracks) > 1 else "SUB"}
189
+
190
+ if updated_category:
191
+ self.tags_dict.update(updated_category)
192
+
193
+ # /// Add S#E#, title, Year
194
+ se_str = ''
195
+ if self.season is not None and self.episode is not None:
196
+ se_str = f"S{self.season:02d}E{self.episode:02d}"
197
+ elif self.season is not None:
198
+ se_str = f"S{self.season:02d}"
199
+ elif self.episode is not None:
200
+ se_str = f"E{self.episode:02d}"
201
+
202
+ self.tags_dict.update({'title': self.title})
203
+ if self.year:
204
+ self.tags_dict.update({'year': self.year})
205
+ if se_str:
206
+ self.tags_dict.update({'season': se_str})
207
+
208
+ if not self.releaser_sign:
209
+ # If releaser_sign is not defined in the configuration file,
210
+ # try to detect a known sign from SIGN_LIST
211
+ self.releaser_sign = self.detect_releaser(self.filename, self.SIGNS_LIST)
212
+
213
+ # /// Order according to tag position
214
+ tags_dict = {
215
+ k: self.tags_dict[k]
216
+ for k in self.tags_position
217
+ if k in self.tags_dict
218
+ }
219
+
220
+ new_title = self.build_title(tags_dict)
221
+ return new_title
222
+
223
+ def mediainfo_audio(self, category: str) -> dict:
224
+ languages = []
225
+ audio_codecs = []
226
+ if self.mediafile.audio_tracks:
227
+ for audio in self.mediafile.audio_tracks:
228
+ other_format = audio.get('other_format', [])
229
+ if other_format:
230
+ codec_translated = audio_translate.get(other_format[0], '')
231
+ if not codec_translated:
232
+ codec_translated = other_format[0]
233
+ # Check Atmos
234
+ dolby = audio.get('commercial_name', "").lower()
235
+ atmos = 'Atmos' if 'atmos' in dolby else ''
236
+ # Add audio codec
237
+ channel_s = audio.get('channel_s', 0)
238
+ # Add channels
239
+ ch = {2: "2.0", 6: "5.1", 8: "7.1"}.get(channel_s, "")
240
+ if f"{codec_translated} {ch} {atmos}".strip() not in audio_codecs:
241
+ audio_codecs.append(f"{codec_translated} {ch} {atmos}".strip())
242
+ # print(f"Mediainfo {other_format} -> {codec_translated} {ch} {atmos}")
243
+
244
+ # Add flags
245
+ for l in audio.get('other_language', []):
246
+ c = ManageTitles.convert_iso(l)
247
+ if c:
248
+ if isinstance(c, list):
249
+ languages.append(c[0])
250
+ else:
251
+ languages.append(c)
252
+ break
253
+ languages = list(dict.fromkeys(languages))
254
+ # Add multilanguage tag when languages > 2
255
+ if len(languages) > 2:
256
+ self.tags_dict.update({'multi': 'MULTI'})
257
+
258
+ audio_codecs.extend(languages)
259
+ return {category: audio_codecs}
260
+
261
+ def mediainfo_video(self, category: str) -> dict:
262
+ codec_translated = {}
263
+ if self.mediafile.video_tracks:
264
+ for video in self.mediafile.video_tracks:
265
+ video_format = video.get('format', "")
266
+ codec_translated = video_translate.get(video_format, video_format)
267
+ if codec_translated:
268
+ return {category: codec_translated}
269
+ return codec_translated
270
+
271
+ def mediainfo_hdr(self, category: str) -> dict:
272
+ if self.mediafile.video_tracks:
273
+ for video in self.mediafile.video_tracks:
274
+ hdr_format_commercial = video.get('hdr_format_commercial', "")
275
+ hdr_format = video.get('hdr_format', "")
276
+ colour_primaries = video.get('color_primaries', "")
277
+ matrix_coefficients = video.get('matrix_coefficients', "")
278
+ bit_depth = video.get('bit_depth', "")
279
+ transfer_characteristics = video.get('transfer_characteristics', "")
280
+
281
+ # Check hdr
282
+ if hdr_format_commercial:
283
+ # print(f"hdr_format_commercial: {hdr_format_commercial}")
284
+ # print(f"hdr_format: {hdr_format}")
285
+ hdr = ''
286
+ if hdr_format_commercial.upper() in hdr_map:
287
+ # print(
288
+ # f"hdr_format_commercial: {hdr_format_commercial} -> Tag: {hdr_map[hdr_format_commercial]}")
289
+ hdr = hdr_map[hdr_format_commercial.upper()]
290
+ # Check dolby vision
291
+ if hdr not in hdr_map:
292
+ self.logger.info(
293
+ f"<> HDR Warning: '{hdr_format_commercial}' not found in hdr_map")
294
+ if 'DOLBY VISION' in hdr_format_commercial.upper() or 'DOLBY VISION' in hdr_format.upper():
295
+ hdr = f"DOLBY VISION {hdr}"
296
+ return {category: hdr_map.get(hdr, '*HDR')}
297
+ else:
298
+ if "2020" in colour_primaries and "2020" in matrix_coefficients:
299
+ if bit_depth == 10 and transfer_characteristics.strip() == 'PQ':
300
+ return {category: 'PQ10'}
301
+ else:
302
+ self.logger.warning(f"<> PQ10 Warning:")
303
+ self.logger.info(f"colour_primaries: {colour_primaries}")
304
+ self.logger.info(f"matrix_coefficients: {matrix_coefficients}")
305
+ self.logger.info(f"bit_depth: |{bit_depth}|")
306
+ self.logger.info(f"transfer_characteristics: |{transfer_characteristics}|")
307
+
308
+ return {}
309
+
310
+ def mediainfo_uhd(self, category: str) -> dict:
311
+ """
312
+ identify resolution based on Height and Width tolerance 5%
313
+ """
314
+ result = {}
315
+ if self.mediafile.video_tracks:
316
+ video_height = int(self.mediafile.video_tracks[0].get('height', 0))
317
+ video_width = int(self.mediafile.video_tracks[0].get('width', 0))
318
+
319
+ # print(f"VideoTrack : W{video_width} x H{video_height}")
320
+
321
+ # Calculate range 5%
322
+ def in_range(value, standard):
323
+ tol = standard * 0.05
324
+ return standard - tol <= value <= standard + tol
325
+
326
+ # /// UHD
327
+ if in_range(video_width, 3840) or in_range(video_height, 2160):
328
+ result[category] = 'UHD'
329
+ result['resolution'] = '2160p'
330
+ # /// Full HD
331
+ elif in_range(video_height, 1080) or in_range(video_width, 1920):
332
+ result[category] = 'FullHD'
333
+ result['resolution'] = '1080p'
334
+ # /// HD
335
+ elif in_range(video_height, 720) or in_range(video_width, 1280):
336
+ result[category] = 'HD'
337
+ result['resolution'] = '720p'
338
+ # /// SD 576p
339
+ elif in_range(video_height, 576) or in_range(video_width, 768):
340
+ result[category] = 'SD'
341
+ result['resolution'] = '576p'
342
+ # /// SD 480p
343
+ elif in_range(video_height, 480) or in_range(video_width, 640):
344
+ result[category] = 'SD'
345
+ result['resolution'] = '480p'
346
+ else:
347
+ result[category] = 'unknown'
348
+ result['resolution'] = f'{video_width}x{video_height}'
349
+
350
+ return result
351
+
352
+ @staticmethod
353
+ def detect_releaser(name: str, signs_list: dict) -> str:
354
+ """
355
+ normalize both signs_list and base_name
356
+ find the start/end position of the matched sign
357
+ extract the substring from the original base_name
358
+ """
359
+ # Strip the title
360
+ base_name = str(name).strip()
361
+
362
+ # sort dictionary from the longest to shortest to avoid partial result (es. 'crew' instead di 'mircrew')
363
+ tokens_signs_list_sorted = sorted(signs_list.keys(), key=len, reverse=True)
364
+
365
+ video_exts = [
366
+ "mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "mpeg", "mpg", "m4v", "ts", "3gp"
367
+ ]
368
+
369
+ # Regex per catturare l'estensione alla fine (case insensitive)
370
+ pattern = rf"\.({'|'.join(video_exts)})$"
371
+
372
+ # # Search for signs in the base_name only at the end of the string
373
+ base_name = re.sub(pattern, "", base_name, flags=re.IGNORECASE)
374
+
375
+ # Search for signs in the base_name_normalized
376
+ for token in tokens_signs_list_sorted:
377
+ token = str(token)
378
+ pattern = re.escape(token)
379
+ match = re.search(pattern, base_name, re.IGNORECASE)
380
+
381
+ if match:
382
+ # Sign must be the last words
383
+ base_name_len = len(base_name)
384
+ match_len = match.end() - base_name_len
385
+ if match_len == 0:
386
+ # Capture any characters from the start to the end of base_name
387
+ sign = base_name[match.start(): match.end()]
388
+ return f"-{sign}"
389
+ return ""