.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..40282ef
--- /dev/null
+++ b/README.md
@@ -0,0 +1,143 @@
+
+ MPGG
+
+ Streamlined MPEG-1 and MPEG-2 source loader and helper utility for VapourSynth
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Features
+
+- 🎥 Supports MPEG-1 and MPEG-2 Sources
+- 🧠 Understands Mixed-scan Sources
+- 🤖 VFR to CFR (Variable to Constant frame rate)
+- 🛠️ Automatic Frame-indexing using DGIndex
+- ⚙️ Zero-configuration
+- 🧩 Easy installation via PIP/PyPI
+- ❤️ Fully Open-Source! Pull Requests Welcome
+
+## Installation
+
+```shell
+$ pip install mpgg
+```
+
+Voilà 🎉! You now have the `mpgg` package installed, and you can now import it from a VapourSynth script.
+
+### Dependencies
+
+The following is a list of software that needs to be installed manually. MPGG cannot install these automatically
+on your behalf.
+
+#### Software
+
+- [MKVToolnix] (specifically mkvextract) for demuxing MPEG streams from MKV containers.
+- [DGIndex] for automatic frame-indexing of MPEG streams.
+
+Make sure you put them in your current working directory, in the installation directory, or put the directory path in
+your `PATH` environment variable. If you do not do this then their binaries will not be able to be found.
+
+ [MKVToolNix]:
+ [DGIndex]:
+
+#### VapourSynth Plugins
+
+- [d2vsource] for loading an indexed DGIndex project file.
+
+These plugins may be installed using [vsrepo] on Windows, or from a package repository on Linux.
+
+ [d2vsource]:
+ [vsrepo]:
+
+## Usage
+
+The following is an example of using MPGG to get a clean CFR Fully Progressive stream from an
+Animated Mixed-scan VFR DVD-Video source.
+
+```python
+import functools
+
+from mpgg import MPGG
+from havsfunc import QTGMC
+
+# load the source with verbose information printed
+mpg = MPGG(r"C:\Users\John\Videos\animated_dvd_video.mkv", verbose=True)
+
+# recover progressive frames where possible, and show which frames were recovered
+mpg.recover(verbose=True)
+
+# deinterlace any remaining interlaced frames with QTGMC, and show which frames were deinterlaced
+mpg.deinterlace(
+ kernel=functools.partial(QTGMC, Preset="Very Slow", FPSDivisor=2),
+ verbose=True
+)
+
+# convert VFR to CFR by duplicating frames in a pattern
+mpg.ceil()
+
+# get the final clip (you may use the clip in between actions as well)
+clip = mpg.clip
+
+# ...
+
+clip.set_output()
+```
+
+You can also chain calls! This is the same script but chained,
+
+```python
+import functools
+
+from mpgg import MPGG
+from havsfunc import QTGMC
+
+# load MPEG, recover progressive frames, deinterlace what's left, and finally VFR to CFR
+clip = MPGG(r"C:\Users\John\Videos\animated_dvd_video.mkv", verbose=True).\
+ recover(verbose=True).\
+ deinterlace(kernel=functools.partial(QTGMC, Preset="Very Slow", FPSDivisor=2), verbose=True).\
+ ceil().\
+ clip
+
+# ...
+
+clip.set_output()
+```
+
+There are more methods not shown here. I recommend taking a look at the MPGG class for further
+information, methods, and more.
+
+> __Warning__ Do not copy/paste and re-use these examples. Read each method's doc-string information
+> as they each have their own warnings, tips, and flaws that you need to be aware of. For example,
+> recover() shouldn't be used on all MPEG sources, floor() shouldn't be used with recover(), you
+> may not want to use ceil() if you want to keep encoding as VFR, and such.
+
+## Terminology
+
+| Term | Meaning |
+|----------------|--------------------------------------------------------------------------------|
+| CFR | Constant frame-rate, the source uses a set frame rate on playback |
+| VFR | Variable frame-rate, the source switches frame rate at least once on playback |
+| Scan | The technology used to show images on screens, i.e., Interlaced or Progressive |
+| Mixed-scan | Source with both Progressive and Interlaced frames within the video data |
+| Frame-indexing | Analyzing a source to index frame/field information for frame-serving |
+
+## Contributors
+
+
+
+## License
+
+© 2021-2023 rlaphoenix — [GNU General Public License, Version 3.0](LICENSE)
diff --git a/mpgg/__init__.py b/mpgg/__init__.py
new file mode 100644
index 0000000..c38de0d
--- /dev/null
+++ b/mpgg/__init__.py
@@ -0,0 +1,5 @@
+__version__ = "1.0.0"
+
+from .mpgg import MPGG
+
+__ALL__ = (__version__, MPGG)
diff --git a/mpgg/mpgg.py b/mpgg/mpgg.py
new file mode 100644
index 0000000..650256f
--- /dev/null
+++ b/mpgg/mpgg.py
@@ -0,0 +1,419 @@
+from __future__ import annotations
+
+import functools
+import math
+from collections import Counter
+from pathlib import Path
+from typing import Optional
+
+import vapoursynth as vs
+from more_itertools import split_at
+from pyd2v import D2V
+from pymediainfo import MediaInfo
+from vapoursynth import core
+
+from mpgg.utilities import get_aspect_ratio, get_par, get_standard, group_numbers, list_select_every
+
+
+class MPGG:
+ def __init__(self, file: str, verbose: bool = False):
+ """
+ Index MPEG-1 or MPEG-2 file with DGIndex and load with Derek dwbuiten's d2v source loader.
+
+ Automatic indexing will take place if no D2V for the file was found. You may
+ explicitly load a D2V file if the MPEG file it indexed is somewhere else.
+
+ Specific settings are used when indexing to have full compatibility will all
+ kinds of scan configurations. Pre-existing D2V files should be deleted before
+ being used with MPGG, unless it was indexed by MPGG. This is because they may
+ have been indexed with unsupported settings.
+
+ Warning: The integrity of the `flags` data relative to the internal `clip` is
+ crucial for the accuracy of any function called from hence forth. If you
+ modify the internal clip, please make sure to update the `flags` data
+ appropriately. Modifying the internal clip is not recommended.
+
+ Parameters:
+ file: The video file to load. It may be an MKV, VOB, MPG/MPEG, or a D2V.
+ verbose: Print useful information about the source, and it's scan type.
+ """
+ if not hasattr(core, "d2v"):
+ raise EnvironmentError(
+ "Required plugin 'd2vsource' is not installed. "
+ "See https://github.com/dwbuiten/d2vsource"
+ )
+
+ # TODO: Somehow add a check that the D2V was indexed by pyd2v
+ self.d2v = D2V.load(Path(file))
+
+ self.file = self.d2v.path
+ self.flags = self._get_flags(self.d2v)
+ self.pulldown, self.pulldown_str = self._get_pulldown(self.flags)
+ self.total_frames = len(self.flags)
+ self.p_frames = sum(f["progressive_frame"] for f in self.flags)
+ self.i_frames = self.total_frames - self.p_frames
+
+ # A DVD-spec MPEG stream is considered VFR if there's interlaced and progressive frames,
+ # otherwise it would only ever be NTSC (30000/1001i) or PAL (25i), therefore constant.
+ self.vfr = any(f["progressive_frame"] and f["rff"] and f["tff"] for f in self.flags) and any(
+ not f["progressive_frame"] for f in self.flags)
+
+ # Do not apply RFF (Repeat First Field) as this would be the flags for Software Pulldown.
+ # We do not want to cause further interlacing. Instead, we will handle this later using
+ # either ceil() or floor(). Or, the user can leave it alone to retain VFR.
+ self.clip = core.d2v.Source(self.file, rff=False)
+
+ # We must copy the flags to each frame/field of the clip, or we cannot make important
+ # decisions after frame rate adjustments as the mapping to `flags` would be lost.
+ self.clip = self._stamp_frames(self.clip, self.flags)
+
+ # Override the _ColorRange value set by core.d2v.Source with one obtained from
+ # the container/stream if available, or fallback and assume limited/TV.
+ # This makes YUVRGB_Scale setting redundant to reduce possibilities of mistakes.
+ video_track = next(iter(MediaInfo.parse(self.d2v.videos[0]).video_tracks), None)
+ if video_track and getattr(video_track, "color_range", None):
+ color_range = {"Full": 0, "Limited": 1}[video_track.color_range]
+ else:
+ color_range = 1 # assume limited/TV as MPEGs most likely are
+ self.clip = core.std.SetFrameProp(self.clip, "_ColorRange", color_range)
+
+ self.standard = get_standard(self.clip.fps.numerator / self.clip.fps.denominator)
+ self.dar = self.d2v.settings["Aspect_Ratio"]
+ if isinstance(self.dar, list):
+ self.dar = self.dar[0]
+ self.sar = get_aspect_ratio(self.clip.width, self.clip.height)
+ self.par = get_par(self.clip.width, self.clip.height, *[int(x) for x in self.dar.split(":")])
+
+ if verbose:
+ progressive_p = (self.p_frames / self.total_frames) * 100
+ self.clip = core.text.Text(
+ self.clip,
+ text=" " + (" \n ".join([
+ f"Progressive: {progressive_p:05.2f}% ({self.p_frames})" + (
+ f" w/ Pulldown {self.pulldown_str} (Cycle: {self.pulldown})" if self.pulldown else
+ " - No Pulldown"
+ ),
+ f"Interlaced: {100 - progressive_p:05.2f}% ({self.total_frames - self.p_frames})",
+ f"VFR? {self.vfr} DAR: {self.dar} SAR: {self.sar} PAR: {self.par}",
+ self.standard
+ ])) + " ",
+ alignment=1,
+ scale=1
+ )
+
+ if not self.vfr and not self.i_frames:
+ # MPEG is fully progressive (via Pulldown flags), so core.d2v.Source gives it
+ # an FPS of e.g., 30000/1001, when it should be e.g., 24000/1001.
+ self.floor()
+ self.pulldown = None
+
+ def recover(self, verbose=False, **kwargs):
+ """
+ Recovers progressive frames from an interlaced clip using VIVTC VFM.
+
+ Do not provide `order` (TFF/BFF) argument manually unless you need to override the auto-detected
+ order, or it could not be auto-detected.
+
+ For possible arguments, see the VIVTC docs here:
+
+
+ Tips: - Only use this on sources where the majority of combed frames are recoverable (e.g. Animation),
+ otherwise you are risking jerkiness and making your script slower for likely no gain.
+ - Use this before *any* frame rate, length, or visual adjustments, including before deinterlacing.
+ - This may add irregular duplicate frames, You should use VDecimate afterwards.
+ - Do NOT use `floor()` if you use this method, it will not be safe.
+ """
+ if not isinstance(self.clip, vs.VideoNode):
+ raise TypeError("This is not a clip")
+
+ matched_tff = core.vivtc.VFM(self.clip, order=1, **kwargs)
+ matched_bff = core.vivtc.VFM(self.clip, order=0, **kwargs)
+
+ def _m(n: int, f: vs.VideoFrame, c: vs.VideoNode, tff: vs.VideoNode, bff: vs.VideoNode):
+ # frame marked as progressive, skip matching
+ if f.props["PVSFlagProgressiveFrame"] or f.props.get("_Combed") == 0:
+ if c.format and tff.format and c.format.id != tff.format.id:
+ c = core.resize.Point(c, format=tff.format.id)
+ return core.text.Text(c, "Progressive", alignment=3) if verbose else c
+ # interlaced frame, match (if _FieldBased is > 0)
+ rc = {0: c, 1: bff, 2: tff}[f.props["_FieldBased"]] # type: ignore
+ return core.text.Text(
+ rc,
+ "Matched (%s)" % {0: "Recovered", 1: "Combed "}[rc.get_frame(n).props["_Combed"]],
+ alignment=3
+ ) if verbose else rc
+
+ self.clip = core.std.FrameEval(
+ matched_tff,
+ functools.partial(
+ _m,
+ c=self.clip,
+ tff=matched_tff,
+ bff=matched_bff
+ ),
+ prop_src=self.clip
+ )
+ return self
+
+ def deinterlace(self, kernel: functools.partial, verbose: bool = False) -> MPGG:
+ """
+ Deinterlace clip only on frames that are marked as interlaced.
+
+ Frames that are recovered with :meth:`recover` will be skipped. However, if
+ VFM detected combing in the frame, it will be deinterlaced.
+
+ The kernel should be a function that accepts a `clip` VideoNode in the first
+ positional argument. It must also accept a `TFF`/`tff` keyword argument.
+ You can pass kernel settings via `functools.partial`. For example::
+
+ kernel = functools.partial(QTGMC, FPSDivisor=2, Preset="VeryFast")
+
+ If the kernel you want to use does not accept a `TFF`/`tff` keyword argument,
+ you can manually proxy it to another argument with a lambda function, e.g.,::
+
+ def foo(clip, field: int, preset: str):
+ # pseudo-kernel using `field` to indicate tff/bff
+ # ...
+
+ kernel = functools.partial(lambda clip, tff: foo(clip, field=tff, preset="Fast"))
+
+ You should not manually specify TFF/field order as the field order is automatically
+ determined. In fact, it actually supports clips which switch field order on the fly!
+
+ Parameters:
+ kernel: A function to pass the clip through when it needs deinterlacing.
+ verbose: Print useful information about the deinterlacing result.
+ """
+ if not isinstance(self.clip, vs.VideoNode):
+ raise TypeError(f"Expected clip to be a {vs.VideoNode}, not {self.clip!r}")
+ if not callable(kernel):
+ raise TypeError(f"Expected kernel to be a function, not {kernel!r}")
+ if len(kernel.args) > 1:
+ # causes conflicts with the clip positional argument
+ raise ValueError("Invalid kernel arguments, no positional arguments are allowed")
+
+ deinterlaced_tff = kernel(self.clip, TFF=True)
+ deinterlaced_bff = kernel(self.clip, TFF=False)
+
+ fps_factor = deinterlaced_tff.fps.numerator / deinterlaced_tff.fps.denominator
+ fps_factor = fps_factor / (self.clip.fps.numerator / self.clip.fps.denominator)
+ if fps_factor not in (1.0, 2.0):
+ # TODO: Add support for more, we might already support mod2, e.g., 2/4/8/16/e.t.c
+ raise ValueError(
+ f"The kernel returned an unsupported frame-rate ({deinterlaced_tff.fps}). " +
+ "Only single-rate and double-rate deinterlacing is currently supported."
+ )
+
+ def _d(n: int, f: vs.VideoFrame, c: vs.VideoNode, tff: vs.VideoNode, bff: vs.VideoNode, ff: int):
+ # Frame marked as progressive by DGIndex or VFM, skip deinterlacing
+ if f.props["PVSFlagProgressiveFrame"] or f.props.get("_Combed") == 0:
+ # duplicate if not a single-rate fps output
+ rc = core.std.Interleave([c] * ff) if ff > 1 else c
+ if rc.format and tff.format and rc.format.id != tff.format.id:
+ rc = core.resize.Spline16(rc, format=tff.format.id)
+ return core.text.Text(
+ rc,
+ # space it to keep recover()'s verbose logs visible
+ "Progressive" + ["", "\n"]["_Combed" in f.props],
+ alignment=3
+ ) if verbose else rc
+ # Frame otherwise assumed to be interlaced or progressively encoded interlacing.
+ # It won't deinterlace progressive frames here unless recover() was run and detected
+ # that the frame was interlaced by detecting visual combing artifacts.
+ # Do note that deinterlacing progressively encoded interlaced frames don't always look
+ # the best, but not much can really be done in those cases.
+ order = f.props["_FieldBased"]
+ if f.props.get("_Combed", 0) != 0:
+ order = 2 # TODO: Don't assume TFF
+ rc = {0: c, 1: bff, 2: tff}[order] # type: ignore
+ field_order = {0: "Progressive ", 1: "BFF", 2: "TFF"}[order] # type: ignore
+ return core.text.Text(
+ rc,
+ ("Deinterlaced (%s)" % field_order) + ["", "\n"]["_Combed" in f.props],
+ alignment=3
+ ) if verbose else rc
+
+ self.clip = core.std.FrameEval(
+ deinterlaced_tff,
+ functools.partial(
+ _d,
+ c=self.clip,
+ tff=deinterlaced_tff,
+ bff=deinterlaced_bff,
+ ff=int(fps_factor) # TODO: floor/ceil instead?
+ ),
+ prop_src=self.clip
+ )
+
+ return self
+
+ def ceil(self) -> MPGG:
+ """
+ VFR to CFR by duplicating progressive frames with the RFF flag.
+
+ This does the same operation as honoring pulldown/RFF, but without
+ interlacing the progressive frames, resulting in a mixed-scan stream
+ and no further spatial loss.
+ """
+ if not self.vfr:
+ return self
+
+ pf = [i for i, f in enumerate(self.flags) if f["progressive_frame"] and f["rff"] and f["tff"]]
+
+ self.clip = core.std.DuplicateFrames(self.clip, pf)
+
+ def disable_rff(n: int, f: vs.VideoFrame) -> vs.VideoFrame:
+ f = f.copy()
+ f.props["PVSFlagRff"] = 0
+ return f
+
+ self.clip = core.std.ModifyFrame(self.clip, self.clip, disable_rff)
+ self.flags = [
+ flag
+ for i, f in enumerate(self.flags)
+ for flag in [dict(f, rff=False)] * (2 if i in pf else 1)
+ ]
+
+ self.vfr = False
+ return self
+
+ def floor(self, cycle: int = None, offsets: list[int] = None) -> MPGG:
+ """
+ VFR to CFR by decimating interlaced sections to match progressive sections.
+
+ Warning: Do not use this function if it causes the duration to change. Do not use
+ this function if you used recover() or VFM. If duplicate frames are in an
+ irregular pattern then the constant cycle and pattern decimation method of
+ this function will result in an incorrect duration with audio and subtitle
+ desyncing. This should only be used on a clean source where any software or
+ hard pulldown is in a consistent pattern. If the duration is changing, use
+ VDecimate instead.
+
+ Parameters:
+ cycle: Defaults to the pulldown cycle.
+ offsets: Defaults to keeping the last frame of each cycle.
+ """
+ cycle = cycle or self.pulldown
+ if cycle:
+ offsets = offsets
+ if offsets is None:
+ offsets = list(range(cycle - 1))
+ if not offsets or len(offsets) >= cycle:
+ raise ValueError("Invalid offsets, cannot be empty or have >= items of cycle")
+
+ wanted_fps_num = self.clip.fps.numerator - (self.clip.fps.numerator / cycle)
+ progressive_sections = group_numbers([n for n, f in enumerate(self.flags) if f["progressive_frame"]])
+ interlaced_sections = group_numbers([n for n, f in enumerate(self.flags) if not f["progressive_frame"]])
+
+ self.clip = core.std.Splice([x for _, x in sorted(
+ [
+ (
+ x[0], # first frame # of the section, used for sorting when splicing
+ core.std.AssumeFPS(
+ self.clip[x[0]:x[-1] + 1],
+ fpsnum=wanted_fps_num,
+ fpsden=self.clip.fps.denominator
+ )
+ ) for x in progressive_sections
+ ] + [
+ (
+ x[0],
+ core.std.SelectEvery(
+ self.clip[x[0]:x[-1] + 1],
+ cycle,
+ offsets
+ )
+ ) for x in interlaced_sections
+ ],
+ key=lambda section: int(section[0])
+ )])
+ interlaced_frames = [
+ n
+ for s in interlaced_sections
+ for n in list_select_every(s, cycle, offsets, inverse=True)
+ ]
+ self.flags = [f for i, f in enumerate(self.flags) if i not in interlaced_frames]
+ self.vfr = False
+ return self
+
+ @staticmethod
+ def _stamp_frames(clip: vs.VideoNode, flags: list[dict]) -> vs.VideoNode:
+ """Stamp frames with prop data that may be needed."""
+
+ def _set_flag_props(n, f, c, fl):
+ for key, value in fl[n].items():
+ if isinstance(value, bool):
+ value = 1 if value else 0
+ if isinstance(value, bytes):
+ value = value.decode("utf-8")
+ c = core.std.SetFrameProp(c, **{
+ ("intval" if isinstance(value, int) else "data"): value
+ }, prop="PVSFlag%s" % key.title().replace("_", ""))
+ return c[n]
+
+ vob_indexes = [v for _, v in {f["vob"]: n for n, f in enumerate(flags)}.items()]
+ vob_indexes = [
+ "%s-%s" % ((0 if n == 0 else (vob_indexes[n - 1] + 1)), i)
+ for n, i in enumerate(vob_indexes)
+ ]
+ clip = core.std.SetFrameProp(clip, prop="PVSVobIdIndexes", data=" ".join(vob_indexes))
+
+ return core.std.FrameEval(
+ clip,
+ functools.partial(
+ _set_flag_props,
+ c=clip,
+ fl=flags
+ ),
+ prop_src=clip
+ )
+
+ @staticmethod
+ def _get_flags(d2v: D2V) -> list[dict]:
+ """Get Flag Data from D2V object."""
+ return [
+ dict(**flag, vob=d["vob"], cell=d["cell"])
+ for d in d2v.data
+ for flag in d["flags"]
+ ]
+
+ @staticmethod
+ def _get_pulldown(flags: list[dict]) -> tuple[int, Optional[str]]:
+ """
+ Get most commonly used Pulldown cycle and syntax string.
+ Returns tuple (pulldown, cycle), or (0, None) if Pulldown is not used.
+ """
+ # TODO: Find a safe way to get cycle, i.e. not resort to most common digit.
+ # Previously I would do this code on all progressive rff indexes, but when it entered and
+ # exited interlaced sections the right index vs left index were very far apart, messing up
+ # the accuracy of the cycles. I cannot find out why my test source (Family Guy S01E01 USA
+ # NTSC) is still having random different numbers in each (now progressive only) sections.
+ sections = [
+ section
+ for split in split_at(
+ [dict(x, i=n) for n, x in enumerate(flags)],
+ lambda flag: not flag["progressive_frame"]
+ )
+ for section in [[flag["i"] for flag in split if flag["rff"] and flag["tff"]]]
+ if section and len(section) > 1
+ ]
+ if not sections:
+ return 0, None
+
+ cycle = Counter([
+ Counter([
+ (right - left)
+ for left, right in zip(indexes[::2], indexes[1::2])
+ ]).most_common(1)[0][0]
+ for indexes in sections
+ ]).most_common(1)[0][0] + 1
+
+ pulldown = ["2"] * math.floor(cycle / 2)
+ if cycle % 2:
+ pulldown.pop()
+ pulldown.append("3")
+
+ return cycle, ":".join(pulldown)
+
+
+__ALL__ = (MPGG,)
diff --git a/mpgg/utilities.py b/mpgg/utilities.py
new file mode 100644
index 0000000..bbe02bf
--- /dev/null
+++ b/mpgg/utilities.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+import math
+from itertools import groupby
+from typing import Any, Union
+
+
+def get_aspect_ratio(width: int, height: int) -> str:
+ """Calculate the aspect-ratio gcd string from resolution."""
+ r = math.gcd(width, height)
+ return "%d:%d" % (int(width / r), int(height / r))
+
+
+def get_par(width: int, height: int, aspect_ratio_w: int, aspect_ratio_h: int) -> str:
+ """Calculate the pixel-aspect-ratio string from resolution."""
+ par_w = height * aspect_ratio_w
+ par_h = width * aspect_ratio_h
+ par_gcd = math.gcd(par_w, par_h)
+ par_w = int(par_w / par_gcd)
+ par_h = int(par_h / par_gcd)
+ return "%d:%d" % (par_w, par_h)
+
+
+def get_standard(aspect: float) -> str:
+ """Convert an aspect float to a standard string."""
+ return {
+ 0: "?",
+ 24 / 1: "FILM",
+ 25 / 1: "PAL",
+ 50 / 1: "PALi",
+ 30000 / 1001: "NTSC",
+ 60000 / 1001: "NTSCi",
+ 24000 / 1001: "NTSC (FILM)"
+ }[aspect]
+
+
+def group_numbers(numbers: list[int]) -> list[list[int]]:
+ """
+ Group consecutive numbers into sub-lists.
+
+ Note: It does not pre-sort the input numbers.
+
+ For example:
+ >>> group_numbers([1, 2, 3, 5, 6, 7, 9])
+ [[1, 2, 3], [5, 6, 7], [9]]
+
+ Parameters:
+ numbers: list of numbers to group.
+ """
+ for k, g in groupby(enumerate(numbers), lambda x: x[0] - x[1]):
+ yield list(map(lambda x: x[1], g))
+
+
+def list_select_every(data: list[Any], cycle: int, offsets: list[int], inverse: Union[bool, int] = False) -> list[Any]:
+ """
+ VapourSynth's SelectEvery for generic list data, and inverse.
+
+ Parameters:
+ data: data to select entries from.
+ cycle: number of entries to assess at a time.
+ offsets: offsets of entries to take per cycle (zero-indexed).
+ inverse: invert the offsets and take the entries it did not want.
+ """
+ if not isinstance(cycle, int) or cycle < 1:
+ raise ValueError("Cycle must be an int greater than or equal to 1.")
+
+ if not isinstance(offsets, list):
+ raise TypeError(f"Expected offsets to be a {list!r}, not {offsets}")
+ if not offsets:
+ raise ValueError("Offsets must not be empty.")
+ if any(not isinstance(x, int) for x in offsets):
+ raise TypeError(f"Expected offsets to be a {list!r} of {int!r}, not {offsets}")
+
+ if not isinstance(inverse, (bool, int)) or (isinstance(inverse, int) and inverse not in (0, 1)):
+ raise TypeError(f"Expected inverse to be a {bool!r} or boolean-like {int!r}, not {inverse}")
+
+ # TODO: Should this be removed to allow duplicates?
+ offsets = set(offsets)
+
+ return [
+ x
+ for n, x in enumerate(data)
+ if (n % cycle in offsets) ^ inverse
+ ]
+
+
+__ALL__ = (get_aspect_ratio, get_par, get_standard, group_numbers, list_select_every)
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..6b8ad83
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,427 @@
+# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+files = [
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.3"
+description = "Composable command line interface toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "construct"
+version = "2.8.8"
+description = "A powerful declarative parser/builder for binary data"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"},
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
+ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
+]
+
+[[package]]
+name = "filelock"
+version = "3.10.7"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "filelock-3.10.7-py3-none-any.whl", hash = "sha256:bde48477b15fde2c7e5a0713cbe72721cb5a5ad32ee0b8f419907960b9d75536"},
+ {file = "filelock-3.10.7.tar.gz", hash = "sha256:892be14aa8efc01673b5ed6589dbccb95f9a8596f0507e232626155495c18105"},
+]
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "identify"
+version = "2.5.22"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "identify-2.5.22-py2.py3-none-any.whl", hash = "sha256:f0faad595a4687053669c112004178149f6c326db71ee999ae4636685753ad2f"},
+ {file = "identify-2.5.22.tar.gz", hash = "sha256:f7a93d6cf98e29bd07663c60728e7a4057615068d7a639d132dc883b2d54d31e"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "isort"
+version = "5.12.0"
+description = "A Python utility / library to sort Python imports."
+category = "dev"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
+ {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.3)"]
+pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
+plugins = ["setuptools"]
+requirements-deprecated-finder = ["pip-api", "pipreqs"]
+
+[[package]]
+name = "jsonpickle"
+version = "2.2.0"
+description = "Python library for serializing any arbitrary object graph into JSON"
+category = "main"
+optional = false
+python-versions = ">=2.7"
+files = [
+ {file = "jsonpickle-2.2.0-py2.py3-none-any.whl", hash = "sha256:de7f2613818aa4f234138ca11243d6359ff83ae528b2185efdd474f62bcf9ae1"},
+ {file = "jsonpickle-2.2.0.tar.gz", hash = "sha256:7b272918b0554182e53dc340ddd62d9b7f902fec7e7b05620c04f3ccef479a0e"},
+]
+
+[package.extras]
+docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
+testing = ["ecdsa", "enum34", "feedparser", "jsonlib", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (<1.1.0)", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"]
+testing-libs = ["simplejson", "ujson", "yajl"]
+
+[[package]]
+name = "more-itertools"
+version = "9.1.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"},
+ {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"},
+]
+
+[[package]]
+name = "mypy"
+version = "1.1.1"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"},
+ {file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"},
+ {file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"},
+ {file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"},
+ {file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"},
+ {file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"},
+ {file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"},
+ {file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"},
+ {file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"},
+ {file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"},
+ {file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"},
+ {file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"},
+ {file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"},
+ {file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"},
+ {file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"},
+ {file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"},
+ {file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"},
+ {file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"},
+ {file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"},
+ {file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"},
+ {file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"},
+ {file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"},
+ {file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"},
+ {file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"},
+ {file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"},
+ {file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.7.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
+ {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "platformdirs"
+version = "3.2.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"},
+ {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"},
+]
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pre-commit"
+version = "3.2.1"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pre_commit-3.2.1-py2.py3-none-any.whl", hash = "sha256:a06a7fcce7f420047a71213c175714216498b49ebc81fe106f7716ca265f5bb6"},
+ {file = "pre_commit-3.2.1.tar.gz", hash = "sha256:b5aee7d75dbba21ee161ba641b01e7ae10c5b91967ebf7b2ab0dfae12d07e1f1"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pyd2v"
+version = "1.3.0"
+description = "A Python Parser for DGMPGDec's D2V Project Files."
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+files = [
+ {file = "pyd2v-1.3.0-py3-none-any.whl", hash = "sha256:813743387bd2d201ac05ee1824d0c042f1adf00e394dc2e540d05d48c7e87b66"},
+ {file = "pyd2v-1.3.0.tar.gz", hash = "sha256:0135fd83ef4843aedeae9e9dbd68440538b2003b4e51fda90b0588dba6af1140"},
+]
+
+[package.dependencies]
+click = ">=8.0.1,<9.0.0"
+jsonpickle = ">=2.0.0,<3.0.0"
+
+[[package]]
+name = "pymediainfo"
+version = "6.0.1"
+description = "A Python wrapper for the mediainfo library."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pymediainfo-6.0.1-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:81165e895e1e362fa11c128ce2bc976cb8a74224f96f309a88ee047106041b0a"},
+ {file = "pymediainfo-6.0.1-py3-none-win32.whl", hash = "sha256:bb3a48ac9706626fd2fa7881f4271728459a1c9a082917deb0c7dd343d8a1be5"},
+ {file = "pymediainfo-6.0.1-py3-none-win_amd64.whl", hash = "sha256:c38e79d4d2062732ae555b564c3cac18a6de4f36e033066c617f386cf5e77564"},
+ {file = "pymediainfo-6.0.1.tar.gz", hash = "sha256:96e04bac0dfcb726bed70c314b1219121c4b9344c66a98f426ce27d7f9abffb0"},
+]
+
+[[package]]
+name = "pymp4"
+version = "1.3.2"
+description = "A Python parser for MP4 boxes"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pymp4-1.3.2-py2.py3-none-any.whl", hash = "sha256:8bb9d761181fcc5513fa89e0f458855e1a62d56a4ff0139a46b8b377a9e13029"},
+ {file = "pymp4-1.3.2.tar.gz", hash = "sha256:a87cf9adbb9cd9db0f3bd08aeb74fb84356274d6380429630c440d0ae68bd2d5"},
+]
+
+[package.dependencies]
+construct = "2.8.8"
+
+[[package]]
+name = "pyyaml"
+version = "6.0"
+description = "YAML parser and emitter for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
+ {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
+ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
+ {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
+ {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
+ {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
+ {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
+ {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
+ {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
+ {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
+ {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
+ {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
+ {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
+ {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
+ {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
+ {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
+ {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
+ {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
+ {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
+ {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
+ {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
+ {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
+ {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
+ {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
+ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
+ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+]
+
+[[package]]
+name = "setuptools"
+version = "67.6.1"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"},
+ {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.5.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
+ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+]
+
+[[package]]
+name = "vapoursynth"
+version = "61"
+description = "A frameserver for the 21st century"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "VapourSynth-61-cp310-cp310-win32.whl", hash = "sha256:4f0d74a626f9ac7da79c016eeacbda41d97065462b01ca7d34d794367c278294"},
+ {file = "VapourSynth-61-cp310-cp310-win_amd64.whl", hash = "sha256:6f3b7c668031840c8612c2e3cf40f7aad35f5f04dc34b4ac76bd221dc2abca4e"},
+ {file = "VapourSynth-61-cp38-cp38-win32.whl", hash = "sha256:a1a7465bafd8727a599a4687a836a19bb9712bd91f29155301c196f03a1a8b00"},
+ {file = "VapourSynth-61-cp38-cp38-win_amd64.whl", hash = "sha256:1ff652f070418afddf635c348dc3595ea4db57ceefcea0d53db2701450af7ec2"},
+ {file = "VapourSynth-61.zip", hash = "sha256:9b0f7f53008ab7a7495ca4e3d8402c8049be279d562a4aef3977ce62b191277c"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.21.0"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"},
+ {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.4.1,<4"
+platformdirs = ">=2.4,<4"
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.8 || ^3.10"
+content-hash = "27e899b68b179502c2aae47d2be70c7e2f76cc63af7fe45c138d1bea0a5b950f"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..35e6f0e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,46 @@
+[build-system]
+requires = ['poetry-core>=1.0.0']
+build-backend = 'poetry.core.masonry.api'
+
+[tool.poetry]
+name = 'mpgg'
+version = '1.0.0'
+description = "Streamlined MPEG-1 and MPEG-2 source loader and helper utility for VapourSynth"
+license = 'GPL-3.0-only'
+authors = ['rlaphoenix ']
+readme = 'README.md'
+homepage = 'https://github.com/rlaphoenix/mpgg'
+repository = 'https://github.com/rlaphoenix/mpgg'
+keywords = ['vapoursynth', 'dvd', 'mpeg', 'mpeg2']
+classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Other Environment',
+ 'Intended Audience :: End Users/Desktop',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Topic :: Multimedia :: Video',
+]
+
+[tool.poetry.dependencies]
+python = "^3.8 || ^3.10"
+pymp4 = "^1.2.0"
+pyd2v = "^1.3.0"
+vapoursynth = ">=50"
+more-itertools = "^9.1.0"
+pymediainfo = "^6.0.1"
+
+[tool.poetry.dev-dependencies]
+pre-commit = "^3.2.1"
+mypy = "^1.1.1"
+isort = "^5.12.0"
+
+[tool.isort]
+line_length = 120
+
+[tool.mypy]
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_untyped_defs = true
+follow_imports = 'silent'
+ignore_missing_imports = true
+no_implicit_optional = true