Netflix-videos-downloader/helpers/Muxer.py

632 wiersze
17 KiB
Python

import re, os, sys, subprocess, contextlib, json, glob
from configs.config import tool
from helpers.ripprocess import ripprocess
from pymediainfo import MediaInfo
import logging
class Muxer(object):
def __init__(self, **kwargs):
self.logger = logging.getLogger(__name__)
self.CurrentName_Original = kwargs.get("CurrentName", None)
self.CurrentName = kwargs.get("CurrentName", None)
self.SeasonFolder = kwargs.get("SeasonFolder", None)
self.CurrentHeigh = kwargs.get("CurrentHeigh", None)
self.CurrentWidth = kwargs.get("CurrentWidth", None)
self.source_tag = kwargs.get("Source", None)
self.AudioProfile = self.get_audio_id() # kwargs.get("AudioProfile", None)
self.VideoProfile = self.get_video_id() # kwargs.get("VideoProfile", None)
self.mkvmerge = tool().bin()["mkvmerge"]
self.merge = []
self.muxer_settings = tool().muxer()
##############################################################################
self.packer = kwargs.get("group", None)
self.extra_output_folder = self.packer["EXTRA_FOLDER"]
self.Group = (
self.packer["GROUP"]
if self.packer["GROUP"]
else self.muxer_settings["GROUP"]
)
self.muxer_scheme = (
self.packer["SCHEME"]
if self.packer["SCHEME"]
else self.muxer_settings["scheme"]
)
self.scheme = self.muxer_settings["schemeslist"][self.muxer_scheme]
self.Extras = self.muxer_settings["EXTRAS"]
self.fps24 = True if self.source_tag in self.muxer_settings["FPS24"] else False
self.default_mux = True if self.muxer_settings["DEFAULT"] else False
self.PrepareMuxer()
def is_extra_folder(self):
extra_folder = None
if self.extra_output_folder:
if not os.path.isabs(self.extra_output_folder):
raise ValueError("Error you should provide full path dir: {}.".format(self.extra_output_folder))
if not os.path.exists(self.extra_output_folder):
try:
os.makedirs(self.extra_output_folder)
except Exception as e:
raise ValueError("Error when create folder dir [{}]: {}.".format(e, self.extra_output_folder))
extra_folder = self.extra_output_folder
return extra_folder
if self.muxer_settings["mkv_folder"]:
if not os.path.isabs(self.muxer_settings["mkv_folder"]):
raise ValueError("Error you should provide full path dir: {}.".format(self.muxer_settings["mkv_folder"]))
if not os.path.exists(self.muxer_settings["mkv_folder"]):
try:
os.makedirs(self.muxer_settings["mkv_folder"])
except Exception as e:
raise ValueError("Error when create folder dir [{}]: {}.".format(e, self.muxer_settings["mkv_folder"]))
extra_folder = self.muxer_settings["mkv_folder"]
return extra_folder
return extra_folder
def PrepareMuxer(self):
if self.muxer_settings["noTitle"]:
self.CurrentName = self.noTitle()
extra_folder = self.is_extra_folder()
if extra_folder:
self.SeasonFolder = extra_folder
else:
if not self.default_mux:
if self.SeasonFolder:
self.SeasonFolder = self.setFolder()
return
def SortFilesBySize(self):
file_list = []
audio_tracks = (
glob.glob(f"{self.CurrentName_Original}*.eac3")
+ glob.glob(f"{self.CurrentName_Original}*.ac3")
+ glob.glob(f"{self.CurrentName_Original}*.aac")
+ glob.glob(f"{self.CurrentName_Original}*.m4a")
+ glob.glob(f"{self.CurrentName_Original}*.dts")
)
if audio_tracks == []:
raise FileNotFoundError("no audio files found")
for file in audio_tracks:
file_list.append({"file": file, "size": os.path.getsize(file)})
file_list = sorted(file_list, key=lambda k: int(k["size"]))
return file_list[-1]["file"]
def GetVideoFile(self):
videofiles = [
"{} [{}p]_Demuxed.mp4",
"{} [{}p]_Demuxed.mp4",
"{} [{}p] [UHD]_Demuxed.mp4",
"{} [{}p] [UHD]_Demuxed.mp4",
"{} [{}p] [VP9]_Demuxed.mp4",
"{} [{}p] [HIGH]_Demuxed.mp4",
"{} [{}p] [VP9]_Demuxed.mp4",
"{} [{}p] [HEVC]_Demuxed.mp4",
"{} [{}p] [HDR]_Demuxed.mp4",
"{} [{}p] [HDR-DV]_Demuxed.mp4",
]
for videofile in videofiles:
filename = videofile.format(self.CurrentName_Original, self.CurrentHeigh)
if os.path.isfile(filename):
return filename
return None
def get_video_id(self):
video_file = self.GetVideoFile()
if not video_file:
raise ValueError("No Video file in Dir...")
media_info = MediaInfo.parse(video_file)
track = [track for track in media_info.tracks if track.track_type == "Video"][0]
if track.format == "AVC":
if track.encoding_settings:
return "x264"
return "H.264"
elif track.format == "HEVC":
if track.commercial_name == "HDR10" and track.color_primaries:
return "HDR.HEVC"
if track.commercial_name == "HEVC" and track.color_primaries:
return "HEVC"
return "DV.HEVC"
return None
def get_audio_id(self):
audio_id = None
media_info = MediaInfo.parse(self.SortFilesBySize())
track = [track for track in media_info.tracks if track.track_type == "Audio"][0]
if track.format == "E-AC-3":
audioCodec = "DDP"
elif track.format == "AC-3":
audioCodec = "DD"
elif track.format == "AAC":
audioCodec = "AAC"
elif track.format == "DTS":
audioCodec = "DTS"
elif "DTS" in track.format:
audioCodec = "DTS"
else:
audioCodec = "DDP"
if track.channel_s == 8:
channels = "7.1"
elif track.channel_s == 6:
channels = "5.1"
elif track.channel_s == 2:
channels = "2.0"
elif track.channel_s == 1:
channels = "1.0"
else:
channels = "5.1"
audio_id = (
f"{audioCodec}{channels}.Atmos"
if "Atmos" in track.commercial_name
else f"{audioCodec}{channels}"
)
return audio_id
def Heigh(self):
try:
Width = int(self.CurrentWidth)
Heigh = int(self.CurrentHeigh)
except Exception:
return self.CurrentHeigh
res1080p = "1080p"
res720p = "720p"
sd = ""
if Width >= 3840:
return "2160p"
if Width >= 2560:
return "1440p"
if Width > 1920:
if Heigh > 1440:
return "2160p"
return "1440p"
if Width == 1920:
return res1080p
elif Width == 1280:
return res720p
if Width >= 1400:
return res1080p
if Width < 1400 and Width >= 1100:
return res720p
if Heigh == 1080:
return res1080p
elif Heigh == 720:
return res720p
if Heigh >= 900:
return res1080p
if Heigh < 900 and Heigh >= 700:
return res720p
return sd
def noTitle(self):
regex = re.compile("(.*) [S]([0-9]+)[E]([0-9]+)")
if regex.search(self.CurrentName):
return regex.search(self.CurrentName).group(0)
return self.CurrentName
def Run(self, command):
self.logger.debug("muxing command: {}".format(command))
def unbuffered(proc, stream="stdout"):
newlines = ["\n", "\r\n", "\r"]
stream = getattr(proc, stream)
with contextlib.closing(stream):
while True:
out = []
last = stream.read(1)
# Don't loop forever
if last == "" and proc.poll() is not None:
break
while last not in newlines:
# Don't loop forever
if last == "" and proc.poll() is not None:
break
out.append(last)
last = stream.read(1)
out = "".join(out)
yield out
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True,
)
self.logger.info("\nStart Muxing...")
for line in unbuffered(proc):
if "Progress:" in line:
sys.stdout.write("\r%s" % (line))
sys.stdout.flush()
elif "Multiplexing" in line:
sys.stdout.write("\r%s" % (line.replace("Multiplexing", "Muxing")))
sys.stdout.flush()
elif "Error" in line:
sys.stdout.write("\r%s" % (line))
sys.stdout.flush()
self.logger.info("")
def setName(self):
outputVideo = (
self.scheme.replace(
"{t}", ripprocess().CleanMyFileNamePlease(self.CurrentName)
)
.replace("{r}", self.Heigh())
.replace("{s}", self.source_tag)
.replace("{ac}", self.AudioProfile)
.replace("{vc}", self.VideoProfile)
.replace("{gr}", self.Group)
)
for i in range(10):
outputVideo = re.sub(r"(\.\.)", ".", outputVideo)
if self.SeasonFolder:
outputVideo = os.path.join(os.path.abspath(self.SeasonFolder), outputVideo)
outputVideo = outputVideo.replace("\\", "/")
return f"{outputVideo}.mkv"
def setFolder(self):
folder = (
self.scheme.replace(
"{t}", ripprocess().CleanMyFileNamePlease(self.SeasonFolder)
)
.replace("{r}", self.Heigh())
.replace("{s}", self.source_tag)
.replace("{ac}", self.AudioProfile)
.replace("{vc}", self.VideoProfile)
.replace("{gr}", self.Group)
)
for i in range(10):
folder = re.sub(r"(\.\.)", ".", folder)
return folder
def LanguageList(self):
LanguageList = [
["Simplified Chinese", "zhoS", "chi", "Simplified Chinese"],
["Traditional Chinese", "zhoT", "chi", "Traditional Chinese"],
["Mandarin (Putonghua)", "zho", "zh-cmn", "Mandarin"],
["Mandarin (Guoyu)", "zho", "chi", "Chinese"],
["Cantonese", "zho", "zh-yue", "Cantonese"],
["Taiwanese", "zho", "zh-nan", "Minnan"],
["Chinese", "zho", "chi", "Chinese"],
["English", "eng", "eng", "English"],
["British English", "enGB", "eng", "British English"],
["Afrikaans", "af", "afr", "Afrikaans"],
["Arabic", "ara", "ara", "Arabic"],
["Arabic (Syria)", "araSy", "ara", "Arabic Syria"],
["Arabic (Egypt)", "araEG", "ara", "Arabic Egypt"],
["Arabic (Kuwait)", "araKW", "ara", "Arabic Kuwait"],
["Arabic (Lebanon)", "araLB", "ara", "Arabic Lebanon"],
["Arabic (Algeria)", "araDZ", "ara", "Arabic Algeria"],
["Arabic (Bahrain)", "araBH", "ara", "Arabic Bahrain"],
["Arabic (Iraq)", "araIQ", "ara", "Arabic Iraq"],
["Arabic (Jordan)", "araJO", "ara", "Arabic Jordan"],
["Arabic (Libya)", "araLY", "ara", "Arabic Libya"],
["Arabic (Morocco)", "araMA", "ara", "Arabic Morocco"],
["Arabic (Oman)", "araOM", "ara", "Arabic Oman"],
["Arabic (Saudi Arabia)", "araSA", "ara", "Arabic Saudi Arabia"],
["Arabic (Tunisia)", "araTN", "ara", "Arabic Tunisia"],
[
"Arabic (United Arab Emirates)",
"araAE",
"ara",
"Arabic United Arab Emirates",
],
["Arabic (Yemen)", "araYE", "ara", "Arabic Yemen"],
["Armenian", "hye", "arm", "Armenian"],
["Assamese", "asm", "asm", "Assamese"],
["Bengali", "ben", "ben", "Bengali"],
["Basque", "eus", "baq", "Basque"],
["Bulgarian", "bul", "bul", "Bulgarian"],
["Catalan", "cat", "cat", "Catalan"],
["Croatian", "hrv", "hrv", "Croatian"],
["Czech", "ces", "cze", "Czech"],
["Danish", "dan", "dan", "Danish"],
["Dutch", "nld", "dut", "Dutch"],
["Hindi", "hin", "hin", "Hindi"],
["Tamil", "tam", "tam", "Tamil"],
["Telugu", "tel", "tel", "Telugu"],
["Estonian", "est", "est", "Estonian"],
["Filipino", "fil", "fil", "Filipino"],
["Finnish", "fin", "fin", "Finnish"],
["Flemish", "nlBE", "dut", "Flemish"],
["French", "fra", "fre", "French"],
["French Canadian", "caFra", "fre", "French Canadian"],
["Canadian French", "caFra", "fre", "Canadian French"],
["German", "deu", "ger", "German"],
["Greek", "ell", "gre", "Greek"],
["Gujarati", "guj", "guj", "Gujarati"],
["Hebrew", "heb", "heb", "Hebrew"],
["Hungarian", "hun", "hun", "Hungarian"],
["Icelandic", "isl", "ice", "Icelandic"],
["Indonesian", "ind", "ind", "Indonesian"],
["Italian", "ita", "ita", "Italian"],
["Japanese", "jpn", "jpn", "Japanese"],
["Kannada (India)", "kan", "kan", "Kannada (India)"],
["Khmer", "khm", "khm", "Khmer"],
["Klingon", "tlh", "tlh", "Klingon"],
["Korean", "kor", "kor", "Korean"],
["Lithuanian", "lit", "lit", "Lithuanian"],
["Latvian", "lav", "lav", "Latvian"],
["Malay", "msa", "may", "Malay"],
["Malayalam", "mal", "mal", "Malayalam"],
["Mandarin", "None", "chi", "Mandarin"],
["Mandarin Chinese (Simplified)", "zh-Hans", "chi", "Simplified"],
["Mandarin Chinese (Traditional)", "zh-Hant", "chi", "Traditional"],
["Yue Chinese", "yue", "chi", "(Yue Chinese)"],
["Manipuri", "mni", "mni", "Manipuri"],
["Marathi", "mar", "mar", "Marathi"],
["No Dialogue", "zxx", "zxx", "No Dialogue"],
["Norwegian", "nor", "nor", "Norwegian"],
["Norwegian Bokmal", "nob", "nob", "Norwegian Bokmal"],
["Persian", "fas", "per", "Persian"],
["Polish", "pol", "pol", "Polish"],
["Portuguese", "por", "por", "Portuguese"],
["Brazilian Portuguese", "brPor", "por", "Brazilian Portuguese"],
["Punjabi", "pan", "pan", "Punjabi"],
["Panjabi", "pan", "pan", "Panjabi"],
["Romanian", "ron", "rum", "Romanian"],
["Russian", "rus", "rus", "Russian"],
["Serbian", "srp", "srp", "Serbian"],
["Sinhala", "sin", "sin", "Sinhala"],
["Slovak", "slk", "slo", "Slovak"],
["Slovenian", "slv", "slv", "Slovenian"],
["Spanish", "spa", "spa", "Spanish"],
["European Spanish", "euSpa", "spa", "European Spanish"],
["Swedish", "swe", "swe", "Swedish"],
["Thai", "tha", "tha", "Thai"],
["Tagalog", "tgl", "tgl", "Tagalog"],
["Turkish", "tur", "tur", "Turkish"],
["Ukrainian", "ukr", "ukr", "Ukrainian"],
["Urdu", "urd", "urd", "Urdu"],
["Vietnamese", "vie", "vie", "Vietnamese"],
]
return LanguageList
def ExtraLanguageList(self):
ExtraLanguageList = [
["Polish - Dubbing", "pol", "pol", "Polish - Dubbing"],
["Polish - Lektor", "pol", "pol", "Polish - Lektor"],
]
return ExtraLanguageList
def AddChapters(self):
if os.path.isfile(self.CurrentName_Original + " Chapters.txt"):
self.merge += [
"--chapter-charset",
"UTF-8",
"--chapters",
self.CurrentName_Original + " Chapters.txt",
]
return
def AddVideo(self):
inputVideo = None
videofiles = [
"{} [{}p]_Demuxed.mp4",
"{} [{}p]_Demuxed.mp4",
"{} [{}p] [UHD]_Demuxed.mp4",
"{} [{}p] [UHD]_Demuxed.mp4",
"{} [{}p] [VP9]_Demuxed.mp4",
"{} [{}p] [HIGH]_Demuxed.mp4",
"{} [{}p] [VP9]_Demuxed.mp4",
"{} [{}p] [HEVC]_Demuxed.mp4",
"{} [{}p] [HDR]_Demuxed.mp4",
"{} [{}p] [HDR-DV]_Demuxed.mp4",
]
for videofile in videofiles:
filename = videofile.format(self.CurrentName_Original, self.CurrentHeigh)
if os.path.isfile(filename):
inputVideo = filename
break
if not inputVideo:
self.logger.info("cannot found video file.")
exit(-1)
if self.default_mux:
outputVideo = (
re.compile("|".join([".h264", ".h265", ".vp9", ".mp4"])).sub("", inputVideo)
+ ".mkv"
)
if self.SeasonFolder:
outputVideo = os.path.join(
os.path.abspath(self.SeasonFolder), outputVideo
)
outputVideo = outputVideo.replace("\\", "/")
else:
outputVideo = self.setName()
self.outputVideo = outputVideo
if self.fps24:
self.merge += [
self.mkvmerge,
"--output",
outputVideo,
"--default-duration",
"0:24000/1001p",
"--language",
"0:und",
"--default-track",
"0:yes",
"(",
inputVideo,
")",
]
else:
self.merge += [
self.mkvmerge,
"--output",
outputVideo,
"(",
inputVideo,
")",
]
return
def AddAudio(self):
audiofiles = [
"{} {}.ac3",
"{} {} - Audio Description.ac3",
"{} {}.eac3",
"{} {} - Audio Description.eac3",
"{} {}.aac",
"{} {} - Audio Description.aac",
]
for (audio_language, subs_language, language_id, language_name,) in (
self.LanguageList() + self.ExtraLanguageList()
):
for audiofile in audiofiles:
filename = audiofile.format(self.CurrentName_Original, audio_language)
if os.path.isfile(filename):
self.merge += [
"--language",
f"0:{language_id}",
"--track-name",
"0:Audio Description" if 'Audio Description' in filename
else f"0:{language_name}",
"--default-track",
"0:yes"
if subs_language == self.muxer_settings["AUDIO"]
else "0:no",
"(",
filename,
")",
]
return
def AddSubtitles(self):
srts = [
"{} {}.srt",
]
forceds = [
"{} forced-{}.srt",
]
sdhs = [
"{} sdh-{}.srt",
]
for (
audio_language,
subs_language,
language_id,
language_name,
) in self.LanguageList():
for subtitle in srts:
filename = subtitle.format(self.CurrentName_Original, subs_language)
if os.path.isfile(filename):
self.merge += [
"--language",
f"0:{language_id}",
"--track-name",
f"0:{language_name}",
"--forced-track",
"0:no",
"--default-track",
"0:yes"
if subs_language == self.muxer_settings["SUB"]
else "0:no",
"--compression",
"0:none",
"(",
filename,
")",
]
for subtitle in forceds:
filename = subtitle.format(self.CurrentName_Original, subs_language)
if os.path.isfile(filename):
self.merge += [
"--language",
f"0:{language_id}",
"--track-name",
f"0:{language_name} Forced",
"--forced-track",
"0:yes",
"--default-track",
"0:no",
"--compression",
"0:none",
"(",
filename,
")",
]
for subtitle in sdhs:
filename = subtitle.format(self.CurrentName_Original, subs_language)
if os.path.isfile(filename):
self.merge += [
"--language",
f"0:{language_id}",
"--track-name",
f"0:{language_name} SDH",
"--forced-track",
"0:no",
"--default-track",
"0:no",
"--compression",
"0:none",
"(",
filename,
")",
]
return
def startMux(self):
self.AddVideo()
self.AddAudio()
self.AddSubtitles()
self.AddChapters()
if not os.path.isfile(self.outputVideo):
self.Run(self.merge + self.Extras)
return self.outputVideo