commit
98970da663
9 changed files with 91 additions and 42 deletions
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
|
|
@ -35,12 +35,7 @@
|
||||||
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
|
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
|
||||||
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
|
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
|
||||||
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
||||||
"YTP_IGNORE_UI": "true",
|
"YTP_IGNORE_UI": "true"
|
||||||
"YTP_PIP_IGNORE_UPDATES": "true",
|
|
||||||
"YTP_LOG_LEVEL": "DEBUG",
|
|
||||||
"YTP_DEBUG": "true",
|
|
||||||
"YTP_YTDL_DEBUG": "false",
|
|
||||||
"YTP_ACCESS_LOG": "true",
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -17,10 +17,13 @@
|
||||||
"aiocron",
|
"aiocron",
|
||||||
"arrowless",
|
"arrowless",
|
||||||
"copyts",
|
"copyts",
|
||||||
|
"daterange",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"finaldir",
|
"finaldir",
|
||||||
"getpid",
|
"getpid",
|
||||||
|
"gpac",
|
||||||
"httpx",
|
"httpx",
|
||||||
|
"levelno",
|
||||||
"libcurl",
|
"libcurl",
|
||||||
"libx",
|
"libx",
|
||||||
"matroska",
|
"matroska",
|
||||||
|
|
|
||||||
13
Dockerfile
13
Dockerfile
|
|
@ -13,7 +13,7 @@ ENV PYTHONFAULTHANDLER=1
|
||||||
|
|
||||||
# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows)
|
# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows)
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN apk add --update coreutils curl gcc g++ musl-dev libffi-dev openssl-dev && pip install pipenv
|
RUN apk add --update coreutils curl gcc g++ musl-dev libffi-dev openssl-dev curl make && pip install pipenv
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
@ -21,6 +21,13 @@ ARG PIPENV_FLAGS="--deploy"
|
||||||
COPY ./Pipfile* .
|
COPY ./Pipfile* .
|
||||||
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install ${PIPENV_FLAGS}
|
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install ${PIPENV_FLAGS}
|
||||||
|
|
||||||
|
# Bundle mp4box into the image
|
||||||
|
WORKDIR /tmp
|
||||||
|
RUN mkdir /gpac-master && curl -k -L https://github.com/gpac/gpac/archive/refs/tags/v2.4.0.zip -o /tmp/gpac.zip && \
|
||||||
|
unzip /tmp/gpac.zip && mv /tmp/gpac-*/* /gpac-master
|
||||||
|
WORKDIR /gpac-master
|
||||||
|
RUN ./configure --static-mp4box --use-zlib=no && make -j4
|
||||||
|
|
||||||
FROM python:3.11-alpine
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
ARG TZ=UTC
|
ARG TZ=UTC
|
||||||
|
|
@ -46,13 +53,15 @@ RUN sed -i 's/\r$//g' /entrypoint.sh && chmod +x /entrypoint.sh
|
||||||
COPY --chown=app:app ./app /app/app
|
COPY --chown=app:app ./app /app/app
|
||||||
COPY --chown=app:app --from=node_builder /app/exported /app/ui/exported
|
COPY --chown=app:app --from=node_builder /app/exported /app/ui/exported
|
||||||
COPY --chown=app:app --from=python_builder /app/.venv /opt/python
|
COPY --chown=app:app --from=python_builder /app/.venv /opt/python
|
||||||
|
COPY --chown=app:app --from=python_builder /gpac-master/bin/gcc/MP4Box /usr/bin/mp4box
|
||||||
COPY --chown=app:app ./healthcheck.sh /usr/local/bin/healthcheck
|
COPY --chown=app:app ./healthcheck.sh /usr/local/bin/healthcheck
|
||||||
|
|
||||||
ENV PATH="/opt/python/bin:$PATH"
|
ENV PATH="/opt/python/bin:$PATH"
|
||||||
|
|
||||||
RUN chown -R app:app /config /downloads && chmod +x /usr/local/bin/healthcheck && \
|
RUN chown -R app:app /config /downloads && chmod +x /usr/local/bin/healthcheck && \
|
||||||
sed -i 's$#!\/app\/\.venv\/bin\/python$#!/opt/python/bin/python$' /opt/python/bin/* && \
|
sed -i 's$#!\/app\/\.venv\/bin\/python$#!/opt/python/bin/python$' /opt/python/bin/* && \
|
||||||
sed -i "s%'\/app\/\.venv'%'/opt/python'%" /opt/python/bin/activate*
|
sed -i "s%'\/app\/\.venv'%'/opt/python'%" /opt/python/bin/activate* && \
|
||||||
|
chmod +x /usr/bin/mp4box
|
||||||
|
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
VOLUME /downloads
|
VOLUME /downloads
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since
|
||||||
* Basic Authentication support.
|
* Basic Authentication support.
|
||||||
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
|
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
|
||||||
* Support for both advanced and basic mode for WebUI.
|
* Support for both advanced and basic mode for WebUI.
|
||||||
|
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
||||||
|
|
||||||
For more API endpoints, please refer to the [API documentation](API.md).
|
For more API endpoints, please refer to the [API documentation](API.md).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import hashlib
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
|
|
@ -16,7 +17,24 @@ from .ffprobe import ffprobe
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
from .YTDLPOpts import YTDLPOpts
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
LOG = logging.getLogger("download")
|
LOG = logging.getLogger("Download")
|
||||||
|
|
||||||
|
|
||||||
|
class NestedLogger:
|
||||||
|
debug_messages = ["[debug] ", "[download] "]
|
||||||
|
|
||||||
|
def __init__(self, logger: logging.Logger):
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def debug(self, msg: str):
|
||||||
|
levelno = logging.DEBUG if any(msg.startswith(x) for x in self.debug_messages) else logging.INFO
|
||||||
|
self.logger.log(level=levelno, msg=re.sub(r"^\[(debug|info)\] ", "", msg, flags=re.IGNORECASE))
|
||||||
|
|
||||||
|
def error(self, msg):
|
||||||
|
self.logger.error(msg)
|
||||||
|
|
||||||
|
def warning(self, msg):
|
||||||
|
self.logger.warning(msg)
|
||||||
|
|
||||||
|
|
||||||
class Download:
|
class Download:
|
||||||
|
|
@ -66,7 +84,7 @@ class Download:
|
||||||
temp_keep: bool = False
|
temp_keep: bool = False
|
||||||
"Keep temp folder after download."
|
"Keep temp folder after download."
|
||||||
|
|
||||||
def __init__(self, info: ItemDTO, info_dict: dict = None, debug: bool = False):
|
def __init__(self, info: ItemDTO, info_dict: dict = None):
|
||||||
config = Config.get_instance()
|
config = Config.get_instance()
|
||||||
|
|
||||||
self.download_dir = info.download_dir
|
self.download_dir = info.download_dir
|
||||||
|
|
@ -78,7 +96,8 @@ class Download:
|
||||||
self.info = info
|
self.info = info
|
||||||
self.id = info._id
|
self.id = info._id
|
||||||
self.default_ytdl_opts = config.ytdl_options
|
self.default_ytdl_opts = config.ytdl_options
|
||||||
self.debug = debug
|
self.debug = bool(config.debug)
|
||||||
|
self.debug_ytdl = bool(config.ytdl_debug)
|
||||||
self.cancelled = False
|
self.cancelled = False
|
||||||
self.tmpfilename = None
|
self.tmpfilename = None
|
||||||
self.status_queue = None
|
self.status_queue = None
|
||||||
|
|
@ -89,6 +108,7 @@ class Download:
|
||||||
self.is_live = bool(info.is_live) or info.live_in is not None
|
self.is_live = bool(info.is_live) or info.live_in is not None
|
||||||
self.is_manifestless = "is_manifestless" in self.info.options and self.info.options["is_manifestless"] is True
|
self.is_manifestless = "is_manifestless" in self.info.options and self.info.options["is_manifestless"] is True
|
||||||
self.info_dict = info_dict
|
self.info_dict = info_dict
|
||||||
|
self.logger = logging.getLogger(f"Download.{info.id if info.id else info._id}")
|
||||||
|
|
||||||
def _progress_hook(self, data: dict):
|
def _progress_hook(self, data: dict):
|
||||||
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
|
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
|
||||||
|
|
@ -99,14 +119,14 @@ class Download:
|
||||||
self.status_queue.put({"id": self.id, **dataDict})
|
self.status_queue.put({"id": self.id, **dataDict})
|
||||||
|
|
||||||
def _postprocessor_hook(self, data: dict):
|
def _postprocessor_hook(self, data: dict):
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug(f"Postprocessor hook: {data}")
|
||||||
|
|
||||||
if "MoveFiles" != data.get("postprocessor") or "finished" != data.get("status"):
|
if "MoveFiles" != data.get("postprocessor") or "finished" != data.get("status"):
|
||||||
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
|
dataDict = {k: v for k, v in data.items() if k in self._ytdlp_fields}
|
||||||
self.status_queue.put({"id": self.id, **dataDict, "status": "postprocessing"})
|
self.status_queue.put({"id": self.id, **dataDict, "status": "postprocessing"})
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.debug:
|
|
||||||
LOG.debug(f"Postprocessor hook: {data}")
|
|
||||||
|
|
||||||
if "__finaldir" in data["info_dict"]:
|
if "__finaldir" in data["info_dict"]:
|
||||||
filename = os.path.join(data["info_dict"]["__finaldir"], os.path.basename(data["info_dict"]["filepath"]))
|
filename = os.path.join(data["info_dict"]["__finaldir"], os.path.basename(data["info_dict"]["filepath"]))
|
||||||
else:
|
else:
|
||||||
|
|
@ -147,19 +167,21 @@ class Download:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.debug:
|
if self.debug_ytdl:
|
||||||
params["verbose"] = True
|
params["verbose"] = True
|
||||||
params["noprogress"] = False
|
params["noprogress"] = False
|
||||||
|
|
||||||
if self.info.cookies:
|
if self.info.cookies:
|
||||||
try:
|
try:
|
||||||
cookie_file = os.path.join(self.temp_path, f"cookie_{self.info._id}.txt")
|
cookie_file = os.path.join(self.temp_path, f"cookie_{self.info._id}.txt")
|
||||||
LOG.debug(f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'.")
|
self.logger.debug(
|
||||||
|
f"Creating cookie file for '{self.info.id}: {self.info.title}' - '{cookie_file}'."
|
||||||
|
)
|
||||||
with open(cookie_file, "w") as f:
|
with open(cookie_file, "w") as f:
|
||||||
f.write(self.info.cookies)
|
f.write(self.info.cookies)
|
||||||
params["cookiefile"] = f.name
|
params["cookiefile"] = f.name
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
LOG.error(f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'.")
|
self.logger.error(f"Failed to create cookie file for '{self.info.id}: {self.info.title}'. '{e!s}'.")
|
||||||
|
|
||||||
if self.is_live or self.is_manifestless:
|
if self.is_live or self.is_manifestless:
|
||||||
hasDeletedOptions = False
|
hasDeletedOptions = False
|
||||||
|
|
@ -171,37 +193,39 @@ class Download:
|
||||||
deletedOpts.append(opt)
|
deletedOpts.append(opt)
|
||||||
|
|
||||||
if hasDeletedOptions:
|
if hasDeletedOptions:
|
||||||
LOG.warning(
|
self.logger.warning(
|
||||||
f"Live stream detected for '{self.info.title}', The following opts '{deletedOpts=}' have been deleted which are known to cause issues with live stream and post stream manifestless mode."
|
f"Live stream detected for '{self.info.title}', The following opts '{deletedOpts=}' have been deleted which are known to cause issues with live stream and post stream manifestless mode."
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.info(
|
self.logger.info(
|
||||||
f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" preset="{self.preset}" started.'
|
f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" preset="{self.preset}" started.'
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.debug("Params before passing to yt-dlp.", extra=params)
|
self.logger.debug("Params before passing to yt-dlp.", extra=params)
|
||||||
|
|
||||||
if "impersonate" in params:
|
if "impersonate" in params:
|
||||||
from yt_dlp.networking.impersonate import ImpersonateTarget
|
from yt_dlp.networking.impersonate import ImpersonateTarget
|
||||||
|
|
||||||
params["impersonate"] = ImpersonateTarget.from_str(params["impersonate"])
|
params["impersonate"] = ImpersonateTarget.from_str(params["impersonate"])
|
||||||
|
|
||||||
|
params["logger"] = NestedLogger(self.logger)
|
||||||
|
|
||||||
cls = yt_dlp.YoutubeDL(params=params)
|
cls = yt_dlp.YoutubeDL(params=params)
|
||||||
|
|
||||||
if isinstance(self.info_dict, dict) and len(self.info_dict) > 1:
|
if isinstance(self.info_dict, dict) and len(self.info_dict) > 1:
|
||||||
LOG.debug(f"Downloading '{self.info.url}' using pre-info.")
|
self.logger.debug(f"Downloading '{self.info.url}' using pre-info.")
|
||||||
cls.process_ie_result(self.info_dict, download=True)
|
cls.process_ie_result(self.info_dict, download=True)
|
||||||
ret = cls._download_retcode
|
ret = cls._download_retcode
|
||||||
else:
|
else:
|
||||||
LOG.debug(f"Downloading using url: {self.info.url}")
|
self.logger.debug(f"Downloading using url: {self.info.url}")
|
||||||
ret = cls.download([self.info.url])
|
ret = cls.download([self.info.url])
|
||||||
|
|
||||||
self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"})
|
self.status_queue.put({"id": self.id, "status": "finished" if ret == 0 else "error"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOG.exception(exc)
|
self.logger.exception(exc)
|
||||||
self.status_queue.put({"id": self.id, "status": "error", "msg": str(exc), "error": str(exc)})
|
self.status_queue.put({"id": self.id, "status": "error", "msg": str(exc), "error": str(exc)})
|
||||||
|
|
||||||
LOG.info(f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" completed.')
|
self.logger.info(f'Task id="{self.info.id}" PID="{os.getpid()}" title="{self.info.title}" completed.')
|
||||||
|
|
||||||
async def start(self, emitter: Emitter):
|
async def start(self, emitter: Emitter):
|
||||||
self.emitter = emitter
|
self.emitter = emitter
|
||||||
|
|
@ -244,28 +268,28 @@ class Download:
|
||||||
self.cancel_in_progress = True
|
self.cancel_in_progress = True
|
||||||
procId = self.proc.ident
|
procId = self.proc.ident
|
||||||
|
|
||||||
LOG.info(f"Closing PID='{procId}' download process.")
|
self.logger.info(f"Closing PID='{procId}' download process.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
if "update_task" in self.__dict__ and self.update_task.done() is False:
|
if "update_task" in self.__dict__ and self.update_task.done() is False:
|
||||||
self.update_task.cancel()
|
self.update_task.cancel()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to close status queue: '{procId}'. {e}")
|
self.logger.error(f"Failed to close status queue: '{procId}'. {e}")
|
||||||
|
|
||||||
self.kill()
|
self.kill()
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
if self.proc.is_alive():
|
if self.proc.is_alive():
|
||||||
LOG.debug(f"Waiting for PID='{procId}' to close.")
|
self.logger.debug(f"Waiting for PID='{procId}' to close.")
|
||||||
await loop.run_in_executor(None, self.proc.join)
|
await loop.run_in_executor(None, self.proc.join)
|
||||||
LOG.debug(f"PID='{procId}' closed.")
|
self.logger.debug(f"PID='{procId}' closed.")
|
||||||
|
|
||||||
if self.status_queue:
|
if self.status_queue:
|
||||||
LOG.debug(f"Closing status queue for PID='{procId}'.")
|
self.logger.debug(f"Closing status queue for PID='{procId}'.")
|
||||||
self.status_queue.put(Terminator())
|
self.status_queue.put(Terminator())
|
||||||
LOG.debug(f"Closed status queue for PID='{procId}'.")
|
self.logger.debug(f"Closed status queue for PID='{procId}'.")
|
||||||
|
|
||||||
if self.proc:
|
if self.proc:
|
||||||
self.proc.close()
|
self.proc.close()
|
||||||
|
|
@ -273,11 +297,11 @@ class Download:
|
||||||
|
|
||||||
self.delete_temp()
|
self.delete_temp()
|
||||||
|
|
||||||
LOG.debug(f"Closed PID='{procId}' download process.")
|
self.logger.debug(f"Closed PID='{procId}' download process.")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to close process: '{procId}'. {e}")
|
self.logger.error(f"Failed to close process: '{procId}'. {e}")
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -295,11 +319,11 @@ class Download:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
LOG.info(f"Killing download process: '{self.proc.ident}'.")
|
self.logger.info(f"Killing download process: '{self.proc.ident}'.")
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to kill process: '{self.proc.ident}'. {e}")
|
self.logger.error(f"Failed to kill process: '{self.proc.ident}'. {e}")
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -308,7 +332,7 @@ class Download:
|
||||||
return
|
return
|
||||||
|
|
||||||
if "finished" != self.info.status and self.is_live:
|
if "finished" != self.info.status and self.is_live:
|
||||||
LOG.warning(
|
self.logger.warning(
|
||||||
f"Keeping live temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'."
|
f"Keeping live temp folder '{self.temp_path}', as the reported status is not finished '{self.info.status}'."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -317,12 +341,12 @@ class Download:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.temp_path == self.temp_dir:
|
if self.temp_path == self.temp_dir:
|
||||||
LOG.warning(
|
self.logger.warning(
|
||||||
f"Attempted to delete video temp folder: {self.temp_path}, but it is the same as main temp folder."
|
f"Attempted to delete video temp folder: {self.temp_path}, but it is the same as main temp folder."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
LOG.info(f"Deleting Temp folder '{self.temp_path}'.")
|
self.logger.info(f"Deleting Temp folder '{self.temp_path}'.")
|
||||||
shutil.rmtree(self.temp_path, ignore_errors=True)
|
shutil.rmtree(self.temp_path, ignore_errors=True)
|
||||||
|
|
||||||
async def progress_update(self):
|
async def progress_update(self):
|
||||||
|
|
@ -343,7 +367,7 @@ class Download:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
LOG.debug(f"Status Update: {self.info._id=} {status=}")
|
self.logger.debug(f"Status Update: {self.info._id=} {status=}")
|
||||||
|
|
||||||
if isinstance(status, str):
|
if isinstance(status, str):
|
||||||
asyncio.create_task(self.emitter.updated(dl=self.info), name=f"emitter-u-{self.id}")
|
asyncio.create_task(self.emitter.updated(dl=self.info), name=f"emitter-u-{self.id}")
|
||||||
|
|
@ -400,7 +424,7 @@ class Download:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.info.extras["is_video"] = True
|
self.info.extras["is_video"] = True
|
||||||
self.info.extras["is_audio"] = True
|
self.info.extras["is_audio"] = True
|
||||||
LOG.exception(e)
|
self.logger.exception(e)
|
||||||
LOG.error(f"Failed to ffprobe: {status.get}. {e}")
|
self.logger.error(f"Failed to ffprobe: {status.get}. {e}")
|
||||||
|
|
||||||
asyncio.create_task(self.emitter.updated(dl=self.info), name=f"emitter-u-{self.id}")
|
asyncio.create_task(self.emitter.updated(dl=self.info), name=f"emitter-u-{self.id}")
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
if property.startswith("playlist"):
|
if property.startswith("playlist"):
|
||||||
dl.template = str(dl.template).replace(f"%({property})s", str(value))
|
dl.template = str(dl.template).replace(f"%({property})s", str(value))
|
||||||
|
|
||||||
dlInfo: Download = Download(info=dl, info_dict=entry, debug=bool(self.config.ytdl_debug))
|
dlInfo: Download = Download(info=dl, info_dict=entry)
|
||||||
|
|
||||||
if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status"):
|
if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status"):
|
||||||
dlInfo.info.status = "not_live"
|
dlInfo.info.status = "not_live"
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,7 @@ def arg_converter(args: str) -> dict:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import yt_dlp.options
|
import yt_dlp.options
|
||||||
|
from yt_dlp.utils import DateRange
|
||||||
|
|
||||||
create_parser = yt_dlp.options.create_parser
|
create_parser = yt_dlp.options.create_parser
|
||||||
|
|
||||||
|
|
@ -443,7 +444,9 @@ def arg_converter(args: str) -> dict:
|
||||||
if "postprocessors" in diff:
|
if "postprocessors" in diff:
|
||||||
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]]
|
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]]
|
||||||
|
|
||||||
return json.loads(json.dumps(diff))
|
from .encoder import Encoder
|
||||||
|
|
||||||
|
return json.loads(json.dumps(diff, cls=Encoder))
|
||||||
|
|
||||||
|
|
||||||
def validate_uuid(uuid_str: str, version: int = 4) -> bool:
|
def validate_uuid(uuid_str: str, version: int = 4) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -132,4 +132,9 @@ class YTDLPOpts(metaclass=Singleton):
|
||||||
if "format" in data and data["format"] in ["not_set", "default"]:
|
if "format" in data and data["format"] in ["not_set", "default"]:
|
||||||
data["format"] = None
|
data["format"] = None
|
||||||
|
|
||||||
|
if "daterange" in data and isinstance(data["daterange"], dict):
|
||||||
|
from yt_dlp.utils import DateRange
|
||||||
|
|
||||||
|
data["daterange"] = DateRange(data["daterange"]["start"], data["daterange"]["end"])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import json
|
import json
|
||||||
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from yt_dlp.utils import DateRange
|
||||||
|
|
||||||
|
|
||||||
class Encoder(json.JSONEncoder):
|
class Encoder(json.JSONEncoder):
|
||||||
"""
|
"""
|
||||||
|
|
@ -13,6 +16,12 @@ class Encoder(json.JSONEncoder):
|
||||||
if isinstance(o, Path):
|
if isinstance(o, Path):
|
||||||
return str(o)
|
return str(o)
|
||||||
|
|
||||||
|
if isinstance(o, DateRange):
|
||||||
|
return {"start": str(o.start).replace("-", ""), "end": str(o.end).replace("-", "")}
|
||||||
|
|
||||||
|
if isinstance(o, date):
|
||||||
|
return str(o)
|
||||||
|
|
||||||
if isinstance(o, object):
|
if isinstance(o, object):
|
||||||
if hasattr(o, "serialize"):
|
if hasattr(o, "serialize"):
|
||||||
return o.serialize()
|
return o.serialize()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue