""" 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}' ]