|
""" |
|
The MkvToolNix module provides a class for manipulating MKV files. It uses tools from the MKVToolNix package such as mkvextract, mkvmerge, and mkvinfo. |
|
|
|
* Example usage: |
|
mkvtoolnix = MkvToolNix(filename='example.mkv') |
|
mkv_info = mkvtoolnix.get_mkv_info() |
|
mkvtoolnix.mkv_extract_track(mkv_info) |
|
|
|
* Example output from get_mkv_info(): |
|
{ |
|
"container": {...}, |
|
"global_tags": [...], |
|
"tracks": [...], |
|
"chapters": [...], |
|
"attachments": [...] |
|
} |
|
""" |
|
|
|
import sys |
|
from subprocess import Popen, PIPE, CalledProcessError |
|
from json import loads |
|
from typing import Dict, List, Set |
|
from os import path |
|
from dataclasses import dataclass |
|
|
|
from constants import (WORKING_SPACE, |
|
WORKING_SPACE_OUTPUT, |
|
WORKING_SPACE_TEMP, |
|
MKV_EXTRACT_PATH, |
|
MKV_MERGE_PATH, MKV_INFO_PATH, |
|
MKV_PROPEDIT_PATH, |
|
console) |
|
|
|
|
|
@dataclass(slots=True) |
|
class MkvToolNix: |
|
""" |
|
A class for manipulating MKV files using the MKVToolNix package. |
|
|
|
Attributes: |
|
- filename (str): The name of the MKV file to be processed. |
|
- working_space (str): The directory where the MKV file is located. |
|
- working_space_output (str): The directory where the output files will be saved. |
|
- working_space_temp (str): The directory where temporary files will be saved during processing. |
|
- mkv_extract_path (str): The path to the mkvextract executable. |
|
- mkv_merge_path (str): The path to the mkvmerge executable. |
|
- mkv_info_path (str): The path to the mkvinfo executable. |
|
- mkv_propedit_path (str): The path to the mkvpropedit executable. |
|
|
|
Methods: |
|
- get_mkv_info(): Retrieves information about the MKV file using the mkvinfo tool. |
|
- mkv_extract_track(data: Dict[str, any]): Extracts the specified tracks from the MKV file using the mkvextract tool. |
|
""" |
|
filename: str |
|
working_space: str = WORKING_SPACE |
|
working_space_output: str = WORKING_SPACE_OUTPUT |
|
working_space_temp: str = WORKING_SPACE_TEMP |
|
|
|
mkv_extract_path: str = MKV_EXTRACT_PATH |
|
mkv_merge_path: str = MKV_MERGE_PATH |
|
mkv_info_path: str = MKV_INFO_PATH |
|
mkv_propedit_path: str = MKV_PROPEDIT_PATH |
|
|
|
def _check_executables(self) -> None: |
|
""" |
|
Checks if the MKVToolNix executables exist at the specified paths. |
|
If any executable is not found, the program will exit with an error message. |
|
""" |
|
executables: List[str] = [self.mkv_extract_path, |
|
self.mkv_merge_path, self.mkv_info_path] |
|
for executable in executables: |
|
if not path.exists(executable): |
|
console.print( |
|
f'Error: {executable} not found.', style='red_bold') |
|
sys.exit() |
|
|
|
def get_mkv_info(self) -> dict: |
|
""" |
|
Retrieves information about the MKV file using the mkvinfo tool. |
|
The information is returned as a dictionary and also printed to the console. |
|
If an error occurs during the process, the program will exit with an error message. |
|
|
|
Returns: |
|
- dict: A dictionary containing information about the MKV file. |
|
""" |
|
try: |
|
self._check_executables() |
|
command: List[str] = self._get_mkv_info_command() |
|
with Popen(command, stdout=PIPE, stderr=PIPE, universal_newlines=True) as process: |
|
output, error = process.communicate() |
|
|
|
if process.returncode == 0: |
|
data: dict = loads(output) |
|
tracks_data: List[dict] = self._parse_tracks_data(data) |
|
self._print_mkv_info(tracks_data) |
|
return data |
|
|
|
console.print(f'Error: {error}', style='red_bold') |
|
except (FileNotFoundError, CalledProcessError) as error: |
|
console.print(f'Error: {error}', style='red_bold') |
|
sys.exit() |
|
return {} |
|
|
|
def _get_mkv_info_command(self) -> List[str]: |
|
""" |
|
Constructs the command to be used for retrieving information about the MKV file with mkvinfo. |
|
|
|
Returns: |
|
- List[str]: A list of strings representing the command to be executed. |
|
""" |
|
return [ |
|
self.mkv_merge_path, |
|
'--ui-language', |
|
'en', |
|
'--identify', |
|
'--identification-format', |
|
'json', |
|
path.join(self.working_space, self.filename) |
|
] |
|
|
|
def _parse_tracks_data(self, data: dict) -> List[dict]: |
|
""" |
|
Parses the track data from the dictionary returned by mkvinfo. |
|
|
|
Args: |
|
- data (dict): A dictionary containing information about the MKV file. |
|
|
|
Returns: |
|
- List[dict]: A list of dictionaries, each representing a track in the MKV file. |
|
""" |
|
tracks_data: List[dict] = [] |
|
|
|
for track in data['tracks']: |
|
track_data: dict = self._parse_track_data(track) |
|
tracks_data.append(track_data) |
|
|
|
return sorted(tracks_data, key=lambda x: x['id']) |
|
|
|
def _parse_track_data(self, track: dict) -> dict: |
|
""" |
|
Parses the data for a single track from the dictionary returned by mkvinfo. |
|
Returns a dictionary with the track's ID, type, codec ID, language, IETF language, and properties. |
|
|
|
Args: |
|
- track (dict): A dictionary containing information about a single track in the MKV file. |
|
|
|
Returns: |
|
- dict: A dictionary containing information about a single track in the MKV file. |
|
""" |
|
properties = track['properties'] |
|
track_data: dict = { |
|
'id': track['id'], |
|
'type': track['type'], |
|
'codec_id': properties.get('codec_id', ''), |
|
'language': properties.get('language', ''), |
|
'language_ietf': properties.get('language_ietf', ''), |
|
'properties': self._get_track_properties(properties) |
|
} |
|
|
|
return track_data |
|
|
|
@staticmethod |
|
def _get_track_properties(properties: dict) -> str: |
|
""" |
|
Retrieves the properties of a track from the dictionary returned by mkvinfo. |
|
|
|
Args: |
|
- properties (dict): A dictionary containing the properties of a track in the MKV file. |
|
|
|
Returns: |
|
- str: A string containing the properties of a track in the MKV file. |
|
""" |
|
if 'display_dimensions' in properties: |
|
return properties['display_dimensions'] |
|
if 'audio_sampling_frequency' in properties: |
|
return f"{properties['audio_sampling_frequency']} Hz" |
|
return 'None' |
|
|
|
def _print_mkv_info(self, tracks_data: List[dict]) -> None: |
|
""" |
|
Prints the information about the MKV file to the console. |
|
The information includes the ID, type, codec ID, language, IETF language, and properties of each track. |
|
|
|
Args: |
|
- tracks_data (List[dict]): A list of dictionaries, each representing a track in the MKV file. |
|
""" |
|
console.print('WYODRĘBNIANIE Z PLIKU:', |
|
style='yellow_bold') |
|
console.print(self.filename, style='white_bold') |
|
console.print('ID TYPE CODEK LANG LANG_IETF PROPERTIES', |
|
style='yellow_bold') |
|
|
|
for track in tracks_data: |
|
console.print( |
|
f'[yellow_bold]{track["id"]:2}[/yellow_bold] ' |
|
f'[white_bold]{track["type"]:10} ' |
|
f'{track["codec_id"]:20} ' |
|
f'{track["language"]:5} ' |
|
f'{track["language_ietf"]:10} ' |
|
f'{track["properties"]}' |
|
) |
|
console.print() |
|
|
|
def mkv_extract_track(self, data: Dict[str, any]) -> None: |
|
""" |
|
Extracts the specified tracks from the MKV file using the mkvextract tool. |
|
The tracks to be extracted are specified by their IDs. |
|
The user is prompted to enter the IDs of the tracks to be extracted. |
|
If an error occurs during the process, the program will exit with an error message. |
|
|
|
Args: |
|
- data (Dict[str, any]): A dictionary containing information about the MKV file. |
|
""" |
|
valid_track_range: range = range(len(data['tracks'])) |
|
tracks_to_extract: Set[int] = set() |
|
|
|
while True: |
|
try: |
|
console.print('Podaj ID ścieżki do wyciągnięcia (naciśnij ENTER, aby zakończyć): ', |
|
style='green_bold', end='') |
|
track_input: str = input().strip() |
|
if not track_input: |
|
break |
|
track_id: int = int(track_input) |
|
|
|
if track_id in valid_track_range: |
|
tracks_to_extract.add(track_id) |
|
else: |
|
console.print( |
|
'Nieprawidłowy ID ścieżki. Proszę podać poprawny numer ścieżki.\n', style='red_bold') |
|
except ValueError: |
|
console.print( |
|
'Pominięto wyciąganie ścieżki.\n', style='red_bold') |
|
|
|
try: |
|
for track_id in tracks_to_extract: |
|
track: str = data['tracks'][track_id] |
|
codec_id: str = track['properties']['codec_id'] |
|
format_extension: str = self._get_format_extension(codec_id) |
|
filename: str = f'{self.filename[:-4]}.{format_extension}' |
|
out_file: str = path.join(self.working_space_temp, filename) |
|
command: List[str] = self._get_extract_command( |
|
track_id, out_file) |
|
|
|
with Popen(command) as process: |
|
console.print( |
|
f'\nEkstrakcja ścieżki {track_id} do pliku {filename}', style='yellow_bold') |
|
process.wait() |
|
|
|
except (IndexError, KeyError): |
|
console.print( |
|
'Znaleziono nieprawidłowe ID ścieżki!', style='red_bold') |
|
self.mkv_extract_track(data) |
|
|
|
console.print( |
|
'Ekstrakcja zakończona pomyślnie.\n', style='green_bold') |
|
|
|
@staticmethod |
|
def _get_format_extension(codec_id: str) -> str: |
|
""" |
|
Determines the file extension for a track based on its codec ID. |
|
|
|
Args: |
|
- codec_id (str): The codec ID of a track in the MKV file. |
|
|
|
Returns: |
|
- str: The file extension for the track. |
|
""" |
|
format_dict: dict = { |
|
'A_AAC/MPEG2/*': 'aac', |
|
'A_AAC/MPEG4/*': 'aac', |
|
'A_AAC': 'aac', |
|
'A_AC3': 'ac3', |
|
'A_EAC3': 'ac3', |
|
'A_ALAC': 'caf', |
|
'A_DTS': 'dts', |
|
'A_FLAC': 'flac', |
|
'A_MPEG/L2': 'mp2', |
|
'A_MPEG/L3': 'mp3', |
|
'A_OPUS': 'opus', |
|
'A_PCM/INT/LIT': 'wav', |
|
'A_PCM/INT/BIG': 'wav', |
|
'A_REAL/*': 'rm', |
|
'A_TRUEHD': 'truehd', |
|
'A_MLP': 'mlp', |
|
'A_TTA1': 'tta', |
|
'A_VORBIS': 'ogg', |
|
'A_WAVPACK4': 'wv', |
|
'S_HDMV/PGS': 'sup', |
|
'S_HDMV/TEXTST': 'txt', |
|
'S_KATE': 'ogg', |
|
'S_TEXT/SSA': 'ssa', |
|
'S_TEXT/ASS': 'ass', |
|
'S_SSA': 'ssa', |
|
'S_ASS': 'ass', |
|
'S_TEXT/UTF8': 'srt', |
|
'S_TEXT/ASCII': 'srt', |
|
'S_VOBSUB': 'sub', |
|
'S_TEXT/USF': 'usf', |
|
'S_TEXT/WEBVTT': 'vtt', |
|
'V_MPEG1': 'mpeg', |
|
'V_MPEG2': 'mpeg', |
|
'V_MPEG4/ISO/AVC': 'h264', |
|
'V_MPEG4/ISO/HEVC': 'h265', |
|
'V_MS/VFW/FOURCC': 'avi', |
|
'V_REAL/*': 'rm', |
|
'V_THEORA': 'ogg', |
|
'V_VP8': 'ivf', |
|
'V_VP9': 'ivf' |
|
} |
|
|
|
return format_dict.get(codec_id, 'mkv') |
|
|
|
def _get_extract_command(self, track_id: int, out_file: str) -> List[str]: |
|
""" |
|
Constructs the command to be used for extracting a track from the MKV file with mkvextract. |
|
Returns the command as a list of strings. |
|
|
|
Args: |
|
- track_id (int): The ID of the track to be extracted. |
|
- out_file (str): The path and filename of the output file. |
|
|
|
Returns: |
|
- List[str]: The command to be used for extracting the track. |
|
""" |
|
return [ |
|
self.mkv_extract_path, |
|
'tracks', |
|
path.join(self.working_space, self.filename), |
|
f'{track_id}:{out_file}' |
|
] |
|
|