[FEATURE] Add ytdl-sub view URL to view source variables (#340)
* [FEATURE] Add `ytdl-sub view URL` to view source variables * update file path
This commit is contained in:
parent
c783131ebf
commit
5705a28842
14 changed files with 193 additions and 15 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -142,3 +142,5 @@ dmypy.json
|
||||||
docker/*.whl
|
docker/*.whl
|
||||||
docker/root/*.whl
|
docker/root/*.whl
|
||||||
docker/root/defaults/examples
|
docker/root/defaults/examples
|
||||||
|
|
||||||
|
.local/
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ from ytdl_sub.utils.logger import Logger
|
||||||
|
|
||||||
logger = Logger.get()
|
logger = Logger.get()
|
||||||
|
|
||||||
|
# View is a command to run a simple dry-run on a URL using the `_view` preset.
|
||||||
|
# Use ytdl-sub dl arguments to use the preset
|
||||||
|
_VIEW_EXTRA_ARGS_FORMATTER = "--preset _view --overrides.url {}"
|
||||||
|
|
||||||
|
|
||||||
def _download_subscriptions_from_yaml_files(
|
def _download_subscriptions_from_yaml_files(
|
||||||
config: ConfigFile, args: argparse.Namespace
|
config: ConfigFile, args: argparse.Namespace
|
||||||
|
|
@ -60,7 +64,7 @@ def _download_subscriptions_from_yaml_files(
|
||||||
|
|
||||||
|
|
||||||
def _download_subscription_from_cli(
|
def _download_subscription_from_cli(
|
||||||
config: ConfigFile, args: argparse.Namespace, extra_args: List[str]
|
config: ConfigFile, dry_run: bool, extra_args: List[str]
|
||||||
) -> Tuple[Subscription, FileHandlerTransactionLog]:
|
) -> Tuple[Subscription, FileHandlerTransactionLog]:
|
||||||
"""
|
"""
|
||||||
Downloads a one-off subscription using the CLI
|
Downloads a one-off subscription using the CLI
|
||||||
|
|
@ -69,8 +73,8 @@ def _download_subscription_from_cli(
|
||||||
----------
|
----------
|
||||||
config
|
config
|
||||||
Configuration file
|
Configuration file
|
||||||
args
|
dry_run
|
||||||
Arguments from argparse
|
Whether this is a dry-run
|
||||||
extra_args
|
extra_args
|
||||||
Extra arguments from argparse that contain dynamic subscription options
|
Extra arguments from argparse that contain dynamic subscription options
|
||||||
|
|
||||||
|
|
@ -88,8 +92,30 @@ def _download_subscription_from_cli(
|
||||||
config=config, preset_name=subscription_name, preset_dict=subscription_args_dict
|
config=config, preset_name=subscription_name, preset_dict=subscription_args_dict
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Beginning CLI %s", ("dry run" if args.dry_run else "download"))
|
logger.info("Beginning CLI %s", ("dry run" if dry_run else "download"))
|
||||||
return subscription, subscription.download(dry_run=args.dry_run)
|
return subscription, subscription.download(dry_run=dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
def _view_url_from_cli(
|
||||||
|
config: ConfigFile, url: str, split_chapters: bool
|
||||||
|
) -> Tuple[Subscription, FileHandlerTransactionLog]:
|
||||||
|
"""
|
||||||
|
`ytdl-sub view` dry-runs a URL to print its source variables. Use the pre-built `_view` preset,
|
||||||
|
inject the URL argument, and dry-run.
|
||||||
|
"""
|
||||||
|
preset = "_view_split_chapters" if split_chapters else "_view"
|
||||||
|
subscription = Subscription.from_dict(
|
||||||
|
config=config,
|
||||||
|
preset_name="ytdl-sub-view",
|
||||||
|
preset_dict={"preset": preset, "overrides": {"url": url}},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Viewing source variables for URL '%s'%s",
|
||||||
|
url,
|
||||||
|
" with split chapters" if split_chapters else "",
|
||||||
|
)
|
||||||
|
return subscription, subscription.download(dry_run=True)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|
@ -146,9 +172,15 @@ def main() -> List[Tuple[Subscription, FileHandlerTransactionLog]]:
|
||||||
transaction_logs = _download_subscriptions_from_yaml_files(config=config, args=args)
|
transaction_logs = _download_subscriptions_from_yaml_files(config=config, args=args)
|
||||||
|
|
||||||
# One-off download
|
# One-off download
|
||||||
if args.subparser == "dl":
|
elif args.subparser == "dl":
|
||||||
transaction_logs.append(
|
transaction_logs.append(
|
||||||
_download_subscription_from_cli(config=config, args=args, extra_args=extra_args)
|
_download_subscription_from_cli(
|
||||||
|
config=config, dry_run=args.dry_run, extra_args=extra_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.subparser == "view":
|
||||||
|
transaction_logs.append(
|
||||||
|
_view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
|
||||||
)
|
)
|
||||||
|
|
||||||
for subscription, transaction_log in transaction_logs:
|
for subscription, transaction_log in transaction_logs:
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,10 @@ parser.add_argument(
|
||||||
choices=LoggerLevels.names(),
|
choices=LoggerLevels.names(),
|
||||||
dest="ytdl_sub_log_level",
|
dest="ytdl_sub_log_level",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="subparser")
|
||||||
###################################################################################################
|
###################################################################################################
|
||||||
# SUBSCRIPTION PARSER
|
# SUBSCRIPTION PARSER
|
||||||
subparsers = parser.add_subparsers(dest="subparser")
|
|
||||||
subscription_parser = subparsers.add_parser("sub")
|
subscription_parser = subparsers.add_parser("sub")
|
||||||
subscription_parser.add_argument(
|
subscription_parser.add_argument(
|
||||||
"subscription_paths",
|
"subscription_paths",
|
||||||
|
|
@ -61,3 +62,28 @@ subscription_parser.add_argument(
|
||||||
###################################################################################################
|
###################################################################################################
|
||||||
# DOWNLOAD PARSER
|
# DOWNLOAD PARSER
|
||||||
download_parser = subparsers.add_parser("dl")
|
download_parser = subparsers.add_parser("dl")
|
||||||
|
###################################################################################################
|
||||||
|
# VIEW PARSER
|
||||||
|
|
||||||
|
|
||||||
|
class ViewArgs(Enum):
|
||||||
|
SPLIT_CHAPTERS = "--split-chapters"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List of all args used in main CLI
|
||||||
|
"""
|
||||||
|
return list(map(lambda arg: arg.value, cls))
|
||||||
|
|
||||||
|
|
||||||
|
view_parser = subparsers.add_parser("view")
|
||||||
|
view_parser.add_argument(
|
||||||
|
"-sc",
|
||||||
|
ViewArgs.SPLIT_CHAPTERS.value,
|
||||||
|
action="store_true",
|
||||||
|
help="View source variables after splitting by chapters",
|
||||||
|
)
|
||||||
|
view_parser.add_argument("url", help="URL to view source variables for")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from ytdl_sub.plugins.audio_extract import AudioExtractPlugin
|
||||||
from ytdl_sub.plugins.chapters import ChaptersPlugin
|
from ytdl_sub.plugins.chapters import ChaptersPlugin
|
||||||
from ytdl_sub.plugins.date_range import DateRangePlugin
|
from ytdl_sub.plugins.date_range import DateRangePlugin
|
||||||
from ytdl_sub.plugins.file_convert import FileConvertPlugin
|
from ytdl_sub.plugins.file_convert import FileConvertPlugin
|
||||||
|
from ytdl_sub.plugins.internal.view import ViewPlugin
|
||||||
from ytdl_sub.plugins.music_tags import MusicTagsPlugin
|
from ytdl_sub.plugins.music_tags import MusicTagsPlugin
|
||||||
from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin
|
from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin
|
||||||
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
|
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
|
||||||
|
|
@ -116,6 +117,7 @@ class PluginMapping:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_MAPPING: Dict[str, Type[Plugin]] = {
|
_MAPPING: Dict[str, Type[Plugin]] = {
|
||||||
|
"_view": ViewPlugin,
|
||||||
"audio_extract": AudioExtractPlugin,
|
"audio_extract": AudioExtractPlugin,
|
||||||
"date_range": DateRangePlugin,
|
"date_range": DateRangePlugin,
|
||||||
"file_convert": FileConvertPlugin,
|
"file_convert": FileConvertPlugin,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from datetime import datetime
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from yt_dlp.utils import sanitize_filename
|
from yt_dlp.utils import sanitize_filename
|
||||||
|
|
@ -447,9 +448,9 @@ class EntryVariables(BaseEntryVariables):
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
str
|
str
|
||||||
The entry's uploaded date, in YYYYMMDD format.
|
The entry's uploaded date, in YYYYMMDD format. If not present, return today's date.
|
||||||
"""
|
"""
|
||||||
return self.kwargs(UPLOAD_DATE)
|
return self.kwargs_get(UPLOAD_DATE, datetime.now().strftime("%Y%m%d"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def upload_year(self: Self) -> int:
|
def upload_year(self: Self) -> int:
|
||||||
|
|
|
||||||
0
src/ytdl_sub/plugins/internal/__init__.py
Normal file
0
src/ytdl_sub/plugins/internal/__init__.py
Normal file
62
src/ytdl_sub/plugins/internal/view.py
Normal file
62
src/ytdl_sub/plugins/internal/view.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import copy
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ytdl_sub.config.preset_options import Overrides
|
||||||
|
from ytdl_sub.entries.entry import Entry
|
||||||
|
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsOptions
|
||||||
|
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.file_handler import FileMetadata
|
||||||
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||||
|
|
||||||
|
|
||||||
|
class ViewOptions(PluginOptions):
|
||||||
|
"""
|
||||||
|
INTERNAL PLUGIN. Do not expose in documentation
|
||||||
|
"""
|
||||||
|
|
||||||
|
_optional_keys = {"_placeholder"}
|
||||||
|
|
||||||
|
|
||||||
|
class ViewPlugin(Plugin[ViewOptions]):
|
||||||
|
plugin_options_type = ViewOptions
|
||||||
|
priority: PluginPriority = PluginPriority(modify_entry=PluginPriority.MODIFY_ENTRY_AFTER_SPLIT)
|
||||||
|
|
||||||
|
_MAX_LINE_WIDTH: int = 80
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
plugin_options: OutputDirectoryNfoTagsOptions,
|
||||||
|
overrides: Overrides,
|
||||||
|
enhanced_download_archive: EnhancedDownloadArchive,
|
||||||
|
):
|
||||||
|
super().__init__(plugin_options, overrides, enhanced_download_archive)
|
||||||
|
self._first_entry: Optional[Entry] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _truncate_value(cls, value: str) -> str:
|
||||||
|
"""No new lines and cut off after 80 characters"""
|
||||||
|
value = " <newline> ".join(str(value).split("\n"))
|
||||||
|
if len(value) > cls._MAX_LINE_WIDTH:
|
||||||
|
value = value[:77] + "..."
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||||
|
"""Only process the first entry received"""
|
||||||
|
if self._first_entry is None:
|
||||||
|
self._first_entry = entry
|
||||||
|
return entry
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]:
|
||||||
|
"""
|
||||||
|
Adds all source variables to the entry
|
||||||
|
"""
|
||||||
|
source_var_dict = copy.deepcopy(entry.to_dict())
|
||||||
|
for key in source_var_dict.keys():
|
||||||
|
source_var_dict[key] = self._truncate_value(source_var_dict[key])
|
||||||
|
|
||||||
|
return FileMetadata.from_dict(title="Source Variables", value_dict=source_var_dict)
|
||||||
|
|
@ -56,7 +56,7 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
||||||
plugin_options_type: Type[PluginOptionsT] = NotImplemented
|
plugin_options_type: Type[PluginOptionsT] = NotImplemented
|
||||||
priority: PluginPriority = PluginPriority()
|
priority: PluginPriority = PluginPriority()
|
||||||
|
|
||||||
# If the plugin creates multile entries from a single entry
|
# If the plugin creates multiple entries from a single entry
|
||||||
is_split_plugin: bool = False
|
is_split_plugin: bool = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -95,7 +95,7 @@ class Plugin(DownloadArchiver, Generic[PluginOptionsT], ABC):
|
||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def modify_entry(self, entry: Entry) -> Optional[Entry | Tuple[Entry, FileMetadata]]:
|
def modify_entry(self, entry: Entry) -> Optional[Entry]:
|
||||||
"""
|
"""
|
||||||
For each entry downloaded, modify the entry in some way before sending it to
|
For each entry downloaded, modify the entry in some way before sending it to
|
||||||
post-processing.
|
post-processing.
|
||||||
|
|
|
||||||
0
src/ytdl_sub/prebuilt_presets/internal/__init__.py
Normal file
0
src/ytdl_sub/prebuilt_presets/internal/__init__.py
Normal file
20
src/ytdl_sub/prebuilt_presets/internal/view.yaml
Normal file
20
src/ytdl_sub/prebuilt_presets/internal/view.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
presets:
|
||||||
|
|
||||||
|
_view:
|
||||||
|
download:
|
||||||
|
download_strategy: "url"
|
||||||
|
url: "{url}"
|
||||||
|
output_options:
|
||||||
|
output_directory: "/tmp/ytdl-sub-view"
|
||||||
|
file_name: "{uid}.{ext}"
|
||||||
|
thumbnail_name: "{uid}.{thumbnail_ext}"
|
||||||
|
ytdl_options:
|
||||||
|
max_downloads: 2
|
||||||
|
_view:
|
||||||
|
_placeholder: ""
|
||||||
|
|
||||||
|
_view_split_chapters:
|
||||||
|
preset:
|
||||||
|
- "_view"
|
||||||
|
split_by_chapters:
|
||||||
|
when_no_chapters: "pass"
|
||||||
|
|
@ -217,7 +217,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
||||||
|
|
||||||
# If split_entry is None from modify_entry, do not post process
|
# If split_entry is None from modify_entry, do not post process
|
||||||
if split_entry:
|
if split_entry:
|
||||||
self._process_entry(
|
self._post_process_entry(
|
||||||
plugins=plugins,
|
plugins=plugins,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
entry=split_entry,
|
entry=split_entry,
|
||||||
|
|
|
||||||
0
tests/e2e/plugins/internal/__init__.py
Normal file
0
tests/e2e/plugins/internal/__init__.py
Normal file
33
tests/e2e/plugins/internal/test_view.py
Normal file
33
tests/e2e/plugins/internal/test_view.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import pytest
|
||||||
|
from e2e.conftest import mock_run_from_cli
|
||||||
|
|
||||||
|
|
||||||
|
class TestView:
|
||||||
|
@pytest.mark.parametrize("split_chapters", [True, False])
|
||||||
|
def test_view_from_cli(
|
||||||
|
self,
|
||||||
|
music_video_config_path,
|
||||||
|
output_directory,
|
||||||
|
split_chapters,
|
||||||
|
):
|
||||||
|
|
||||||
|
args = f"--config {music_video_config_path} view "
|
||||||
|
args += "--split-chapters " if split_chapters else ""
|
||||||
|
args += f"https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i"
|
||||||
|
subscription_transaction_log = mock_run_from_cli(args=args)
|
||||||
|
|
||||||
|
assert len(subscription_transaction_log) == 1
|
||||||
|
transaction_log = subscription_transaction_log[0][1]
|
||||||
|
|
||||||
|
# Ensure the video and thumbnail are recognized
|
||||||
|
assert len(transaction_log.files_created) == 2
|
||||||
|
|
||||||
|
file_name = f"avUT-zd9v68{'___0' if split_chapters else ''}.webm"
|
||||||
|
video_file = transaction_log.files_created.get(file_name)
|
||||||
|
assert video_file is not None
|
||||||
|
|
||||||
|
assert "Source Variables:" in video_file.metadata
|
||||||
|
if split_chapters:
|
||||||
|
assert " chapter_index: 1" in video_file.metadata
|
||||||
|
else:
|
||||||
|
assert " chapter_index: 1" not in video_file.metadata
|
||||||
|
|
@ -68,7 +68,7 @@ class TestChapters:
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
expected_download_summary_file_name="plugins/test_chapters_sb_and_embedded_subs.json",
|
expected_download_summary_file_name="plugins/test_chapters_sb_and_embedded_subs.json",
|
||||||
ignore_md5_hashes_for=[
|
ignore_md5_hashes_for=[
|
||||||
"JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4"
|
"JMC/JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -107,6 +107,6 @@ class TestChapters:
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
expected_download_summary_file_name="plugins/test_chapters_from_ts_with_subs.json",
|
expected_download_summary_file_name="plugins/test_chapters_from_ts_with_subs.json",
|
||||||
ignore_md5_hashes_for=[
|
ignore_md5_hashes_for=[
|
||||||
"JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4"
|
"JMC/JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue