205 lines
7.8 KiB
Python
205 lines
7.8 KiB
Python
import logging
|
|
import re
|
|
import unicodedata
|
|
from enum import Enum
|
|
|
|
from langcodes import Language
|
|
from unidecode import unidecode
|
|
|
|
from vinetrimmer.objects.tracks import Tracks
|
|
|
|
VIDEO_CODEC_MAP = {
|
|
"AVC": "H.264",
|
|
"HEVC": "H.265"
|
|
}
|
|
DYNAMIC_RANGE_MAP = {
|
|
"HDR10": "HDR",
|
|
"HDR10+": "HDR",
|
|
"HDR10 / HDR10+": "HDR",
|
|
"Dolby Vision": "DV"
|
|
}
|
|
AUDIO_CODEC_MAP = {
|
|
"E-AC-3": "DDP",
|
|
"AC-3": "DD"
|
|
}
|
|
|
|
|
|
class Title:
|
|
def __init__(self, id_, type_, name=None, year=None, season=None, episode=None, episode_name=None,
|
|
original_lang=None, source=None, service_data=None, tracks=None, filename=None):
|
|
self.id = id_
|
|
self.type = type_
|
|
self.name = name
|
|
self.year = int(year or 0)
|
|
self.season = int(season or 0)
|
|
self.episode = int(episode or 0)
|
|
self.episode_name = episode_name
|
|
self.original_lang = Language.get(original_lang) if original_lang else None
|
|
self.source = source
|
|
self.service_data = service_data or {}
|
|
self.tracks = tracks or Tracks()
|
|
self.filename = filename
|
|
|
|
if not self.filename:
|
|
# auto generated initial filename
|
|
self.filename = self.parse_filename()
|
|
|
|
def parse_filename(self, media_info=None, folder=False):
|
|
from vinetrimmer.config import config
|
|
|
|
if media_info:
|
|
video_track = next(iter(media_info.video_tracks), None)
|
|
if config.output_template.get("use_last_audio", False):
|
|
audio_track = next(iter(reversed(media_info.audio_tracks)), None)
|
|
else:
|
|
audio_track = next(iter(media_info.audio_tracks), None)
|
|
else:
|
|
video_track = None
|
|
audio_track = None
|
|
|
|
# create the initial filename string
|
|
|
|
if video_track:
|
|
quality = video_track.height
|
|
aspect = [int(float(x)) for x in video_track.other_display_aspect_ratio[0].split(":")]
|
|
if len(aspect) == 1:
|
|
aspect.append(1)
|
|
aspect_w, aspect_h = aspect
|
|
if aspect_w / aspect_h not in (16 / 9, 4 / 3):
|
|
# We want the resolution represented in a 4:3 or 16:9 canvas
|
|
# if it's not 4:3 or 16:9, calculate as if it's inside a 16:9 canvas
|
|
# otherwise the track's height value is fine
|
|
# We are assuming this title is some weird aspect ratio so most
|
|
# likely a movie or HD source, so it's most likely widescreen so
|
|
# 16:9 canvas makes the most sense
|
|
quality = int(video_track.width * (9 / 16))
|
|
if video_track.width == 1248: # AMZN weird resolution (1248x520)
|
|
quality = 720
|
|
if isinstance(self.tracks.videos[0].extra, dict) and self.tracks.videos[0].extra.get("quality"):
|
|
quality = self.tracks.videos[0].extra["quality"]
|
|
else:
|
|
quality = ""
|
|
|
|
if audio_track:
|
|
audio = f"{AUDIO_CODEC_MAP.get(audio_track.format) or audio_track.format}"
|
|
audio += f"{float(sum({'LFE': 0.1}.get(x, 1) for x in audio_track.channel_layout.split(' '))):.1f} "
|
|
if audio_track.format_additionalfeatures and "JOC" in audio_track.format_additionalfeatures:
|
|
audio += "Atmos "
|
|
else:
|
|
audio = ""
|
|
|
|
video = ""
|
|
if video_track:
|
|
if (video_track.hdr_format or "").startswith("Dolby Vision"):
|
|
video += "DV "
|
|
elif video_track.hdr_format_commercial:
|
|
video += f"{DYNAMIC_RANGE_MAP.get(video_track.hdr_format_commercial)} "
|
|
elif ("HLG" in (video_track.transfer_characteristics or "")
|
|
or "HLG" in (video_track.transfer_characteristics_original or "")):
|
|
video += "HLG "
|
|
if float(video_track.frame_rate) > 30 and self.source != "iP":
|
|
video += "HFR "
|
|
video += f"{VIDEO_CODEC_MAP.get(video_track.format) or video_track.format}"
|
|
|
|
tag = config.tag
|
|
if quality and quality <= 576:
|
|
tag = config.tag_sd or tag
|
|
|
|
if self.type == Title.Types.MOVIE:
|
|
filename = config.output_template["movies"].format(
|
|
title=self.name,
|
|
year=self.year or "",
|
|
quality=f"{quality}p" if quality else "",
|
|
source=self.source,
|
|
audio=audio,
|
|
video=video,
|
|
tag=tag,
|
|
)
|
|
else:
|
|
episode_name = self.episode_name
|
|
# TODO: Maybe we should only strip these if all episodes have such names.
|
|
if re.fullmatch(r"(?:Episode|Chapter|Capitulo|Folge) \d+", episode_name or ""):
|
|
episode_name = None
|
|
|
|
filename = config.output_template["series"].format(
|
|
title=self.name,
|
|
season_episode=(f"S{self.season:02}"
|
|
+ (f"E{self.episode:02}" if (self.episode is not None and not folder) else "")),
|
|
episode_name=(episode_name or "") if not folder else "",
|
|
quality=f"{quality}p" if quality else "",
|
|
source=self.source,
|
|
audio=audio,
|
|
video=video,
|
|
tag=tag,
|
|
)
|
|
|
|
filename = re.sub(r"\s+", ".", filename)
|
|
filename = re.sub(r"\.\.+", ".", filename)
|
|
filename = re.sub(fr"\.+(-{re.escape(config.tag)})$", r"\1", filename)
|
|
filename = filename.rstrip().rstrip(".") # remove whitespace and last right-sided . if needed
|
|
|
|
return self.normalize_filename(filename)
|
|
|
|
@staticmethod
|
|
def normalize_filename(filename):
|
|
# replace all non-ASCII characters with ASCII equivalents
|
|
filename = filename.replace("æ", "ae")
|
|
filename = filename.replace("ø", "oe")
|
|
filename = filename.replace("å", "aa")
|
|
filename = filename.replace("'", "")
|
|
filename = unidecode(filename)
|
|
filename = "".join(c for c in filename if unicodedata.category(c) != "Mn")
|
|
|
|
# remove or replace further characters as needed
|
|
filename = filename.replace("/", " - ") # e.g. amazon multi-episode titles
|
|
filename = filename.replace("&", " and ")
|
|
filename = filename.replace("$", "S")
|
|
filename = re.sub(r"[:; ]", ".", filename) # structural chars to .
|
|
filename = re.sub(r"[\\*!?¿,'\"()<>|#]", "", filename) # unwanted chars
|
|
filename = re.sub(r"[. ]{2,}", ".", filename) # replace 2+ neighbour dots and spaces with .
|
|
return filename
|
|
|
|
def is_wanted(self, wanted):
|
|
if self.type != Title.Types.TV or not wanted:
|
|
return True
|
|
return f"{self.season}x{self.episode}" in wanted
|
|
|
|
class Types(Enum):
|
|
MOVIE = 1
|
|
TV = 2
|
|
|
|
|
|
class Titles(list):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.title_name = None
|
|
|
|
if self:
|
|
self.title_name = self[0].name
|
|
|
|
def print(self):
|
|
log = logging.getLogger("Titles")
|
|
log.info(f"Title: {self.title_name}")
|
|
if any(x.type == Title.Types.TV for x in self):
|
|
log.info(f"Total Episodes: {len(self)}")
|
|
log.info(
|
|
"By Season: {}".format(
|
|
", ".join(list(dict.fromkeys(
|
|
f"{x.season} ({len([y for y in self if y.season == x.season])})"
|
|
for x in self if x.type == Title.Types.TV
|
|
)))
|
|
)
|
|
)
|
|
|
|
def order(self):
|
|
"""This will order the Titles to be oldest first."""
|
|
self.sort(key=lambda t: int(t.year or 0))
|
|
self.sort(key=lambda t: int(t.episode or 0))
|
|
self.sort(key=lambda t: int(t.season or 0))
|
|
|
|
def with_wanted(self, wanted):
|
|
"""Yield only wanted tracks."""
|
|
for title in self:
|
|
if title.is_wanted(wanted):
|
|
yield title
|