ittup 1.2.3__tar.gz → 1.4.0__tar.gz
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.
- {ittup-1.2.3 → ittup-1.4.0}/PKG-INFO +1 -1
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/command.py +6 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/mediaresult.py +8 -4
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/api.py +105 -6
- ittup-1.4.0/ittup/common/rename.py +129 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/settings.py +5 -2
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/tags.py +100 -13
- ittup-1.4.0/ittup/common/trackers/trackers.py +107 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/utility.py +60 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/automode.py +3 -2
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/bot.py +3 -1
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media.py +42 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/ContentManager.py +25 -10
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/VideoManager.py +101 -24
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/upload.py +10 -2
- {ittup-1.2.3 → ittup-1.4.0}/ittup.egg-info/PKG-INFO +1 -1
- {ittup-1.2.3 → ittup-1.4.0}/ittup.egg-info/SOURCES.txt +10 -0
- {ittup-1.2.3 → ittup-1.4.0}/pyproject.toml +1 -1
- ittup-1.4.0/tests/test_acodec.py +85 -0
- ittup-1.4.0/tests/test_command.py +35 -0
- ittup-1.4.0/tests/test_ids.py +42 -0
- ittup-1.4.0/tests/test_order.py +28 -0
- ittup-1.4.0/tests/test_pack.py +53 -0
- ittup-1.4.0/tests/test_rename.py +132 -0
- ittup-1.4.0/tests/test_title.py +59 -0
- ittup-1.4.0/tests/test_tmdb_search.py +87 -0
- ittup-1.4.0/tests/test_type.py +43 -0
- {ittup-1.2.3 → ittup-1.4.0}/tests/test_vcodec.py +8 -0
- ittup-1.2.3/ittup/common/trackers/trackers.py +0 -46
- {ittup-1.2.3 → ittup-1.4.0}/LICENSE +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/README.rst +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/__main__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/about.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/bdinfo_string.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/bittorrent.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/constants.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/database.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/client.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/core/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/core/ftpx_service.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/core/ftpx_session.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/core/menu.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/core/models/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/ftpx/core/models/list.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/client.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/core/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/core/api.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/core/models/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/core/models/search.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/core/platformid.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/igdb/core/tags.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/imageHost.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/sessions/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/sessions/agents.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/sessions/exceptions.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/sessions/session.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/keywords.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/alternative.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/details.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/movie.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/nowplaying.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/release_info.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/movie/translations.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/tvshow/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/tvshow/alternative.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/tvshow/details.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/tvshow/on_the_air.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/tvshow/translations.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/models/tvshow/tvshow.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/theMovieDB/core/videos.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/trailers/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/trailers/api.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/trailers/response.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/external_services/tvdb.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/extractor.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/frames.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/mediainfo.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/mediainfo_string.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/title.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/torrent_clients.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/trackers/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/trackers/ban_list.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/trackers/data.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/trackers/itt.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/trackers/signs_list.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/common/trackers/tags_list.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/__main__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/duplicate.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/exceptions.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/DocuManager.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/GameManager.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/MediaInfoManager.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/SeedManager.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/TorrentManager.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/media_manager/common.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/pvtDocu.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/pvtTorrent.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/pvtTracker.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/pvtVideo.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/core/torrent.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/view/__init__.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup/view/custom_console.py +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup.egg-info/dependency_links.txt +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup.egg-info/entry_points.txt +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup.egg-info/requires.txt +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/ittup.egg-info/top_level.txt +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/requirements.txt +0 -0
- {ittup-1.2.3 → ittup-1.4.0}/setup.cfg +0 -0
|
@@ -70,6 +70,12 @@ class CommandLine:
|
|
|
70
70
|
upload_group.add_argument("-personal", "--personal", action="store_true", help="Personal release")
|
|
71
71
|
upload_group.add_argument("-ftp", "--ftp", action="store_true", help="Connect FTP")
|
|
72
72
|
|
|
73
|
+
# Freeleech of the uploaded torrents
|
|
74
|
+
freeleech_group = upload_group.add_mutually_exclusive_group()
|
|
75
|
+
for percent in (25, 50, 75, 100):
|
|
76
|
+
freeleech_group.add_argument(f"-fl{percent}", f"--freeleech{percent}", dest="freeleech",
|
|
77
|
+
action="store_const", const=percent, help=f"Freeleech {percent}%%")
|
|
78
|
+
|
|
73
79
|
# Removed flags: ITTup uploads only to ItaTorrents. Without this '-mt' would be parsed as '-m t'
|
|
74
80
|
upload_group.add_argument("-mt", "--mt", "-tracker", "--tracker", nargs='?', action=RemovedFlag,
|
|
75
81
|
help=argparse.SUPPRESS)
|
|
@@ -4,8 +4,9 @@ from datetime import datetime
|
|
|
4
4
|
|
|
5
5
|
class MediaResult:
|
|
6
6
|
def __init__(self, result=None, video_id: int = 0, imdb_id = None, tvdb_id = None, trailer_key: str = None,
|
|
7
|
-
keywords_list: str = None, season_title = None):
|
|
7
|
+
keywords_list: str = None, season_title = None, original_language: str = None):
|
|
8
8
|
self.result = result
|
|
9
|
+
self._original_language = original_language
|
|
9
10
|
self.trailer_key = trailer_key
|
|
10
11
|
self.keywords_list = keywords_list
|
|
11
12
|
self.video_id = video_id
|
|
@@ -20,6 +21,9 @@ class MediaResult:
|
|
|
20
21
|
except ValueError:
|
|
21
22
|
pass
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
@property
|
|
25
|
+
def original_language(self) -> str | None:
|
|
26
|
+
""" TMDB original language (ISO 639-1, es. 'en'), None when unknown """
|
|
27
|
+
# Results cached by older versions have no _original_language
|
|
28
|
+
return (getattr(self, '_original_language', None) or getattr(self.result, 'original_language', None)
|
|
29
|
+
or None)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
import hashlib
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
|
|
@@ -32,6 +33,10 @@ from ittup.view import custom_console
|
|
|
32
33
|
from ittup.core import config_settings
|
|
33
34
|
|
|
34
35
|
base_url = "https://api.themoviedb.org/3"
|
|
36
|
+
|
|
37
|
+
# The title ends at the year, the season or the resolution
|
|
38
|
+
TITLE_END = re.compile(r"\(?(?<![0-9])(?:19|20)\d{2}(?![0-9])\)?|(?<![a-z0-9])S\d{1,2}(?:E\d+)?(?![a-z0-9])|"
|
|
39
|
+
r"(?<![0-9])\d{3,4}[pi](?![a-z0-9])", re.IGNORECASE)
|
|
35
40
|
ENABLE_LOG = True
|
|
36
41
|
T = TypeVar('T')
|
|
37
42
|
|
|
@@ -235,6 +240,26 @@ class TmdbAPI(MyHttp):
|
|
|
235
240
|
return results[0].get('id')
|
|
236
241
|
return None
|
|
237
242
|
|
|
243
|
+
def original_language(self, video_id: int, category: str) -> str | None:
|
|
244
|
+
""" Original language (ISO 639-1) of the movie or tv show """
|
|
245
|
+
if endpoint_class := self.ENDPOINTS.get(category):
|
|
246
|
+
request = {**endpoint_class.details(video_id), 'datatype': dict}
|
|
247
|
+
response = self.request(endpoint=request)
|
|
248
|
+
if response:
|
|
249
|
+
return response[0].get('original_language') or None
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
def italian_details(self, video_id: int, category: str) -> dict | None:
|
|
253
|
+
""" Details of the movie or tv show in Italian (title, seasons..), whatever the preferred language """
|
|
254
|
+
if not video_id:
|
|
255
|
+
return None
|
|
256
|
+
if endpoint_class := self.ENDPOINTS.get(category):
|
|
257
|
+
request = {**endpoint_class.details(video_id), 'datatype': dict, 'params': {'language': 'it-IT'}}
|
|
258
|
+
response = self.request(endpoint=request)
|
|
259
|
+
if response:
|
|
260
|
+
return response[0]
|
|
261
|
+
return None
|
|
262
|
+
|
|
238
263
|
def external_ids(self, video_id: int, category: str) -> dict | None:
|
|
239
264
|
if endpoint_class := self.ENDPOINTS.get(category):
|
|
240
265
|
request = endpoint_class.external_ids(video_id)
|
|
@@ -273,6 +298,8 @@ class DbOnline(TmdbAPI):
|
|
|
273
298
|
self.category = category
|
|
274
299
|
self.imdb_id = None
|
|
275
300
|
self.tvdb_id = None
|
|
301
|
+
self.no_title = no_title
|
|
302
|
+
self.relaxed_year = False
|
|
276
303
|
|
|
277
304
|
# Load the cache file
|
|
278
305
|
self.cache = None
|
|
@@ -366,9 +393,18 @@ class DbOnline(TmdbAPI):
|
|
|
366
393
|
return False
|
|
367
394
|
|
|
368
395
|
def year_matches(self, result: T) -> bool:
|
|
369
|
-
"""
|
|
396
|
+
"""
|
|
397
|
+
False only when both years are known and they differ
|
|
398
|
+
With relaxed_year a movie may differ by one year and a tv show may have the year of a later season
|
|
399
|
+
"""
|
|
370
400
|
if result.get_date() and self.media.guess_filename.guessit_year:
|
|
371
|
-
|
|
401
|
+
tmdb_year = datetime.strptime(result.get_date(), '%Y-%m-%d').year
|
|
402
|
+
year = self.media.guess_filename.guessit_year
|
|
403
|
+
if not self.relaxed_year:
|
|
404
|
+
return tmdb_year == year
|
|
405
|
+
if self.category == 'tv':
|
|
406
|
+
return year >= tmdb_year - 1
|
|
407
|
+
return abs(tmdb_year - year) <= 1
|
|
372
408
|
return True
|
|
373
409
|
|
|
374
410
|
def debug_scores(self, result: T, alternatives: list[T] | None, translations: list[T] | None) -> None:
|
|
@@ -406,7 +442,9 @@ class DbOnline(TmdbAPI):
|
|
|
406
442
|
if not tmdb_id:
|
|
407
443
|
custom_console.bot_warning_log(f"TMDB ID not found for IMDB ID '{imdb_id}'")
|
|
408
444
|
|
|
445
|
+
original_language = None
|
|
409
446
|
if tmdb_id > 0:
|
|
447
|
+
original_language = self.original_language(video_id=tmdb_id, category=self.category)
|
|
410
448
|
# Fill the IDs missing from the title string
|
|
411
449
|
self.add_external_ids(tmdb_id)
|
|
412
450
|
# Request trailer and keywords
|
|
@@ -414,7 +452,8 @@ class DbOnline(TmdbAPI):
|
|
|
414
452
|
keywords_list = self.keywords(tmdb_id) if trailer_key else ''
|
|
415
453
|
|
|
416
454
|
search_results = MediaResult(video_id=tmdb_id, imdb_id=self.imdb_id, tvdb_id=self.tvdb_id,
|
|
417
|
-
trailer_key=trailer_key, keywords_list=keywords_list
|
|
455
|
+
trailer_key=trailer_key, keywords_list=keywords_list,
|
|
456
|
+
original_language=original_language)
|
|
418
457
|
self.print_results(results=search_results)
|
|
419
458
|
return search_results
|
|
420
459
|
|
|
@@ -440,7 +479,8 @@ class DbOnline(TmdbAPI):
|
|
|
440
479
|
return search_results
|
|
441
480
|
|
|
442
481
|
# or start an on-line search
|
|
443
|
-
|
|
482
|
+
main_query = self.query
|
|
483
|
+
result, results = self.search_queries()
|
|
444
484
|
|
|
445
485
|
# Use imdb_id when tmdb_id is not available
|
|
446
486
|
tvdb_result = self.tvdb_search()
|
|
@@ -449,7 +489,7 @@ class DbOnline(TmdbAPI):
|
|
|
449
489
|
self.imdb_id = tvdb_result.get('imdb_id', None)
|
|
450
490
|
|
|
451
491
|
if results:
|
|
452
|
-
if result
|
|
492
|
+
if result:
|
|
453
493
|
# Get the trailer
|
|
454
494
|
trailer_key = self.trailer(result.id)
|
|
455
495
|
keywords_list = self.keywords(result.id)
|
|
@@ -463,7 +503,7 @@ class DbOnline(TmdbAPI):
|
|
|
463
503
|
self.print_results(results=search_results)
|
|
464
504
|
# Write to the cache if it is enabled
|
|
465
505
|
if config_settings.user_preferences.CACHE_DBONLINE:
|
|
466
|
-
self.cache[self.hash_key(
|
|
506
|
+
self.cache[self.hash_key(main_query)] = search_results
|
|
467
507
|
# return the result
|
|
468
508
|
return search_results
|
|
469
509
|
|
|
@@ -490,6 +530,65 @@ class DbOnline(TmdbAPI):
|
|
|
490
530
|
self.cache[self.hash_key(self.query)] = search_results
|
|
491
531
|
return search_results
|
|
492
532
|
|
|
533
|
+
def search_queries(self) -> tuple[T | None, list[T] | None]:
|
|
534
|
+
"""
|
|
535
|
+
Search TMDB with the title from the file name, then with the other titles written in the name
|
|
536
|
+
(es. '(500) Days of Summer', the title in parentheses). Each query is checked first with the exact year,
|
|
537
|
+
then with a tolerance (movies released at the end of the year, the year of a season..)
|
|
538
|
+
Returns the result found and the results of the first query (None if TMDB doesn't answer)
|
|
539
|
+
On success self.query is the query that found the result, otherwise the first query
|
|
540
|
+
"""
|
|
541
|
+
queries = [self.query] if self.no_title else self.title_queries(self.query, self.media.title)
|
|
542
|
+
responses = {}
|
|
543
|
+
for relaxed in (False, True):
|
|
544
|
+
self.relaxed_year = relaxed
|
|
545
|
+
for query in queries:
|
|
546
|
+
if query not in responses:
|
|
547
|
+
responses[query] = self._search(query, self.category)
|
|
548
|
+
# No response from TMDB
|
|
549
|
+
if responses[query] is None:
|
|
550
|
+
if query == queries[0]:
|
|
551
|
+
return None, None
|
|
552
|
+
responses[query] = []
|
|
553
|
+
self.query = query
|
|
554
|
+
result = self.is_like(responses[query]) if responses[query] else None
|
|
555
|
+
if result:
|
|
556
|
+
if query != queries[0] or relaxed:
|
|
557
|
+
custom_console.bot_log(f"TMDB: found with the title '{query}'"
|
|
558
|
+
f"{' and a year tolerance' if relaxed else ''}")
|
|
559
|
+
self.relaxed_year = False
|
|
560
|
+
return result, responses[query]
|
|
561
|
+
self.relaxed_year = False
|
|
562
|
+
self.query = queries[0]
|
|
563
|
+
return None, responses[queries[0]]
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def title_queries(query: str, name: str) -> list[str]:
|
|
567
|
+
"""
|
|
568
|
+
TMDB queries: the title from guessit, then the name before the year, the season or the resolution,
|
|
569
|
+
and the titles in parentheses ('Lo Chiamavano Jeeg Robot (They Call Me Jeeg) (2015)')
|
|
570
|
+
"""
|
|
571
|
+
queries = [query]
|
|
572
|
+
name = re.sub(r"\.(mkv|mp4|avi|m2ts|ts|mov|wmv|m4v|mpg|mpeg|webm)$", "", name, flags=re.IGNORECASE)
|
|
573
|
+
# Dots and underscores as spaces, but not in the acronyms (S.W.A.T.)
|
|
574
|
+
name = re.sub(r"(?<=[^\W\d_]{2})[._]|[._](?=[^\W\d_]{2})|_", " ", name)
|
|
575
|
+
prefix = TITLE_END.split(name, maxsplit=1)[0]
|
|
576
|
+
prefix = re.sub(r"[\[{].*", "", prefix).strip(" -.")
|
|
577
|
+
# Titles in parentheses, not numbers like '(500)'
|
|
578
|
+
groups = [group for group in re.findall(r"\(([^()]+)\)", prefix) if re.search(r"[^\W\d_]{2}", group)]
|
|
579
|
+
# The whole prefix, e.g. '(500) Days of Summer'
|
|
580
|
+
queries.append(re.sub(r"[()]", "", prefix))
|
|
581
|
+
# The prefix without the parentheses and each title in parentheses
|
|
582
|
+
queries.append(re.sub(r"\([^()]*\)", "", prefix))
|
|
583
|
+
queries.extend(groups)
|
|
584
|
+
|
|
585
|
+
unique = []
|
|
586
|
+
for item in queries:
|
|
587
|
+
item = re.sub(r"\s+", " ", item or "").strip(" -.")
|
|
588
|
+
if item and item.lower() not in [u.lower() for u in unique]:
|
|
589
|
+
unique.append(item)
|
|
590
|
+
return unique[:4]
|
|
591
|
+
|
|
493
592
|
def add_external_ids(self, video_id: int, override: bool = False) -> None:
|
|
494
593
|
""" Set the IMDB and TVDB IDs from TMDB; override=False keeps the ones already known """
|
|
495
594
|
ext = self.external_ids(video_id=video_id, category=self.category)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ittup.view import custom_console
|
|
6
|
+
|
|
7
|
+
# Characters the tracker (UNIT3D) doesn't accept in the names of the torrent files
|
|
8
|
+
INVALID_CHARS = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def safe_name(name: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Name without the characters refused by the tracker: 'Titolo: Sub' -> 'Titolo - Sub'
|
|
14
|
+
The extension is kept, the trailing dots and spaces are removed
|
|
15
|
+
"""
|
|
16
|
+
stem, ext = os.path.splitext(name)
|
|
17
|
+
# Keep only a real extension (es. '.mkv'), not the end of 'Mr. Robot'
|
|
18
|
+
if not re.fullmatch(r"\.[A-Za-z0-9]{1,5}", ext):
|
|
19
|
+
stem, ext = name, ""
|
|
20
|
+
if " " in stem:
|
|
21
|
+
stem = re.sub(r"\s*:\s*", " - ", stem)
|
|
22
|
+
else:
|
|
23
|
+
stem = stem.replace(":", ".")
|
|
24
|
+
stem = INVALID_CHARS.sub("", stem)
|
|
25
|
+
stem = re.sub(r"\s{2,}", " ", stem)
|
|
26
|
+
stem = re.sub(r"\.{2,}", ".", stem)
|
|
27
|
+
stem = re.sub(r"(\s-)+\s", " - ", stem)
|
|
28
|
+
return stem.strip(" .") + ext
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_valid_name(name: str) -> bool:
|
|
32
|
+
return not INVALID_CHARS.search(name) and name == name.rstrip(" .")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rename_path(path: str, new_name: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Rename a file or a folder in the same folder. Returns the new path, the old one if it can't be renamed
|
|
38
|
+
"""
|
|
39
|
+
new_path = os.path.join(os.path.dirname(path), new_name)
|
|
40
|
+
if new_path == path:
|
|
41
|
+
return path
|
|
42
|
+
if os.path.exists(new_path):
|
|
43
|
+
custom_console.bot_error_log(f"Can't rename '{path}': '{new_name}' already exists")
|
|
44
|
+
return path
|
|
45
|
+
try:
|
|
46
|
+
os.rename(path, new_path)
|
|
47
|
+
except OSError as e:
|
|
48
|
+
custom_console.bot_error_log(f"Can't rename '{path}': {e}")
|
|
49
|
+
return path
|
|
50
|
+
custom_console.bot_warning_log(f"Renamed '{os.path.basename(path)}' -> '{new_name}'")
|
|
51
|
+
return new_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def rename_invalid(path: str) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Rename the file, or the folder and everything in it, whose names have characters refused by the tracker
|
|
57
|
+
Returns the new path
|
|
58
|
+
"""
|
|
59
|
+
path = os.path.normpath(path)
|
|
60
|
+
if os.path.isdir(path):
|
|
61
|
+
for root, dirs, files in os.walk(path, topdown=False):
|
|
62
|
+
for name in files + dirs:
|
|
63
|
+
if not is_valid_name(name):
|
|
64
|
+
rename_path(os.path.join(root, name), safe_name(name))
|
|
65
|
+
name = os.path.basename(path)
|
|
66
|
+
if is_valid_name(name):
|
|
67
|
+
return path
|
|
68
|
+
return rename_path(path, safe_name(name))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def title_name(display_name: str) -> str:
|
|
72
|
+
"""
|
|
73
|
+
File name from the title shown on the tracker, with dots:
|
|
74
|
+
'Spider-Man: No Way Home 2021 1080p WEB-DL H.264-SIGN' -> 'Spider-Man.No.Way.Home.2021.1080p.WEB-DL.H.264-SIGN'
|
|
75
|
+
"""
|
|
76
|
+
name = INVALID_CHARS.sub(" ", display_name)
|
|
77
|
+
name = re.sub(r"\s+-\s+", ".", name.strip())
|
|
78
|
+
name = re.sub(r"\s+", ".", name)
|
|
79
|
+
name = re.sub(r"\.{2,}", ".", name)
|
|
80
|
+
return name.strip(".")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def episode_name(name: str, pack: str | None, season: int | None, episode: int | None) -> str | None:
|
|
84
|
+
"""
|
|
85
|
+
Name of an episode of a pack: the pack tag of the title is replaced by its SxxExx
|
|
86
|
+
None if the episode or the pack tag are unknown
|
|
87
|
+
"""
|
|
88
|
+
if not pack or season is None or episode is None:
|
|
89
|
+
return None
|
|
90
|
+
pattern = rf"(?<![^.]){re.escape(pack)}(?![^.])"
|
|
91
|
+
if not re.search(pattern, name):
|
|
92
|
+
return None
|
|
93
|
+
return re.sub(pattern, f"S{int(season):02d}E{int(episode):02d}", name, count=1)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def rename_to_title(path: str, display_name: str, video_files: list[str], pack: str | None,
|
|
97
|
+
episodes: dict[str, tuple[int | None, int | None]]) -> tuple[str, dict[str, str]]:
|
|
98
|
+
"""
|
|
99
|
+
Rename a file, or a folder and its video files, as the title shown on the tracker
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
path: the file or the folder of the torrent
|
|
103
|
+
display_name: the title shown on the tracker
|
|
104
|
+
video_files: the video files in the folder (names)
|
|
105
|
+
pack: the season tag of the title (es. 'S04') for a tv pack
|
|
106
|
+
episodes: (season, episode) of each video file of a pack
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
the new path and the new name of each renamed file of the folder
|
|
110
|
+
"""
|
|
111
|
+
name = title_name(display_name)
|
|
112
|
+
if os.path.isfile(path):
|
|
113
|
+
_, ext = os.path.splitext(path)
|
|
114
|
+
return rename_path(path, name + ext), {}
|
|
115
|
+
|
|
116
|
+
renamed = {}
|
|
117
|
+
for file in video_files:
|
|
118
|
+
_, ext = os.path.splitext(file)
|
|
119
|
+
if pack:
|
|
120
|
+
new_name = episode_name(name, pack, *episodes.get(file, (None, None)))
|
|
121
|
+
else:
|
|
122
|
+
# A movie folder: only its single video file takes the title
|
|
123
|
+
new_name = name if len(video_files) == 1 else None
|
|
124
|
+
if not new_name:
|
|
125
|
+
continue
|
|
126
|
+
new_path = rename_path(os.path.join(path, file), new_name + ext)
|
|
127
|
+
if new_path != os.path.join(path, file):
|
|
128
|
+
renamed[file] = os.path.basename(new_path)
|
|
129
|
+
return rename_path(path, name), renamed
|
|
@@ -147,6 +147,7 @@ class UserPreferences(BaseModel):
|
|
|
147
147
|
CACHE_DBONLINE: bool = False
|
|
148
148
|
TMDB_DEBUG: bool = False
|
|
149
149
|
PERSONAL_RELEASE: bool = False
|
|
150
|
+
RENAME_FILES: bool = False
|
|
150
151
|
FAST_LOAD: int = 0
|
|
151
152
|
MY_SIGN: str | None = None
|
|
152
153
|
MY_URL: str | None = None
|
|
@@ -460,13 +461,14 @@ class Config(BaseModel):
|
|
|
460
461
|
field = field.upper()
|
|
461
462
|
|
|
462
463
|
# New attribute added to an existing json file with an empty value: use the default
|
|
463
|
-
if field
|
|
464
|
+
if field in ['TMDB_DEBUG', 'RENAME_FILES'] and section[field] == '':
|
|
464
465
|
section[field] = 'False'
|
|
465
466
|
if field == 'WATCHER_TIMEOUT' and section[field] == '':
|
|
466
467
|
section[field] = UserPreferences.model_fields['WATCHER_TIMEOUT'].default
|
|
467
468
|
|
|
468
469
|
if field in ['DUPLICATE_ON', 'SKIP_DUPLICATE', 'SKIP_TMDB', 'SKIP_YOUTUBE', 'RESIZE_SCSHOT', 'ANON',
|
|
469
|
-
'WEBP_ENABLED', 'CACHE_SCR', 'CACHE_DBONLINE', 'TMDB_DEBUG', 'PERSONAL_RELEASE'
|
|
470
|
+
'WEBP_ENABLED', 'CACHE_SCR', 'CACHE_DBONLINE', 'TMDB_DEBUG', 'PERSONAL_RELEASE',
|
|
471
|
+
'RENAME_FILES']:
|
|
470
472
|
section[field] = Validate.boolean(value=section[field], field_name=field)
|
|
471
473
|
|
|
472
474
|
if field in ['TORRENT_COMMENT', 'WATCHER_PATH']:
|
|
@@ -653,6 +655,7 @@ class Load:
|
|
|
653
655
|
"CACHE_DBONLINE": "False",
|
|
654
656
|
"TMDB_DEBUG": "False",
|
|
655
657
|
"PERSONAL_RELEASE": "False",
|
|
658
|
+
"RENAME_FILES": "False",
|
|
656
659
|
"FAST_LOAD": "0",
|
|
657
660
|
"RELEASER_SIGN": "",
|
|
658
661
|
"MY_SIGN": "",
|
|
@@ -46,6 +46,14 @@ audio_translate = {
|
|
|
46
46
|
"MPEG Audio": "MPEG",
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
# Audio codecs from the best to the worst: only the best ones go in the title
|
|
50
|
+
audio_rank = ["TrueHD", "DTS-HD MA", "FLAC", "PCM", "DTS-ES", "DTS", "DD+", "DD", "AAC", "HE-AAC", "Opus", "MPEG"]
|
|
51
|
+
# The title keeps the best ITA codec and the best codec in the original language, then fills up to
|
|
52
|
+
# MAX_AUDIO_CODECS with the tracks with no language. Tracks in other languages only add their flag
|
|
53
|
+
MAX_AUDIO_CODECS = 2
|
|
54
|
+
# Original language (TMDB ISO 639-1) when it is not known yet
|
|
55
|
+
DEFAULT_ORIGINAL_LANGUAGE = "en"
|
|
56
|
+
|
|
49
57
|
# Codec family from the MediaInfo 'format' of the video track
|
|
50
58
|
video_family = {
|
|
51
59
|
"AVC": "264",
|
|
@@ -97,6 +105,7 @@ class SearchTags(object):
|
|
|
97
105
|
self.tags_dict = {}
|
|
98
106
|
self.release_type: str | None = None
|
|
99
107
|
self.name_family: str | None = None
|
|
108
|
+
self.original_language: str = DEFAULT_ORIGINAL_LANGUAGE
|
|
100
109
|
|
|
101
110
|
self.TAG_TYPES: dict = tags_list
|
|
102
111
|
self.SIGNS_LIST: dict = sign_list
|
|
@@ -142,6 +151,16 @@ class SearchTags(object):
|
|
|
142
151
|
tag_esc = re.sub(r'([A-Z])(\d+)', r'\1[._-]?\2', tag_esc)
|
|
143
152
|
return tag_esc
|
|
144
153
|
|
|
154
|
+
def expand_platform_rip(self, filename: str) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Split a platform rip into the platform and the WEBRip source (es. 'NFRip' -> 'NF WEBRip')
|
|
157
|
+
"""
|
|
158
|
+
platforms = [tag for tag, category in self.TAG_TYPES.items() if category == "platform"]
|
|
159
|
+
if not platforms:
|
|
160
|
+
return filename
|
|
161
|
+
pattern = r'(?<![a-z0-9])(' + "|".join(re.escape(tag) for tag in platforms) + r')rip(?![a-z0-9])'
|
|
162
|
+
return re.sub(pattern, r'\1 WEBRip', filename, flags=re.IGNORECASE)
|
|
163
|
+
|
|
145
164
|
def build_title(self, dictionary: dict) -> str:
|
|
146
165
|
# /// Build the title
|
|
147
166
|
build = []
|
|
@@ -179,10 +198,13 @@ class SearchTags(object):
|
|
|
179
198
|
# Save a regex pattern for each category
|
|
180
199
|
patterns.append([norm, category])
|
|
181
200
|
|
|
201
|
+
# NFRip -> NF WEBRip: the streaming platform and the source
|
|
202
|
+
filename = self.expand_platform_rip(self.filename)
|
|
203
|
+
|
|
182
204
|
# Run regex
|
|
183
205
|
for p, category in patterns:
|
|
184
206
|
regex = re.compile(r'(?<!\w)' + p + r'(?!\w)', re.IGNORECASE)
|
|
185
|
-
matches = regex.findall(
|
|
207
|
+
matches = regex.findall(filename)
|
|
186
208
|
if matches:
|
|
187
209
|
# Normalize Tag_list
|
|
188
210
|
normalized_tag = TAG_NORMALIZE.get(matches[0].upper(), None)
|
|
@@ -267,6 +289,36 @@ class SearchTags(object):
|
|
|
267
289
|
# try to detect a known sign from SIGN_LIST
|
|
268
290
|
self.releaser_sign = self.detect_releaser(self.filename, self.SIGNS_LIST)
|
|
269
291
|
|
|
292
|
+
return self.ordered_title()
|
|
293
|
+
|
|
294
|
+
def set_original_language(self, original_language: str | None) -> tuple[str, dict]:
|
|
295
|
+
"""
|
|
296
|
+
Rebuild the audio tags of the title processed before with the original language from TMDB (es. 'ja')
|
|
297
|
+
"""
|
|
298
|
+
self.original_language = (original_language or DEFAULT_ORIGINAL_LANGUAGE).lower()
|
|
299
|
+
if "acodec" in self.tags_position:
|
|
300
|
+
self.tags_dict.update(self.mediainfo_audio(category="acodec"))
|
|
301
|
+
return self.ordered_title()
|
|
302
|
+
|
|
303
|
+
def set_title(self, title: str) -> tuple[str, dict]:
|
|
304
|
+
"""
|
|
305
|
+
Rebuild the title with a new media title (es. the Italian title from TMDB)
|
|
306
|
+
"""
|
|
307
|
+
self.title = title
|
|
308
|
+
self.tags_dict.update({'title': title})
|
|
309
|
+
# The part is already in the new title (es. 'Wicked - Parte 2')
|
|
310
|
+
if re.search(r'(?<![a-z])part[ei]?(?![a-z])', title, re.IGNORECASE):
|
|
311
|
+
self.tags_dict.pop('part', None)
|
|
312
|
+
return self.ordered_title()
|
|
313
|
+
|
|
314
|
+
def set_season(self, se_str: str) -> tuple[str, dict]:
|
|
315
|
+
"""
|
|
316
|
+
Rebuild the title with a new season tag (es. 'S04' for a pack with every episode of the season)
|
|
317
|
+
"""
|
|
318
|
+
self.tags_dict.update({'season': se_str})
|
|
319
|
+
return self.ordered_title()
|
|
320
|
+
|
|
321
|
+
def ordered_title(self) -> tuple[str, dict]:
|
|
270
322
|
# /// Order according to tag position
|
|
271
323
|
tags_dict = {
|
|
272
324
|
k: self.tags_dict[k]
|
|
@@ -276,11 +328,38 @@ class SearchTags(object):
|
|
|
276
328
|
new_title = self.build_title(tags_dict)
|
|
277
329
|
return new_title, tags_dict
|
|
278
330
|
|
|
331
|
+
@staticmethod
|
|
332
|
+
def audio_language(audio: dict) -> str | None:
|
|
333
|
+
"""
|
|
334
|
+
ISO alpha-3 language of an audio track, None when it is not tagged
|
|
335
|
+
"""
|
|
336
|
+
for l in audio.get('other_language', []):
|
|
337
|
+
c = ManageTitles.convert_iso(l)
|
|
338
|
+
if c:
|
|
339
|
+
return c[0] if isinstance(c, list) else c
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def audio_score(codec: str, atmos: bool, channel_s: int) -> tuple:
|
|
344
|
+
"""
|
|
345
|
+
Sort key of an audio track: codec, then Atmos, then channels
|
|
346
|
+
"""
|
|
347
|
+
rank = len(audio_rank) - audio_rank.index(codec) if codec in audio_rank else 0
|
|
348
|
+
return rank, atmos, channel_s
|
|
349
|
+
|
|
350
|
+
def is_original_language(self, audio: dict) -> bool:
|
|
351
|
+
codes = [str(code).lower() for code in audio.get('other_language', []) + [audio.get('language') or '']]
|
|
352
|
+
return self.original_language in codes
|
|
353
|
+
|
|
279
354
|
def mediainfo_audio(self, category: str) -> dict:
|
|
280
355
|
languages = []
|
|
281
|
-
|
|
356
|
+
original_languages = []
|
|
357
|
+
# [(score, label, language, original language)]
|
|
358
|
+
tracks = []
|
|
282
359
|
if self.mediafile.audio_track:
|
|
283
360
|
for audio in self.mediafile.audio_track:
|
|
361
|
+
language = self.audio_language(audio)
|
|
362
|
+
original = self.is_original_language(audio)
|
|
284
363
|
other_format = audio.get('other_format', [])
|
|
285
364
|
if other_format:
|
|
286
365
|
codec_translated = audio_translate.get(other_format[0], '')
|
|
@@ -293,24 +372,32 @@ class SearchTags(object):
|
|
|
293
372
|
channel_s = audio.get('channel_s', 0)
|
|
294
373
|
# Add channels
|
|
295
374
|
ch = {2: "2.0", 6: "5.1", 8: "7.1"}.get(channel_s, "")
|
|
296
|
-
|
|
297
|
-
|
|
375
|
+
tracks.append((self.audio_score(codec_translated, bool(atmos), channel_s),
|
|
376
|
+
f"{codec_translated} {ch} {atmos}".strip(), language, original))
|
|
298
377
|
|
|
299
378
|
# Add flags
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
languages.append(c[0])
|
|
305
|
-
else:
|
|
306
|
-
languages.append(c)
|
|
307
|
-
break
|
|
379
|
+
if language:
|
|
380
|
+
languages.append(language)
|
|
381
|
+
if original:
|
|
382
|
+
original_languages.append(language)
|
|
308
383
|
languages = list(dict.fromkeys(languages))
|
|
309
384
|
# Add multilanguage tag when languages > 2
|
|
310
385
|
if len(languages) > 2:
|
|
311
386
|
self.tags_dict.update({'multi': 'MULTI'})
|
|
312
387
|
|
|
313
|
-
|
|
388
|
+
# Best ITA codec + best original language codec, then the tracks with no language up to MAX_AUDIO_CODECS
|
|
389
|
+
best_first = sorted(tracks, key=lambda track: track[0], reverse=True)
|
|
390
|
+
picked = [track for track in best_first if track[2] == "ITA"][:1]
|
|
391
|
+
picked += [track for track in best_first if track[3]][:1]
|
|
392
|
+
picked += [track for track in best_first if track[2] is None]
|
|
393
|
+
audio_codecs = []
|
|
394
|
+
for _, label, _, _ in picked:
|
|
395
|
+
if label not in audio_codecs and len(audio_codecs) < MAX_AUDIO_CODECS:
|
|
396
|
+
audio_codecs.append(label)
|
|
397
|
+
|
|
398
|
+
# Flags: ITA, the original language, then the other languages with no codec
|
|
399
|
+
first = [language for language in ["ITA"] + original_languages[:1] if language in languages]
|
|
400
|
+
audio_codecs.extend(dict.fromkeys(first + languages))
|
|
314
401
|
return {category: audio_codecs}
|
|
315
402
|
|
|
316
403
|
@staticmethod
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from ittup.common.utility import ManageTitles
|
|
8
|
+
from ittup.common.trackers.tags_list import TAGS_LIST
|
|
9
|
+
from . import tracker_list
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TRACKData:
|
|
13
|
+
category: dict[str, int]
|
|
14
|
+
freelech: dict[str, int]
|
|
15
|
+
type_id: dict[str, int]
|
|
16
|
+
resolution: dict[str, int]
|
|
17
|
+
codec: list
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def load_from_module(cls, tracker_name: str) -> "TRACKData":
|
|
21
|
+
"""
|
|
22
|
+
Load tracker data from module
|
|
23
|
+
"""
|
|
24
|
+
tracker_data= tracker_list[tracker_name.upper()]
|
|
25
|
+
|
|
26
|
+
return cls(
|
|
27
|
+
category=tracker_data.get("CATEGORY"),
|
|
28
|
+
freelech=tracker_data.get("FREELECH"),
|
|
29
|
+
type_id=tracker_data.get("TYPE_ID"),
|
|
30
|
+
resolution=tracker_data.get("RESOLUTION"),
|
|
31
|
+
codec=tracker_data.get("CODEC"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def filter_type(self, *names: str) -> int:
|
|
35
|
+
"""
|
|
36
|
+
Tracker type ID from the release names (es. the folder name, then the file name)
|
|
37
|
+
The first name with a source (WEB-DL, REMUX, WEBRip..) wins, so the type always follows the source
|
|
38
|
+
written in the title. Without a source, a codec in the name means 'encode'
|
|
39
|
+
"""
|
|
40
|
+
names = [os.path.basename(str(name)) for name in names if name]
|
|
41
|
+
|
|
42
|
+
# Caso 1: Cerca la sorgente nel nome, dalla piu' importante
|
|
43
|
+
for name in names:
|
|
44
|
+
found = self.type_tags(name)
|
|
45
|
+
if found:
|
|
46
|
+
return self.type_id[min(found, key=type_priority)]
|
|
47
|
+
|
|
48
|
+
# Caso 2: Se non trova un TYPE_ID, cerca un codec e ritorna 'encode'
|
|
49
|
+
for name in names:
|
|
50
|
+
word_list = ManageTitles.clean(name).replace("-", " ").lower().split(" ")
|
|
51
|
+
if any(word in self.codec for word in word_list):
|
|
52
|
+
return self.type_id.get("encode", -1)
|
|
53
|
+
return self.type_id.get("altro", -1)
|
|
54
|
+
|
|
55
|
+
def type_tags(self, name: str) -> list[str]:
|
|
56
|
+
"""
|
|
57
|
+
TYPE_ID tags written in the name. A longer tag hides the shorter ones inside it (WEB-DLMux is not DLMux)
|
|
58
|
+
"""
|
|
59
|
+
name = re.sub(r"[._\s()\[\]{}+,]+", " ", name.lower())
|
|
60
|
+
# Skip the title: the tags start from the year, the season or the resolution ('Charlotte's Web 2006 BluRay')
|
|
61
|
+
start = TITLE_END.search(name)
|
|
62
|
+
if start and start.start() > 0:
|
|
63
|
+
name = name[start.start():]
|
|
64
|
+
# NFRip, AMZNRip.. are WEBRip from a streaming platform
|
|
65
|
+
name = PLATFORM_RIP.sub("webrip", name)
|
|
66
|
+
name = re.sub(r"(?<![a-z0-9])full disc(?![a-z0-9])", "full-disc", name)
|
|
67
|
+
|
|
68
|
+
found = []
|
|
69
|
+
for tag in sorted(self.type_id, key=len, reverse=True):
|
|
70
|
+
# 'altro' is the default type, not a tag (es. 'Un altro giro')
|
|
71
|
+
if tag == "altro":
|
|
72
|
+
continue
|
|
73
|
+
pattern = re.compile(rf"(?<![a-z0-9]){re.escape(tag)}(?![a-z0-9])")
|
|
74
|
+
if pattern.search(name):
|
|
75
|
+
found.append(tag)
|
|
76
|
+
# Hide the matched tag from the shorter ones
|
|
77
|
+
name = pattern.sub(" ", name)
|
|
78
|
+
return found
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Type tags from the most to the least important: the release source before the generic tags
|
|
82
|
+
# (BluRay, HEVC, FullHD..) and before the file extensions. Tags not in the list come last
|
|
83
|
+
TYPE_PRIORITY = [
|
|
84
|
+
"full-disc", "fulldisc",
|
|
85
|
+
"bdmux", "web-dlmux", "dlmux", "webmux",
|
|
86
|
+
"bd-untouched", "untouched", "bdremux", "remux", "vu", "vh",
|
|
87
|
+
"web-dl", "webdl", "web",
|
|
88
|
+
"webrip",
|
|
89
|
+
"hdtv",
|
|
90
|
+
"bdrip", "dvdrip",
|
|
91
|
+
"cinema-md", "hdts", "wrs", "md",
|
|
92
|
+
"3d",
|
|
93
|
+
"encode", "hdrip", "bluray", "hevc", "fullhd",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def type_priority(tag: str) -> int:
|
|
98
|
+
return TYPE_PRIORITY.index(tag) if tag in TYPE_PRIORITY else len(TYPE_PRIORITY)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Year, season or resolution after the title
|
|
102
|
+
TITLE_END = re.compile(r"(?<![a-z0-9])(?:(?:19|20)\d{2}|s\d{1,2}(?:e\d+)?|\d{3,4}[pi])(?![a-z0-9])")
|
|
103
|
+
|
|
104
|
+
# NFRip, AMZNRip, DSNPRip..
|
|
105
|
+
PLATFORM_RIP = re.compile(
|
|
106
|
+
r"(?<![a-z0-9])(?:" + "|".join(re.escape(tag.lower()) for tag, category in TAGS_LIST.items()
|
|
107
|
+
if category == "platform") + r")rip(?![a-z0-9])")
|