ittup 1.2.1__tar.gz → 1.2.3__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.
Files changed (108) hide show
  1. {ittup-1.2.1 → ittup-1.2.3}/PKG-INFO +2 -2
  2. {ittup-1.2.1 → ittup-1.2.3}/README.rst +1 -1
  3. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/settings.py +3 -5
  4. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/tags.py +123 -20
  5. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/bot.py +10 -4
  6. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/DocuManager.py +4 -4
  7. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/GameManager.py +4 -5
  8. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/VideoManager.py +4 -3
  9. {ittup-1.2.1 → ittup-1.2.3}/ittup.egg-info/PKG-INFO +2 -2
  10. {ittup-1.2.1 → ittup-1.2.3}/ittup.egg-info/SOURCES.txt +2 -1
  11. {ittup-1.2.1 → ittup-1.2.3}/pyproject.toml +1 -1
  12. ittup-1.2.3/tests/test_vcodec.py +129 -0
  13. {ittup-1.2.1 → ittup-1.2.3}/LICENSE +0 -0
  14. {ittup-1.2.1 → ittup-1.2.3}/ittup/__init__.py +0 -0
  15. {ittup-1.2.1 → ittup-1.2.3}/ittup/__main__.py +0 -0
  16. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/__init__.py +0 -0
  17. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/about.py +0 -0
  18. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/bdinfo_string.py +0 -0
  19. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/bittorrent.py +0 -0
  20. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/command.py +0 -0
  21. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/constants.py +0 -0
  22. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/database.py +0 -0
  23. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/__init__.py +0 -0
  24. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/__init__.py +0 -0
  25. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/client.py +0 -0
  26. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/core/__init__.py +0 -0
  27. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/core/ftpx_service.py +0 -0
  28. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/core/ftpx_session.py +0 -0
  29. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/core/menu.py +0 -0
  30. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/core/models/__init__.py +0 -0
  31. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/ftpx/core/models/list.py +0 -0
  32. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/__init__.py +0 -0
  33. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/client.py +0 -0
  34. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/core/__init__.py +0 -0
  35. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/core/api.py +0 -0
  36. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/core/models/__init__.py +0 -0
  37. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/core/models/search.py +0 -0
  38. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/core/platformid.py +0 -0
  39. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/igdb/core/tags.py +0 -0
  40. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/imageHost.py +0 -0
  41. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/mediaresult.py +0 -0
  42. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/sessions/__init__.py +0 -0
  43. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/sessions/agents.py +0 -0
  44. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/sessions/exceptions.py +0 -0
  45. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/sessions/session.py +0 -0
  46. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/__init__.py +0 -0
  47. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/__init__.py +0 -0
  48. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/api.py +0 -0
  49. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/keywords.py +0 -0
  50. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/__init__.py +0 -0
  51. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/__init__.py +0 -0
  52. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/alternative.py +0 -0
  53. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/details.py +0 -0
  54. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/movie.py +0 -0
  55. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/nowplaying.py +0 -0
  56. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/release_info.py +0 -0
  57. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/movie/translations.py +0 -0
  58. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/tvshow/__init__.py +0 -0
  59. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/tvshow/alternative.py +0 -0
  60. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/tvshow/details.py +0 -0
  61. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/tvshow/on_the_air.py +0 -0
  62. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/tvshow/translations.py +0 -0
  63. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/models/tvshow/tvshow.py +0 -0
  64. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/theMovieDB/core/videos.py +0 -0
  65. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/trailers/__init__.py +0 -0
  66. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/trailers/api.py +0 -0
  67. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/trailers/response.py +0 -0
  68. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/external_services/tvdb.py +0 -0
  69. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/extractor.py +0 -0
  70. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/frames.py +0 -0
  71. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/mediainfo.py +0 -0
  72. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/mediainfo_string.py +0 -0
  73. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/title.py +0 -0
  74. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/torrent_clients.py +0 -0
  75. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/__init__.py +0 -0
  76. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/ban_list.py +0 -0
  77. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/data.py +0 -0
  78. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/itt.py +0 -0
  79. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/signs_list.py +0 -0
  80. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/tags_list.py +0 -0
  81. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/trackers/trackers.py +0 -0
  82. {ittup-1.2.1 → ittup-1.2.3}/ittup/common/utility.py +0 -0
  83. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/__init__.py +0 -0
  84. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/__main__.py +0 -0
  85. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/automode.py +0 -0
  86. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/duplicate.py +0 -0
  87. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/exceptions.py +0 -0
  88. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media.py +0 -0
  89. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/ContentManager.py +0 -0
  90. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/MediaInfoManager.py +0 -0
  91. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/SeedManager.py +0 -0
  92. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/TorrentManager.py +0 -0
  93. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/__init__.py +0 -0
  94. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/media_manager/common.py +0 -0
  95. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/pvtDocu.py +0 -0
  96. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/pvtTorrent.py +0 -0
  97. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/pvtTracker.py +0 -0
  98. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/pvtVideo.py +0 -0
  99. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/torrent.py +0 -0
  100. {ittup-1.2.1 → ittup-1.2.3}/ittup/core/upload.py +0 -0
  101. {ittup-1.2.1 → ittup-1.2.3}/ittup/view/__init__.py +0 -0
  102. {ittup-1.2.1 → ittup-1.2.3}/ittup/view/custom_console.py +0 -0
  103. {ittup-1.2.1 → ittup-1.2.3}/ittup.egg-info/dependency_links.txt +0 -0
  104. {ittup-1.2.1 → ittup-1.2.3}/ittup.egg-info/entry_points.txt +0 -0
  105. {ittup-1.2.1 → ittup-1.2.3}/ittup.egg-info/requires.txt +0 -0
  106. {ittup-1.2.1 → ittup-1.2.3}/ittup.egg-info/top_level.txt +0 -0
  107. {ittup-1.2.1 → ittup-1.2.3}/requirements.txt +0 -0
  108. {ittup-1.2.1 → ittup-1.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ittup
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: ItaTorrents uploader
5
5
  Author: Parzival, TehMaat
6
6
  License-Expression: MIT
@@ -68,7 +68,7 @@ It performs the following tasks:
68
68
  - Seeding in qBittorrent, Transmission or rTorrent
69
69
  - Reseeding one or more torrents at a time
70
70
  - Seed your torrents across different OS
71
- - Generate various tags for titles: ``version``, ``resolution``, ``uhd``, ``platform``, ``source``, ``remux``, ``multi``, ``acodec``, ``channels``, ``flag``, ``subtitle``, ``vcodec``, ``hdr``, ``video_encoder``
71
+ - Generate various tags for titles: ``version``, ``resolution``, ``uhd``, ``platform``, ``source``, ``remux``, ``multi``, ``acodec``, ``channels``, ``flag``, ``subtitle``, ``vcodec``, ``hdr``
72
72
 
73
73
 
74
74
  ITTup can grab the first page, convert it to an image (using xpdf),
@@ -35,7 +35,7 @@ It performs the following tasks:
35
35
  - Seeding in qBittorrent, Transmission or rTorrent
36
36
  - Reseeding one or more torrents at a time
37
37
  - Seed your torrents across different OS
38
- - Generate various tags for titles: ``version``, ``resolution``, ``uhd``, ``platform``, ``source``, ``remux``, ``multi``, ``acodec``, ``channels``, ``flag``, ``subtitle``, ``vcodec``, ``hdr``, ``video_encoder``
38
+ - Generate various tags for titles: ``version``, ``resolution``, ``uhd``, ``platform``, ``source``, ``remux``, ``multi``, ``acodec``, ``channels``, ``flag``, ``subtitle``, ``vcodec``, ``hdr``
39
39
 
40
40
 
41
41
  ITTup can grab the first page, convert it to an image (using xpdf),
@@ -625,13 +625,11 @@ class Load:
625
625
  "NUMBER_OF_SCREENSHOTS": 4,
626
626
  "TAGS_POSITION_MOVIE": ["title", "year", "part", "version", "resolution", "uhd", "platform", "source",
627
627
  "remux",
628
- "multi", "acodec", "channels", "flag", "subtitle", "hdr", "vcodec",
629
- "video_encoder"],
628
+ "multi", "acodec", "channels", "flag", "subtitle", "hdr", "vcodec"],
630
629
 
631
630
  "TAGS_POSITION_SERIE": ["title", "season", "version", "resolution", "uhd", "platform", "source",
632
631
  "remux",
633
- "multi", "acodec", "channels", "flag", "subtitle", "hdr", "vcodec",
634
- "video_encoder"],
632
+ "multi", "acodec", "channels", "flag", "subtitle", "hdr", "vcodec"],
635
633
 
636
634
  "YOUTUBE_PREF_LANG": "IT",
637
635
  "DUPLICATE_ON": "true",
@@ -662,7 +660,7 @@ class Load:
662
660
  "MY_TEXT_1": "",
663
661
  "MY_TEXT_2": "",
664
662
  "MY_TEXT_3": "",
665
- "MY_TEXT_H": "[center]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/center]"
663
+ "MY_TEXT_H": ""
666
664
  },
667
665
  "options": {
668
666
  "FTPX_USER": "user",
@@ -46,18 +46,28 @@ audio_translate = {
46
46
  "MPEG Audio": "MPEG",
47
47
  }
48
48
 
49
- video_translate = {
50
- "AVC": "H.264",
51
- "HEVC": "H.265",
52
- "H265": "H.265",
53
- "H264": "H.264",
49
+ # Codec family from the MediaInfo 'format' of the video track
50
+ video_family = {
51
+ "AVC": "264",
52
+ "HEVC": "265",
54
53
  }
55
54
 
56
- video_encoder_translate = {
57
- "X265": "x.265",
58
- "X264": "x.264",
55
+ # Release type from the source/remux tags of the file name. Each tag is mapped on its own:
56
+ # DLMUX is in the "remux" category but it is a WEB-DL
57
+ UNTOUCHED_TAGS = {"REMUX", "BDREMUX", "BDMUX", "VU", "UNTOUCHED", "DVDMUX", "FULL DISC"}
58
+ WEB_TAGS = {"WEB-DL", "WEB", "WEBDL", "WEB-DLMUX", "WEBDLMUX", "WEB-MUX", "WEBMUX", "DLMUX", "HDTV"}
59
+ ENCODE_TAGS = {"ENCODE", "WEBRIP", "BDRIP", "BRRIP", "DVDRIP", "UHDRIP", "FHDRIP", "WEB-DLRIP", "WEBDLRIP"}
60
+
61
+ # How each release type writes the 264/265 codec
62
+ video_codec_labels = {
63
+ "untouched": {"264": "AVC", "265": "HEVC"},
64
+ "web": {"264": "H.264", "265": "H.265"},
65
+ "encode": {"264": "x264", "265": "x265"},
59
66
  }
60
67
 
68
+ # x264, x.264, H264, H.265, AVC, HEVC... in the file name
69
+ NAME_CODEC_PATTERN = re.compile(r'(?<![a-z0-9])(?:[xh][._ -]?26([45])|(avc)|(hevc))(?![a-z0-9])', re.IGNORECASE)
70
+
61
71
  TAG_NORMALIZE = {
62
72
  "BLURAY": "BluRay",
63
73
  "BLU-RAY": "BluRay",
@@ -85,6 +95,8 @@ class SearchTags(object):
85
95
  self.title = title
86
96
  self.year = year
87
97
  self.tags_dict = {}
98
+ self.release_type: str | None = None
99
+ self.name_family: str | None = None
88
100
 
89
101
  self.TAG_TYPES: dict = tags_list
90
102
  self.SIGNS_LIST: dict = sign_list
@@ -187,6 +199,14 @@ class SearchTags(object):
187
199
  if self.tags_dict.get("source", None):
188
200
  self.tags_dict["source"] = list(dict.fromkeys(self.tags_dict["source"]))
189
201
 
202
+ # x264/x265 from the file name only tell the codec family: the codec is written by vcodec
203
+ name_encoders = self.tags_dict.pop('video_encoder', [])
204
+ self.name_family = self.codec_family_from_name(self.filename, name_encoders)
205
+ # Read the release type before 'remux' gets the REMUX tag appended below
206
+ self.release_type = self.detect_release_type(
207
+ self.tags_dict.get('source', []) + self.tags_dict.get('remux', [])
208
+ )
209
+
190
210
  if norm:
191
211
  # Skip if it is part of title es: "Wicked.Parte.2.2025.iTA" Title = Wicked Parte 2
192
212
  if not any(t in self.title.lower() for t in ['part', 'parte']):
@@ -208,10 +228,6 @@ class SearchTags(object):
208
228
  elif category == "vcodec":
209
229
  updated_category = self.mediainfo_video(category=category)
210
230
 
211
- elif category == "video_encoder":
212
- if self.tags_dict.get('video_encoder', None):
213
- self.tags_dict['video_encoder'][0] = self.tags_dict['video_encoder'][0].lower()
214
-
215
231
  elif category == "hdr":
216
232
  updated_category = self.mediainfo_hdr(category=category)
217
233
  if not updated_category:
@@ -297,16 +313,103 @@ class SearchTags(object):
297
313
  audio_codecs.extend(languages)
298
314
  return {category: audio_codecs}
299
315
 
316
+ @staticmethod
317
+ def detect_release_type(tags: list) -> str | None:
318
+ """
319
+ Release type from the source/remux tags: 'encode', 'untouched', 'web' or None when uncertain
320
+ (es. BLURAY alone or no tags)
321
+ """
322
+ tags = {str(tag).upper() for tag in tags}
323
+ if tags & ENCODE_TAGS:
324
+ return "encode"
325
+ if tags & UNTOUCHED_TAGS:
326
+ return "untouched"
327
+ if tags & WEB_TAGS:
328
+ return "web"
329
+ return None
330
+
331
+ @staticmethod
332
+ def codec_family_from_name(filename: str, name_encoders: list) -> str | None:
333
+ """
334
+ Codec family ('264' or '265') written in the file name, None if missing or ambiguous
335
+ """
336
+ families = {str(tag)[-3:] for tag in name_encoders}
337
+ for match in NAME_CODEC_PATTERN.finditer(filename):
338
+ if match.group(1):
339
+ families.add(f"26{match.group(1)}")
340
+ elif match.group(2):
341
+ families.add("264")
342
+ else:
343
+ families.add("265")
344
+ families &= {"264", "265"}
345
+ return families.pop() if len(families) == 1 else None
346
+
347
+ @staticmethod
348
+ def encoder_signature(video: dict) -> str | None:
349
+ """
350
+ x264/x265 signature left by the encoder in the video track ('264', '265' or None)
351
+ """
352
+ library = str(video.get('writing_library') or '').lower()
353
+ for family in ("264", "265"):
354
+ if f"x{family}" in library:
355
+ return family
356
+ settings = str(video.get('encoding_settings') or '').lower()
357
+ if "cabac=" in settings:
358
+ return "264"
359
+ if "cpuid=" in settings or "ctu=" in settings:
360
+ return "265"
361
+ return None
362
+
363
+ @staticmethod
364
+ def normalize_other_codec(video: dict, filename: str) -> str:
365
+ """
366
+ Codecs other than 264/265 (AV1, VP9, VC-1, MPEG-2, XviD...)
367
+ """
368
+ video_format = str(video.get('format') or '')
369
+ if video_format == "MPEG Video":
370
+ version = re.search(r'\d', str(video.get('format_version') or ''))
371
+ return f"MPEG-{version.group(0)}" if version else "MPEG"
372
+ if video_format == "MPEG-4 Visual":
373
+ hint = f"{video.get('codec_id') or ''} {video.get('writing_library') or ''} {filename}".upper()
374
+ if "XVID" in hint:
375
+ return "XviD"
376
+ if "DIVX" in hint or "DX50" in hint:
377
+ return "DivX"
378
+ return "MPEG-4"
379
+ return video_format
380
+
300
381
  def mediainfo_video(self, category: str) -> dict:
301
- codec_translated = {}
302
- if self.mediafile.video_track:
303
- for video in self.mediafile.video_track:
304
- video_format = video.get('format', "")
305
- codec_translated = video_translate.get(video_format, video_format)
306
- if codec_translated:
307
- return {category: codec_translated}
308
- return codec_translated
382
+ video = self.mediafile.video_track[0] if self.mediafile.video_track else {}
383
+ video_format = str(video.get('format') or '')
384
+ mediainfo_family = video_family.get(video_format)
385
+
386
+ # The file name tells the family, MediaInfo wins when they don't agree
387
+ family = self.name_family
388
+ if video_format:
389
+ if self.name_family and self.name_family != mediainfo_family:
390
+ custom_console.bot_warning_log(
391
+ f"<> Video codec warning: the file name says {self.name_family} but MediaInfo says"
392
+ f" '{video_format}'. Using MediaInfo")
393
+ family = mediainfo_family
394
+
395
+ if not family:
396
+ if video_format:
397
+ return {category: self.normalize_other_codec(video, self.filename)}
398
+ return {}
399
+
400
+ # The encoder in MediaInfo is only a check: it is never read on a WEB-DL
401
+ if self.release_type == "untouched":
402
+ if self.encoder_signature(video):
403
+ custom_console.bot_warning_log(
404
+ f"<> Warning: possible fake REMUX, the video track was encoded with"
405
+ f" '{video.get('writing_library') or 'x' + family}'")
406
+ label = "untouched"
407
+ elif self.release_type in ("encode", "web"):
408
+ label = self.release_type
409
+ else:
410
+ label = "encode" if self.encoder_signature(video) else "web"
309
411
 
412
+ return {category: video_codec_labels[label][family]}
310
413
 
311
414
  def mediainfo_hdr(self, category: str) -> dict:
312
415
  if self.mediafile.video_track:
@@ -198,11 +198,10 @@ class Bot:
198
198
 
199
199
  # Start uploading only the new files, one at a time
200
200
  print()
201
- self.run(include_paths=moved_paths)
202
- print()
203
- announce_idle = True
204
201
  for moved_path in moved_paths:
205
202
  self._run_isolated(include_path=moved_path, timeout=timeout)
203
+ print()
204
+ announce_idle = True
206
205
 
207
206
  except KeyboardInterrupt:
208
207
  custom_console.bot_log("Exiting...")
@@ -216,8 +215,15 @@ class Bot:
216
215
  Returns:
217
216
  True if the child process ended without errors
218
217
  """
218
+ # The child must be a fork of the watcher: it shares the loaded config and the console (the questions
219
+ # are answered on stdin), and the Bot can't be pickled for 'spawn' or 'forkserver' (the default
220
+ # since Python 3.14). Where fork isn't available or safe (Windows, macOS) the entry runs in the watcher
221
+ if "fork" not in multiprocessing.get_all_start_methods() or sys.platform == "darwin":
222
+ return self.run(include_paths=[include_path])
223
+
219
224
  name = os.path.basename(include_path)
220
- process = multiprocessing.Process(target=_run_worker, args=(self, include_path), daemon=True)
225
+ process = multiprocessing.get_context("fork").Process(target=_run_worker, args=(self, include_path),
226
+ daemon=True)
221
227
  process.start()
222
228
  try:
223
229
  process.join(timeout if timeout > 0 else None)
@@ -38,14 +38,14 @@ class DocuManager:
38
38
  custom_console.bot_log(f"Watcher Active.. skip the old upload '{content.file_name}'")
39
39
  continue
40
40
 
41
- torrent_response = UserContent.torrent(content=content, tracker_name_list=tracker_name_list,
42
- selected_tracker=selected_tracker, this_path=torrent_filepath)
43
-
44
- # Skip if it is a duplicate
41
+ # Skip if it is a duplicate, before hashing the files
45
42
  if ((self.cli.duplicate or config_settings.user_preferences.DUPLICATE_ON)
46
43
  and UserContent.is_duplicate(content=content, tracker_name=selected_tracker, cli=self.cli)):
47
44
  continue
48
45
 
46
+ torrent_response = UserContent.torrent(content=content, tracker_name_list=tracker_name_list,
47
+ selected_tracker=selected_tracker, this_path=torrent_filepath)
48
+
49
49
  # print the title will be shown on the torrent page
50
50
  custom_console.bot_log(f"'DISPLAYNAME'...{{{content.display_name}}}\n")
51
51
 
@@ -61,15 +61,14 @@ class GameManager:
61
61
  custom_console.bot_log(f"Watcher Active.. skip the old upload '{content.file_name}'")
62
62
  continue
63
63
 
64
- torrent_response = UserContent.torrent(content=content, tracker_name_list=tracker_name_list,
65
- selected_tracker=selected_tracker, this_path=torrent_filepath)
66
-
67
-
68
- # Skip if it is a duplicate
64
+ # Skip if it is a duplicate, before hashing the files
69
65
  if ((self.cli.duplicate or config_settings.user_preferences.DUPLICATE_ON)
70
66
  and UserContent.is_duplicate(content=content, tracker_name=selected_tracker, cli=self.cli)):
71
67
  continue
72
68
 
69
+ torrent_response = UserContent.torrent(content=content, tracker_name_list=tracker_name_list,
70
+ selected_tracker=selected_tracker, this_path=torrent_filepath)
71
+
73
72
  # Login to IGDB only when the first game needs it
74
73
  if not self.igdb_logged:
75
74
  if not self.igdb.connect():
@@ -83,15 +83,16 @@ class VideoManager:
83
83
  # Filter contents based on the preferred language or duplicates
84
84
  if UserContent.is_preferred_language(content=content):
85
85
 
86
- torrent_response = UserContent.torrent(content=content, tracker_name_list=tracker_name_list,
87
- selected_tracker=selected_tracker, this_path=torrent_filepath)
88
-
89
86
  # Skip(S) if it is a duplicate or let the user choose to continue (C)
87
+ # Checked before hashing so a skipped duplicate doesn't cost a torrent file
90
88
  if ((self.cli.duplicate or config_settings.user_preferences.DUPLICATE_ON)
91
89
  and UserContent.is_duplicate(content=content, tracker_name=selected_tracker,
92
90
  cli=self.cli)):
93
91
  continue
94
92
 
93
+ torrent_response = UserContent.torrent(content=content, tracker_name_list=tracker_name_list,
94
+ selected_tracker=selected_tracker, this_path=torrent_filepath)
95
+
95
96
  # Search for VIDEO ID
96
97
  db_online = DbOnline(media=content, category=content.category, no_title=self.cli.notitle)
97
98
  db = db_online.media_result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ittup
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: ItaTorrents uploader
5
5
  Author: Parzival, TehMaat
6
6
  License-Expression: MIT
@@ -68,7 +68,7 @@ It performs the following tasks:
68
68
  - Seeding in qBittorrent, Transmission or rTorrent
69
69
  - Reseeding one or more torrents at a time
70
70
  - Seed your torrents across different OS
71
- - Generate various tags for titles: ``version``, ``resolution``, ``uhd``, ``platform``, ``source``, ``remux``, ``multi``, ``acodec``, ``channels``, ``flag``, ``subtitle``, ``vcodec``, ``hdr``, ``video_encoder``
71
+ - Generate various tags for titles: ``version``, ``resolution``, ``uhd``, ``platform``, ``source``, ``remux``, ``multi``, ``acodec``, ``channels``, ``flag``, ``subtitle``, ``vcodec``, ``hdr``
72
72
 
73
73
 
74
74
  ITTup can grab the first page, convert it to an image (using xpdf),
@@ -102,4 +102,5 @@ ittup/core/media_manager/VideoManager.py
102
102
  ittup/core/media_manager/__init__.py
103
103
  ittup/core/media_manager/common.py
104
104
  ittup/view/__init__.py
105
- ittup/view/custom_console.py
105
+ ittup/view/custom_console.py
106
+ tests/test_vcodec.py
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  dynamic = ["dependencies"]
7
7
  name = "ittup"
8
- version = "1.2.1"
8
+ version = "1.2.3"
9
9
  description = "ItaTorrents uploader"
10
10
  readme = "README.rst"
11
11
  requires-python = ">=3.10"
@@ -0,0 +1,129 @@
1
+ # -*- coding: utf-8 -*-
2
+ from types import SimpleNamespace
3
+
4
+ import pytest
5
+
6
+ from ittup.common import tags
7
+ from ittup.common.settings import Validate
8
+ from ittup.common.tags import SearchTags
9
+ from ittup.common.trackers.tags_list import TAGS_LIST
10
+
11
+ TAGS_POSITION = ["title", "year", "part", "version", "resolution", "uhd", "platform", "source", "remux",
12
+ "multi", "acodec", "channels", "flag", "subtitle", "hdr", "vcodec"]
13
+
14
+ X264_LIBRARY = "x264 core 164 r3095 baee400"
15
+ X265_LIBRARY = "x265 3.5+1-f0c1022b6:[Linux][GCC 9.3.0][64 bit] 10bit"
16
+
17
+
18
+ @pytest.fixture
19
+ def warnings(monkeypatch):
20
+ logged = []
21
+ monkeypatch.setattr(tags.custom_console, "bot_warning_log", logged.append)
22
+ return logged
23
+
24
+
25
+ def build_title(filename: str, video_format: str | None, writing_library: str = "",
26
+ encoding_settings: str = "", tags_position: list | None = None,
27
+ **video_fields) -> tuple[str, dict]:
28
+ video_track = []
29
+ if video_format:
30
+ video_track.append({"format": video_format, "writing_library": writing_library,
31
+ "encoding_settings": encoding_settings, **video_fields})
32
+ mediafile = SimpleNamespace(video_track=video_track, audio_track=[], subtitle_track=[])
33
+ media = SimpleNamespace(mediafile=mediafile, torrent_pack=False, resolution="1080p", pack=None)
34
+ search = SearchTags(filename=filename, title="Movie", year="2020", season=None, episode=None, media=media,
35
+ tags_position=list(tags_position or TAGS_POSITION), tags_list=TAGS_LIST, sign_list={},
36
+ ban_list={"dummy": "banned"}, releaser_sign="")
37
+ return search.process()
38
+
39
+
40
+ def vcodec(filename: str, video_format: str | None, **kwargs) -> str | None:
41
+ _, tags_dict = build_title(filename, video_format, **kwargs)
42
+ return tags_dict.get("vcodec")
43
+
44
+
45
+ def test_remux_hevc(warnings):
46
+ assert vcodec("Movie.2020.2160p.BluRay.REMUX.HEVC.TrueHD.7.1-GRP.mkv", "HEVC") == "HEVC"
47
+ assert not warnings
48
+
49
+
50
+ def test_remux_with_x265_signature_warns(warnings):
51
+ codec = vcodec("Movie.2020.2160p.BluRay.REMUX.HEVC-GRP.mkv", "HEVC", writing_library=X265_LIBRARY)
52
+ assert codec == "HEVC"
53
+ assert any("fake REMUX" in message for message in warnings)
54
+
55
+
56
+ def test_web_dl_avc_with_x264_signature(warnings):
57
+ codec = vcodec("Movie.2020.1080p.AMZN.WEB-DL.DDP5.1.H.264-GRP.mkv", "AVC", writing_library=X264_LIBRARY)
58
+ assert codec == "H.264"
59
+ assert not warnings
60
+
61
+
62
+ def test_dlmux_is_web_dl():
63
+ assert vcodec("Movie.2020.1080p.DLMux.ITA.ENG.AC3-GRP.mkv", "AVC", writing_library=X264_LIBRARY) == "H.264"
64
+
65
+
66
+ def test_webrip_x265():
67
+ assert vcodec("Movie.2020.1080p.WEBRip.x265-GRP.mkv", "HEVC") == "x265"
68
+
69
+
70
+ def test_encode_x265_is_not_doubled(warnings):
71
+ title, tags_dict = build_title("Movie.2020.1080p.BDRip.x265.10bit-GRP.mkv", "HEVC",
72
+ writing_library=X265_LIBRARY)
73
+ assert tags_dict["vcodec"] == "x265"
74
+ assert "video_encoder" not in tags_dict
75
+ assert "H.265" not in title
76
+ assert title.count("x265") == 1
77
+ assert not warnings
78
+
79
+
80
+ def test_bluray_alone_with_x264_signature():
81
+ assert vcodec("Movie.2020.1080p.BluRay.DTS-GRP.mkv", "AVC", writing_library=X264_LIBRARY) == "x264"
82
+
83
+
84
+ def test_bluray_alone_without_signature():
85
+ assert vcodec("Movie.2020.1080p.BluRay.DTS-GRP.mkv", "AVC") == "H.264"
86
+
87
+
88
+ def test_no_release_type_no_signature():
89
+ assert vcodec("Movie.2020.1080p.ITA.ENG-GRP.mkv", "AVC") == "H.264"
90
+
91
+
92
+ def test_signature_from_encoding_settings():
93
+ settings = "cabac=1 / ref=4 / deblock=1:-3:-3 / analyse=0x3:0x133"
94
+ assert vcodec("Movie.2020.1080p.ITA-GRP.mkv", "AVC", encoding_settings=settings) == "x264"
95
+
96
+
97
+ def test_name_and_mediainfo_disagree(warnings):
98
+ codec = vcodec("Movie.2020.1080p.BDRip.x264-GRP.mkv", "HEVC", writing_library=X265_LIBRARY)
99
+ assert codec == "x265"
100
+ assert any("MediaInfo" in message for message in warnings)
101
+
102
+
103
+ def test_family_from_name_without_mediainfo():
104
+ assert vcodec("Movie.2020.1080p.WEBRip.x264-GRP.mkv", None) == "x264"
105
+ assert vcodec("Movie.2020.1080p.BluRay.REMUX.AVC-GRP.mkv", None) == "AVC"
106
+
107
+
108
+ @pytest.mark.parametrize("video_format, video_fields, expected", [
109
+ ("AV1", {}, "AV1"),
110
+ ("VP9", {}, "VP9"),
111
+ ("VC-1", {}, "VC-1"),
112
+ ("MPEG Video", {"format_version": "Version 2"}, "MPEG-2"),
113
+ ("MPEG-4 Visual", {"codec_id": "XVID"}, "XviD"),
114
+ ])
115
+ def test_other_codecs(warnings, video_format, video_fields, expected):
116
+ # The release type rules don't apply to codecs other than 264/265
117
+ assert vcodec("Movie.2020.1080p.BluRay.REMUX-GRP.mkv", video_format, **video_fields) == expected
118
+ assert vcodec("Movie.2020.1080p.WEBRip-GRP.mkv", video_format, writing_library=X264_LIBRARY,
119
+ **video_fields) == expected
120
+ assert not warnings
121
+
122
+
123
+ def test_video_encoder_in_tags_position():
124
+ position = TAGS_POSITION + ["video_encoder"]
125
+ assert Validate.validate_tags_position(position) == position
126
+
127
+ title, tags_dict = build_title("Movie.2020.1080p.WEBRip.x265-GRP.mkv", "HEVC", tags_position=position)
128
+ assert "video_encoder" not in tags_dict
129
+ assert title.lower().count("265") == 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes