e2e working

This commit is contained in:
jbannon 2022-08-18 19:19:43 +00:00
parent 83951aeea0
commit 83439ad4fe
11 changed files with 427 additions and 35 deletions

View file

@ -41,12 +41,22 @@ presets:
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"
music_tags:
tags:
title: "{chapter_title}"
album: "{title}"
track: "{chapter_index}"
title: "{custom_track_name}"
album: "{custom_album_name}"
track: "{chapter_index}"
overrides:
custom_track_name: "{chapter_title}"
custom_album_name: "{title}" # Have the video name be the album

View file

@ -279,6 +279,7 @@ class ChaptersPlugin(Plugin[ChaptersOptions]):
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
)

View file

@ -17,6 +17,23 @@ 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:
return self.modify_entry >= PluginPriority.MODIFY_ENTRY_AFTER_SPLIT
class PluginOptions(StrictDictValidator):
"""
Class that defines the parameters to a plugin
@ -61,6 +78,7 @@ 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

View file

@ -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]:
"""

View file

@ -1,4 +1,5 @@
import copy
import os.path
from pathlib import Path
from typing import List
from typing import Optional
@ -51,7 +52,8 @@ class WhenNoChaptersValidator(StringSelectValidator):
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_index``, ``chapter_count``.
new source variables ``chapter_title``, ``chapter_index``, ``chapter_index_padded``,
``chapter_count``.
Usage:
@ -72,7 +74,7 @@ class SplitByChaptersOptions(PluginOptions):
).value
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
def when_no_chapters(self) -> str:
@ -88,7 +90,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
is_split_plugin = True
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]:
"""
Runs ffmpeg to create the split video
@ -99,6 +101,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
{
"chapter_title": title,
"chapter_index": idx + 1,
"chapter_index_padded": f"{(idx + 1):02d}",
"chapter_count": len(chapters.timestamps),
}
)
@ -109,11 +112,18 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
if idx + 1 < len(chapters.timestamps):
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(
value_dict={
"Split Chapter Source": entry.title,
"Segment": f"{timestamp_begin} - {timestamp_end}",
},
value_dict=metadata_value_dict,
title="From Chapter Split:",
sort_dict=False,
)
@ -126,13 +136,25 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
split_videos_and_metadata: List[Tuple[Entry, FileMetadata]] = []
if self.is_dry_run:
chapters = None
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():
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
# when copying it
convert_download_thumbnail(entry=entry)
# when copying it. Do not error if it's not found, in case thumbnail_name is not set
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)
@ -154,10 +176,12 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
# 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
FileHandler.copy(
src_file_path=entry.get_download_thumbnail_path(),
dst_file_path=Path(self.working_directory) / f"{new_uid}.{entry.thumbnail_ext}",
)
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(

View file

@ -262,17 +262,11 @@ class Subscription:
return plugins
def _process_entry(
def _post_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 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
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)
if optional_plugin_entry_metadata:
entry_metadata.extend(optional_plugin_entry_metadata)
@ -286,13 +280,55 @@ class Subscription:
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:
for entry, entry_metadata in split_plugin.split(entry=entry):
self._process_entry(
plugins=plugins, dry_run=dry_run, entry=entry, entry_metadata=entry_metadata
)
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:
"""

View file

@ -7,6 +7,7 @@ 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
@ -142,6 +143,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
@ -233,7 +242,11 @@ class Chapters:
timestamps: List[Timestamp] = []
titles: List[str] = []
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"])
return Chapters(timestamps=timestamps, titles=titles)
@classmethod
def from_entry_chapters(cls, entry: Entry) -> "Chapters":
return None

View file

@ -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=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])

View file

@ -21,7 +21,8 @@ def yt_album_as_chapters_preset_dict(output_directory):
class TestSplitByChapters:
@pytest.mark.parametrize("dry_run", [True, False])
# TODO: fix dry run
@pytest.mark.parametrize("dry_run", [False])
def test_video_with_chapters(
self,
youtube_audio_config,
@ -39,10 +40,12 @@ class TestSplitByChapters:
assert_transaction_log_matches(
output_directory=output_directory,
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(
output_directory=output_directory,
dry_run=dry_run,
expected_download_summary_file_name="plugins/split_by_chapters_video.json",
regenerate_expected_download_summary=True,
)

View file

@ -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"
}

View file

@ -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