Aswin f8c4accd54 Reset
Reset dev
2025-03-18 00:17:27 +05:30

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