commit
2f8af4c7ba
@ -0,0 +1,142 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Local stuff
|
||||
/config_local.py
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Current File",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
MIT License Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -0,0 +1,3 @@
|
||||
# video-compression-handbrake
|
||||
|
||||
Computes best way to compress with very little destruction a video library using the AMD VCE H.265. Creates Handbrake queue file that can be imported and ran. Uses MediaInfo CLI
|
||||
@ -0,0 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
class Config:
|
||||
QUEUE_FILE = "queue.json"
|
||||
MEDIAINFO_BINARY=Path("/")
|
||||
BASE_PATH = Path("/path/to/videos")
|
||||
DESTINATION_PATH = Path("/where/to/save/result")
|
||||
LOW_BITRATE_THRESHOLD = 550
|
||||
@ -0,0 +1,234 @@
|
||||
from minfo import MediaInfo
|
||||
from pathlib import Path
|
||||
|
||||
class Configuration:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"IsDvdNavDisabled": False,
|
||||
"EnableQuickSyncDecoding": False,
|
||||
"UseQSVDecodeForNonQSVEnc": False,
|
||||
"ScalingMode": 0,
|
||||
"PreviewScanCount": 10,
|
||||
"Verbosity": 1,
|
||||
"MinScanDuration": 10,
|
||||
"SaveLogToCopyDirectory": False,
|
||||
"SaveLogWithVideo": False,
|
||||
"SaveLogCopyDirectory": "",
|
||||
"RemoteServiceEnabled": False,
|
||||
"RemoteServicePort": 0,
|
||||
"EnableVceEncoder": True,
|
||||
"EnableNvencEncoder": False,
|
||||
"EnableQsvEncoder": False
|
||||
}
|
||||
|
||||
class Statistics:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"StartTime": "0001-01-01T00:00:00",
|
||||
"StartTimeDisplay": "Not Available",
|
||||
"EndTime": "0001-01-01T00:00:00",
|
||||
"EndTimeDisplay": "",
|
||||
"PausedDuration": "00:00:00",
|
||||
"PausedDisplay": "",
|
||||
"Duration": "00:00:00",
|
||||
"DurationDisplay": "",
|
||||
"FinalFileSizeInMegaBytes": 0,
|
||||
"FileSizeDisplay": "",
|
||||
"IsNotifying": True
|
||||
}
|
||||
|
||||
class AllowedPassthruOptions:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"AudioAllowAACPass": True,
|
||||
"AudioAllowAC3Pass": True,
|
||||
"AudioAllowDTSHDPass": True,
|
||||
"AudioAllowDTSPass": True,
|
||||
"AudioAllowMP3Pass": True,
|
||||
"AudioAllowTrueHDPass": True,
|
||||
"AudioAllowFlacPass": True,
|
||||
"AudioAllowEAC3Pass": True,
|
||||
"AudioEncoderFallback": 5,
|
||||
"AllowedPassthruOptions": [
|
||||
13,
|
||||
8,
|
||||
11,
|
||||
10,
|
||||
14,
|
||||
12,
|
||||
18,
|
||||
9
|
||||
]
|
||||
}
|
||||
|
||||
class AudioTrack:
|
||||
def __init__(self, minfo: MediaInfo):
|
||||
self.minfo = minfo
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"DRC": 0.0,
|
||||
"Gain": 0,
|
||||
"Encoder": 7,
|
||||
"SampleRate": 0.0,
|
||||
"EncoderRateType": 0,
|
||||
"Bitrate": 160,
|
||||
"Quality": -1.0,
|
||||
"ScannedTrack": {
|
||||
"TrackNumber": 1,
|
||||
"Language": self.minfo.tracks[0].language,
|
||||
# "LanguageCode": "und",
|
||||
"Name": self.minfo.tracks[0].name,
|
||||
#"Codec": 65536,
|
||||
# aac is 65536
|
||||
# mp3 is 524288
|
||||
#"SampleRate": 32000,
|
||||
#"Bitrate": 32002,
|
||||
#"ChannelLayout": 4
|
||||
},
|
||||
"IsNotifying": True
|
||||
}
|
||||
|
||||
class Chapters:
|
||||
def __init__(self, minfo: MediaInfo):
|
||||
self.minfo = minfo
|
||||
|
||||
def as_dict(self):
|
||||
return [
|
||||
{
|
||||
"ChapterNumber": index + 1,
|
||||
"Duration": c.duration, # self.mediainfo.duration,
|
||||
"ChapterName": c.name, # "Chapter 1",
|
||||
"IsNotifying": True
|
||||
} for index, c in enumerate(self.minfo.menu.items)
|
||||
]
|
||||
|
||||
class Task:
|
||||
def __init__(self, path: Path, destination: Path):
|
||||
self.path = path
|
||||
self.destination = destination / (path.name.rsplit(".", 1)[0] + '.m4v')
|
||||
self.mediainfo = MediaInfo(self.path)
|
||||
self.allowed_passthru_options = AllowedPassthruOptions()
|
||||
self.audio_track = AudioTrack(self.mediainfo)
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"Source": str(self.path),
|
||||
"Title": 1,
|
||||
"Angle": 1,
|
||||
"PointToPointMode": 0,
|
||||
"StartPoint": 1,
|
||||
"EndPoint": len(self.mediainfo.menu.items),
|
||||
"Destination": str(self.destination),
|
||||
"OutputFormat": 0,
|
||||
"OptimizeMP4": False,
|
||||
"IPod5GSupport": False,
|
||||
"AlignAVStart": True,
|
||||
"Width": self.mediainfo.resolution.width,
|
||||
"Height": self.mediainfo.resolution.height,
|
||||
"Cropping": {
|
||||
"Top": 0,
|
||||
"Bottom": 0,
|
||||
"Left": 0,
|
||||
"Right": 0
|
||||
},
|
||||
"HasCropping": False,
|
||||
"Anamorphic": 4,
|
||||
"DisplayWidth": self.mediainfo.display_width,
|
||||
"KeepDisplayAspect": self.mediainfo.keep_display_aspect,
|
||||
"PixelAspectX": int(self.mediainfo.pixel_aspect_ratio.x),
|
||||
"PixelAspectY": int(self.mediainfo.pixel_aspect_ratio.y),
|
||||
"Modulus": 2,
|
||||
"DeinterlaceFilter": 2,
|
||||
"DeinterlacePreset": {
|
||||
"Name": "Default",
|
||||
"ShortName": "default"
|
||||
},
|
||||
"CombDetect": 2,
|
||||
"CustomDeinterlaceSettings": "",
|
||||
"CustomCombDetect": "",
|
||||
"Detelecine": 0,
|
||||
"CustomDetelecine": "",
|
||||
"Denoise": 0,
|
||||
"DenoisePreset": 5,
|
||||
"DenoiseTune": 0,
|
||||
"CustomDenoise": "",
|
||||
"Grayscale": False,
|
||||
"Rotation": 0,
|
||||
"FlipVideo": False,
|
||||
"Sharpen": 0,
|
||||
"SharpenPreset": {
|
||||
"DisplayName": "Medium",
|
||||
"Key": "medium"
|
||||
},
|
||||
"SharpenTune": {
|
||||
"DisplayName": "None",
|
||||
"Key": "none"
|
||||
},
|
||||
"SharpenCustom": "",
|
||||
"DeblockPreset": {
|
||||
"DisplayName": "Off",
|
||||
"Key": "off"
|
||||
},
|
||||
"DeblockTune": {
|
||||
"DisplayName": "Medium (8x8)",
|
||||
"Key": "medium"
|
||||
},
|
||||
"CustomDeblock": "strength=strong:thresh=20:blocksize=8",
|
||||
"VideoEncodeRateType": 1,
|
||||
"VideoEncoder": 13,
|
||||
"VideoProfile": {
|
||||
"DisplayName": "Main",
|
||||
"ShortName": "main"
|
||||
},
|
||||
"VideoLevel": {
|
||||
"DisplayName": "Auto",
|
||||
"ShortName": "auto"
|
||||
},
|
||||
"VideoPreset": {
|
||||
"DisplayName": "Quality",
|
||||
"ShortName": "quality"
|
||||
},
|
||||
"VideoTunes": [],
|
||||
"ExtraAdvancedArguments": "",
|
||||
"FramerateMode": 1,
|
||||
"Quality": 22.0,
|
||||
"VideoBitrate": self.mediainfo.output_bitrate,
|
||||
"TwoPass": False,
|
||||
"TurboFirstPass": False,
|
||||
"Framerate": self.mediainfo.framerate,
|
||||
"AudioTracks": [
|
||||
self.audio_track.as_dict()
|
||||
],
|
||||
"AllowedPassthruOptions": self.allowed_passthru_options.as_dict(),
|
||||
"SubtitleTracks": [],
|
||||
"IncludeChapterMarkers": len(self.mediainfo.menu.items) > 1,
|
||||
"ChapterNames": Chapters(self.mediainfo).as_dict(),
|
||||
"MetaData": {},
|
||||
"IsPreviewEncode": False
|
||||
}
|
||||
|
||||
class QueueItem:
|
||||
def __init__(self, path: Path, destination: Path):
|
||||
self.path = path
|
||||
self.task = Task(path, destination)
|
||||
self.configuration = Configuration()
|
||||
self.statistics = Statistics()
|
||||
|
||||
def as_dict(self):
|
||||
return {
|
||||
"ScannedSourcePath": str(self.path),
|
||||
"Status": 0,
|
||||
"Task": self.task.as_dict(),
|
||||
"Configuration": self.configuration.as_dict(),
|
||||
"Statistics": self.statistics.as_dict()
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
from config_local import Config
|
||||
from jobdef import QueueItem
|
||||
from minfo import MediaInfo
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
|
||||
extensions = {
|
||||
"mp4",
|
||||
"mkv",
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"m4v",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
}
|
||||
|
||||
jobs = []
|
||||
for dir, dirs, files in os.walk(str(Config.BASE_PATH)):
|
||||
for file in files:
|
||||
parts = file.rsplit(".", 1)
|
||||
if len(parts) == 2:
|
||||
extension = parts[1].lower()
|
||||
if extension in extensions:
|
||||
current_path = Path(dir)
|
||||
relative_path = current_path.relative_to(Config.BASE_PATH)
|
||||
print(relative_path / file, end=" ")
|
||||
item = QueueItem(Path(dir) / file, Config.DESTINATION_PATH / relative_path)
|
||||
info = item.task.mediainfo
|
||||
if info.codec == 'HEVC':
|
||||
print("")
|
||||
continue
|
||||
if info.bitrate < Config.LOW_BITRATE_THRESHOLD:
|
||||
print("")
|
||||
continue
|
||||
item.task.destination.parent.mkdir(exist_ok=True)
|
||||
print(" ... added")
|
||||
jobs.append(item)
|
||||
|
||||
with open("queue.json", "w") as fp:
|
||||
json.dump(jobs, fp, default=lambda o: getattr(o, 'as_dict', str)(), indent=2)
|
||||
@ -0,0 +1,235 @@
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, Iterable, List, NamedTuple, Optional
|
||||
from config_local import Config
|
||||
from subprocess import Popen, PIPE
|
||||
from pathlib import Path
|
||||
from io import BytesIO, TextIOWrapper
|
||||
from pytimeparse.timeparse import timeparse
|
||||
from datetime import timedelta
|
||||
from fractions import Fraction
|
||||
|
||||
|
||||
class Translation(NamedTuple):
|
||||
start: int
|
||||
end: int
|
||||
multiplier: float
|
||||
min_br: int
|
||||
max_br: int
|
||||
|
||||
|
||||
class Resolution:
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Resolution(width={self.width},height={self.height})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.width}x{self.height}"
|
||||
|
||||
|
||||
class Ratio:
|
||||
def __init__(self, x: int, y: int):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Ratio(x={self.x},y={self.y})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.x}x{self.y}"
|
||||
|
||||
|
||||
class AudioInfo:
|
||||
def __init__(self, audio_dict: Dict[str, str]):
|
||||
self.data = audio_dict
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
return self.data.get("Language", "Unknown")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.data.get("Title", "Unknown")
|
||||
|
||||
|
||||
class MenuItem:
|
||||
def __init__(self, *, start: str, duration: str, name: str):
|
||||
self.start = start
|
||||
self.duration = duration
|
||||
self.name = name
|
||||
|
||||
|
||||
class MenuInfo:
|
||||
REGEX = re.compile(r"^_\d{2}_\d{2}_\d{2}")
|
||||
|
||||
def __init__(self, menu_dict: Dict[str, str]):
|
||||
self.data = menu_dict
|
||||
|
||||
@property
|
||||
def items(self) -> List[MenuItem]:
|
||||
if not "extra" in self.data:
|
||||
duration = str(timedelta(seconds=round(timeparse(list(self.data.keys())[0]))))
|
||||
return [MenuItem(start="00:00:00", duration=f"{duration:0>8}", name='Chapter 1')]
|
||||
keys = sorted(key for key in self.data["extra"] if type(self).REGEX.match(key))
|
||||
td0 = timedelta(seconds=0)
|
||||
td = None
|
||||
result = []
|
||||
for key in keys:
|
||||
td = timedelta(
|
||||
hours=int(key[1:3]),
|
||||
minutes=int(key[4:6]),
|
||||
seconds=int(key[7:9]),
|
||||
milliseconds=float(key[10:])
|
||||
)
|
||||
dur_td = timedelta(seconds=round((td - td0).total_seconds()))
|
||||
duration = str(dur_td)
|
||||
duration = f"{duration:0>8}"
|
||||
td0 = td
|
||||
name = self.data["extra"][key].replace(" ", " ")
|
||||
if result:
|
||||
result[-1].duration = duration
|
||||
result.append(MenuItem(start=key, duration="", name=name))
|
||||
if result:
|
||||
td = timedelta(seconds=float(self.data["Duration"]))
|
||||
dur_td = timedelta(seconds=round((td - td0).total_seconds()))
|
||||
duration = str(dur_td)
|
||||
duration = f"{duration:0>8}"
|
||||
result[-1].duration = duration
|
||||
return result
|
||||
|
||||
|
||||
class MediaInfo:
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
procinfo = Popen([Config.MEDIAINFO_BINARY, path, "--Output=JSON"], stdout=PIPE)
|
||||
stdout, _ = procinfo.communicate()
|
||||
data = json.loads(stdout.decode('utf-8'))["media"]["track"]
|
||||
|
||||
self.general: Optional[Dict[str, Any]] = None
|
||||
self.video: Optional[Dict[str, Any]] = None
|
||||
self.audio: List[Dict[str, Any]] = []
|
||||
self.menu_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
for track in data:
|
||||
if track["@type"] == "General":
|
||||
self.general = track
|
||||
elif track["@type"] == "Video":
|
||||
self.video = track
|
||||
elif track["@type"] == "Audio":
|
||||
self.audio.append(track)
|
||||
elif track["@type"] == "Menu":
|
||||
self.menu_data = track
|
||||
|
||||
# print(json.dumps(self.video, indent=2))
|
||||
|
||||
@property
|
||||
def codec(self) -> str:
|
||||
return self.video["Format"]
|
||||
|
||||
@property
|
||||
def bitrate(self) -> int:
|
||||
try:
|
||||
bitrate = self.video["BitRate"]
|
||||
except KeyError:
|
||||
bitrate = self.general["OverallBitRate"]
|
||||
|
||||
ibitrate = int(bitrate) // 1000
|
||||
return ibitrate
|
||||
|
||||
@property
|
||||
def framerate(self) -> float:
|
||||
keys = ["FrameRate_Maximum", "FrameRate", "FrameRate_Nominal"]
|
||||
for key in keys:
|
||||
if key in self.video:
|
||||
return float(self.video[key])
|
||||
print(json.dumps(self.general, indent=2))
|
||||
print(json.dumps(self.video, indent=2))
|
||||
raise Exception("No frame rate for video " + str(self.path))
|
||||
|
||||
@property
|
||||
def duration(self) -> str:
|
||||
places_to_look = [self.video, self.general]
|
||||
for place in places_to_look:
|
||||
if "Duration" in place:
|
||||
seconds = float(place["Duration"])
|
||||
td = str(timedelta(seconds=seconds))
|
||||
return td # f"{td:0>8}"
|
||||
raise Exception("No duration for video " + str(self.path))
|
||||
|
||||
@property
|
||||
def resolution(self) -> Resolution:
|
||||
width = int(self.video["Width"])
|
||||
height = int(self.video["Height"])
|
||||
|
||||
return Resolution(width=width, height=height)
|
||||
|
||||
@property
|
||||
def display_aspect_ratio(self) -> Ratio:
|
||||
ratio = [float(x) for x in str(self.video["DisplayAspectRatio"]).split(':')]
|
||||
if len(ratio) != 2:
|
||||
res = self.resolution
|
||||
if -0.01 <= ratio[0] - res.width / res.height <= 0.01:
|
||||
frac = Fraction(res.width, res.height)
|
||||
else:
|
||||
new_width = round(res.height * ratio[0])
|
||||
frac = Fraction(new_width, res.height)
|
||||
ratio = (frac.numerator, frac.denominator)
|
||||
return Ratio(x=ratio[0], y=ratio[1])
|
||||
|
||||
@property
|
||||
def keep_display_aspect(self) -> bool:
|
||||
ratio = self.display_aspect_ratio
|
||||
resolution = self.resolution
|
||||
return 0.95 < (resolution.width / resolution.height) / (ratio.x / ratio.y) < 1.05
|
||||
|
||||
@property
|
||||
def display_width(self) -> float:
|
||||
ratio = self.display_aspect_ratio
|
||||
|
||||
if not self.keep_display_aspect:
|
||||
display_width = self.resolution.height * ratio.x / ratio.y
|
||||
else:
|
||||
display_width = float(self.resolution.width)
|
||||
return display_width
|
||||
|
||||
@property
|
||||
def pixel_aspect_ratio(self) -> Ratio:
|
||||
if self.keep_display_aspect:
|
||||
return Ratio(1.0, 1.0)
|
||||
else:
|
||||
return Ratio(self.display_aspect_ratio.x, self.resolution.width)
|
||||
|
||||
@property
|
||||
def output_bitrate(self) -> int:
|
||||
print(self.codec)
|
||||
if self.codec == "MPEG-4 Visual":
|
||||
translations = [
|
||||
Translation(0, 800, 0.5, 200, 600),
|
||||
Translation(800, 1500, 0.4, 400, 800),
|
||||
Translation(1500, 99999999999, 0.33, 600, 1200),
|
||||
]
|
||||
else:
|
||||
translations = [
|
||||
Translation(0, 800, 0.7, 450, 700),
|
||||
Translation(800, 1500, 0.6, 550, 1000),
|
||||
Translation(1500, 99999999999, 0.45, 800, 5000),
|
||||
]
|
||||
bitrate = self.bitrate
|
||||
for t in translations:
|
||||
if t.start <= bitrate < t.end:
|
||||
return int(min(t.max_br, max(t.min_br, bitrate * t.multiplier)))
|
||||
|
||||
@property
|
||||
def tracks(self) -> List[AudioInfo]:
|
||||
result = []
|
||||
for audio in self.audio:
|
||||
track = AudioInfo(audio)
|
||||
result.append(track)
|
||||
return result
|
||||
|
||||
@property
|
||||
def menu(self) -> MenuInfo:
|
||||
return MenuInfo(self.menu_data or { self.duration: "Chapter 1" })
|
||||
Loading…
Reference in new issue