[FEATURE] Add split_by_chapters plugin (#184)
This commit is contained in:
parent
c443d2e295
commit
790c65859a
28 changed files with 1281 additions and 62 deletions
|
|
@ -219,6 +219,14 @@ regex
|
|||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
split_by_chapters
|
||||
'''''''''''''''''
|
||||
.. autoclass:: ytdl_sub.plugins.split_by_chapters.SplitByChaptersOptions()
|
||||
:members: when_no_chapters
|
||||
:member-order: bysource
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
subtitles
|
||||
'''''''''
|
||||
.. autoclass:: ytdl_sub.plugins.subtitles.SubtitleOptions()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ presets:
|
|||
|
||||
output_options:
|
||||
output_directory: "{music_directory}"
|
||||
file_name: "{title_sanitized}.{ext}"
|
||||
file_name: "{custom_track_name_sanitized}.{ext}"
|
||||
|
||||
audio_extract:
|
||||
codec: "mp3"
|
||||
|
|
@ -17,22 +17,42 @@ presets:
|
|||
|
||||
music_tags:
|
||||
tags:
|
||||
artist: "{artist}"
|
||||
albumartist: "{artist}"
|
||||
title: "{title}"
|
||||
album: "Singles"
|
||||
track: "1"
|
||||
artist: "{custom_artist_name}"
|
||||
albumartist: "{custom_artist_name}"
|
||||
title: "{custom_track_name}"
|
||||
album: "{custom_album_name}"
|
||||
track: "{custom_track_number}"
|
||||
year: "{upload_year}"
|
||||
genre: "Unset"
|
||||
|
||||
overrides:
|
||||
music_directory: "/path/to/music"
|
||||
custom_track_name: "{title}"
|
||||
custom_album_name: "Singles"
|
||||
custom_artist_name: "{artist}"
|
||||
custom_track_number: "1"
|
||||
|
||||
# TODO: make a playlist of individual songs into an album. Need playlist_title
|
||||
yt_song_playlist:
|
||||
preset: yt_song
|
||||
youtube:
|
||||
download_strategy: "playlist"
|
||||
overrides:
|
||||
custom_track_number: "{playlist_index}"
|
||||
|
||||
music_tags:
|
||||
tags:
|
||||
track: "{playlist_index}"
|
||||
yt_album_as_chapters:
|
||||
preset: "yt_song"
|
||||
output_options:
|
||||
file_name: "{custom_album_name_sanitized}/{chapter_index_padded} - {custom_track_name_sanitized}.{ext}"
|
||||
thumbnail_name: "{custom_album_name_sanitized}/folder.{thumbnail_ext}"
|
||||
|
||||
chapters:
|
||||
embed_chapters: True
|
||||
|
||||
split_by_chapters:
|
||||
when_no_chapters: "pass"
|
||||
|
||||
overrides:
|
||||
custom_track_name: "{chapter_title}"
|
||||
custom_album_name: "{title}" # Have the video name be the album
|
||||
custom_track_number: "{chapter_index}"
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@ class Preset(StrictDictValidator):
|
|||
return downloader, download_options
|
||||
|
||||
def __validate_and_get_plugins(self) -> PresetPlugins:
|
||||
plugins = PresetPlugins()
|
||||
preset_plugins = PresetPlugins()
|
||||
source_variables = copy.deepcopy(self._source_variables)
|
||||
|
||||
for key in self._keys:
|
||||
if key not in PluginMapping.plugins():
|
||||
|
|
@ -168,13 +169,22 @@ class Preset(StrictDictValidator):
|
|||
|
||||
plugin = PluginMapping.get(plugin=key)
|
||||
plugin_options = self._validate_key(key=key, validator=plugin.plugin_options_type)
|
||||
|
||||
preset_plugins.add(plugin_type=plugin, plugin_options=plugin_options)
|
||||
|
||||
for plugin, plugin_options in sorted(
|
||||
preset_plugins.zipped(),
|
||||
key=lambda _plugin_and_options: _plugin_and_options[0].priority.modify_entry,
|
||||
):
|
||||
# Validate current plugin using source + added plugin variables
|
||||
plugin_options.validate_with_variables(
|
||||
source_variables=self._source_variables, override_variables=self.overrides.keys
|
||||
source_variables=source_variables, override_variables=self.overrides.keys
|
||||
)
|
||||
|
||||
plugins.add(plugin_type=plugin, plugin_options=plugin_options)
|
||||
# Extend existing source variables with ones created from this plugin
|
||||
source_variables.extend(plugin_options.added_source_variables())
|
||||
|
||||
return plugins
|
||||
return preset_plugins
|
||||
|
||||
def __validate_override_string_formatter_validator(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin
|
|||
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
|
||||
from ytdl_sub.plugins.plugin import Plugin
|
||||
from ytdl_sub.plugins.regex import RegexPlugin
|
||||
from ytdl_sub.plugins.split_by_chapters import SplitByChaptersPlugin
|
||||
from ytdl_sub.plugins.subtitles import SubtitlesPlugin
|
||||
from ytdl_sub.plugins.video_tags import VideoTagsPlugin
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ class PluginMapping:
|
|||
"regex": RegexPlugin,
|
||||
"subtitles": SubtitlesPlugin,
|
||||
"chapters": ChaptersPlugin,
|
||||
"split_by_chapters": SplitByChaptersPlugin,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader
|
|||
from ytdl_sub.downloaders.youtube.video import YoutubeVideoDownloaderOptions
|
||||
from ytdl_sub.entries.youtube import YoutubePlaylistVideo
|
||||
from ytdl_sub.entries.youtube import YoutubeVideo
|
||||
from ytdl_sub.plugins.split_by_chapters import _split_video_ffmpeg_cmd
|
||||
from ytdl_sub.utils.chapters import Chapters
|
||||
from ytdl_sub.utils.chapters import Timestamp
|
||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||
|
|
@ -31,19 +32,6 @@ def _split_video_uid(source_uid: str, idx: int) -> str:
|
|||
return f"{source_uid}___{idx}"
|
||||
|
||||
|
||||
def _split_video_ffmpeg_cmd(
|
||||
input_file: str, output_file: str, timestamps: List[Timestamp], idx: int
|
||||
) -> List[str]:
|
||||
timestamp_begin = timestamps[idx].standardized_str
|
||||
timestamp_end = timestamps[idx + 1].standardized_str if idx + 1 < len(timestamps) else ""
|
||||
|
||||
cmd = ["-i", input_file, "-ss", timestamp_begin]
|
||||
if timestamp_end:
|
||||
cmd += ["-to", timestamp_end]
|
||||
cmd += ["-vcodec", "copy", "-acodec", "copy", output_file]
|
||||
return cmd
|
||||
|
||||
|
||||
class YoutubeSplitVideoDownloaderOptions(YoutubeVideoDownloaderOptions):
|
||||
r"""
|
||||
Downloads a single youtube video, then splits in to separate videos using a file containing
|
||||
|
|
@ -158,7 +146,9 @@ class YoutubeSplitVideoDownloader(
|
|||
"""Download a single Youtube video, then split it into multiple videos"""
|
||||
split_videos_and_metadata: List[Tuple[YoutubePlaylistVideo, FileMetadata]] = []
|
||||
|
||||
chapters = Chapters.from_file(chapters_file_path=self.download_options.split_timestamps)
|
||||
chapters = Chapters.from_timestamps_file(
|
||||
chapters_file_path=self.download_options.split_timestamps
|
||||
)
|
||||
entry_dict = self.extract_info(url=self.download_options.video_url)
|
||||
|
||||
entry = YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions, Yo
|
|||
return [video]
|
||||
|
||||
# Otherwise, add the chapters and return the video + chapter metadata
|
||||
chapters = Chapters.from_file(chapters_file_path=self.download_options.chapter_timestamps)
|
||||
chapters = Chapters.from_timestamps_file(
|
||||
chapters_file_path=self.download_options.chapter_timestamps
|
||||
)
|
||||
if not self.is_dry_run:
|
||||
set_ffmpeg_metadata_chapters(
|
||||
file_path=video.get_download_file_path(),
|
||||
|
|
|
|||
|
|
@ -30,16 +30,20 @@ SPONSORBLOCK_CATEGORIES: Set[str] = SPONSORBLOCK_HIGHLIGHT_CATEGORIES | {
|
|||
|
||||
def _chapters(entry: Entry) -> List[Dict]:
|
||||
if entry.kwargs_contains("chapters"):
|
||||
return entry.kwargs("chapters")
|
||||
return entry.kwargs("chapters") or []
|
||||
return []
|
||||
|
||||
|
||||
def _sponsorblock_chapters(entry: Entry) -> List[Dict]:
|
||||
if entry.kwargs_contains("sponsorblock_chapters"):
|
||||
return entry.kwargs("sponsorblock_chapters")
|
||||
return entry.kwargs("sponsorblock_chapters") or []
|
||||
return []
|
||||
|
||||
|
||||
def _contains_any_chapters(entry: Entry) -> bool:
|
||||
return len(_chapters(entry)) > 0 or len(_sponsorblock_chapters(entry)) > 0
|
||||
|
||||
|
||||
class SponsorBlockCategoriesValidator(StringSelectValidator):
|
||||
_expected_value_type_name = "sponsorblock category"
|
||||
_select_values = {"all"} | SPONSORBLOCK_CATEGORIES
|
||||
|
|
@ -274,11 +278,16 @@ class ChaptersPlugin(Plugin[ChaptersOptions]):
|
|||
removed_chapters = self._get_removed_chapters(entry)
|
||||
removed_sponsorblock = self._get_removed_sponsorblock_category_counts(entry)
|
||||
|
||||
# If no chapters are on the entry, do not report any embedded chapters
|
||||
if not _contains_any_chapters(entry):
|
||||
return None
|
||||
|
||||
if removed_chapters:
|
||||
metadata_dict["Removed Chapter(s)"] = ", ".join(removed_chapters)
|
||||
if removed_sponsorblock:
|
||||
metadata_dict["Removed SponsorBlock Category Count(s)"] = removed_sponsorblock
|
||||
|
||||
# TODO: check if file actually has embedded chapters
|
||||
return FileMetadata.from_dict(
|
||||
value_dict=metadata_dict, title="Embedded Chapters", sort_dict=False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from typing import Dict
|
|||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import final
|
||||
|
|
@ -16,6 +17,28 @@ from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
|||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
|
||||
class PluginPriority:
|
||||
"""
|
||||
Defines priority for plugins, 0 is highest priority
|
||||
"""
|
||||
|
||||
# If modify_entry priority is >= to this value, run after split
|
||||
MODIFY_ENTRY_AFTER_SPLIT = 10
|
||||
|
||||
def __init__(self, modify_entry: int = 0, post_process: int = 0):
|
||||
self.modify_entry = modify_entry
|
||||
self.post_process = post_process
|
||||
|
||||
@property
|
||||
def modify_entry_after_split(self) -> bool:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
True if the plugin should modify an entry after a potential split. False otherwise.
|
||||
"""
|
||||
return self.modify_entry >= PluginPriority.MODIFY_ENTRY_AFTER_SPLIT
|
||||
|
||||
|
||||
class PluginOptions(StrictDictValidator):
|
||||
"""
|
||||
Class that defines the parameters to a plugin
|
||||
|
|
@ -60,6 +83,10 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
|||
"""
|
||||
|
||||
plugin_options_type: Type[PluginOptionsT] = NotImplemented
|
||||
priority: PluginPriority = PluginPriority()
|
||||
|
||||
# If the plugin creates multile entries from a single entry
|
||||
is_split_plugin: bool = False
|
||||
|
||||
@final
|
||||
def __init__(
|
||||
|
|
@ -74,6 +101,7 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
|||
# TODO pass yaml snake case name in the class somewhere, and use it for the logger
|
||||
self._logger = Logger.get(self.__class__.__name__)
|
||||
|
||||
# pylint: disable=no-self-use,unused-argument
|
||||
def ytdl_options(self) -> Optional[Dict]:
|
||||
"""
|
||||
Returns
|
||||
|
|
@ -81,7 +109,22 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
|||
ytdl options to enable/disable when downloading entries for this specific plugin
|
||||
"""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def split(self, entry: Entry) -> List[Tuple[Entry, FileMetadata]]:
|
||||
"""
|
||||
Very specialized function that takes an entry and creates multiple entries from it.
|
||||
Should mark ``is_split_plugin`` on the plugin class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
entry
|
||||
Entry to create multiple entries from
|
||||
|
||||
Returns
|
||||
-------
|
||||
List of entries and metadata created from the source entry
|
||||
"""
|
||||
return []
|
||||
|
||||
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||
"""
|
||||
For each entry downloaded, modify the entry in some way before sending it to
|
||||
|
|
@ -98,8 +141,6 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
|||
"""
|
||||
return entry
|
||||
|
||||
# pylint: enable=no-self-use
|
||||
|
||||
def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]:
|
||||
"""
|
||||
For each entry downloaded, apply post processing to it.
|
||||
|
|
@ -113,6 +154,9 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
|||
-------
|
||||
Optional file metadata for the entry media file.
|
||||
"""
|
||||
return None
|
||||
|
||||
# pylint: enable=no-self-use,unused-argument
|
||||
|
||||
def post_process_subscription(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from yt_dlp.utils import sanitize_filename
|
|||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.plugins.plugin import Plugin
|
||||
from ytdl_sub.plugins.plugin import PluginOptions
|
||||
from ytdl_sub.plugins.plugin import PluginPriority
|
||||
from ytdl_sub.utils.exceptions import RegexNoMatchException
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
from ytdl_sub.validators.regex_validator import RegexListValidator
|
||||
|
|
@ -224,6 +225,9 @@ class RegexOptions(PluginOptions):
|
|||
|
||||
class RegexPlugin(Plugin[RegexOptions]):
|
||||
plugin_options_type = RegexOptions
|
||||
priority = PluginPriority(
|
||||
modify_entry=PluginPriority.MODIFY_ENTRY_AFTER_SPLIT + 0,
|
||||
)
|
||||
|
||||
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||
"""
|
||||
|
|
|
|||
217
src/ytdl_sub/plugins/split_by_chapters.py
Normal file
217
src/ytdl_sub/plugins/split_by_chapters.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import copy
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.plugins.plugin import Plugin
|
||||
from ytdl_sub.plugins.plugin import PluginOptions
|
||||
from ytdl_sub.utils.chapters import Chapters
|
||||
from ytdl_sub.utils.chapters import Timestamp
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
|
||||
from ytdl_sub.validators.string_select_validator import StringSelectValidator
|
||||
|
||||
|
||||
def _split_video_ffmpeg_cmd(
|
||||
input_file: str, output_file: str, timestamps: List[Timestamp], idx: int
|
||||
) -> List[str]:
|
||||
timestamp_begin = timestamps[idx].standardized_str
|
||||
timestamp_end = timestamps[idx + 1].standardized_str if idx + 1 < len(timestamps) else ""
|
||||
|
||||
cmd = ["-i", input_file, "-ss", timestamp_begin]
|
||||
if timestamp_end:
|
||||
cmd += ["-to", timestamp_end]
|
||||
cmd += ["-vcodec", "copy", "-acodec", "copy", output_file]
|
||||
return cmd
|
||||
|
||||
|
||||
def _split_video_uid(source_uid: str, idx: int) -> str:
|
||||
return f"{source_uid}___{idx}"
|
||||
|
||||
|
||||
class WhenNoChaptersValidator(StringSelectValidator):
|
||||
_expected_value_type_name = "when no chapters option"
|
||||
_select_values = {"pass", "drop", "error"}
|
||||
|
||||
|
||||
class SplitByChaptersOptions(PluginOptions):
|
||||
"""
|
||||
Splits a file by chapters into multiple files. Each file becomes its own entry with the
|
||||
new source variables ``chapter_title``, ``chapter_title_sanitized``, ``chapter_index``,
|
||||
``chapter_index_padded``, ``chapter_count``.
|
||||
|
||||
If a file has no chapters, and ``when_no_chapters`` is set to "pass", then ``chapter_title`` is
|
||||
set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1.
|
||||
|
||||
Note that when using this plugin and performing dry-run, it assumes embedded chapters are being
|
||||
used with no modifications.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
presets:
|
||||
my_example_preset:
|
||||
split_by_chapters:
|
||||
when_no_chapters: "pass"
|
||||
"""
|
||||
|
||||
_required_keys = {"when_no_chapters"}
|
||||
|
||||
def __init__(self, name, value):
|
||||
super().__init__(name, value)
|
||||
self._when_no_chapters = self._validate_key(
|
||||
key="when_no_chapters", validator=WhenNoChaptersValidator
|
||||
).value
|
||||
|
||||
def added_source_variables(self) -> List[str]:
|
||||
return [
|
||||
"chapter_title",
|
||||
"chapter_title_sanitized",
|
||||
"chapter_index",
|
||||
"chapter_index_padded",
|
||||
"chapter_count",
|
||||
]
|
||||
|
||||
@property
|
||||
def when_no_chapters(self) -> str:
|
||||
"""
|
||||
Behavior to perform when no chapters are present. Supports "pass" (continue processing),
|
||||
"drop" (exclude it from output), and "error" (stop processing for everything).
|
||||
"""
|
||||
return self._when_no_chapters
|
||||
|
||||
|
||||
class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
||||
plugin_options_type = SplitByChaptersOptions
|
||||
is_split_plugin = True
|
||||
|
||||
def _create_split_entry(
|
||||
self, source_entry: Entry, title: str, idx: int, chapters: Chapters
|
||||
) -> Tuple[Entry, FileMetadata]:
|
||||
"""
|
||||
Runs ffmpeg to create the split video
|
||||
"""
|
||||
entry = copy.deepcopy(source_entry)
|
||||
|
||||
entry.add_variables(
|
||||
{
|
||||
"chapter_title": title,
|
||||
"chapter_title_sanitized": sanitize_filename(title),
|
||||
"chapter_index": idx + 1,
|
||||
"chapter_index_padded": f"{(idx + 1):02d}",
|
||||
"chapter_count": len(chapters.timestamps),
|
||||
}
|
||||
)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
entry._kwargs["id"] = _split_video_uid(source_uid=entry.uid, idx=idx)
|
||||
if "chapters" in entry._kwargs:
|
||||
del entry._kwargs["chapters"]
|
||||
if "sponsorblock_chapters" in entry._kwargs:
|
||||
del entry._kwargs["sponsorblock_chapters"]
|
||||
# pylint: enable=protected-access
|
||||
|
||||
timestamp_begin = chapters.timestamps[idx].readable_str
|
||||
timestamp_end = Timestamp(entry.kwargs("duration")).readable_str
|
||||
if idx + 1 < len(chapters.timestamps):
|
||||
timestamp_end = chapters.timestamps[idx + 1].readable_str
|
||||
|
||||
metadata_value_dict = {}
|
||||
if self.is_dry_run:
|
||||
metadata_value_dict[
|
||||
"Warning"
|
||||
] = "Dry-run assumes embedded chapters with no modifications"
|
||||
|
||||
metadata_value_dict["Source Title"] = entry.title
|
||||
metadata_value_dict["Segment"] = f"{timestamp_begin} - {timestamp_end}"
|
||||
|
||||
metadata = FileMetadata.from_dict(
|
||||
value_dict=metadata_value_dict,
|
||||
title="From Chapter Split:",
|
||||
sort_dict=False,
|
||||
)
|
||||
|
||||
return entry, metadata
|
||||
|
||||
def split(self, entry: Entry) -> Optional[List[Tuple[Entry, FileMetadata]]]:
|
||||
"""
|
||||
Tags the entry's audio file using values defined in the metadata options
|
||||
"""
|
||||
split_videos_and_metadata: List[Tuple[Entry, FileMetadata]] = []
|
||||
|
||||
if self.is_dry_run:
|
||||
chapters = Chapters.from_entry_chapters(entry=entry)
|
||||
else:
|
||||
chapters = Chapters.from_embedded_chapters(file_path=entry.get_download_file_path())
|
||||
|
||||
# If no chapters, do not split anything
|
||||
if not chapters.contains_any_chapters():
|
||||
if self.plugin_options.when_no_chapters == "pass":
|
||||
entry.add_variables(
|
||||
{
|
||||
"chapter_title": entry.title,
|
||||
"chapter_index": 1,
|
||||
"chapter_index_padded": "01",
|
||||
"chapter_count": 1,
|
||||
}
|
||||
)
|
||||
return [(entry, FileMetadata())]
|
||||
if self.plugin_options.when_no_chapters == "drop":
|
||||
return []
|
||||
|
||||
raise ValidationException(
|
||||
f"Tried to split '{entry.title}' by chapters but it has no chapters"
|
||||
)
|
||||
|
||||
# convert the entry thumbnail early so we do not have to guess the thumbnail extension
|
||||
# when copying it. Do not error if it's not found, in case thumbnail_name is not set
|
||||
if not self.is_dry_run:
|
||||
convert_download_thumbnail(entry=entry, error_if_not_found=False)
|
||||
|
||||
for idx, title in enumerate(chapters.titles):
|
||||
new_uid = _split_video_uid(source_uid=entry.uid, idx=idx)
|
||||
|
||||
if not self.is_dry_run:
|
||||
# Get the input/output file paths
|
||||
input_file = entry.get_download_file_path()
|
||||
output_file = str(Path(self.working_directory) / f"{new_uid}.{entry.ext}")
|
||||
|
||||
# Run ffmpeg to create the split the video
|
||||
FFMPEG.run(
|
||||
_split_video_ffmpeg_cmd(
|
||||
input_file=input_file,
|
||||
output_file=output_file,
|
||||
timestamps=chapters.timestamps,
|
||||
idx=idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Copy the original vid thumbnail to the working directory with the new uid. This so
|
||||
# downstream logic thinks this split video has its own thumbnail
|
||||
if os.path.isfile(entry.get_download_thumbnail_path()):
|
||||
FileHandler.copy(
|
||||
src_file_path=entry.get_download_thumbnail_path(),
|
||||
dst_file_path=Path(self.working_directory)
|
||||
/ f"{new_uid}.{entry.thumbnail_ext}",
|
||||
)
|
||||
|
||||
# Format the split video as a YoutubePlaylistVideo
|
||||
split_videos_and_metadata.append(
|
||||
self._create_split_entry(
|
||||
source_entry=entry,
|
||||
title=title,
|
||||
idx=idx,
|
||||
chapters=chapters,
|
||||
)
|
||||
)
|
||||
|
||||
return split_videos_and_metadata
|
||||
|
|
@ -19,12 +19,23 @@ from ytdl_sub.downloaders.downloader import DownloaderValidator
|
|||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.plugins.plugin import Plugin
|
||||
from ytdl_sub.subscriptions.subscription_ytdl_options import SubscriptionYTDLOptions
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
|
||||
def _get_split_plugin(plugins: List[Plugin]) -> Optional[Plugin]:
|
||||
split_plugins = [plugin for plugin in plugins if plugin.is_split_plugin]
|
||||
|
||||
if len(split_plugins) == 1:
|
||||
return split_plugins[0]
|
||||
if len(split_plugins) > 1:
|
||||
raise ValidationException("Can not use more than one split plugins at a time")
|
||||
return None
|
||||
|
||||
|
||||
class Subscription:
|
||||
"""
|
||||
Subscription classes are the 'controllers' that perform...
|
||||
|
|
@ -251,6 +262,74 @@ class Subscription:
|
|||
|
||||
return plugins
|
||||
|
||||
def _post_process_entry(
|
||||
self, plugins: List[Plugin], dry_run: bool, entry: Entry, entry_metadata: FileMetadata
|
||||
):
|
||||
# Post-process the entry with all plugins
|
||||
for plugin in sorted(plugins, key=lambda _plugin: _plugin.priority.post_process):
|
||||
optional_plugin_entry_metadata = plugin.post_process_entry(entry)
|
||||
if optional_plugin_entry_metadata:
|
||||
entry_metadata.extend(optional_plugin_entry_metadata)
|
||||
|
||||
# Then, move it to the output directory
|
||||
self._move_entry_files_to_output_directory(
|
||||
dry_run=dry_run, entry=entry, entry_metadata=entry_metadata
|
||||
)
|
||||
|
||||
# Re-save the download archive after each entry is moved to the output directory
|
||||
if self.maintain_download_archive:
|
||||
self._enhanced_download_archive.save_download_mappings()
|
||||
|
||||
def _process_entry(
|
||||
self, plugins: List[Plugin], dry_run: bool, entry: Entry, entry_metadata: FileMetadata
|
||||
) -> None:
|
||||
# First, modify the entry with all plugins
|
||||
for plugin in sorted(plugins, key=lambda _plugin: _plugin.priority.modify_entry):
|
||||
# Return if it is None, it is indicated to not process any further
|
||||
if (entry := plugin.modify_entry(entry)) is None:
|
||||
return
|
||||
|
||||
self._post_process_entry(
|
||||
plugins=plugins, dry_run=dry_run, entry=entry, entry_metadata=entry_metadata
|
||||
)
|
||||
|
||||
def _process_split_entry(
|
||||
self, split_plugin: Plugin, plugins: List[Plugin], dry_run: bool, entry: Entry
|
||||
) -> None:
|
||||
plugins_pre_split = sorted(
|
||||
[plugin for plugin in plugins if not plugin.priority.modify_entry_after_split],
|
||||
key=lambda _plugin: _plugin.priority.modify_entry,
|
||||
)
|
||||
|
||||
plugins_post_split = sorted(
|
||||
[plugin for plugin in plugins if plugin.priority.modify_entry_after_split],
|
||||
key=lambda _plugin: _plugin.priority.modify_entry,
|
||||
)
|
||||
|
||||
# First, modify the entry with pre_split plugins
|
||||
for plugin in plugins_pre_split:
|
||||
# Return if it is None, it is indicated to not process any further
|
||||
if (entry := plugin.modify_entry(entry)) is None:
|
||||
return
|
||||
|
||||
# Then, perform the split
|
||||
for split_entry, split_entry_metadata in split_plugin.split(entry=entry):
|
||||
|
||||
for plugin in plugins_post_split:
|
||||
# Return if it is None, it is indicated to not process any further.
|
||||
# Break out of the plugin loop
|
||||
if (split_entry := plugin.modify_entry(split_entry)) is None:
|
||||
break
|
||||
|
||||
# If split_entry is None from modify_entry, do not post process
|
||||
if split_entry:
|
||||
self._process_entry(
|
||||
plugins=plugins,
|
||||
dry_run=dry_run,
|
||||
entry=split_entry,
|
||||
entry_metadata=split_entry_metadata,
|
||||
)
|
||||
|
||||
def download(self, dry_run: bool = False) -> FileHandlerTransactionLog:
|
||||
"""
|
||||
Performs the subscription download
|
||||
|
|
@ -284,29 +363,14 @@ class Subscription:
|
|||
if isinstance(entry, tuple):
|
||||
entry, entry_metadata = entry
|
||||
|
||||
# First, modify the entry with all plugins
|
||||
for plugin in plugins:
|
||||
# Break out of this plugin loop if entry is None, it is indicated to not DL it
|
||||
if (entry := plugin.modify_entry(entry)) is None:
|
||||
break
|
||||
|
||||
# If entry is None from the broken out loop, continue over the other entries
|
||||
if entry is None:
|
||||
continue
|
||||
|
||||
# Then, post-process the entry with all plugins
|
||||
for plugin in plugins:
|
||||
optional_plugin_entry_metadata = plugin.post_process_entry(entry)
|
||||
if optional_plugin_entry_metadata:
|
||||
entry_metadata.extend(optional_plugin_entry_metadata)
|
||||
|
||||
self._move_entry_files_to_output_directory(
|
||||
dry_run=dry_run, entry=entry, entry_metadata=entry_metadata
|
||||
)
|
||||
|
||||
# Re-save the download archive after each entry is moved to the output directory
|
||||
if self.maintain_download_archive:
|
||||
self._enhanced_download_archive.save_download_mappings()
|
||||
if split_plugin := _get_split_plugin(plugins):
|
||||
self._process_split_entry(
|
||||
split_plugin=split_plugin, plugins=plugins, dry_run=dry_run, entry=entry
|
||||
)
|
||||
else:
|
||||
self._process_entry(
|
||||
plugins=plugins, dry_run=dry_run, entry=entry, entry_metadata=entry_metadata
|
||||
)
|
||||
|
||||
downloader.post_download(overrides=self.overrides)
|
||||
for plugin in plugins:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
|
||||
|
|
@ -139,6 +142,14 @@ class Chapters:
|
|||
if timestamps[idx].timestamp_sec >= timestamps[idx + 1].timestamp_sec:
|
||||
raise ValueError("Timestamps must be in ascending order")
|
||||
|
||||
def contains_any_chapters(self) -> bool:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
True if there are chapters. False otherwise.
|
||||
"""
|
||||
return len(self.timestamps) > 0
|
||||
|
||||
def contains_zero_timestamp(self) -> bool:
|
||||
"""
|
||||
Returns
|
||||
|
|
@ -165,7 +176,7 @@ class Chapters:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, chapters_file_path: str) -> "Chapters":
|
||||
def from_timestamps_file(cls, chapters_file_path: str) -> "Chapters":
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -207,3 +218,65 @@ class Chapters:
|
|||
titles.append(title)
|
||||
|
||||
return cls(timestamps=timestamps, titles=titles)
|
||||
|
||||
@classmethod
|
||||
def from_embedded_chapters(cls, file_path: str) -> "Chapters":
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
file_path
|
||||
File to read ffmpeg chapter metadata from
|
||||
|
||||
Returns
|
||||
-------
|
||||
Chapters object
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-loglevel",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_chapters",
|
||||
"--",
|
||||
file_path,
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
embedded_chapters = json.loads(proc.stdout)
|
||||
timestamps: List[Timestamp] = []
|
||||
titles: List[str] = []
|
||||
for chapter in embedded_chapters["chapters"]:
|
||||
timestamps.append(Timestamp.from_seconds(int(float(chapter["start_time"]))))
|
||||
titles.append(chapter["tags"]["title"])
|
||||
|
||||
return Chapters(timestamps=timestamps, titles=titles)
|
||||
|
||||
@classmethod
|
||||
def from_entry_chapters(cls, entry: Entry) -> "Chapters":
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
entry
|
||||
Entry with yt-dlp chapter metadata
|
||||
|
||||
Returns
|
||||
-------
|
||||
Chapters object
|
||||
"""
|
||||
timestamps: List[Timestamp] = []
|
||||
titles: List[str] = []
|
||||
|
||||
chapters = {}
|
||||
if entry.kwargs_contains("chapters"):
|
||||
chapters = entry.kwargs("chapters") or []
|
||||
|
||||
for chapter in chapters:
|
||||
timestamps.append(Timestamp.from_seconds(int(float(chapter["start_time"]))))
|
||||
titles.append(chapter["title"])
|
||||
|
||||
return Chapters(timestamps=timestamps, titles=titles)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from ytdl_sub.entries.entry import Entry
|
|||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||
|
||||
|
||||
def convert_download_thumbnail(entry: Entry):
|
||||
def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None:
|
||||
"""
|
||||
Converts an entry's downloaded thumbnail into jpg format
|
||||
|
||||
|
|
@ -13,6 +13,8 @@ def convert_download_thumbnail(entry: Entry):
|
|||
----------
|
||||
entry
|
||||
Entry with the thumbnail
|
||||
error_if_not_found
|
||||
If the thumbnail is not found, error if True.
|
||||
|
||||
Raises
|
||||
------
|
||||
|
|
@ -22,7 +24,9 @@ def convert_download_thumbnail(entry: Entry):
|
|||
download_thumbnail_path = entry.get_ytdlp_download_thumbnail_path()
|
||||
download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path()
|
||||
if not download_thumbnail_path:
|
||||
raise ValueError("Thumbnail not found")
|
||||
if error_if_not_found:
|
||||
raise ValueError("Thumbnail not found")
|
||||
return
|
||||
|
||||
if not download_thumbnail_path == download_thumbnail_path_as_jpg:
|
||||
FFMPEG.run(["-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import mergedeep
|
||||
import pytest
|
||||
from e2e.expected_download import assert_expected_downloads
|
||||
from e2e.expected_transaction_log import assert_transaction_log_matches
|
||||
|
|
|
|||
162
tests/e2e/plugins/test_split_by_chapters.py
Normal file
162
tests/e2e/plugins/test_split_by_chapters.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import re
|
||||
|
||||
import mergedeep
|
||||
import pytest
|
||||
from e2e.expected_download import assert_expected_downloads
|
||||
from e2e.expected_transaction_log import assert_transaction_log_matches
|
||||
|
||||
from ytdl_sub.subscriptions.subscription import Subscription
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yt_album_as_chapters_preset_dict(output_directory):
|
||||
return {
|
||||
"preset": "yt_album_as_chapters",
|
||||
"youtube": {"video_url": "https://www.youtube.com/watch?v=zeR2_YjlXWA"},
|
||||
# override the output directory with our fixture-generated dir
|
||||
"output_options": {"output_directory": output_directory},
|
||||
# download the worst format so it is fast
|
||||
"ytdl_options": {
|
||||
"format": "worst[ext=mp4]",
|
||||
"postprocessor_args": {"ffmpeg": ["-bitexact"]}, # Must add this for reproducibility
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def yt_album_as_chapters_with_regex_preset_dict(yt_album_as_chapters_preset_dict):
|
||||
mergedeep.merge(
|
||||
yt_album_as_chapters_preset_dict,
|
||||
{
|
||||
"regex": {
|
||||
"from": {
|
||||
"chapter_title": {
|
||||
"match": r"\d+\. (.+)",
|
||||
"capture_group_names": "captured_chapter_title",
|
||||
"capture_group_defaults": "{chapter_title}",
|
||||
},
|
||||
"title": {
|
||||
"match": [
|
||||
"(.+) - (.+) [-[\\(\\{].+",
|
||||
"(.+) - (.+)",
|
||||
],
|
||||
"capture_group_names": [
|
||||
"captured_artist",
|
||||
"captured_album",
|
||||
],
|
||||
"capture_group_defaults": [
|
||||
"{artist}",
|
||||
"{title}",
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"custom_track_name": "{captured_chapter_title}",
|
||||
"custom_album_name": "{captured_album}",
|
||||
"custom_artist_name": "{captured_artist}",
|
||||
},
|
||||
},
|
||||
)
|
||||
return yt_album_as_chapters_preset_dict
|
||||
|
||||
|
||||
class TestSplitByChapters:
|
||||
@pytest.mark.parametrize("dry_run", [True, False])
|
||||
def test_video_with_chapters(
|
||||
self,
|
||||
youtube_audio_config,
|
||||
yt_album_as_chapters_preset_dict,
|
||||
output_directory,
|
||||
dry_run,
|
||||
):
|
||||
subscription = Subscription.from_dict(
|
||||
config=youtube_audio_config,
|
||||
preset_name="split_by_chapters_video",
|
||||
preset_dict=yt_album_as_chapters_preset_dict,
|
||||
)
|
||||
|
||||
transaction_log = subscription.download(dry_run=dry_run)
|
||||
assert_transaction_log_matches(
|
||||
output_directory=output_directory,
|
||||
transaction_log=transaction_log,
|
||||
transaction_log_summary_file_name=f"plugins/split_by_chapters_video{'-dry-run' if dry_run else ''}.txt",
|
||||
)
|
||||
assert_expected_downloads(
|
||||
output_directory=output_directory,
|
||||
dry_run=dry_run,
|
||||
expected_download_summary_file_name="plugins/split_by_chapters_video.json",
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("dry_run", [True, False])
|
||||
def test_video_with_chapters_and_regex(
|
||||
self,
|
||||
youtube_audio_config,
|
||||
yt_album_as_chapters_with_regex_preset_dict,
|
||||
output_directory,
|
||||
dry_run,
|
||||
):
|
||||
subscription = Subscription.from_dict(
|
||||
config=youtube_audio_config,
|
||||
preset_name="split_by_chapters_with_regex_video",
|
||||
preset_dict=yt_album_as_chapters_with_regex_preset_dict,
|
||||
)
|
||||
|
||||
transaction_log = subscription.download(dry_run=dry_run)
|
||||
assert_transaction_log_matches(
|
||||
output_directory=output_directory,
|
||||
transaction_log=transaction_log,
|
||||
transaction_log_summary_file_name=f"plugins/split_by_chapters_with_regex_video{'-dry-run' if dry_run else ''}.txt",
|
||||
)
|
||||
assert_expected_downloads(
|
||||
output_directory=output_directory,
|
||||
dry_run=dry_run,
|
||||
expected_download_summary_file_name="plugins/split_by_chapters_with_regex_video.json",
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("dry_run", [True, False])
|
||||
@pytest.mark.parametrize("when_no_chapters", ["pass", "drop", "error"])
|
||||
def test_video_with_no_chapters_and_regex(
|
||||
self,
|
||||
youtube_audio_config,
|
||||
yt_album_as_chapters_with_regex_preset_dict,
|
||||
output_directory,
|
||||
dry_run,
|
||||
when_no_chapters,
|
||||
):
|
||||
mergedeep.merge(
|
||||
yt_album_as_chapters_with_regex_preset_dict,
|
||||
{
|
||||
"youtube": {"video_url": "https://youtube.com/watch?v=HKTNxEqsN3Q"},
|
||||
"split_by_chapters": {"when_no_chapters": when_no_chapters},
|
||||
},
|
||||
)
|
||||
|
||||
subscription = Subscription.from_dict(
|
||||
config=youtube_audio_config,
|
||||
preset_name="split_by_chapters_with_regex_video",
|
||||
preset_dict=yt_album_as_chapters_with_regex_preset_dict,
|
||||
)
|
||||
|
||||
if when_no_chapters == "error":
|
||||
with pytest.raises(
|
||||
ValidationException,
|
||||
match=re.escape(
|
||||
"Tried to split 'Oblivion Mod \"Falcor\" p.1' by chapters but it has no chapters"
|
||||
),
|
||||
):
|
||||
_ = subscription.download(dry_run=dry_run)
|
||||
return
|
||||
|
||||
transaction_log = subscription.download(dry_run=dry_run)
|
||||
assert_transaction_log_matches(
|
||||
output_directory=output_directory,
|
||||
transaction_log=transaction_log,
|
||||
transaction_log_summary_file_name=f"plugins/split_by_chapters_with_regex_no_chapters_video_{when_no_chapters}.txt",
|
||||
)
|
||||
assert_expected_downloads(
|
||||
output_directory=output_directory,
|
||||
dry_run=dry_run,
|
||||
expected_download_summary_file_name=f"plugins/split_by_chapters_with_regex_no_chapters_video_{when_no_chapters}.txt",
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"Alfa Mist - Nocturne [Full Album]/01 - 01. Intro (Feat. Racheal Ofori & Barney Artist).mp3": "9313382b938547fa9cc02c708068ae42",
|
||||
"Alfa Mist - Nocturne [Full Album]/02 - 02. Answers (Feat. Rick David & Kaya Thomas - Dyke).mp3": "c38c7186eb52e89246ed8c79be8485e7",
|
||||
"Alfa Mist - Nocturne [Full Album]/03 - 03. Blaze (Feat. Kaya Thomas - Dyke).mp3": "3d873907e61d4af8e20539961913d486",
|
||||
"Alfa Mist - Nocturne [Full Album]/04 - 04. What If (Interlude).mp3": "846898842d60b7d1d3b455dfa44821bd",
|
||||
"Alfa Mist - Nocturne [Full Album]/05 - 05. No Peace (Feat. Tom Misch).mp3": "23e40cccc86e106ea576dc78dc8e6376",
|
||||
"Alfa Mist - Nocturne [Full Album]/06 - 06. Closer (Feat. Lester Duval).mp3": "cf0fe6eea473f64bbeedc3ee82d91e17",
|
||||
"Alfa Mist - Nocturne [Full Album]/07 - 07. Delusions: Rumination (Interlude) (Feat. Racheal Ofori).mp3": "d9eae099a8b25f603c0491c9184092da",
|
||||
"Alfa Mist - Nocturne [Full Album]/08 - 08. Dreams (Feat. Carmody).mp3": "e07de2c95fb2561540048948f1478c25",
|
||||
"Alfa Mist - Nocturne [Full Album]/09 - 09. Dreaming (Interlude) (Feat. Racheal Ofori).mp3": "218386492a03e912a29dfa1a56fd983c",
|
||||
"Alfa Mist - Nocturne [Full Album]/10 - 10. Hopeful (Feat. Jordan Rakei).mp3": "0fb815e3648ad55c62328a3621aadd88",
|
||||
"Alfa Mist - Nocturne [Full Album]/11 - 11. Sunrise (Pillows) (Feat. Emmavie).mp3": "860154a2aa2b0b0b31daca0a481ceb87",
|
||||
"Alfa Mist - Nocturne [Full Album]/folder.jpg": "13be3c1a9ba600f7cab82a042cffbf72"
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"Oblivion Mod \"Falcor\" p.1/01 - Oblivion Mod \"Falcor\" p.1.mp3": "703ffb93964ac025ee66221b98ee4d49",
|
||||
"Oblivion Mod \"Falcor\" p.1/folder.jpg": "fb95b510681676e81c321171fc23143e"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"Oblivion Mod \"Falcor\" p.1/01 - Oblivion Mod \"Falcor\" p.1.mp3": "703ffb93964ac025ee66221b98ee4d49",
|
||||
"Oblivion Mod \"Falcor\" p.1/folder.jpg": "fb95b510681676e81c321171fc23143e"
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"Nocturne/01 - Intro (Feat. Racheal Ofori & Barney Artist).mp3": "75e53ffa551ab9aebe5d25a7d7bc6757",
|
||||
"Nocturne/02 - Answers (Feat. Rick David & Kaya Thomas - Dyke).mp3": "76f5e0a03e0984ed959809721ad2ae4a",
|
||||
"Nocturne/03 - Blaze (Feat. Kaya Thomas - Dyke).mp3": "bd6495ad5b46ee7e46215e5687ac7e7a",
|
||||
"Nocturne/04 - What If (Interlude).mp3": "928a7668b9e0d653cba66cb4f9e3474a",
|
||||
"Nocturne/05 - No Peace (Feat. Tom Misch).mp3": "563d0416c1c9b1eeedb3bd6fdc9f648a",
|
||||
"Nocturne/06 - Closer (Feat. Lester Duval).mp3": "eb36390ef7064ee0145b4b42e6745a1e",
|
||||
"Nocturne/07 - Delusions: Rumination (Interlude) (Feat. Racheal Ofori).mp3": "9cba05a39738aadf2cd36d7941484a02",
|
||||
"Nocturne/08 - Dreams (Feat. Carmody).mp3": "6833e149f8ec0c98dbd0c3b3caf9362c",
|
||||
"Nocturne/09 - Dreaming (Interlude) (Feat. Racheal Ofori).mp3": "2bce7f06baadb6c9cc9742e2a0288c6b",
|
||||
"Nocturne/10 - Hopeful (Feat. Jordan Rakei).mp3": "2e359012ffa86e231ec6694508da1455",
|
||||
"Nocturne/11 - Sunrise (Pillows) (Feat. Emmavie).mp3": "dded786c10aade8bb0821507225b9bd2",
|
||||
"Nocturne/folder.jpg": "13be3c1a9ba600f7cab82a042cffbf72"
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "3a156b122bd79c956cce5079d3530cc3"
|
||||
"YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3": "3a156b122bd79c956cce5079d3530cc3"
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
Files created in '{output_directory}'
|
||||
----------------------------------------
|
||||
Alfa Mist - Nocturne [Full Album]/01 - 01. Intro (Feat. Racheal Ofori & Barney Artist).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 0:00 - 2:06
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 01. Intro (Feat. Racheal Ofori & Barney Artist)
|
||||
track: 1
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/02 - 02. Answers (Feat. Rick David & Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 2:06 - 5:40
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 02. Answers (Feat. Rick David & Kaya Thomas - Dyke)
|
||||
track: 2
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/03 - 03. Blaze (Feat. Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 5:40 - 8:11
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 03. Blaze (Feat. Kaya Thomas - Dyke)
|
||||
track: 3
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/04 - 04. What If (Interlude).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 8:11 - 9:35
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 04. What If (Interlude)
|
||||
track: 4
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/05 - 05. No Peace (Feat. Tom Misch).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 9:35 - 13:19
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 05. No Peace (Feat. Tom Misch)
|
||||
track: 5
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/06 - 06. Closer (Feat. Lester Duval).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 13:19 - 17:57
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 06. Closer (Feat. Lester Duval)
|
||||
track: 6
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/07 - 07. Delusions: Rumination (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 17:57 - 20:05
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 07. Delusions: Rumination (Interlude) (Feat. Racheal Ofori)
|
||||
track: 7
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/08 - 08. Dreams (Feat. Carmody).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 20:05 - 23:58
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 08. Dreams (Feat. Carmody)
|
||||
track: 8
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/09 - 09. Dreaming (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 23:58 - 24:45
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 09. Dreaming (Interlude) (Feat. Racheal Ofori)
|
||||
track: 9
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/10 - 10. Hopeful (Feat. Jordan Rakei).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 24:45 - 28:53
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 10. Hopeful (Feat. Jordan Rakei)
|
||||
track: 10
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/11 - 11. Sunrise (Pillows) (Feat. Emmavie).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 28:53 - 31:43
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 11. Sunrise (Pillows) (Feat. Emmavie)
|
||||
track: 11
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/folder.jpg
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
Files created in '{output_directory}'
|
||||
----------------------------------------
|
||||
Alfa Mist - Nocturne [Full Album]/01 - 01. Intro (Feat. Racheal Ofori & Barney Artist).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 0:00 - 2:06
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 01. Intro (Feat. Racheal Ofori & Barney Artist)
|
||||
track: 1
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/02 - 02. Answers (Feat. Rick David & Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 2:06 - 5:40
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 02. Answers (Feat. Rick David & Kaya Thomas - Dyke)
|
||||
track: 2
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/03 - 03. Blaze (Feat. Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 5:40 - 8:11
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 03. Blaze (Feat. Kaya Thomas - Dyke)
|
||||
track: 3
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/04 - 04. What If (Interlude).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 8:11 - 9:35
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 04. What If (Interlude)
|
||||
track: 4
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/05 - 05. No Peace (Feat. Tom Misch).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 9:35 - 13:19
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 05. No Peace (Feat. Tom Misch)
|
||||
track: 5
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/06 - 06. Closer (Feat. Lester Duval).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 13:19 - 17:57
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 06. Closer (Feat. Lester Duval)
|
||||
track: 6
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/07 - 07. Delusions: Rumination (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 17:57 - 20:05
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 07. Delusions: Rumination (Interlude) (Feat. Racheal Ofori)
|
||||
track: 7
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/08 - 08. Dreams (Feat. Carmody).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 20:05 - 23:58
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 08. Dreams (Feat. Carmody)
|
||||
track: 8
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/09 - 09. Dreaming (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 23:58 - 24:45
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 09. Dreaming (Interlude) (Feat. Racheal Ofori)
|
||||
track: 9
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/10 - 10. Hopeful (Feat. Jordan Rakei).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 24:45 - 28:53
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 10. Hopeful (Feat. Jordan Rakei)
|
||||
track: 10
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/11 - 11. Sunrise (Pillows) (Feat. Emmavie).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 28:53 - 31:43
|
||||
Music Tags:
|
||||
album: Alfa Mist - Nocturne [Full Album]
|
||||
albumartist: Proved Records
|
||||
artist: Proved Records
|
||||
genre: Unset
|
||||
title: 11. Sunrise (Pillows) (Feat. Emmavie)
|
||||
track: 11
|
||||
year: 2017
|
||||
Alfa Mist - Nocturne [Full Album]/folder.jpg
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
Files created in '{output_directory}'
|
||||
----------------------------------------
|
||||
Oblivion Mod "Falcor" p.1/01 - Oblivion Mod "Falcor" p.1.mp3
|
||||
Music Tags:
|
||||
album: Oblivion Mod "Falcor" p.1
|
||||
albumartist: Project Zombie
|
||||
artist: Project Zombie
|
||||
genre: Unset
|
||||
title: Oblivion Mod "Falcor" p.1
|
||||
track: 1
|
||||
year: 2010
|
||||
Oblivion Mod "Falcor" p.1/folder.jpg
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
Files created in '{output_directory}'
|
||||
----------------------------------------
|
||||
Nocturne/01 - Intro (Feat. Racheal Ofori & Barney Artist).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 0:00 - 2:06
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Intro (Feat. Racheal Ofori & Barney Artist)
|
||||
track: 1
|
||||
year: 2017
|
||||
Nocturne/02 - Answers (Feat. Rick David & Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 2:06 - 5:40
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Answers (Feat. Rick David & Kaya Thomas - Dyke)
|
||||
track: 2
|
||||
year: 2017
|
||||
Nocturne/03 - Blaze (Feat. Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 5:40 - 8:11
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Blaze (Feat. Kaya Thomas - Dyke)
|
||||
track: 3
|
||||
year: 2017
|
||||
Nocturne/04 - What If (Interlude).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 8:11 - 9:35
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: What If (Interlude)
|
||||
track: 4
|
||||
year: 2017
|
||||
Nocturne/05 - No Peace (Feat. Tom Misch).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 9:35 - 13:19
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: No Peace (Feat. Tom Misch)
|
||||
track: 5
|
||||
year: 2017
|
||||
Nocturne/06 - Closer (Feat. Lester Duval).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 13:19 - 17:57
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Closer (Feat. Lester Duval)
|
||||
track: 6
|
||||
year: 2017
|
||||
Nocturne/07 - Delusions: Rumination (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 17:57 - 20:05
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Delusions: Rumination (Interlude) (Feat. Racheal Ofori)
|
||||
track: 7
|
||||
year: 2017
|
||||
Nocturne/08 - Dreams (Feat. Carmody).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 20:05 - 23:58
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Dreams (Feat. Carmody)
|
||||
track: 8
|
||||
year: 2017
|
||||
Nocturne/09 - Dreaming (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 23:58 - 24:45
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Dreaming (Interlude) (Feat. Racheal Ofori)
|
||||
track: 9
|
||||
year: 2017
|
||||
Nocturne/10 - Hopeful (Feat. Jordan Rakei).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 24:45 - 28:53
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Hopeful (Feat. Jordan Rakei)
|
||||
track: 10
|
||||
year: 2017
|
||||
Nocturne/11 - Sunrise (Pillows) (Feat. Emmavie).mp3
|
||||
From Chapter Split:
|
||||
Warning: Dry-run assumes embedded chapters with no modifications
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 28:53 - 31:43
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Sunrise (Pillows) (Feat. Emmavie)
|
||||
track: 11
|
||||
year: 2017
|
||||
Nocturne/folder.jpg
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
Files created in '{output_directory}'
|
||||
----------------------------------------
|
||||
Nocturne/01 - Intro (Feat. Racheal Ofori & Barney Artist).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 0:00 - 2:06
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Intro (Feat. Racheal Ofori & Barney Artist)
|
||||
track: 1
|
||||
year: 2017
|
||||
Nocturne/02 - Answers (Feat. Rick David & Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 2:06 - 5:40
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Answers (Feat. Rick David & Kaya Thomas - Dyke)
|
||||
track: 2
|
||||
year: 2017
|
||||
Nocturne/03 - Blaze (Feat. Kaya Thomas - Dyke).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 5:40 - 8:11
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Blaze (Feat. Kaya Thomas - Dyke)
|
||||
track: 3
|
||||
year: 2017
|
||||
Nocturne/04 - What If (Interlude).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 8:11 - 9:35
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: What If (Interlude)
|
||||
track: 4
|
||||
year: 2017
|
||||
Nocturne/05 - No Peace (Feat. Tom Misch).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 9:35 - 13:19
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: No Peace (Feat. Tom Misch)
|
||||
track: 5
|
||||
year: 2017
|
||||
Nocturne/06 - Closer (Feat. Lester Duval).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 13:19 - 17:57
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Closer (Feat. Lester Duval)
|
||||
track: 6
|
||||
year: 2017
|
||||
Nocturne/07 - Delusions: Rumination (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 17:57 - 20:05
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Delusions: Rumination (Interlude) (Feat. Racheal Ofori)
|
||||
track: 7
|
||||
year: 2017
|
||||
Nocturne/08 - Dreams (Feat. Carmody).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 20:05 - 23:58
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Dreams (Feat. Carmody)
|
||||
track: 8
|
||||
year: 2017
|
||||
Nocturne/09 - Dreaming (Interlude) (Feat. Racheal Ofori).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 23:58 - 24:45
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Dreaming (Interlude) (Feat. Racheal Ofori)
|
||||
track: 9
|
||||
year: 2017
|
||||
Nocturne/10 - Hopeful (Feat. Jordan Rakei).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 24:45 - 28:53
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Hopeful (Feat. Jordan Rakei)
|
||||
track: 10
|
||||
year: 2017
|
||||
Nocturne/11 - Sunrise (Pillows) (Feat. Emmavie).mp3
|
||||
From Chapter Split:
|
||||
Source Title: Alfa Mist - Nocturne [Full Album]
|
||||
Segment: 28:53 - 31:43
|
||||
Music Tags:
|
||||
album: Nocturne
|
||||
albumartist: Alfa Mist
|
||||
artist: Alfa Mist
|
||||
genre: Unset
|
||||
title: Sunrise (Pillows) (Feat. Emmavie)
|
||||
track: 11
|
||||
year: 2017
|
||||
Nocturne/folder.jpg
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
Files created in '{output_directory}'
|
||||
----------------------------------------
|
||||
YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3
|
||||
YouTube Rewind 2019: For the Record | #YouTubeRewind.mp3
|
||||
Music Tags:
|
||||
album: Singles
|
||||
albumartist: YouTube
|
||||
|
|
|
|||
Loading…
Reference in a new issue