305 lines
10 KiB
Python
305 lines
10 KiB
Python
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
import coloredlogs
|
|
from .version import APP_VERSION
|
|
from dotenv import load_dotenv
|
|
from yt_dlp.version import __version__ as YTDLP_VERSION
|
|
from .Utils import load_file
|
|
from pathlib import Path
|
|
|
|
|
|
class Config:
|
|
__instance = None
|
|
config_path: str = '.'
|
|
download_path: str = '.'
|
|
temp_path: str = '/tmp'
|
|
temp_keep: bool = False
|
|
db_file: str = '{config_path}/ytptube.db'
|
|
manual_archive: str = '{config_path}/archive.manual.log'
|
|
|
|
url_host: str = ''
|
|
url_prefix: str = ''
|
|
url_socketio: str = '{url_prefix}socket.io'
|
|
|
|
output_template: str = '%(title)s.%(ext)s'
|
|
output_template_chapter: str = '%(title)s - %(section_number)s %(section_title)s.%(ext)s'
|
|
|
|
ytdl_options: dict | str = {}
|
|
ytdl_debug: bool = False
|
|
|
|
host: str = '0.0.0.0'
|
|
port: int = 8081
|
|
|
|
keep_archive: bool = True
|
|
|
|
base_path: str = ''
|
|
|
|
log_level: str = 'info'
|
|
|
|
allow_manifestless: bool = False
|
|
|
|
max_workers: int = 1
|
|
|
|
version: str = APP_VERSION
|
|
|
|
debug: bool = False
|
|
|
|
debugpy_port: int = 5678
|
|
|
|
new_version_available: bool = False
|
|
|
|
extract_info_timeout: int = 70
|
|
|
|
socket_timeout: int = 30
|
|
|
|
started: int = 0
|
|
|
|
streamer_vcodec = 'libx264'
|
|
streamer_acodec = 'aac'
|
|
|
|
auth_username: str = None
|
|
auth_password: str = None
|
|
|
|
ytdlp_version: str = YTDLP_VERSION
|
|
tasks: list = []
|
|
presets: list = [
|
|
{'name': 'default', 'format': 'default', 'postprocessors': [],
|
|
'args': {}},
|
|
{'name': 'Video - 1080p h264/acc', 'format': 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]',
|
|
'args': {'format_sort': ['vcodec:h264'], }, },
|
|
{'name': 'Video - 720p h264/acc', 'format': 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]',
|
|
'args': {'format_sort': ['vcodec:h264'], }, },
|
|
{
|
|
'name': 'Audio - Best audio [MP3]',
|
|
'format': 'bestaudio/best',
|
|
'postprocessors': [
|
|
{
|
|
"key": "FFmpegExtractAudio",
|
|
"preferredcodec": "mp3",
|
|
"preferredquality": "5",
|
|
"nopostoverwrites": False
|
|
},
|
|
{
|
|
"key": "FFmpegMetadata",
|
|
"add_chapters": True,
|
|
"add_metadata": True,
|
|
"add_infojson": "if_exists"
|
|
},
|
|
{
|
|
"key": "EmbedThumbnail",
|
|
"already_have_thumbnail": False
|
|
},
|
|
{
|
|
"key": "FFmpegConcat",
|
|
"only_multi_video": True,
|
|
"when": "playlist"
|
|
}
|
|
],
|
|
'args': {
|
|
"outtmpl": {
|
|
"pl_thumbnail": ""
|
|
},
|
|
"ignoreerrors": "only_download",
|
|
"retries": 10,
|
|
"fragment_retries": 10,
|
|
"writethumbnail": True,
|
|
"extract_flat": "discard_in_playlist",
|
|
"final_ext": "mp3",
|
|
}
|
|
}
|
|
]
|
|
|
|
_manual_vars: tuple = ('temp_path', 'config_path', 'download_path',)
|
|
_immutable: tuple = (
|
|
'version', '__instance', 'ytdl_options', 'tasks',
|
|
'new_version_available', 'ytdlp_version', 'started', 'presets',
|
|
)
|
|
|
|
_int_vars: tuple = ('port', 'max_workers', 'socket_timeout', 'extract_info_timeout', 'debugpy_port',)
|
|
_boolean_vars: tuple = ('keep_archive', 'ytdl_debug', 'debug', 'temp_keep', 'allow_manifestless',)
|
|
|
|
_frontend_vars: tuple = (
|
|
'download_path', 'keep_archive', 'output_template',
|
|
'ytdlp_version', 'version', 'url_host', 'started', 'url_prefix',
|
|
)
|
|
|
|
@ staticmethod
|
|
def get_instance():
|
|
""" Static access method. """
|
|
return Config() if not Config.__instance else Config.__instance
|
|
|
|
def __init__(self):
|
|
""" Virtually private constructor. """
|
|
if Config.__instance is not None:
|
|
raise Exception("This class is a singleton. Use Config.get_instance() instead.")
|
|
else:
|
|
Config.__instance = self
|
|
|
|
baseDefaultPath: str = str(Path(__file__).parent.parent.parent.absolute())
|
|
|
|
self.temp_path = os.environ.get('YTP_TEMP_PATH', None) or os.path.join(baseDefaultPath, 'var', 'tmp')
|
|
self.config_path = os.environ.get('YTP_CONFIG_PATH', None) or os.path.join(baseDefaultPath, 'var', 'config')
|
|
self.download_path = os.environ.get(
|
|
'YTP_DOWNLOAD_PATH', None) or os.path.join(
|
|
baseDefaultPath, 'var', 'downloads')
|
|
|
|
envFile: str = os.path.join(self.config_path, '.env')
|
|
|
|
if os.path.exists(envFile):
|
|
logging.info(f"Loading environment variables from '{envFile}'.")
|
|
load_dotenv(envFile)
|
|
|
|
for k, v in self._getAttributes().items():
|
|
if k.startswith('_') or k in self._manual_vars:
|
|
continue
|
|
|
|
# If the variable declared as immutable, set it to the default value.
|
|
if k in self._immutable:
|
|
setattr(self, k, v)
|
|
continue
|
|
|
|
lookUpKey: str = f'YTP_{k}'.upper()
|
|
setattr(self, k, os.environ[lookUpKey] if lookUpKey in os.environ else v)
|
|
|
|
for k, v in self.__dict__.items():
|
|
if k.startswith('_') or k in self._immutable or k in self._manual_vars:
|
|
continue
|
|
|
|
if isinstance(v, str) and '{' in v and '}' in v:
|
|
for key in re.findall(r'\{.*?\}', v):
|
|
localKey: str = key[1:-1]
|
|
if localKey not in self.__dict__:
|
|
logging.error(f"Config variable '{k}' had non-existing config reference '{key}'.")
|
|
sys.exit(1)
|
|
|
|
v = v.replace(key, getattr(self, localKey))
|
|
|
|
setattr(self, k, v)
|
|
|
|
if k in self._boolean_vars:
|
|
if str(v).lower() not in (True, False, 'true', 'false', 'on', 'off', '1', '0'):
|
|
raise ValueError(f"Config variable '{k}' is set to a non-boolean value '{v}'.")
|
|
|
|
setattr(self, k, str(v).lower() in (True, 'true', 'on', '1'))
|
|
|
|
if k in self._int_vars:
|
|
setattr(self, k, int(v))
|
|
|
|
if not self.url_prefix.endswith('/'):
|
|
self.url_prefix += '/'
|
|
|
|
numeric_level = getattr(logging, self.log_level.upper(), None)
|
|
if not isinstance(numeric_level, int):
|
|
raise ValueError(f"Invalid log level '{self.log_level}' specified.")
|
|
|
|
coloredlogs.install(
|
|
level=numeric_level,
|
|
fmt="%(asctime)s [%(name)s] [%(levelname)-5.5s] %(message)s",
|
|
datefmt='%H:%M:%S'
|
|
)
|
|
|
|
LOG = logging.getLogger('config')
|
|
|
|
if self.debug:
|
|
try:
|
|
import debugpy
|
|
debugpy.listen(("0.0.0.0", self.debugpy_port))
|
|
LOG.info(f"starting debugpy server on '0.0.0.0:{self.debugpy_port}'.")
|
|
except ImportError:
|
|
LOG.error("debugpy package not found, please install it with 'pip install debugpy'.")
|
|
except Exception as e:
|
|
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {e}")
|
|
|
|
optsFile: str = os.path.join(self.config_path, 'ytdlp.json')
|
|
if os.path.exists(optsFile) and os.path.getsize(optsFile) > 4:
|
|
LOG.info(f"Loading yt-dlp custom options from '{optsFile}'.")
|
|
|
|
(opts, status, error) = load_file(optsFile, dict)
|
|
if not status:
|
|
LOG.error(f"Could not load yt-dlp custom options from '{optsFile}'. {error}")
|
|
sys.exit(1)
|
|
|
|
self.ytdl_options.update(opts)
|
|
else:
|
|
LOG.info(f"No yt-dlp custom options found at '{optsFile}'.")
|
|
|
|
tasksFile = os.path.join(self.config_path, 'tasks.json')
|
|
if os.path.exists(tasksFile) and os.path.getsize(tasksFile) > 0:
|
|
LOG.info(f"Loading tasks from '{tasksFile}'.")
|
|
try:
|
|
(tasks, status, error) = load_file(tasksFile, list)
|
|
if not status:
|
|
LOG.error(f"Could not load tasks file from '{tasksFile}'. '{error}'.")
|
|
sys.exit(1)
|
|
self.tasks.extend(tasks)
|
|
except Exception as e:
|
|
pass
|
|
|
|
presetsFile = os.path.join(self.config_path, 'presets.json')
|
|
if os.path.exists(presetsFile) and os.path.getsize(presetsFile) > 0:
|
|
LOG.info(f"Loading extra presets from '{presetsFile}'.")
|
|
try:
|
|
(presets, status, error) = load_file(presetsFile, list)
|
|
if not status:
|
|
LOG.error(f"Could not load presets file from '{presetsFile}'. '{error}'.")
|
|
sys.exit(1)
|
|
|
|
for preset in presets:
|
|
if 'name' not in preset:
|
|
LOG.error(f"Missing 'name' key in preset '{preset}'.")
|
|
continue
|
|
|
|
if 'format' not in preset:
|
|
LOG.error(f"Missing 'format' key in preset '{preset}'.")
|
|
continue
|
|
|
|
if 'args' not in preset:
|
|
preset['args'] = {}
|
|
|
|
if 'postprocessors' not in preset:
|
|
preset['postprocessors'] = []
|
|
|
|
self.presets.append(preset)
|
|
except Exception as e:
|
|
pass
|
|
|
|
self.ytdl_options['socket_timeout'] = self.socket_timeout
|
|
|
|
if self.keep_archive:
|
|
LOG.info(f"keep archive option is enabled.")
|
|
self.ytdl_options['download_archive'] = os.path.join(self.config_path, 'archive.log')
|
|
|
|
if self.temp_keep:
|
|
LOG.info(f'Keep temp files option is enabled.')
|
|
|
|
if self.auth_password and self.auth_username:
|
|
LOG.warning(f"Basic authentication enabled with username '{self.auth_username}'.")
|
|
|
|
self.started = time.time()
|
|
|
|
def _getAttributes(self) -> dict:
|
|
attrs: dict = {}
|
|
vClass: str = self.__class__
|
|
|
|
for attribute in vClass.__dict__.keys():
|
|
if attribute.startswith('_'):
|
|
continue
|
|
|
|
value = getattr(vClass, attribute)
|
|
if not callable(value):
|
|
attrs[attribute] = value
|
|
|
|
return attrs
|
|
|
|
def frontend(self) -> dict:
|
|
"""
|
|
Returns configuration variables relevant to the frontend.
|
|
|
|
Returns:
|
|
dict: A dictionary with the frontend configuration
|
|
"""
|
|
|
|
return {k: getattr(self, k) for k in self._frontend_vars}
|