From 65e65dd21ac3f5c58453187025a1b7527c637620 Mon Sep 17 00:00:00 2001 From: Sheldon Woodward Date: Sat, 21 Mar 2020 10:53:49 -0700 Subject: [PATCH] Feature/docs (#51) * Updated module docstrings. * Updated README.md with roadmap. * Updated docs index.rst. * Improved bug-report issue template. --- .../{bug_report.md => bug-report.md} | 8 +- README.md | 41 +- docs/source/index.rst | 15 +- pymkv/MKVAttachment.py | 72 ++- pymkv/MKVFile.py | 470 ++++++++++++++---- pymkv/MKVTrack.py | 155 +++++- 6 files changed, 607 insertions(+), 154 deletions(-) rename .github/ISSUE_TEMPLATE/{bug_report.md => bug-report.md} (66%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 66% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/bug-report.md index c8a8b57..fcce946 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -11,7 +11,7 @@ assignees: '' A clear and concise description of what the bug is. **To Reproduce** -Steps to reproduce the behavior: +Steps to reproduce the behavior. Please link to relevant MKV or track files to reproduce the issue. **Expected behavior** A clear and concise description of what you expected to happen. @@ -19,9 +19,9 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. -**Desktop (please complete the following information):** - - OS: [e.g. macOS] - - Version: [e.g. 10.14] +**Software (please complete the following information):** + - OS: [e.g. macOS 10.14] + - MKVToolNix version [e.g. v44.0.0] **Additional context** Add any other context about the problem here. diff --git a/README.md b/README.md index 030152c..3c69d91 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,52 @@ [![License](https://img.shields.io/github/license/sheldonkwoodward/pymkv.svg)](https://github.com/sheldonkwoodward/pymkv/LICENSE.txt) [![Code Quality](https://api.codacy.com/project/badge/Grade/e1fe077d95f74a5886c557024777c26c)](https://api.codacy.com/project/badge/Grade/e1fe077d95f74a5886c557024777c26c) -pymkv is a Python wrapper for mkvmerge. It provides support for muxing, splitting, linking, chapters, tags, and attachments through the use of mkvmerge. +pymkv is a Python wrapper for mkvmerge and other tools in the MKVToolNix suite. It provides support for muxing, +splitting, linking, chapters, tags, and attachments through the use of mkvmerge. ## About pymkv -pymkv is a Python 3 library for manipulating MKV files with mkvmerge. Previously, I was constructing mkvmerge commands manually. They were becoming overly complex and unmanageable. To remedy this, I decided to write this library to make mkvmerge more scriptable and easier to use. Please open new issues for any bugs you find, support is greatly appreciated! +pymkv is a Python 3 library for manipulating MKV files with mkvmerge. Constructing mkvmerge commands manually can +quickly become confusing and complex. To remedy this, I decided to write this library to make mkvmerge more +scriptable and easier to use. Please open new issues for any bugs you find, support is greatly appreciated! ## Installation -mkvmerge must be installed on your computer. It is recommended to add it to your \$PATH variable but a different path can be manually specified. mkvmerge can be found and downloaded from [here] or in most package managers. +mkvmerge must be installed on your computer, it is needed to process files when creating MKV objects. It is also +recommended to add it to your $PATH variable but a different path can be manually specified. mkvmerge can be found +and downloaded from [here](https://mkvtoolnix.download/downloads.html) or from most package managers. To install pymkv from PyPI, use the following command: $ pip install pymkv -You can also clone the repo and run the following command in the project root to edit the source code: +You can also clone the repo and run the following command in the project root to install the source code as editable: $ pip install -e . ## Documentation -The documentation for pymkv can be found [here](https://pymkv.shel.dev) or in the docstrings. +The documentation for pymkv can be found [here](https://pymkv.shel.dev) or in the project's docstrings. + +## Roadmap +pymkv was a project started a few years ago when I was first learning Python. There were a number of things that I +did that could use improvement. The planned changes and future features are outlined below. Keep an eye on the [Github +Projects page](https://github.com/sheldonkwoodward/pymkv/projects) for the current roadmap status. + +### ~~Documentation~~ +The current documentation for pymkv is lacking. Instead of manually managing a GitHub Wiki, Sphinx will be setup to +automatically generate documentation from docstrings. The docstrings will also need to be updated and improved to +ensure this documentation is complete. + +### Tests +After completing documentation for the existing features, unit tests need to be written to "lock in" the existing +functionality. Generating mkvmerge commands can be complex and it is easy to subtly modify an existing feature when +adding a new one. Unit tests will ensure that features remain the same and help prevent bugs in the future. + +### Cleanup +The existing code base could use some tidying, better commenting, debugging, and a general styling overhaul. Setting up +[pre-commit](https://pre-commit.com/) and the [Black code formatter](https://github.com/psf/black) will help keep the +code base more readable and maintainable. + +### Features +Once these first three steps are complete, pymkv will be ready to start adding new features. The goal is for pymkv to +implement the functionality of mkvmerge and other MKVToolNix tools as closely as possible. New features and bugs will +be added to the [GitHub issues page](https://github.com/sheldonkwoodward/pymkv/issues). As pymkv progresses through +the previous steps, this roadmap will be expanded to outline new features. diff --git a/docs/source/index.rst b/docs/source/index.rst index 34a5eba..8bebf65 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,11 +1,20 @@ -Home -==== +pymkv +===== -Welcome the pymkv documentation! +Welcome to the pymkv documentation! Here you will find links to the core modules and examples of how to use each. Modules ------- +The three primary modules of pymkv are :class:`~pymkv.MKVFile`, :class:`~pymkv.MKVTrack`, and +:class:`~pymkv.MKVAttachment`. The :class:`~pymkv.MKVFile` class is used to import existing or create new MKV files. +The :class:`~pymkv.MKVTrack` class is used to add individual tracks to an :class:`~pymkv.MKVFile`. The +:class:`~pymkv.MKVAttachment` class is used to add attachments to an :class:`~pymkv.MKVFile`. + +Each module supports or mimics many of the same operations as mkvmerge but are not necessarily complete. If there is +functionality that is missing or an error in the docs, please open a new issue `here `_. + .. toctree:: :maxdepth: 1 diff --git a/pymkv/MKVAttachment.py b/pymkv/MKVAttachment.py index b59f360..18a46ba 100644 --- a/pymkv/MKVAttachment.py +++ b/pymkv/MKVAttachment.py @@ -1,26 +1,61 @@ -# sheldon woodward -# 3/28/18 +""":class:`~pymkv.MKVAttachment` classes are used to represent attachment files within an MKV or to be used in an +MKV. -"""MKVAttachment Class""" +Examples +-------- +Below are some basic examples of how the :class:`~pymkv.MKVAttachment` objects can be used. + +Create a new :class:`~pymkv.MKVAttachment` and add it to an :class:`~pymkv.MKVFile`. + +>>> from pymkv import MKVAttachment +>>> attachment = MKVAttachment('path/to/attachment.jpg', name='NAME') +>>> attachment.description = 'DESCRIPTION' + +Attachments can also be added directly to an :class:`~pymkv.MKVFile`. + +>>> from pymkv import MKVFile +>>> mkv = MKVFile('path/to/file.mkv') +>>> mkv.add_attachment('path/to/other/attachment.png') + +Now, the MKV can be muxed with both attachments. + +>>> mkv.add_attachment(attachment) +>>> mkv.mux('path/to/output.mkv') +""" from os.path import expanduser, isfile from mimetypes import guess_type class MKVAttachment: + """A class that represents an MKV attachment for an :class:`~pymkv.MKVFile` object. + + Parameters + ---------- + file_path : str + The path to the attachment file. + name : str, optional + The name that will be given to the attachment when muxed into a file. + description : str, optional + The description that will be given to the attachment when muxed into a file. + attach_once : bool, optional + Determines if the attachment should be added to all split files or only the first. Default is False, + which will attach to all files. + + Attributes + ---------- + mime_type : str + The attachment's MIME type. The type will be guessed when :attr:`~pymkv.MKVAttachment.file_path` is set. + name : str + The name that will be given to the attachment when muxed into a file. + description : str + The description that will be given to the attachment when muxed into a file. + attach_once : bool + Determines if the attachment should be added to all split files or only the first. Default is False, + which will attach to all files. + """ + def __init__(self, file_path, name=None, description=None, attach_once=False): - """A class that represents an MKV attachment. - - file_path (str): - Path to a an attachment. - name (str): - The name of the attachment. - description (str): - The description of the attachment. - attach_once (bool): - Determines if the attachment should be added to all split files or only the first. Attach to all files is - default with the option as false. - """ self.mime_type = None self._file_path = None self.file_path = file_path @@ -33,6 +68,13 @@ def __repr__(self): @property def file_path(self): + """str: The path to the attachment file. + + Raises + ------ + FileNotFoundError + Raised if `file_path` does not exist. + """ return self._file_path @file_path.setter diff --git a/pymkv/MKVFile.py b/pymkv/MKVFile.py index b3e5ff4..4a94c52 100644 --- a/pymkv/MKVFile.py +++ b/pymkv/MKVFile.py @@ -1,7 +1,39 @@ -# sheldon woodward -# 2/25/2018 +""":class:`~pymkv.MKVFile` is the core class of pymkv. It is used to import, create, modify, and mux MKV files. -"""MKVFile Class""" +Examples +-------- +Below are some basic examples of how the :class:`~pymkv.MKVFile` objects can be used. + +Create and mux a new MKV. This example takes an standalone video and audio track and combines them into an MKV file. + +>>> from pymkv import MKVFile +>>> mkv = MKVFile() +>>> mkv.add_track('/path/to/track.h264') +>>> mkv.add_track(MKVTrack('/path/to/another/track.aac')) +>>> mkv.mux('/path/to/output.mkv') + +Generate the mkvmerge command to mux an MKV. This is example is identical to the first example except the command is +only generated, not executed. + +>>> mkv = MKVFile() +>>> mkv.add_track('/path/to/track.h264') +>>> mkv.add_track(MKVTrack('/path/to/another/track.aac')) +>>> mkv.command('/path/to/output.mkv') + +Import an existing MKV and remove a track. This example will import an MKV that already exists on your filesystem, +remove a track and allow you to mux that change into a new file. + +>>> mkv = MKVFile('/path/to/file.mkv') +>>> mkv.remove_track(0) +>>> mkv.mux('/path/to/output.mkv') + +Combine two MKVs. This example takes two existing MKVs and combines their tracks into a single MKV file. + +>>> mkv1 = MKVFile('/path/to/file1.mkv') +>>> mkv2 = MKVFile('/path/to/file2.mkv') +>>> mkv1.add_file(mkv2) +>>> mkv1.mux('/path/to/output.mkv') +""" import json from os import devnull @@ -18,25 +50,36 @@ class MKVFile: - def __init__(self, file_path=None, title=None): - """A class that represents an MKV file. - - The MKVFile class can either import a pre-existing MKV file or create a new one. After an MKVFile object has - been instantiated, MKVTracks or other MKVFile objects can be added using add_track() and add_file() - respectively. - - Tracks are always added in the same order that they exist in a file or are added in. They can be reordered - using move_track_front(), move_track_end(), move_track_forward(), move_track_backward(), or swap_tracks(). + """A class that represents an MKV file. + + The :class:`~pymkv.MKVFile` class can either import a pre-existing MKV file or create a new one. After an + :class:`~pymkv.MKVFile` object has been instantiated, :class:`~pymkv.MKVTrack` objects or other + :class:`~pymkv.MKVFile` objects can be added using :meth:`~pymkv.MKVFile.add_track` and + :meth:`~pymkv.MKVFile.add_file` respectively. + + Tracks are always added in the same order that they exist in a file or are added in. They can be reordered + using :meth:`~pymkv.MKVFile.move_track_front`, :meth:`~pymkv.MKVFile.move_track_end`, + :meth:`~pymkv.MKVFile.move_track_forward`, :meth:`~pymkv.MKVFile.move_track_backward`, + or :meth:`~pymkv.MKVFile.swap_tracks`. + + After an :class:`~pymkv.MKVFile` has been created, an mkvmerge command can be generated using + :meth:`~pymkv.MKVFile.command` or the file can be muxed using :meth:`~pymkv.MKVFile.mux`. + + Parameters + ---------- + file_path : str, optional + Path to a pre-existing MKV file. The file will be imported into the new :class:`~pymkv.MKVFile` object. + title : str, optional + The internal title given to the :class:`~pymkv.MKVFile`. If `title` is not specified, the title of the + pre-existing file will be used if it exists. + + Raises + ------ + FileNotFoundError + Raised if the path to mkvmerge could not be verified. + """ - After an MKVFile has been created, an mkvmerge command can be generated using command() or the file can be - muxed using mux(). - - file_path (str, optional): - Path to a pre-existing MKV file. The file will be imported into the new MKVFile object. - title (str, optional): - The internal title given to the MKVFile. If no title is given, the title of the pre-existing file will - be used if it exists. - """ + def __init__(self, file_path=None, title=None): self.mkvmerge_path = 'mkvmerge' self.title = title self._chapters_file = None @@ -77,6 +120,13 @@ def __repr__(self): @property def chapter_language(self): + """str: The language code of the chapters in the :class:`~pymkv.MKVFile` object. + + Raises + ------ + ValueError + Raised if not a valid ISO 639-2 language code. + """ return self._chapter_language @chapter_language.setter @@ -86,13 +136,22 @@ def chapter_language(self, language): self._chapter_language = language def command(self, output_path, subprocess=False): - """Generates an mkvmerge command based on the configured MKVFile. + """Generates an mkvmerge command based on the configured :class:`~pymkv.MKVFile`. - output_file (str): + Parameters + ---------- + output_path : str The path to be used as the output file in the mkvmerge command. - subprocess (bool): - Will return the command as a list so it can be used easily with the subprocess module. + subprocess : bool + Will return the command as a list so it can be used easily with the :mod:`subprocess` module. + + Returns + ------- + str, list of str + The full command to mux the :class:`~pymkv.MKVFile` as a string containing spaces. Will be returned as a + list of strings with no spaces if `subprocess` is True. """ + output_path = expanduser(output_path) command = [self.mkvmerge_path, '-o', output_path] if self.title: @@ -182,12 +241,19 @@ def command(self, output_path, subprocess=False): return " ".join(command) def mux(self, output_path, silent=False): - """Muxes the specified MKVFile. + """Muxes the specified :class:`~pymkv.MKVFile`. - output_file (str): + Parameters + ---------- + output_path : str The path to be used as the output file in the mkvmerge command. - silent (bool): + silent : bool, optional By default the mkvmerge output will be shown unless silent is True. + + Raises + ------ + FileNotFoundError + Raised if the path to mkvmerge could not be verified. """ if not verify_mkvmerge(mkvmerge_path=self.mkvmerge_path): raise FileNotFoundError('mkvmerge is not at the specified path, add it there or change the mkvmerge_path ' @@ -201,10 +267,17 @@ def mux(self, output_path, silent=False): sp.run(self.command(output_path, subprocess=True)) def add_file(self, file): - """Combine an MKVFile with another MKVFile. + """Add an MKV file into the :class:`~pymkv.MKVFile` object. + + Parameters + ---------- + file : str, :class:`~pymkv.MKVFile` + The file to be combined with the :class:`~pymkv.MKVFile` object. - file (MKVFile): - The MKVFile to be combined with the MKVFile. + Raises + ------ + TypeError + Raised if if `file` is not a string-like path to an MKV file or an :class:`~pymkv.MKVFile` object. """ if isinstance(file, str): self.tracks = self.tracks + MKVFile(file).tracks @@ -214,10 +287,17 @@ def add_file(self, file): raise TypeError('track is not str or MKVFile') def add_track(self, track): - """Add an MKVTrack to the MKVFile. + """Add a track to the :class:`~pymkv.MKVFile`. + + Parameters + ---------- + track : str, :class:`~pymkv.MKVTrack` + The track to be added to the :class:`~pymkv.MKVFile` object. - track (str, MKVTrack): - The MKVTrack to be added to the MKVFile. + Raises + ------ + TypeError + Raised if `track` is not a string-like path to a track file or an :class:`~pymkv.MKVTrack`. """ if isinstance(track, str): self.tracks.append(MKVTrack(track)) @@ -227,10 +307,17 @@ def add_track(self, track): raise TypeError('track is not str or MKVTrack') def add_attachment(self, attachment): - """Add an attachment to the MKVFile. + """Add an attachment to the :class:`~pymkv.MKVFile`. - attachment (str, MKVAttachment): - The MKVAttachment to be added to the MKVAttachment. + Parameters + ---------- + attachment : str, :class:`~pymkv.MKVAttachment` + The attachment to be added to the :class:`~pymkv.MKVFile` object. + + Raises + ------ + TypeError + Raised if if `attachment` is not a string-like path to an attachment file or an :class:`~pymkv.MKVAttachment`. """ if isinstance(attachment, str): self.attachments.append(MKVAttachment(attachment)) @@ -240,20 +327,36 @@ def add_attachment(self, attachment): raise TypeError('attachment is not str of MKVAttachment') def get_track(self, track_num=None): - """Get a track from the MKVFile. - - track_num (int): - Index of track to retrieve. Will return list if argument is not provided. + """Get a :class:`~pymkv.MKVTrack` from the :class:`~pymkv.MKVFile` object. + + Parameters + ---------- + track_num : int, optional + Index of track to retrieve. Will return list of :class:`~pymkv.MKVTrack` objects if argument is not + provided. + + Returns + ------- + :class:`~pymkv.MKVTrack`, list of :class:`~pymkv.MKVTrack` + A list of all :class:`~pymkv.MKVTrack` objects in an :class:`~pymkv.MKVFile`. Returns a specific + :class:`~pymkv.MKVTrack` if `track_num` is specified. """ if track_num is None: return self.tracks return self.tracks[track_num] def move_track_front(self, track_num): - """Set a track as the first in an MKVFile. + """Set a track as the first in the :class:`~pymkv.MKVFile` object. - track_num (int): + Parameters + ---------- + track_num : int The track number of the track to move to the front. + + Raises + ------ + IndexError + Raised if `track_num` is is out of range of the track list. """ if 0 <= track_num < len(self.tracks): self.tracks.insert(0, self.tracks.pop(track_num)) @@ -261,10 +364,17 @@ def move_track_front(self, track_num): raise IndexError('track index out of range') def move_track_end(self, track_num): - """Set as track as the last in an MKVFile. + """Set as track as the last in the :class:`~pymkv.MKVFile` object. - track_num (int): + Parameters + ---------- + track_num : int The track number of the track to move to the back. + + Raises + ------ + IndexError + Raised if `track_num` is is out of range of the track list. """ if 0 <= track_num < len(self.tracks): self.tracks.append(self.tracks.pop(track_num)) @@ -272,10 +382,17 @@ def move_track_end(self, track_num): raise IndexError('track index out of range') def move_track_forward(self, track_num): - """Move a track forward in an MKVFile. + """Move a track forward in the :class:`~pymkv.MKVFile` object. - track_num (int): + Parameters + ---------- + track_num : int The track number of the track to move forward. + + Raises + ------ + IndexError + Raised if `track_num` is is out of range of the track list. """ if 0 <= track_num < len(self.tracks) - 1: self.tracks[track_num], self.tracks[track_num + 1] = self.tracks[track_num + 1], self.tracks[track_num] @@ -283,10 +400,17 @@ def move_track_forward(self, track_num): raise IndexError('track index out of range') def move_track_backward(self, track_num): - """Move a track backward in an MKVFile. + """Move a track backward in the :class:`~pymkv.MKVFile` object. - track_num (int): + Parameters + ---------- + track_num : int The track number of the track to move backward. + + Raises + ------ + IndexError + Raised if `track_num` is is out of range of the track list. """ if 0 < track_num < len(self.tracks): self.tracks[track_num], self.tracks[track_num - 1] = self.tracks[track_num - 1], self.tracks[track_num] @@ -294,12 +418,19 @@ def move_track_backward(self, track_num): raise IndexError('track index out of range') def swap_tracks(self, track_num_1, track_num_2): - """Swap the position of two tracks in an MKVFile. + """Swap the position of two tracks in the :class:`~pymkv.MKVFile` object. - track_num_1 (int): + Parameters + ---------- + track_num_1 : int The track number of one track to swap. - track_num_2 (int): + track_num_2 : int The track number of the other track to swap + + Raises + ------ + IndexError + Raised if `track_num_1` or `track_num_2` are out of range of the track list. """ if 0 <= track_num_1 < len(self.tracks) and 0 <= track_num_2 < len(self.tracks): self.tracks[track_num_1], self.tracks[track_num_2] = self.tracks[track_num_2], self.tracks[track_num_1] @@ -307,12 +438,19 @@ def swap_tracks(self, track_num_1, track_num_2): raise IndexError('track index out of range') def replace_track(self, track_num, track): - """Replace a track with another track in an MKVFile. + """Replace a track with another track in the :class:`~pymkv.MKVFile` object. - track_num (int): + Parameters + ---------- + track_num : int The track number of the track to replace. - track (MKVTrack): - The MKVTrack to be replaced into the file. + track : :class:`~pymkv.MKVTrack` + The :class:`~pymkv.MKVTrack` to be replaced into the file. + + Raises + ------ + IndexError + Raised if `track_num` is is out of range of the track list. """ if 0 <= track_num < len(self.tracks): self.tracks[track_num] = track @@ -320,10 +458,17 @@ def replace_track(self, track_num, track): raise IndexError('track index out of range') def remove_track(self, track_num): - """Remove a track from the MKVFile. + """Remove a track from the :class:`~pymkv.MKVFile` object. - track_num (int): + Parameters + ---------- + track_num : int The track number of the track to remove. + + Raises + ------ + IndexError + Raised if `track_num` is is out of range of the track list. """ if 0 <= track_num < len(self.tracks): del self.tracks[track_num] @@ -337,9 +482,18 @@ def split_none(self): def split_size(self, size, link=False): """Split the output file into parts by size. - size (bitmath obj, int): - The size of each split file. Takes either a bitmath size object or an integer representing the number of - bytes. + Parameters + ---------- + size : :obj:`bitmath`, int + The size of each split file. Takes either a :obj:`bitmath` size object or an integer representing the + number of bytes. + link : bool, optional + Determines if the split files should be linked together after splitting. + + Raises + ------ + TypeError + Raised if if `size` is not a bitmath object or an integer. """ if getattr(size, '__module__', None) == bitmath.__name__: size = size.bytes @@ -352,10 +506,12 @@ def split_size(self, size, link=False): def split_duration(self, duration, link=False): """Split the output file into parts by duration. - duration (str, int): + Parameters + ---------- + duration : str, int The duration of each split file. Takes either a str formatted to HH:MM:SS.nnnnnnnnn or an integer representing the number of seconds. The duration string requires formatting of at least M:S. - link (bool): + link : bool, optional Determines if the split files should be linked together after splitting. """ self._split_options = ['--split', 'duration:' + str(Timestamp(duration))] @@ -365,12 +521,19 @@ def split_duration(self, duration, link=False): def split_timestamps(self, *timestamps, link=False): """Split the output file into parts by timestamps. - *timestamps (str, int, list, tuple): + Parameters + ---------- + *timestamps : str, int, list, tuple The timestamps to split the file by. Can be passed as any combination of strs and ints, inside or outside - an Iterable object. Any lists will be flattened. Timestamps must be ints, representing seconds, or strs in - the form HH:MM:SS.nnnnnnnnn. The timestamp string requires formatting of at least M:S. - link (bool): + an :obj:`Iterable` object. Any lists will be flattened. Timestamps must be ints, representing seconds, + or strs in the form HH:MM:SS.nnnnnnnnn. The timestamp string requires formatting of at least M:S. + link : bool, optional Determines if the split files should be linked together after splitting. + + Raises + ------ + ValueError + Raised if invalid or improperly formatted timestamps are passed in for `*timestamps`. """ # check if in timestamps form ts_flat = MKVFile.flatten(timestamps) @@ -393,11 +556,20 @@ def split_timestamps(self, *timestamps, link=False): def split_frames(self, *frames, link=False): """Split the output file into parts by frames. - *frames (int, list, tuple): - The frames to split the file by. Can be passed as any combination of ints, inside or outside an Iterable - object. Any lists will be flattened. Frames must be ints. - link (bool): + Parameters + ---------- + *frames : int, list, tuple + The frames to split the file by. Can be passed as any combination of ints, inside or outside an + :obj:`Iterable` object. Any lists will be flattened. Frames must be ints. + link : bool, optional Determines if the split files should be linked together after splitting. + + Raises + ------ + TypeError + Raised if non-int frames are passed in for `*frames` or within the `*frames` iterable. + ValueError + Raised if improperly formatted frames are passed in for `*frames`. """ # check if in frames form f_flat = MKVFile.flatten(frames) @@ -421,12 +593,21 @@ def split_frames(self, *frames, link=False): def split_timestamp_parts(self, timestamp_parts, link=False): """Split the output in parts by time parts. - parts (list, tuple): + Parameters + ---------- + timestamp_parts : list, tuple An Iterable of timestamp sets. Each timestamp set should be an Iterable of an even number of timestamps or any number of timestamp pairs. The very first and last timestamps are permitted to be None. Timestamp sets containing 4 or more timestamps will output as one file containing the parts specified. - link (bool): + link : bool, optional Determines if the split files should be linked together after splitting. + + Raises + ------ + TypeError + Raised if any of the timestamp sets are not a list or tuple. + ValueError + Raised if `timestamp_parts` contains improperly formatted parts. """ # check if in parts form ts_flat = MKVFile.flatten(timestamp_parts) @@ -467,12 +648,22 @@ def split_timestamp_parts(self, timestamp_parts, link=False): def split_parts_frames(self, frame_parts, link=False): """Split the output in parts by frames. - parts (list, tuple): - An Iterable of frame sets. Each frame set should be an Iterable of an even number of frames or any - number of frame pairs. The very first and last frames are permitted to be None. Frame sets containing 4 + Parameters + ---------- + frame_parts : list, tuple + An Iterable of frame sets. Each frame set should be an :obj:`Iterable` object of an even number of frames + or any + number of frame pairs. The very first and last frames are permitted to be None. Frame sets containing four or more frames will output as one file containing the parts specified. - link (bool): + link : bool, optional Determines if the split files should be linked together after splitting. + + Raises + ------ + TypeError + Raised if any of the frame sets are not a list or tuple. + ValueError + Raised if `frame_parts` contains improperly formatted parts. """ # check if in parts form f_flat = MKVFile.flatten(frame_parts) @@ -517,11 +708,20 @@ def split_parts_frames(self, frame_parts, link=False): def split_chapters(self, *chapters, link=False): """Split the output file into parts by chapters. - *chapters (int, list, tuple): + Parameters + ---------- + *chapters : int, list, tuple The chapters to split the file by. Can be passed as any combination of ints, inside or outside an - Iterable object. Any lists will be flattened. Chapters must be ints. - link (bool): + :obj:`Iterable` object. Any lists will be flattened. Chapters must be ints. + link : bool, optional Determines if the split files should be linked together after splitting. + + Raises + ------ + TypeError + Raised if any chapters in `*chapters` are not of type int. + ValueError + Raised if `*chapters` contains improperly formatted chapters. """ # check if in chapters form c_flat = MKVFile.flatten(chapters) @@ -546,10 +746,19 @@ def split_chapters(self, *chapters, link=False): self._split_options += '--link' def link_to_previous(self, file_path): - """Link the output file as the predecessor of the file_path file. + """Link the output file as the predecessor of the `file_path` file. - file_path (str): + Parameters + ---------- + file_path : str Path of the file to be linked to. + + Raises + ------ + TypeError + Raised if `file_path` is not of type str. + ValueError + Raised if file at `file_path` cannot be verified as an MKV. """ # check if valid file if not isinstance(str, file_path): @@ -560,10 +769,19 @@ def link_to_previous(self, file_path): self._link_to_previous_file = file_path def link_to_next(self, file_path): - """Link the output file as the successor of the file_path file. + """Link the output file as the successor of the `file_path` file. - file_path (str): + Parameters + ---------- + file_path : str Path of the file to be linked to. + + Raises + ------ + TypeError + Raised if `file_path` is not of type str. + ValueError + Raised if file at `file_path` cannot be verified as an MKV. """ # check if valid file if not isinstance(file_path, str): @@ -579,12 +797,21 @@ def link_to_none(self): self._link_to_next_file = None def chapters(self, file_path, language=None): - """Add a chapters file to an MKVFile. - - file_path (str): - The chapters file to be added to the MKVFile. - language (str, optional): - Must be an ISO639-2 language code. Only works if no existing language information exists in chapters. + """Add a chapters file to the :class:`~pymkv.MKVFile` object. + + Parameters + ---------- + file_path : str + The chapters file to be added to the :class:`~pymkv.MKVFile` object. + language : str, optional + Must be an ISO639-2 language code. Only applied if no existing language information exists in chapters. + + Raises + ------ + FileNotFoundError + Raised if the file at `file_path` does not exist. + TypeError + Raised if `file_path` is not of type str. """ if not isinstance(file_path, str): raise TypeError('"{}" is not of type str'.format(file_path)) @@ -595,10 +822,19 @@ def chapters(self, file_path, language=None): self.chapter_language = language def global_tags(self, file_path): - """Add a global tags to an MKVFile. - - file_path (str): - The tags file to be added to the MKVFile. + """Add global tags to the :class:`~pymkv.MKVFile` object. + + Parameters + ---------- + file_path : str + The tags file to be added to the :class:`~pymkv.MKVFile` object. + + Raises + ------ + FileNotFoundError + Raised if the file at `file_path` does not exist. + TypeError + Raised if `file_path` is not of type str. """ if not isinstance(file_path, str): raise TypeError('"{}" is not of type str'.format(file_path)) @@ -610,11 +846,22 @@ def global_tags(self, file_path): def track_tags(self, *track_ids, exclusive=False): """Include or exclude tags from specific tracks. - *track_ids (int, list, tuple): + Parameters + ---------- + *track_ids : int, list, tuple Track ids to have tags included or excluded from. - exclusive: - Determines if the track_ids or the unspecified track_ids should have their tags kept. False by default - and will remove tags from unspecified tracks. + exclusive : bool, optional + Determines if the `track_ids` should have their tags kept or removed. `exclusive` is False by default and + will remove tags from unspecified tracks. + + Raises + ------ + IndexError + Raised if any ids from `*track_ids` is is out of range of the track list. + TypeError + Raised if an ids from `*track_ids` are not of type int. + ValueError + Raised if `*track_ids` are improperly formatted. """ # check if in track_ids form ids_flat = MKVFile.flatten(track_ids) @@ -630,22 +877,22 @@ def track_tags(self, *track_ids, exclusive=False): self.tracks[tid].no_track_tags = True def no_chapters(self): - """Ignore the existing chapters of the MKVFile.""" + """Ignore the existing chapters of the :class:`~pymkv.MKVFile` object.""" for track in self.tracks: track.no_chapters = True def no_global_tags(self): - """Ignore the existing global tags of the MKVFile.""" + """Ignore the existing global tags of the :class:`~pymkv.MKVFile` object.""" for track in self.tracks: track.no_global_tags = True def no_track_tags(self): - """Ignore the existing track tags of the MKVFile.""" + """Ignore the existing track tags of the :class:`~pymkv.MKVFile` object.""" for track in self.tracks: track.no_track_tags = True def no_attachments(self): - """Ignore the existing attachments of the MKVFile.""" + """Ignore the existing attachments of the :class:`~pymkv.MKVFile` object.""" for track in self.tracks: track.no_attachments = True @@ -653,8 +900,23 @@ def no_attachments(self): def flatten(item): """Flatten a list or a tuple. - item (list, tuple): - An iterable object with nested iterables to be flattened. + Takes a list or a tuple that contains other lists or tuples and flattens into a non-nested list. + + Examples + -------- + >>> tup = ((1, 2), (3, (4, 5))) + >>> print(MKVFile.flatten(tup)) + [1, 2, 3, 4, 5] + + Parameters + ---------- + item : list, tuple + A list or a tuple object with nested lists or tuples to be flattened. + + Returns + ------- + list + A flattened version of `item`. """ flat_list = [] if isinstance(item, (list, tuple)): diff --git a/pymkv/MKVTrack.py b/pymkv/MKVTrack.py index dec2036..329d4d1 100644 --- a/pymkv/MKVTrack.py +++ b/pymkv/MKVTrack.py @@ -1,7 +1,39 @@ -# sheldon woodward -# 2/25/2018 +""":class:`~pymkv.MKVTrack` classes are used to represent tracks within an MKV or to be used in an MKV. They can +represent a video, audio, or subtitle track. -"""MKVTrack Class""" +Examples +-------- +Below are some basic examples of how the :class:`~pymkv.MKVTrack` objects can be used. + +Create a new :class:`~pymkv.MKVTrack` from a track file. This example takes a standalone track file and uses it in an +:class:`~pymkv.MKVTrack`. + +>>> from pymkv import MKVTrack +>>> track1 = MKVTrack('path/to/track.h264') +>>> track1.track_name = 'Some Name' +>>> track1.language = 'eng' + +Create a new :class:`~pymkv.MKVTrack` from an MKV file. This example will take a specific track from an MKV and also +prevent any global tags from being included if the :class:`~pymkv.MKVTrack` is muxed into an :class:`~pymkv.MKVFile`. + +>>> track2 = MKVTrack('path/to/track.aac') +>>> track2.language = 'eng' + +Create a new :class:`~pymkv.MKVTrack` from an MKV file. This example will take a specific track from an MKV and also +prevent any global tags from being included if the :class:`~pymkv.MKVTrack` is muxed into an :class:`~pymkv.MKVFile`. + +>>> track3 = MKVTrack('path/to/MKV.mkv', track_id=1) +>>> track3.no_global_tags = True + +Now all these tracks can be added to an :class:`~pymkv.MKVFile` object and muxed together. + +>>> from pymkv import MKVFile +>>> file = MKVFile() +>>> file.add_track(track1) +>>> file.add_track(track2) +>>> file.add_track(track3) +>>> file.mux('path/to/output.mkv') +""" import json from os.path import expanduser, isfile @@ -12,27 +44,61 @@ class MKVTrack: + """A class that represents a track for an :class:`~pymkv.MKVFile` object. + + :class:`~pymkv.MKVTrack` objects are video, audio, or subtitles. Tracks can be standalone files or a single track + within an MKV file, both can be handled by pymkv. An :class:`~pymkv.MKVTrack` object can be added to an + :class:`~pymkv.MKVFile` and will be included when the MKV is muxed. + + Parameters + ---------- + file_path : str + Path to the track file. This can also be an MKV where the `track_id` is the track represented in the MKV. + track_id : int, optional + The id of the track to be used from the file. `track_id` only needs to be set when importing a track from + an MKV. In this case, you can specify `track_id` to indicate which track from the MKV should be used. If not + set, it will import the first track. Track 0 is imported by default because mkvmerge sees standalone + track files as having one track with track_id set as 0. + track_name : str, optional + The name that will be given to the track when muxed into a file. + language : str, optional + The language of the track. It must be an ISO639-2 language code. + default_track : bool, optional + Determines if the track should be the default track of its type when muxed into an MKV file. + forced_track : bool, optional + Determines if the track should be a forced track when muxed into an MKV file. + + Attributes + ---------- + mkvmerge_path : str + The path where pymkv looks for the mkvmerge executable. pymkv relies on the mkvmerge executable to parse + files. By default, it is assumed mkvmerge is in your shell's $PATH variable. If it is not, you need to set + *mkvmerge_path* to the executable location. + track_name : str + The name that will be given to the track when muxed into a file. + default_track : bool + Determines if the track should be the default track of its type when muxed into an MKV file. + forced_track : bool + Determines if the track should be a forced track when muxed into an MKV file. + no_chapters : bool + If chapters exist in the track file, don't include them when this :class:`~pymkv.MKVTrack` object is a track + in an :class:`~pymkv.MKVFile` mux operation. This option has no effect on standalone track files, only tracks + that are already part of an MKV file. + no_global_tags : bool + If global tags exist in the track file, don't include them when this :class:`~pymkv.MKVTrack` object is a track + in an :class:`~pymkv.MKVFile` mux operation. This option has no effect on standalone track files, only tracks + that are already part of an MKV file. + no_track_tags : bool + If track tags exist in the specified track within the track file, don't include them when this + :class:`~pymkv.MKVTrack` object is a track in an :class:`~pymkv.MKVFile` mux operation. This option has no + effect on standalone track files, only tracks that are already part of an MKV file. + no_attachments : bool + If attachments exist in the track file, don't include them when this :class:`~pymkv.MKVTrack` object is a track + in an :class:`~pymkv.MKVFile` mux operation. This option has no effect on standalone track files, only tracks + that are already part of an MKV file. + """ + def __init__(self, file_path, track_id=0, track_name=None, language=None, default_track=False, forced_track=False): - """A class that represents an MKV track such as video, audio, or subtitles. - - MKVTracks can be added to an MKVFile. MKVTracks can be video, audio, or subtitle tracks. The only required - argument is path which gives the path to a track file. - - file_path (str): - Path to the track file. This can also be an MKV where the *track_id* is the track represented in the MKV. - This is the only required argument. - track_id (int, optional): - The id of the track to be used in the file. Does not need to be set unless the input file has multiple - tracks. - track_name (str, optional): - The name that will be given to the track when muxed into a file. - language (str, optional): - The language of the track. It must be an ISO639-2 language code. - default_track (bool, optional): - Determines if the track should be the default track of its type when muxed into an MKV file. - forced_track (bool, optional): - Determines if the track should be a forced track when muxed into an MKV file. - """ # track info self._track_codec = None self._track_type = None @@ -63,6 +129,16 @@ def __repr__(self): @property def file_path(self): + """str: The path to the track or MKV file containing the desired track. + + Setting this property will verify the passed in file is supported by mkvmerge and set the track_id to 0. It + is recommended to recreate MKVTracks instead of setting their file path after instantiation. + + Raises + ------ + ValueError + Raised if `file_path` is not a supported file type. + """ return self._file_path @file_path.setter @@ -75,6 +151,16 @@ def file_path(self, file_path): @property def track_id(self): + """int: The ID of the track within the file. + + Setting *track_id* will check that the ID passed in exists in the file. It will then look at the new track + and set the codec and track type. Should be left at 0 unless extracting a specific track from an MKV. + + Raises + ------ + IndexError + Raised if the passed in index is out of range of the file's tracks. + """ return self._track_id @track_id.setter @@ -88,6 +174,16 @@ def track_id(self, track_id): @property def language(self): + """str: The language of the track. + + Setting this property will verify that the passed in language is an ISO-639 language code and use it as the + language for the track. + + Raises + ------ + ValueError + Raised if the passed in language is not an ISO 639-2 language code. + """ return self._language @language.setter @@ -99,6 +195,17 @@ def language(self, language): @property def tags(self): + """str: The tags file to include with the track. + + Setting this property will check that the file path passed in exists and set it as the tags file. + + Raises + ------ + FileNotFoundError + Raised if the passed in file does not exist or is not a file. + TypeError + Raises if the passed in file is not of type str. + """ return self._tags @tags.setter @@ -112,8 +219,10 @@ def tags(self, file_path): @property def track_codec(self): + """str: The codec of the track such as h264 or AAC.""" return self._track_codec @property def track_type(self): + """str: The type of track such as video or audio.""" return self._track_type