feat: Add support for MKV Attachments via Attachment class

You add these new Attachment objects to the Tracks object just like you would with Video, Audio, and Subtitle objects.
This commit is contained in:
rlaphoenix 2024-03-25 05:38:32 +00:00
parent a51e1b4f3c
commit 057e4efb56
3 changed files with 117 additions and 10 deletions

View File

@ -632,7 +632,7 @@ class dl:
task_id = progress.add_task(f"{task_description}...", total=None, start=False)
task_tracks = Tracks(title.tracks) + title.tracks.chapters
task_tracks = Tracks(title.tracks) + title.tracks.chapters + title.tracks.attachments
if video_track:
task_tracks.videos = [video_track]

View File

@ -0,0 +1,70 @@
from __future__ import annotations
import mimetypes
from pathlib import Path
from typing import Optional, Union
from zlib import crc32
class Attachment:
def __init__(
self,
path: Union[Path, str],
name: Optional[str] = None,
mime_type: Optional[str] = None,
description: Optional[str] = None
):
"""
Create a new Attachment.
If name is not provided it will use the file name (without extension).
If mime_type is not provided, it will try to guess it.
"""
if not isinstance(path, (str, Path)):
raise ValueError("The attachment path must be provided.")
if not isinstance(name, (str, type(None))):
raise ValueError("The attachment name must be provided.")
path = Path(path)
if not path.exists():
raise ValueError("The attachment file does not exist.")
name = (name or path.stem).strip()
mime_type = (mime_type or "").strip() or None
description = (description or "").strip() or None
if not mime_type:
mime_type = {
".ttf": "application/x-truetype-font",
".otf": "application/vnd.ms-opentype"
}.get(path.suffix, mimetypes.guess_type(path)[0])
if not mime_type:
raise ValueError("The attachment mime-type could not be automatically detected.")
self.path = path
self.name = name
self.mime_type = mime_type
self.description = description
def __repr__(self) -> str:
return "{name}({items})".format(
name=self.__class__.__name__,
items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()])
)
def __str__(self) -> str:
return " | ".join(filter(bool, [
"ATT",
self.name,
self.mime_type,
self.description
]))
@property
def id(self) -> str:
"""Compute an ID from the attachment data."""
checksum = crc32(self.path.read_bytes())
return hex(checksum)
__all__ = ("Attachment",)

View File

@ -14,6 +14,7 @@ from rich.tree import Tree
from devine.core.config import config
from devine.core.console import console
from devine.core.constants import LANGUAGE_MAX_DISTANCE, AnyTrack, TrackT
from devine.core.tracks.attachment import Attachment
from devine.core.tracks.audio import Audio
from devine.core.tracks.chapters import Chapter, Chapters
from devine.core.tracks.subtitle import Subtitle
@ -25,7 +26,7 @@ from devine.core.utils.collections import as_list, flatten
class Tracks:
"""
Video, Audio, Subtitle, and Chapter Track Store.
Video, Audio, Subtitle, Chapter, and Attachment Track Store.
It provides convenience functions for listing, sorting, and selecting tracks.
"""
@ -33,14 +34,23 @@ class Tracks:
Video: 0,
Audio: 1,
Subtitle: 2,
Chapter: 3
Chapter: 3,
Attachment: 4
}
def __init__(self, *args: Union[Tracks, Sequence[Union[AnyTrack, Chapter, Chapters]], Track, Chapter, Chapters]):
def __init__(self, *args: Union[
Tracks,
Sequence[Union[AnyTrack, Chapter, Chapters, Attachment]],
Track,
Chapter,
Chapters,
Attachment
]):
self.videos: list[Video] = []
self.audio: list[Audio] = []
self.subtitles: list[Subtitle] = []
self.chapters = Chapters()
self.attachments: list[Attachment] = []
if args:
self.add(args)
@ -53,7 +63,14 @@ class Tracks:
def __add__(
self,
other: Union[Tracks, Sequence[Union[AnyTrack, Chapter, Chapters]], Track, Chapter, Chapters]
other: Union[
Tracks,
Sequence[Union[AnyTrack, Chapter, Chapters, Attachment]],
Track,
Chapter,
Chapters,
Attachment
]
) -> Tracks:
self.add(other)
return self
@ -69,7 +86,8 @@ class Tracks:
Video: [],
Audio: [],
Subtitle: [],
Chapter: []
Chapter: [],
Attachment: []
}
tracks = [*list(self), *self.chapters]
@ -98,7 +116,7 @@ class Tracks:
return rep
def tree(self, add_progress: bool = False) -> tuple[Tree, list[partial]]:
all_tracks = [*list(self), *self.chapters]
all_tracks = [*list(self), *self.chapters, *self.attachments]
progress_callables = []
@ -111,7 +129,7 @@ class Tracks:
track_type_plural = track_type.__name__ + ("s" if track_type != Audio and num_tracks != 1 else "")
tracks_tree = tree.add(f"[repr.number]{num_tracks}[/] {track_type_plural}")
for track in tracks:
if add_progress and track_type != Chapter:
if add_progress and track_type not in (Chapter, Attachment):
progress = Progress(
SpinnerColumn(finished_text=""),
BarColumn(),
@ -143,12 +161,19 @@ class Tracks:
def add(
self,
tracks: Union[Tracks, Sequence[Union[AnyTrack, Chapter, Chapters]], Track, Chapter, Chapters],
tracks: Union[
Tracks,
Sequence[Union[AnyTrack, Chapter, Chapters, Attachment]],
Track,
Chapter,
Chapters,
Attachment
],
warn_only: bool = False
) -> None:
"""Add a provided track to its appropriate array and ensuring it's not a duplicate."""
if isinstance(tracks, Tracks):
tracks = [*list(tracks), *tracks.chapters]
tracks = [*list(tracks), *tracks.chapters, *tracks.attachments]
duplicates = 0
for track in flatten(tracks):
@ -173,6 +198,8 @@ class Tracks:
self.subtitles.append(track)
elif isinstance(track, Chapter):
self.chapters.add(track)
elif isinstance(track, Attachment):
self.attachments.append(track)
else:
raise ValueError("Track type was not set or is invalid.")
@ -363,6 +390,16 @@ class Tracks:
else:
chapters_path = None
for attachment in self.attachments:
if not attachment.path or not attachment.path.exists():
raise ValueError("Attachment File was not found...")
cl.extend([
"--attachment-description", attachment.description or "",
"--attachment-mime-type", attachment.mime_type,
"--attachment-name", attachment.name,
"--attach-file", str(attachment.path.resolve())
])
output_path = (
self.videos[0].path.with_suffix(".muxed.mkv") if self.videos else
self.audio[0].path.with_suffix(".muxed.mka") if self.audio else