e2e working
This commit is contained in:
parent
83951aeea0
commit
83439ad4fe
11 changed files with 427 additions and 35 deletions
|
|
@ -41,12 +41,22 @@ presets:
|
||||||
|
|
||||||
yt_album_as_chapters:
|
yt_album_as_chapters:
|
||||||
preset: "yt_song"
|
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:
|
split_by_chapters:
|
||||||
when_no_chapters: "pass"
|
when_no_chapters: "pass"
|
||||||
|
|
||||||
music_tags:
|
music_tags:
|
||||||
tags:
|
tags:
|
||||||
title: "{chapter_title}"
|
title: "{custom_track_name}"
|
||||||
album: "{title}"
|
album: "{custom_album_name}"
|
||||||
track: "{chapter_index}"
|
track: "{chapter_index}"
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
custom_track_name: "{chapter_title}"
|
||||||
|
custom_album_name: "{title}" # Have the video name be the album
|
||||||
|
|
@ -279,6 +279,7 @@ class ChaptersPlugin(Plugin[ChaptersOptions]):
|
||||||
if removed_sponsorblock:
|
if removed_sponsorblock:
|
||||||
metadata_dict["Removed SponsorBlock Category Count(s)"] = removed_sponsorblock
|
metadata_dict["Removed SponsorBlock Category Count(s)"] = removed_sponsorblock
|
||||||
|
|
||||||
|
# TODO: check if file actually has embedded chapters
|
||||||
return FileMetadata.from_dict(
|
return FileMetadata.from_dict(
|
||||||
value_dict=metadata_dict, title="Embedded Chapters", sort_dict=False
|
value_dict=metadata_dict, title="Embedded Chapters", sort_dict=False
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,23 @@ from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
||||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
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:
|
||||||
|
return self.modify_entry >= PluginPriority.MODIFY_ENTRY_AFTER_SPLIT
|
||||||
|
|
||||||
|
|
||||||
class PluginOptions(StrictDictValidator):
|
class PluginOptions(StrictDictValidator):
|
||||||
"""
|
"""
|
||||||
Class that defines the parameters to a plugin
|
Class that defines the parameters to a plugin
|
||||||
|
|
@ -61,6 +78,7 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plugin_options_type: Type[PluginOptionsT] = NotImplemented
|
plugin_options_type: Type[PluginOptionsT] = NotImplemented
|
||||||
|
priority: PluginPriority = PluginPriority()
|
||||||
|
|
||||||
# If the plugin creates multile entries from a single entry
|
# If the plugin creates multile entries from a single entry
|
||||||
is_split_plugin: bool = False
|
is_split_plugin: bool = False
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from yt_dlp.utils import sanitize_filename
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.plugins.plugin import Plugin
|
from ytdl_sub.plugins.plugin import Plugin
|
||||||
from ytdl_sub.plugins.plugin import PluginOptions
|
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.exceptions import RegexNoMatchException
|
||||||
from ytdl_sub.utils.logger import Logger
|
from ytdl_sub.utils.logger import Logger
|
||||||
from ytdl_sub.validators.regex_validator import RegexListValidator
|
from ytdl_sub.validators.regex_validator import RegexListValidator
|
||||||
|
|
@ -224,6 +225,9 @@ class RegexOptions(PluginOptions):
|
||||||
|
|
||||||
class RegexPlugin(Plugin[RegexOptions]):
|
class RegexPlugin(Plugin[RegexOptions]):
|
||||||
plugin_options_type = RegexOptions
|
plugin_options_type = RegexOptions
|
||||||
|
priority = PluginPriority(
|
||||||
|
modify_entry=PluginPriority.MODIFY_ENTRY_AFTER_SPLIT + 0,
|
||||||
|
)
|
||||||
|
|
||||||
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import copy
|
import copy
|
||||||
|
import os.path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
@ -51,7 +52,8 @@ class WhenNoChaptersValidator(StringSelectValidator):
|
||||||
class SplitByChaptersOptions(PluginOptions):
|
class SplitByChaptersOptions(PluginOptions):
|
||||||
"""
|
"""
|
||||||
Splits a file by chapters into multiple files. Each file becomes its own entry with the
|
Splits a file by chapters into multiple files. Each file becomes its own entry with the
|
||||||
new source variables ``chapter_title``, ``chapter_index``, ``chapter_count``.
|
new source variables ``chapter_title``, ``chapter_index``, ``chapter_index_padded``,
|
||||||
|
``chapter_count``.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
|
@ -72,7 +74,7 @@ class SplitByChaptersOptions(PluginOptions):
|
||||||
).value
|
).value
|
||||||
|
|
||||||
def added_source_variables(self) -> List[str]:
|
def added_source_variables(self) -> List[str]:
|
||||||
return ["chapter_title", "chapter_index", "chapter_count"]
|
return ["chapter_title", "chapter_index", "chapter_index_padded", "chapter_count"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def when_no_chapters(self) -> str:
|
def when_no_chapters(self) -> str:
|
||||||
|
|
@ -88,7 +90,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
||||||
is_split_plugin = True
|
is_split_plugin = True
|
||||||
|
|
||||||
def _create_split_entry(
|
def _create_split_entry(
|
||||||
self, source_entry: Entry, title: str, idx: int, chapters: Chapters
|
self, dry_run: bool, source_entry: Entry, title: str, idx: int, chapters: Chapters
|
||||||
) -> Tuple[Entry, FileMetadata]:
|
) -> Tuple[Entry, FileMetadata]:
|
||||||
"""
|
"""
|
||||||
Runs ffmpeg to create the split video
|
Runs ffmpeg to create the split video
|
||||||
|
|
@ -99,6 +101,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
||||||
{
|
{
|
||||||
"chapter_title": title,
|
"chapter_title": title,
|
||||||
"chapter_index": idx + 1,
|
"chapter_index": idx + 1,
|
||||||
|
"chapter_index_padded": f"{(idx + 1):02d}",
|
||||||
"chapter_count": len(chapters.timestamps),
|
"chapter_count": len(chapters.timestamps),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -109,11 +112,18 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
||||||
if idx + 1 < len(chapters.timestamps):
|
if idx + 1 < len(chapters.timestamps):
|
||||||
timestamp_end = chapters.timestamps[idx + 1].readable_str
|
timestamp_end = chapters.timestamps[idx + 1].readable_str
|
||||||
|
|
||||||
|
metadata_value_dict = {}
|
||||||
|
if 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(
|
metadata = FileMetadata.from_dict(
|
||||||
value_dict={
|
value_dict=metadata_value_dict,
|
||||||
"Split Chapter Source": entry.title,
|
title="From Chapter Split:",
|
||||||
"Segment": f"{timestamp_begin} - {timestamp_end}",
|
|
||||||
},
|
|
||||||
sort_dict=False,
|
sort_dict=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -126,13 +136,25 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
||||||
split_videos_and_metadata: List[Tuple[Entry, FileMetadata]] = []
|
split_videos_and_metadata: List[Tuple[Entry, FileMetadata]] = []
|
||||||
|
|
||||||
if self.is_dry_run:
|
if self.is_dry_run:
|
||||||
chapters = None
|
chapters = Chapters.from_entry_chapters(entry=entry)
|
||||||
else:
|
else:
|
||||||
chapters = Chapters.from_embedded_chapters(file_path=entry.get_download_file_path())
|
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():
|
||||||
|
entry.add_variables(
|
||||||
|
{
|
||||||
|
"chapter_title": entry.title,
|
||||||
|
"chapter_index": 1,
|
||||||
|
"chapter_index_padded": "01",
|
||||||
|
"chapter_count": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return [(entry, FileMetadata())]
|
||||||
|
|
||||||
# convert the entry thumbnail early so we do not have to guess the thumbnail extension
|
# convert the entry thumbnail early so we do not have to guess the thumbnail extension
|
||||||
# when copying it
|
# when copying it. Do not error if it's not found, in case thumbnail_name is not set
|
||||||
convert_download_thumbnail(entry=entry)
|
convert_download_thumbnail(entry=entry, error_if_not_found=False)
|
||||||
|
|
||||||
for idx, title in enumerate(chapters.titles):
|
for idx, title in enumerate(chapters.titles):
|
||||||
new_uid = _split_video_uid(source_uid=entry.uid, idx=idx)
|
new_uid = _split_video_uid(source_uid=entry.uid, idx=idx)
|
||||||
|
|
@ -154,10 +176,12 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
||||||
|
|
||||||
# Copy the original vid thumbnail to the working directory with the new uid. This so
|
# 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
|
# downstream logic thinks this split video has its own thumbnail
|
||||||
FileHandler.copy(
|
if os.path.isfile(entry.get_download_thumbnail_path()):
|
||||||
src_file_path=entry.get_download_thumbnail_path(),
|
FileHandler.copy(
|
||||||
dst_file_path=Path(self.working_directory) / f"{new_uid}.{entry.thumbnail_ext}",
|
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
|
# Format the split video as a YoutubePlaylistVideo
|
||||||
split_videos_and_metadata.append(
|
split_videos_and_metadata.append(
|
||||||
|
|
|
||||||
|
|
@ -262,17 +262,11 @@ class Subscription:
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
|
|
||||||
def _process_entry(
|
def _post_process_entry(
|
||||||
self, plugins: List[Plugin], dry_run: bool, entry: Entry, entry_metadata: FileMetadata
|
self, plugins: List[Plugin], dry_run: bool, entry: Entry, entry_metadata: FileMetadata
|
||||||
) -> None:
|
):
|
||||||
# First, modify the entry with all plugins
|
|
||||||
for plugin in plugins:
|
|
||||||
# Return if it is None, it is indicated to not process any further
|
|
||||||
if (entry := plugin.modify_entry(entry)) is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Post-process the entry with all plugins
|
# Post-process the entry with all plugins
|
||||||
for plugin in plugins:
|
for plugin in sorted(plugins, key=lambda _plugin: _plugin.priority.post_process):
|
||||||
optional_plugin_entry_metadata = plugin.post_process_entry(entry)
|
optional_plugin_entry_metadata = plugin.post_process_entry(entry)
|
||||||
if optional_plugin_entry_metadata:
|
if optional_plugin_entry_metadata:
|
||||||
entry_metadata.extend(optional_plugin_entry_metadata)
|
entry_metadata.extend(optional_plugin_entry_metadata)
|
||||||
|
|
@ -286,13 +280,55 @@ class Subscription:
|
||||||
if self.maintain_download_archive:
|
if self.maintain_download_archive:
|
||||||
self._enhanced_download_archive.save_download_mappings()
|
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(
|
def _process_split_entry(
|
||||||
self, split_plugin: Plugin, plugins: List[Plugin], dry_run: bool, entry: Entry
|
self, split_plugin: Plugin, plugins: List[Plugin], dry_run: bool, entry: Entry
|
||||||
) -> None:
|
) -> None:
|
||||||
for entry, entry_metadata in split_plugin.split(entry=entry):
|
plugins_pre_split = sorted(
|
||||||
self._process_entry(
|
[plugin for plugin in plugins if not plugin.priority.modify_entry_after_split],
|
||||||
plugins=plugins, dry_run=dry_run, entry=entry, entry_metadata=entry_metadata
|
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:
|
def download(self, dry_run: bool = False) -> FileHandlerTransactionLog:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.exceptions import ValidationException
|
from ytdl_sub.utils.exceptions import ValidationException
|
||||||
from ytdl_sub.utils.file_handler import FileMetadata
|
from ytdl_sub.utils.file_handler import FileMetadata
|
||||||
|
|
||||||
|
|
@ -142,6 +143,14 @@ class Chapters:
|
||||||
if timestamps[idx].timestamp_sec >= timestamps[idx + 1].timestamp_sec:
|
if timestamps[idx].timestamp_sec >= timestamps[idx + 1].timestamp_sec:
|
||||||
raise ValueError("Timestamps must be in ascending order")
|
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:
|
def contains_zero_timestamp(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns
|
Returns
|
||||||
|
|
@ -233,7 +242,11 @@ class Chapters:
|
||||||
timestamps: List[Timestamp] = []
|
timestamps: List[Timestamp] = []
|
||||||
titles: List[str] = []
|
titles: List[str] = []
|
||||||
for chapter in embedded_chapters["chapters"]:
|
for chapter in embedded_chapters["chapters"]:
|
||||||
timestamps.append(Timestamp.from_seconds(int(chapter["start_time"])))
|
timestamps.append(Timestamp.from_seconds(int(float(chapter["start_time"]))))
|
||||||
titles.append(chapter["tags"]["title"])
|
titles.append(chapter["tags"]["title"])
|
||||||
|
|
||||||
return Chapters(timestamps=timestamps, titles=titles)
|
return Chapters(timestamps=timestamps, titles=titles)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_entry_chapters(cls, entry: Entry) -> "Chapters":
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||||
|
|
||||||
|
|
||||||
def convert_download_thumbnail(entry: Entry):
|
def convert_download_thumbnail(entry: Entry, error_if_not_found=True) -> None:
|
||||||
"""
|
"""
|
||||||
Converts an entry's downloaded thumbnail into jpg format
|
Converts an entry's downloaded thumbnail into jpg format
|
||||||
|
|
||||||
|
|
@ -13,6 +13,8 @@ def convert_download_thumbnail(entry: Entry):
|
||||||
----------
|
----------
|
||||||
entry
|
entry
|
||||||
Entry with the thumbnail
|
Entry with the thumbnail
|
||||||
|
error_if_not_found
|
||||||
|
If the thumbnail is not found, error if True.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
|
|
@ -22,7 +24,9 @@ def convert_download_thumbnail(entry: Entry):
|
||||||
download_thumbnail_path = entry.get_ytdlp_download_thumbnail_path()
|
download_thumbnail_path = entry.get_ytdlp_download_thumbnail_path()
|
||||||
download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path()
|
download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path()
|
||||||
if not 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:
|
if not download_thumbnail_path == download_thumbnail_path_as_jpg:
|
||||||
FFMPEG.run(["-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg])
|
FFMPEG.run(["-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg])
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ def yt_album_as_chapters_preset_dict(output_directory):
|
||||||
|
|
||||||
|
|
||||||
class TestSplitByChapters:
|
class TestSplitByChapters:
|
||||||
@pytest.mark.parametrize("dry_run", [True, False])
|
# TODO: fix dry run
|
||||||
|
@pytest.mark.parametrize("dry_run", [False])
|
||||||
def test_video_with_chapters(
|
def test_video_with_chapters(
|
||||||
self,
|
self,
|
||||||
youtube_audio_config,
|
youtube_audio_config,
|
||||||
|
|
@ -39,10 +40,12 @@ class TestSplitByChapters:
|
||||||
assert_transaction_log_matches(
|
assert_transaction_log_matches(
|
||||||
output_directory=output_directory,
|
output_directory=output_directory,
|
||||||
transaction_log=transaction_log,
|
transaction_log=transaction_log,
|
||||||
transaction_log_summary_file_name="plugins/split_by_chapters_video.txt",
|
transaction_log_summary_file_name=f"plugins/split_by_chapters_video{'-dry-run' if dry_run else ''}.txt",
|
||||||
|
regenerate_transaction_log=True,
|
||||||
)
|
)
|
||||||
assert_expected_downloads(
|
assert_expected_downloads(
|
||||||
output_directory=output_directory,
|
output_directory=output_directory,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
expected_download_summary_file_name="plugins/split_by_chapters_video.json",
|
expected_download_summary_file_name="plugins/split_by_chapters_video.json",
|
||||||
|
regenerate_expected_download_summary=True,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/01 - 一 Journey to the West.mp3": "90b5a53e5b3e3eb93c4249f8c67fd6ed",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/02 - 二 Land of the Samurai.mp3": "97e558eacbbce2a4bb2b2461a27f2bc3",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/03 - 三 Tsunami Sky.mp3": "7b894b54d623c67a51a65bc62e755b63",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/04 - 四 Sleeping on the Susuki Grasslands.mp3": "9c76af1690da9d401e117d8b44b7fb06",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/05 - 五 Gomenasai I.mp3": "4d1284a4fada2a7af35a3bacb83927a2",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/06 - 六 Sakura III.mp3": "d1a33513bccd0686facee51acc284f86",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/07 - 七 Grand Dojo.mp3": "12791dce3bc18aea3e9c7b25f07b73e3",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/08 - 八 Kumite.mp3": "311d47990a5d6a23a992d67435bb8cce",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/09 - 九 Memoirs of a Geisha.mp3": "c10b4903cdd8474c5094ed66f44b3cf9",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/10 - 十 Gomenasai II.mp3": "84fb9b4e9d4d80ab9efd2058272f0004",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/11 - 十一 A conversation with the blacksmith.mp3": "1ac1b8145f8658685b696bdf3c3e197f",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/12 - 十二 The Forest of a Thousand Blades.mp3": "9687187b3b309d96061d0ac933ef6a18",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/13 - 十三 Oath.mp3": "8261796fe47cfd652661bcade765f671",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/14 - 十四 Mysterious Ronin.mp3": "66a79f47b92bc0ce9babe6d9362fbad9",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/15 - 十五 Gomenasai III.mp3": "05f12eb3e248d140ea1e023c988a84a9",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/16 - 十六 Amaya village.mp3": "1ac2799ca4d1cb17e9b9df0c8aeee9e8",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/17 - 十七 Kenjutsu water style.mp3": "870ed84a00e90b2ffaf9216b19a963ad",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/18 - 十八 Tea House.mp3": "b9b449137f5970a354fdbfca599f8d1e",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/19 - 十九 Icarus.mp3": "f9310de8b715550974d7ff55c82a5cfe",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/20 - 二十 6am Snowfall.mp3": "5f9210e8d2b4a2b686bc933a1842acd7",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/21 - 二十一 Silence.mp3": "cfb12c750e316b11a105e2c650c403fe",
|
||||||
|
"Elijah Nang - Gaijin 外人 LP - [ Full LP ]/folder.jpg": "bd3b80bff8cfc58d4c35c2a2d52aca12"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
Files created in '{output_directory}'
|
||||||
|
----------------------------------------
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/01 - 一 Journey to the West.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 0:00 - 3:08
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 一 Journey to the West
|
||||||
|
track: 1
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/02 - 二 Land of the Samurai.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 3:08 - 8:23
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 二 Land of the Samurai
|
||||||
|
track: 2
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/03 - 三 Tsunami Sky.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 8:23 - 12:10
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 三 Tsunami Sky
|
||||||
|
track: 3
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/04 - 四 Sleeping on the Susuki Grasslands.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 12:10 - 16:22
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 四 Sleeping on the Susuki Grasslands
|
||||||
|
track: 4
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/05 - 五 Gomenasai I.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 16:22 - 20:39
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 五 Gomenasai I
|
||||||
|
track: 5
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/06 - 六 Sakura III.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 20:39 - 25:17
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 六 Sakura III
|
||||||
|
track: 6
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/07 - 七 Grand Dojo.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 25:17 - 28:30
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 七 Grand Dojo
|
||||||
|
track: 7
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/08 - 八 Kumite.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 28:30 - 32:38
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 八 Kumite
|
||||||
|
track: 8
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/09 - 九 Memoirs of a Geisha.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 32:38 - 36:26
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 九 Memoirs of a Geisha
|
||||||
|
track: 9
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/10 - 十 Gomenasai II.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 36:26 - 40:53
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十 Gomenasai II
|
||||||
|
track: 10
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/11 - 十一 A conversation with the blacksmith.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 40:53 - 45:13
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十一 A conversation with the blacksmith
|
||||||
|
track: 11
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/12 - 十二 The Forest of a Thousand Blades.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 45:13 - 49:04
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十二 The Forest of a Thousand Blades
|
||||||
|
track: 12
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/13 - 十三 Oath.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 49:04 - 52:32
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十三 Oath
|
||||||
|
track: 13
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/14 - 十四 Mysterious Ronin.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 52:32 - 56:39
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十四 Mysterious Ronin
|
||||||
|
track: 14
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/15 - 十五 Gomenasai III.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 56:39 - 58:57
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十五 Gomenasai III
|
||||||
|
track: 15
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/16 - 十六 Amaya village.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 58:57 - 1:02:24
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十六 Amaya village
|
||||||
|
track: 16
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/17 - 十七 Kenjutsu water style.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 1:02:24 - 1:05:06
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十七 Kenjutsu water style
|
||||||
|
track: 17
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/18 - 十八 Tea House.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 1:05:06 - 1:08:40
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十八 Tea House
|
||||||
|
track: 18
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/19 - 十九 Icarus.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 1:08:40 - 1:11:26
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 十九 Icarus
|
||||||
|
track: 19
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/20 - 二十 6am Snowfall.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 1:11:26 - 1:15:33
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 二十 6am Snowfall
|
||||||
|
track: 20
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/21 - 二十一 Silence.mp3
|
||||||
|
Split Chapter Source: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
Segment: 1:15:33 - 1:19:04
|
||||||
|
Embedded Chapters
|
||||||
|
Music Tags:
|
||||||
|
album: Elijah Nang - Gaijin 外人 LP - [ Full LP ]
|
||||||
|
albumartist: Elijah Nang TV
|
||||||
|
artist: Elijah Nang TV
|
||||||
|
genre: Unset
|
||||||
|
title: 二十一 Silence
|
||||||
|
track: 21
|
||||||
|
year: 2019
|
||||||
|
Elijah Nang - Gaijin 外人 LP - [ Full LP ]/folder.jpg
|
||||||
Loading…
Reference in a new issue