322 lines
10 KiB
Python
322 lines
10 KiB
Python
import os
|
|
import posixpath
|
|
from typing import Any, Dict, Optional
|
|
|
|
from mergedeep import mergedeep
|
|
from yt_dlp.utils import datetime_from_str
|
|
|
|
from ytdl_sub.config.defaults import (
|
|
DEFAULT_FFMPEG_PATH,
|
|
DEFAULT_FFPROBE_PATH,
|
|
DEFAULT_LOCK_DIRECTORY,
|
|
MAX_FILE_NAME_BYTES,
|
|
)
|
|
from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS
|
|
from ytdl_sub.utils.exceptions import SubscriptionPermissionError
|
|
from ytdl_sub.utils.file_handler import FileHandler
|
|
from ytdl_sub.validators.file_path_validators import FFmpegFileValidator, FFprobeFileValidator
|
|
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
|
from ytdl_sub.validators.validators import (
|
|
BoolValidator,
|
|
IntValidator,
|
|
LiteralDictValidator,
|
|
StringValidator,
|
|
)
|
|
|
|
|
|
class ExperimentalValidator(StrictDictValidator):
|
|
"""
|
|
Experimental flags reside under the ``experimental`` key.
|
|
"""
|
|
|
|
_optional_keys = {"enable_update_with_info_json"}
|
|
_allow_extra_keys = True
|
|
|
|
def __init__(self, name: str, value: Any):
|
|
super().__init__(name, value)
|
|
|
|
self._enable_update_with_info_json = self._validate_key(
|
|
key="enable_update_with_info_json", validator=BoolValidator, default=False
|
|
)
|
|
|
|
@property
|
|
def enable_update_with_info_json(self) -> bool:
|
|
"""
|
|
Enables modifying subscription files using info.json files using the argument
|
|
``--update-with-info-json``. This feature is still being tested and has the ability to
|
|
destroy files. Ensure you have a full backup before usage. You have been warned!
|
|
"""
|
|
return self._enable_update_with_info_json.value
|
|
|
|
|
|
class PersistLogsValidator(StrictDictValidator):
|
|
"""
|
|
By default, no logs are persisted. Specifying this key will enable persisted logs. The following
|
|
options are available.
|
|
"""
|
|
|
|
_required_keys = {"logs_directory"}
|
|
_optional_keys = {"keep_logs_after", "keep_successful_logs"}
|
|
|
|
def __init__(self, name: str, value: Any):
|
|
super().__init__(name, value)
|
|
|
|
self._logs_directory = self._validate_key(key="logs_directory", validator=StringValidator)
|
|
|
|
self._keep_logs_after: Optional[str] = None
|
|
if keep_logs_validator := self._validate_key_if_present(
|
|
key="keep_logs_after", validator=StringValidator
|
|
):
|
|
try:
|
|
self._keep_logs_after = datetime_from_str(keep_logs_validator.value)
|
|
except Exception as exc:
|
|
raise self._validation_exception(f"Invalid datetime string: {str(exc)}")
|
|
|
|
self._keep_successful_logs = self._validate_key(
|
|
key="keep_successful_logs", validator=BoolValidator, default=True
|
|
)
|
|
|
|
@property
|
|
def logs_directory(self) -> str:
|
|
"""
|
|
Required field. Write log files to this directory with names like
|
|
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
|
|
"""
|
|
return self._logs_directory.value
|
|
|
|
# pylint: disable=line-too-long
|
|
# @property
|
|
# def keep_logs_after(self) -> Optional[str]:
|
|
# """
|
|
# Optional. Keep logs after this date, in yt-dlp datetime format.
|
|
#
|
|
# .. code-block:: Markdown
|
|
#
|
|
# A string in the format YYYYMMDD or
|
|
# (now|today|yesterday|date)[+-][0-9](microsecond|second|minute|hour|day|week|month|year)(s)
|
|
#
|
|
# For example, ``today-1week`` means keep 1 week's worth of logs. By default, ytdl-sub will
|
|
# keep all log files.
|
|
# """
|
|
# return self._keep_logs_after
|
|
|
|
# pylint: enable=line-too-long
|
|
|
|
@property
|
|
def keep_successful_logs(self) -> bool:
|
|
"""
|
|
Defaults to ``True``. When this key is ``False``, only write log files for failed
|
|
subscriptions.
|
|
"""
|
|
return self._keep_successful_logs.value
|
|
|
|
|
|
class ConfigOptions(StrictDictValidator):
|
|
"""
|
|
ytdl-sub is configured using a ``config.yaml`` file.
|
|
|
|
The ``config.yaml`` is made up of two sections:
|
|
|
|
.. code-block:: yaml
|
|
|
|
configuration:
|
|
presets:
|
|
|
|
|
|
Note for Windows users, paths can be represented with ``C:/forward/slashes/like/linux``.
|
|
If you prefer to use a Windows backslash, note that it must have
|
|
``C:\\\\double\\\\bashslash\\\\paths`` in order to escape the backslash character. This is due
|
|
to it being a YAML escape character.
|
|
|
|
.. code-block:: yaml
|
|
|
|
configuration:
|
|
dl_aliases:
|
|
mv: "--preset music_video"
|
|
u: "--download.url"
|
|
|
|
experimental:
|
|
enable_update_with_info_json: True
|
|
|
|
ffmpeg_path: "/usr/bin/ffmpeg"
|
|
ffprobe_path: "/usr/bin/ffprobe"
|
|
|
|
file_name_max_bytes: 255
|
|
lock_directory: "/tmp"
|
|
|
|
persist_logs:
|
|
keep_successful_logs: True
|
|
logs_directory: "/var/log/ytdl-sub-logs"
|
|
|
|
umask: "022"
|
|
working_directory: ".ytdl-sub-working-directory"
|
|
|
|
"""
|
|
|
|
_optional_keys = {
|
|
"working_directory",
|
|
"umask",
|
|
"dl_aliases",
|
|
"persist_logs",
|
|
"lock_directory",
|
|
"ffmpeg_path",
|
|
"ffprobe_path",
|
|
"file_name_max_bytes",
|
|
"experimental",
|
|
}
|
|
|
|
def __init__(self, name: str, value: Any):
|
|
super().__init__(name, value)
|
|
|
|
self._working_directory = self._validate_key_if_present(
|
|
key="working_directory",
|
|
validator=StringValidator,
|
|
default=".ytdl-sub-working-directory",
|
|
)
|
|
self._umask = self._validate_key_if_present(
|
|
key="umask", validator=StringValidator, default="022"
|
|
)
|
|
self._dl_aliases = self._validate_key_if_present(
|
|
key="dl_aliases", validator=LiteralDictValidator
|
|
)
|
|
self._persist_logs = self._validate_key_if_present(
|
|
key="persist_logs", validator=PersistLogsValidator
|
|
)
|
|
self._lock_directory = self._validate_key(
|
|
key="lock_directory", validator=StringValidator, default=DEFAULT_LOCK_DIRECTORY
|
|
)
|
|
self._ffmpeg_path = self._validate_key(
|
|
key="ffmpeg_path", validator=FFmpegFileValidator, default=DEFAULT_FFMPEG_PATH
|
|
)
|
|
self._ffprobe_path = self._validate_key(
|
|
key="ffprobe_path", validator=FFprobeFileValidator, default=DEFAULT_FFPROBE_PATH
|
|
)
|
|
self._experimental = self._validate_key(
|
|
key="experimental", validator=ExperimentalValidator, default={}
|
|
)
|
|
self._file_name_max_bytes = self._validate_key(
|
|
key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
|
|
)
|
|
|
|
if not FileHandler.is_path_writable(self.working_directory):
|
|
raise SubscriptionPermissionError(
|
|
"ytdl-sub does not have permissions to the working directory: "
|
|
f"{self.working_directory}"
|
|
)
|
|
|
|
@property
|
|
def working_directory(self) -> str:
|
|
"""
|
|
The directory to temporarily store downloaded files before moving them into their final
|
|
directory. Defaults to ``.ytdl-sub-working-directory``, created in the same directory
|
|
that ytdl-sub is invoked from.
|
|
"""
|
|
# Expands tildas to actual paths, use native os sep
|
|
return os.path.expanduser(self._working_directory.value.replace(posixpath.sep, os.sep))
|
|
|
|
@property
|
|
def umask(self) -> Optional[str]:
|
|
"""
|
|
Umask in octal format to apply to every created file. Defaults to ``022``.
|
|
"""
|
|
return self._umask.value
|
|
|
|
@property
|
|
def dl_aliases(self) -> Optional[Dict[str, str]]:
|
|
"""
|
|
.. _dl_aliases:
|
|
|
|
Alias definitions to shorten :ref:`dl arguments <usage:Download Options>`. For example,
|
|
|
|
.. code-block:: yaml
|
|
|
|
configuration:
|
|
dl_aliases:
|
|
mv: "--preset music_video"
|
|
u: "--download.url"
|
|
|
|
Simplifies
|
|
|
|
.. code-block:: bash
|
|
|
|
ytdl-sub dl --preset "Jellyfin Music Videos" --download.url "youtube.com/watch?v=a1b2c3"
|
|
|
|
to
|
|
|
|
.. code-block:: bash
|
|
|
|
ytdl-sub dl --mv --u "youtube.com/watch?v=a1b2c3"
|
|
"""
|
|
if self._dl_aliases:
|
|
return self._dl_aliases.dict
|
|
return {}
|
|
|
|
@property
|
|
def persist_logs(self) -> Optional[PersistLogsValidator]:
|
|
"""
|
|
Persist logs validator. readthedocs in the validator itself!
|
|
"""
|
|
return self._persist_logs
|
|
|
|
@property
|
|
def file_name_max_bytes(self) -> int:
|
|
"""
|
|
Max file name size in bytes. Most OS's typically default to 255 bytes.
|
|
"""
|
|
return self._file_name_max_bytes.value
|
|
|
|
@property
|
|
def experimental(self) -> ExperimentalValidator:
|
|
"""
|
|
Experimental validator. readthedocs in the validator itself!
|
|
"""
|
|
return self._experimental
|
|
|
|
@property
|
|
def lock_directory(self) -> str:
|
|
"""
|
|
The directory to temporarily store file locks, which prevents multiple instances
|
|
of ``ytdl-sub`` from running. Note that file locks do not work on
|
|
network-mounted directories. Ensure that this directory resides on the host
|
|
machine. Defaults to ``/tmp``.
|
|
"""
|
|
return self._lock_directory.value
|
|
|
|
@property
|
|
def ffmpeg_path(self) -> str:
|
|
"""
|
|
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux,
|
|
``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows.
|
|
"""
|
|
return self._ffmpeg_path.value
|
|
|
|
@property
|
|
def ffprobe_path(self) -> str:
|
|
"""
|
|
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux,
|
|
``./ffprobe.exe`` in the same directory as ytdl-sub for Windows.
|
|
"""
|
|
return self._ffprobe_path.value
|
|
|
|
|
|
class ConfigValidator(StrictDictValidator):
|
|
_optional_keys = {"configuration", "presets"}
|
|
|
|
def __init__(self, name: str, value: Any):
|
|
super().__init__(name, value)
|
|
self.config_options = self._validate_key_if_present(
|
|
"configuration", ConfigOptions, default={}
|
|
)
|
|
|
|
# Make sure presets is a dictionary. Will be validated in `PresetValidator`
|
|
self.presets = self._validate_key_if_present("presets", LiteralDictValidator, default={})
|
|
|
|
# Ensure custom presets do not collide with prebuilt presets
|
|
for preset_name in self.presets.keys:
|
|
if preset_name in PREBUILT_PRESETS:
|
|
raise self._validation_exception(
|
|
f"preset name '{preset_name}' conflicts with a prebuilt preset"
|
|
)
|
|
|
|
# Merge prebuilt presets into the config so custom presets can use them
|
|
mergedeep.merge(self.presets._value, PREBUILT_PRESETS)
|