updated some type checks
This commit is contained in:
parent
252970f66f
commit
df86cc0001
6 changed files with 333 additions and 232 deletions
|
|
@ -15,3 +15,4 @@ trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.py]
|
[*.py]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
max_line_length = 120
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ from .config import Config
|
||||||
from .Download import Download
|
from .Download import Download
|
||||||
from .ItemDTO import ItemDTO
|
from .ItemDTO import ItemDTO
|
||||||
from .DataStore import DataStore
|
from .DataStore import DataStore
|
||||||
from .Utils import ag, calcDownloadPath, ExtractInfo, isDownloaded, mergeConfig
|
from .Utils import calcDownloadPath, ExtractInfo, isDownloaded, mergeConfig
|
||||||
from .AsyncPool import AsyncPool
|
from .AsyncPool import AsyncPool
|
||||||
from .Emitter import Emitter
|
from .Emitter import Emitter
|
||||||
|
|
||||||
LOG = logging.getLogger('DownloadQueue')
|
LOG = logging.getLogger("DownloadQueue")
|
||||||
TYPE_DONE: str = 'done'
|
TYPE_DONE: str = "done"
|
||||||
TYPE_QUEUE: str = 'queue'
|
TYPE_QUEUE: str = "queue"
|
||||||
|
|
||||||
|
|
||||||
class DownloadQueue:
|
class DownloadQueue:
|
||||||
|
|
@ -41,11 +41,11 @@ class DownloadQueue:
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
self.event = asyncio.Event()
|
self.event = asyncio.Event()
|
||||||
LOG.info(f'Using {self.config.max_workers} workers for downloading.')
|
LOG.info(f"Using {self.config.max_workers} workers for downloading.")
|
||||||
|
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.__download_pool() if self.config.max_workers > 1 else self.__download(),
|
self.__download_pool() if self.config.max_workers > 1 else self.__download(),
|
||||||
name='download_pool' if self.config.max_workers > 1 else 'download_worker'
|
name="download_pool" if self.config.max_workers > 1 else "download_worker",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def __add_entry(
|
async def __add_entry(
|
||||||
|
|
@ -54,29 +54,29 @@ class DownloadQueue:
|
||||||
preset: str,
|
preset: str,
|
||||||
folder: str,
|
folder: str,
|
||||||
ytdlp_config: dict = {},
|
ytdlp_config: dict = {},
|
||||||
ytdlp_cookies: str = '',
|
ytdlp_cookies: str = "",
|
||||||
output_template: str = '',
|
output_template: str = "",
|
||||||
already=None
|
already=None,
|
||||||
):
|
):
|
||||||
if not entry:
|
if not entry:
|
||||||
return {'status': 'error', 'msg': 'Invalid/empty data was given.'}
|
return {"status": "error", "msg": "Invalid/empty data was given."}
|
||||||
|
|
||||||
options: dict = {}
|
options: dict = {}
|
||||||
|
|
||||||
error: str = None
|
error: str = None
|
||||||
live_in: str = None
|
live_in: str = None
|
||||||
|
|
||||||
eventType = entry.get('_type') or 'video'
|
eventType = entry.get("_type") or "video"
|
||||||
if eventType == 'playlist':
|
if eventType == "playlist":
|
||||||
entries = entry['entries']
|
entries = entry["entries"]
|
||||||
playlist_index_digits = len(str(len(entries)))
|
playlist_index_digits = len(str(len(entries)))
|
||||||
results = []
|
results = []
|
||||||
for i, etr in enumerate(entries, start=1):
|
for i, etr in enumerate(entries, start=1):
|
||||||
etr['playlist'] = entry.get('id')
|
etr["playlist"] = entry.get("id")
|
||||||
etr['playlist_index'] = '{{0:0{0:d}d}}'.format(playlist_index_digits).format(i)
|
etr["playlist_index"] = "{{0:0{0:d}d}}".format(playlist_index_digits).format(i)
|
||||||
for property in ('id', 'title', 'uploader', 'uploader_id'):
|
for property in ("id", "title", "uploader", "uploader_id"):
|
||||||
if property in entry:
|
if property in entry:
|
||||||
etr[f'playlist_{property}'] = entry.get(property)
|
etr[f"playlist_{property}"] = entry.get(property)
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
await self.__add_entry(
|
await self.__add_entry(
|
||||||
|
|
@ -86,60 +86,63 @@ class DownloadQueue:
|
||||||
ytdlp_config=ytdlp_config,
|
ytdlp_config=ytdlp_config,
|
||||||
ytdlp_cookies=ytdlp_cookies,
|
ytdlp_cookies=ytdlp_cookies,
|
||||||
output_template=output_template,
|
output_template=output_template,
|
||||||
already=already
|
already=already,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if any(res['status'] == 'error' for res in results):
|
if any(res["status"] == "error" for res in results):
|
||||||
return {'status': 'error', 'msg': ', '.join(res['msg'] for res in results if res['status'] == 'error' and 'msg' in res)}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"msg": ", ".join(res["msg"] for res in results if res["status"] == "error" and "msg" in res),
|
||||||
|
}
|
||||||
|
|
||||||
return {'status': 'ok'}
|
return {"status": "ok"}
|
||||||
elif (eventType == 'video' or eventType.startswith('url')) and 'id' in entry and 'title' in entry:
|
elif (eventType == "video" or eventType.startswith("url")) and "id" in entry and "title" in entry:
|
||||||
# check if the video is live stream.
|
# check if the video is live stream.
|
||||||
if 'live_status' in entry and entry.get('live_status') == 'is_upcoming':
|
if "live_status" in entry and entry.get("live_status") == "is_upcoming":
|
||||||
if 'release_timestamp' in entry and entry.get('release_timestamp'):
|
if "release_timestamp" in entry and entry.get("release_timestamp"):
|
||||||
live_in = formatdate(entry.get('release_timestamp'))
|
live_in = formatdate(entry.get("release_timestamp"))
|
||||||
else:
|
else:
|
||||||
error = 'Live stream not yet started. And no date is set.'
|
error = "Live stream not yet started. And no date is set."
|
||||||
else:
|
else:
|
||||||
error = entry.get('msg', None)
|
error = entry.get("msg", None)
|
||||||
|
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
f"Entry id '{entry.get('id', None)}' url '{entry.get('webpage_url', None)} - {entry.get('url', None)}'."
|
f"Entry id '{entry.get('id', None)}' url '{entry.get('webpage_url', None)} - {entry.get('url', None)}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.done.exists(key=entry['id'], url=entry.get('webpage_url') or entry.get('url')):
|
if self.done.exists(key=entry["id"], url=entry.get("webpage_url") or entry.get("url")):
|
||||||
item = self.done.get(key=entry['id'], url=entry.get('webpage_url') or entry['url'])
|
item = self.done.get(key=entry["id"], url=entry.get("webpage_url") or entry["url"])
|
||||||
LOG.warning(f"Item '{item.info.id}' - '{item.info.title}' already downloaded. Removing from history.")
|
LOG.warning(f"Item '{item.info.id}' - '{item.info.title}' already downloaded. Removing from history.")
|
||||||
await self.clear([item.info._id])
|
await self.clear([item.info._id])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item = self.queue.get(key=entry.get('id'), url=entry.get('webpage_url') or entry.get('url'))
|
item = self.queue.get(key=entry.get("id"), url=entry.get("webpage_url") or entry.get("url"))
|
||||||
if item is not None:
|
if item is not None:
|
||||||
err_message = f"Item ID '{item.info.id}' - '{item.info.title}' already in download queue."
|
err_message = f"Item ID '{item.info.id}' - '{item.info.title}' already in download queue."
|
||||||
LOG.info(err_message)
|
LOG.info(err_message)
|
||||||
return {'status': 'error', 'msg': err_message}
|
return {"status": "error", "msg": err_message}
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
is_manifestless = entry.get('is_manifestless', False)
|
is_manifestless = entry.get("is_manifestless", False)
|
||||||
options.update({'is_manifestless': is_manifestless})
|
options.update({"is_manifestless": is_manifestless})
|
||||||
|
|
||||||
live_status: list = ['is_live', 'is_upcoming']
|
live_status: list = ["is_live", "is_upcoming"]
|
||||||
is_live = entry.get('is_live', None) or live_in or entry.get('live_status', None) in live_status
|
is_live = entry.get("is_live", None) or live_in or entry.get("live_status", None) in live_status
|
||||||
|
|
||||||
try:
|
try:
|
||||||
download_dir = calcDownloadPath(basePath=self.config.download_path, folder=folder)
|
download_dir = calcDownloadPath(basePath=self.config.download_path, folder=folder)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
return {'status': 'error', 'msg': str(e)}
|
return {"status": "error", "msg": str(e)}
|
||||||
|
|
||||||
dl = ItemDTO(
|
dl = ItemDTO(
|
||||||
id=entry.get('id'),
|
id=entry.get("id"),
|
||||||
title=entry.get('title'),
|
title=entry.get("title"),
|
||||||
url=entry.get('webpage_url') or entry.get('url'),
|
url=entry.get("webpage_url") or entry.get("url"),
|
||||||
preset=preset,
|
preset=preset,
|
||||||
thumbnail=entry.get('thumbnail', None),
|
thumbnail=entry.get("thumbnail", None),
|
||||||
folder=folder,
|
folder=folder,
|
||||||
download_dir=download_dir,
|
download_dir=download_dir,
|
||||||
temp_dir=self.config.temp_path,
|
temp_dir=self.config.temp_path,
|
||||||
|
|
@ -160,42 +163,37 @@ class DownloadQueue:
|
||||||
|
|
||||||
dlInfo: Download = Download(info=dl, info_dict=entry, debug=bool(self.config.ytdl_debug))
|
dlInfo: Download = Download(info=dl, info_dict=entry, debug=bool(self.config.ytdl_debug))
|
||||||
|
|
||||||
if dlInfo.info.live_in or 'is_upcoming' == entry.get('live_status', None):
|
if dlInfo.info.live_in or "is_upcoming" == entry.get("live_status", None):
|
||||||
dlInfo.info.status = 'not_live'
|
dlInfo.info.status = "not_live"
|
||||||
itemDownload = self.done.put(dlInfo)
|
itemDownload = self.done.put(dlInfo)
|
||||||
NotifyEvent = 'completed'
|
NotifyEvent = "completed"
|
||||||
elif self.config.allow_manifestless is False and is_manifestless is True:
|
elif self.config.allow_manifestless is False and is_manifestless is True:
|
||||||
dlInfo.info.status = 'error'
|
dlInfo.info.status = "error"
|
||||||
dlInfo.info.error = 'Video is in post-live manifestless mode.'
|
dlInfo.info.error = "Video is in post-live manifestless mode."
|
||||||
itemDownload = self.done.put(dlInfo)
|
itemDownload = self.done.put(dlInfo)
|
||||||
NotifyEvent = 'completed'
|
NotifyEvent = "completed"
|
||||||
else:
|
else:
|
||||||
NotifyEvent = 'added'
|
NotifyEvent = "added"
|
||||||
itemDownload = self.queue.put(dlInfo)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
self.event.set()
|
self.event.set()
|
||||||
|
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.emitter.emit(NotifyEvent, itemDownload.info),
|
self.emitter.emit(NotifyEvent, itemDownload.info), name=f"notifier-{NotifyEvent}-{itemDownload.info.id}"
|
||||||
name=f'notifier-{NotifyEvent}-{itemDownload.info.id}')
|
)
|
||||||
|
|
||||||
return {
|
return {"status": "ok"}
|
||||||
'status': 'ok'
|
elif eventType.startswith("url"):
|
||||||
}
|
|
||||||
elif eventType.startswith('url'):
|
|
||||||
return await self.add(
|
return await self.add(
|
||||||
url=entry.get('url'),
|
url=entry.get("url"),
|
||||||
preset=preset,
|
preset=preset,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
ytdlp_config=ytdlp_config,
|
ytdlp_config=ytdlp_config,
|
||||||
ytdlp_cookies=ytdlp_cookies,
|
ytdlp_cookies=ytdlp_cookies,
|
||||||
output_template=output_template,
|
output_template=output_template,
|
||||||
already=already
|
already=already,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {"status": "error", "msg": f'Unsupported resource "{eventType}"'}
|
||||||
'status': 'error',
|
|
||||||
'msg': f'Unsupported resource "{eventType}"'
|
|
||||||
}
|
|
||||||
|
|
||||||
async def add(
|
async def add(
|
||||||
self,
|
self,
|
||||||
|
|
@ -203,26 +201,28 @@ class DownloadQueue:
|
||||||
preset: str,
|
preset: str,
|
||||||
folder: str,
|
folder: str,
|
||||||
ytdlp_config: dict = {},
|
ytdlp_config: dict = {},
|
||||||
ytdlp_cookies: str = '',
|
ytdlp_cookies: str = "",
|
||||||
output_template: str = '',
|
output_template: str = "",
|
||||||
already=None
|
already=None,
|
||||||
):
|
):
|
||||||
ytdlp_config = ytdlp_config if ytdlp_config else {}
|
ytdlp_config = ytdlp_config if ytdlp_config else {}
|
||||||
|
|
||||||
LOG.info(f"Adding url '{url}' to folder '{folder}' with the following options 'Preset: {preset}' 'Naming: {output_template}', 'Cookies: {ytdlp_cookies}' 'YTConfig: {ytdlp_config}'.")
|
LOG.info(
|
||||||
|
f"Adding url '{url}' to folder '{folder}' with the following options 'Preset: {preset}' 'Naming: {output_template}', 'Cookies: {ytdlp_cookies}' 'YTConfig: {ytdlp_config}'."
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(ytdlp_config, str):
|
if isinstance(ytdlp_config, str):
|
||||||
try:
|
try:
|
||||||
ytdlp_config = json.loads(ytdlp_config)
|
ytdlp_config = json.loads(ytdlp_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Unable to load '{ytdlp_config=}'. {str(e)}")
|
LOG.error(f"Unable to load '{ytdlp_config=}'. {str(e)}")
|
||||||
return {'status': 'error', 'msg': f"Failed to parse json yt-dlp config. {str(e)}"}
|
return {"status": "error", "msg": f"Failed to parse json yt-dlp config. {str(e)}"}
|
||||||
|
|
||||||
already = set() if already is None else already
|
already = set() if already is None else already
|
||||||
|
|
||||||
if url in already:
|
if url in already:
|
||||||
LOG.warning(f"Recursion detected with url '{url}' skipping.")
|
LOG.warning(f"Recursion detected with url '{url}' skipping.")
|
||||||
return {'status': 'ok'}
|
return {"status": "ok"}
|
||||||
|
|
||||||
already.add(url)
|
already.add(url)
|
||||||
|
|
||||||
|
|
@ -231,10 +231,10 @@ class DownloadQueue:
|
||||||
if downloaded is True:
|
if downloaded is True:
|
||||||
message = f"This url with ID '{id_dict.get('id')}' has been downloaded already and recorded in archive."
|
message = f"This url with ID '{id_dict.get('id')}' has been downloaded already and recorded in archive."
|
||||||
LOG.info(message)
|
LOG.info(message)
|
||||||
return {'status': 'error', 'msg': message}
|
return {"status": "error", "msg": message}
|
||||||
|
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
LOG.debug(f'extract_info: checking {url=}')
|
LOG.debug(f"extract_info: checking {url=}")
|
||||||
|
|
||||||
entry = await asyncio.wait_for(
|
entry = await asyncio.wait_for(
|
||||||
fut=asyncio.get_running_loop().run_in_executor(
|
fut=asyncio.get_running_loop().run_in_executor(
|
||||||
|
|
@ -242,24 +242,29 @@ class DownloadQueue:
|
||||||
ExtractInfo,
|
ExtractInfo,
|
||||||
mergeConfig(self.config.ytdl_options, ytdlp_config),
|
mergeConfig(self.config.ytdl_options, ytdlp_config),
|
||||||
url,
|
url,
|
||||||
bool(self.config.ytdl_debug)
|
bool(self.config.ytdl_debug),
|
||||||
),
|
),
|
||||||
timeout=self.config.extract_info_timeout)
|
timeout=self.config.extract_info_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
if not entry:
|
if not entry:
|
||||||
return {'status': 'error', 'msg': 'Unable to extract info check logs.'}
|
return {"status": "error", "msg": "Unable to extract info check logs."}
|
||||||
|
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
f"extract_info: for 'URL: {url}' is done in '{time.perf_counter() - started}'. Length: '{len(entry)}'.")
|
f"extract_info: for 'URL: {url}' is done in '{time.perf_counter() - started}'. Length: '{len(entry)}'."
|
||||||
|
)
|
||||||
except yt_dlp.utils.ExistingVideoReached as exc:
|
except yt_dlp.utils.ExistingVideoReached as exc:
|
||||||
LOG.error(f'Video has been downloaded already and recorded in archive.log file. {str(exc)}')
|
LOG.error(f"Video has been downloaded already and recorded in archive.log file. {str(exc)}")
|
||||||
return {'status': 'error', 'msg': 'Video has been downloaded already and recorded in archive.log file.'}
|
return {"status": "error", "msg": "Video has been downloaded already and recorded in archive.log file."}
|
||||||
except yt_dlp.utils.YoutubeDLError as exc:
|
except yt_dlp.utils.YoutubeDLError as exc:
|
||||||
LOG.error(f'YoutubeDLError: Unable to extract info. {str(exc)}')
|
LOG.error(f"YoutubeDLError: Unable to extract info. {str(exc)}")
|
||||||
return {'status': 'error', 'msg': str(exc)}
|
return {"status": "error", "msg": str(exc)}
|
||||||
except asyncio.exceptions.TimeoutError as exc:
|
except asyncio.exceptions.TimeoutError as exc:
|
||||||
LOG.error(f'TimeoutError: Unable to extract info. {str(exc)}')
|
LOG.error(f"TimeoutError: Unable to extract info. {str(exc)}")
|
||||||
return {'status': 'error', 'msg': f'TimeoutError: {self.config.extract_info_timeout}s reached Unable to extract info.'}
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"msg": f"TimeoutError: {self.config.extract_info_timeout}s reached Unable to extract info.",
|
||||||
|
}
|
||||||
|
|
||||||
return await self.__add_entry(
|
return await self.__add_entry(
|
||||||
entry=entry,
|
entry=entry,
|
||||||
|
|
@ -279,27 +284,27 @@ class DownloadQueue:
|
||||||
item = self.queue.get(key=id)
|
item = self.queue.get(key=id)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
status[id] = str(e)
|
status[id] = str(e)
|
||||||
status['status'] = 'error'
|
status["status"] = "error"
|
||||||
LOG.warning(f'Requested cancel for non-existent download {id=}. {str(e)}')
|
LOG.warning(f"Requested cancel for non-existent download {id=}. {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
||||||
|
|
||||||
if item.running():
|
if item.running():
|
||||||
LOG.debug(f'Canceling {itemMessage}')
|
LOG.debug(f"Canceling {itemMessage}")
|
||||||
item.cancel()
|
item.cancel()
|
||||||
LOG.info(f'Cancelled {itemMessage}')
|
LOG.info(f"Cancelled {itemMessage}")
|
||||||
await item.close()
|
await item.close()
|
||||||
else:
|
else:
|
||||||
await item.close()
|
await item.close()
|
||||||
LOG.debug(f'Deleting from queue {itemMessage}')
|
LOG.debug(f"Deleting from queue {itemMessage}")
|
||||||
self.queue.delete(id)
|
self.queue.delete(id)
|
||||||
asyncio.create_task(self.emitter.canceled(id=id, dl=item), name=f'notifier-c-{id}')
|
asyncio.create_task(self.emitter.canceled(id=id, dl=item), name=f"notifier-c-{id}")
|
||||||
self.done.put(item)
|
self.done.put(item)
|
||||||
asyncio.create_task(self.emitter.completed(dl=item), name=f'notifier-d-{id}')
|
asyncio.create_task(self.emitter.completed(dl=item), name=f"notifier-d-{id}")
|
||||||
LOG.info(f'Deleted from queue {itemMessage}')
|
LOG.info(f"Deleted from queue {itemMessage}")
|
||||||
|
|
||||||
status[id] = 'ok'
|
status[id] = "ok"
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
@ -311,35 +316,35 @@ class DownloadQueue:
|
||||||
item = self.done.get(key=id)
|
item = self.done.get(key=id)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
status[id] = str(e)
|
status[id] = str(e)
|
||||||
status['status'] = 'error'
|
status["status"] = "error"
|
||||||
LOG.warning(f'Requested delete for non-existent download {id=}. {str(e)}')
|
LOG.warning(f"Requested delete for non-existent download {id=}. {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
||||||
LOG.debug(f'Deleting completed download {itemMessage}')
|
LOG.debug(f"Deleting completed download {itemMessage}")
|
||||||
self.done.delete(id)
|
self.done.delete(id)
|
||||||
asyncio.create_task(self.emitter.cleared(id, dl=item), name=f'notifier-c-{id}')
|
asyncio.create_task(self.emitter.cleared(id, dl=item), name=f"notifier-c-{id}")
|
||||||
LOG.info(f'Deleted completed download {itemMessage}')
|
LOG.info(f"Deleted completed download {itemMessage}")
|
||||||
status[id] = 'ok'
|
status[id] = "ok"
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def get(self) -> dict[str, list[dict[str, ItemDTO]]]:
|
def get(self) -> dict[str, list[dict[str, ItemDTO]]]:
|
||||||
items = {'queue': {}, 'done': {}}
|
items = {"queue": {}, "done": {}}
|
||||||
|
|
||||||
for k, v in self.queue.saved_items():
|
for k, v in self.queue.saved_items():
|
||||||
items['queue'][k] = v
|
items["queue"][k] = v
|
||||||
|
|
||||||
for k, v in self.done.saved_items():
|
for k, v in self.done.saved_items():
|
||||||
items['done'][k] = v
|
items["done"][k] = v
|
||||||
|
|
||||||
for k, v in self.queue.items():
|
for k, v in self.queue.items():
|
||||||
if not k in items['queue']:
|
if k not in items["queue"]:
|
||||||
items['queue'][k] = v.info
|
items["queue"][k] = v.info
|
||||||
|
|
||||||
for k, v in self.done.items():
|
for k, v in self.done.items():
|
||||||
if not k in items['done']:
|
if k not in items["done"]:
|
||||||
items['done'][k] = v.info
|
items["done"][k] = v.info
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
@ -348,8 +353,8 @@ class DownloadQueue:
|
||||||
loop=asyncio.get_running_loop(),
|
loop=asyncio.get_running_loop(),
|
||||||
num_workers=self.config.max_workers,
|
num_workers=self.config.max_workers,
|
||||||
worker_co=self.__downloadFile,
|
worker_co=self.__downloadFile,
|
||||||
name='download_pool',
|
name="download_pool",
|
||||||
logger=logging.getLogger('WorkerPool'),
|
logger=logging.getLogger("WorkerPool"),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.pool.start()
|
self.pool.start()
|
||||||
|
|
@ -362,14 +367,14 @@ class DownloadQueue:
|
||||||
break
|
break
|
||||||
if time.time() - lastLog > 120:
|
if time.time() - lastLog > 120:
|
||||||
lastLog = time.time()
|
lastLog = time.time()
|
||||||
LOG.info(f'Waiting for worker to be free. {self.pool.get_workers_status()}')
|
LOG.info(f"Waiting for worker to be free. {self.pool.get_workers_status()}")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
while not self.queue.hasDownloads():
|
while not self.queue.hasDownloads():
|
||||||
LOG.info(f"Waiting for item to download. '{self.pool.get_available_workers()}' free workers.")
|
LOG.info(f"Waiting for item to download. '{self.pool.get_available_workers()}' free workers.")
|
||||||
await self.event.wait()
|
await self.event.wait()
|
||||||
self.event.clear()
|
self.event.clear()
|
||||||
LOG.debug(f"Cleared wait event.")
|
LOG.debug("Cleared wait event.")
|
||||||
|
|
||||||
entry = self.queue.getNextDownload()
|
entry = self.queue.getNextDownload()
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
@ -387,7 +392,7 @@ class DownloadQueue:
|
||||||
async def __download(self):
|
async def __download(self):
|
||||||
while True:
|
while True:
|
||||||
while self.queue.empty():
|
while self.queue.empty():
|
||||||
LOG.info('Waiting for item to download.')
|
LOG.info("Waiting for item to download.")
|
||||||
await self.event.wait()
|
await self.event.wait()
|
||||||
self.event.clear()
|
self.event.clear()
|
||||||
|
|
||||||
|
|
@ -396,34 +401,34 @@ class DownloadQueue:
|
||||||
|
|
||||||
async def __downloadFile(self, id: str, entry: Download):
|
async def __downloadFile(self, id: str, entry: Download):
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' to 'folder: {entry.info.folder}'.")
|
f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' to 'folder: {entry.info.folder}'."
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await entry.start(self.emitter)
|
await entry.start(self.emitter)
|
||||||
|
|
||||||
if entry.info.status != 'finished':
|
if entry.info.status != "finished":
|
||||||
if entry.tmpfilename and os.path.isfile(entry.tmpfilename):
|
if entry.tmpfilename and os.path.isfile(entry.tmpfilename):
|
||||||
try:
|
try:
|
||||||
os.remove(entry.tmpfilename)
|
os.remove(entry.tmpfilename)
|
||||||
entry.tmpfilename = None
|
entry.tmpfilename = None
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
entry.info.status = 'error'
|
entry.info.status = "error"
|
||||||
finally:
|
finally:
|
||||||
await entry.close()
|
await entry.close()
|
||||||
|
|
||||||
if self.queue.exists(key=id):
|
if self.queue.exists(key=id):
|
||||||
|
|
||||||
self.queue.delete(key=id)
|
self.queue.delete(key=id)
|
||||||
|
|
||||||
if entry.is_canceled() is True:
|
if entry.is_canceled() is True:
|
||||||
asyncio.create_task(self.emitter.canceled(id, dl=entry.info), name=f'notifier-c-{id}')
|
asyncio.create_task(self.emitter.canceled(id, dl=entry.info), name=f"notifier-c-{id}")
|
||||||
entry.info.status = 'canceled'
|
entry.info.status = "canceled"
|
||||||
entry.info.error = 'Canceled by user.'
|
entry.info.error = "Canceled by user."
|
||||||
|
|
||||||
self.done.put(value=entry)
|
self.done.put(value=entry)
|
||||||
asyncio.create_task(self.emitter.completed(dl=entry.info), name=f'notifier-d-{id}')
|
asyncio.create_task(self.emitter.completed(dl=entry.info), name=f"notifier-d-{id}")
|
||||||
|
|
||||||
self.event.set()
|
self.event.set()
|
||||||
|
|
||||||
|
|
@ -431,4 +436,4 @@ class DownloadQueue:
|
||||||
if not url or not self.config.keep_archive:
|
if not url or not self.config.keep_archive:
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
return isDownloaded(self.config.ytdl_options.get('download_archive', None), url)
|
return isDownloaded(self.config.ytdl_options.get("download_archive", None), url)
|
||||||
|
|
|
||||||
|
|
@ -448,7 +448,7 @@ class HttpAPI(common):
|
||||||
'Access-Control-Max-Age': "300",
|
'Access-Control-Max-Age': "300",
|
||||||
})
|
})
|
||||||
|
|
||||||
@route('GET', 'player/segments/{segment:\d+}/{file:.*}.ts')
|
@route('GET', 'player/segments/{segment:d+}/{file:.*}.ts')
|
||||||
async def segments(self, request: Request) -> Response:
|
async def segments(self, request: Request) -> Response:
|
||||||
file: str = request.match_info.get('file')
|
file: str = request.match_info.get('file')
|
||||||
segment: int = request.match_info.get('segment')
|
segment: int = request.match_info.get('segment')
|
||||||
|
|
|
||||||
|
|
@ -13,31 +13,31 @@ from pathlib import Path
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
__instance = None
|
__instance = None
|
||||||
config_path: str = '.'
|
config_path: str = "."
|
||||||
download_path: str = '.'
|
download_path: str = "."
|
||||||
temp_path: str = '/tmp'
|
temp_path: str = "/tmp"
|
||||||
temp_keep: bool = False
|
temp_keep: bool = False
|
||||||
db_file: str = '{config_path}/ytptube.db'
|
db_file: str = "{config_path}/ytptube.db"
|
||||||
manual_archive: str = '{config_path}/archive.manual.log'
|
manual_archive: str = "{config_path}/archive.manual.log"
|
||||||
|
|
||||||
url_host: str = ''
|
url_host: str = ""
|
||||||
url_prefix: str = ''
|
url_prefix: str = ""
|
||||||
url_socketio: str = '{url_prefix}socket.io'
|
url_socketio: str = "{url_prefix}socket.io"
|
||||||
|
|
||||||
output_template: str = '%(title)s.%(ext)s'
|
output_template: str = "%(title)s.%(ext)s"
|
||||||
output_template_chapter: str = '%(title)s - %(section_number)s %(section_title)s.%(ext)s'
|
output_template_chapter: str = "%(title)s - %(section_number)s %(section_title)s.%(ext)s"
|
||||||
|
|
||||||
ytdl_options: dict | str = {}
|
ytdl_options: dict | str = {}
|
||||||
ytdl_debug: bool = False
|
ytdl_debug: bool = False
|
||||||
|
|
||||||
host: str = '0.0.0.0'
|
host: str = "0.0.0.0"
|
||||||
port: int = 8081
|
port: int = 8081
|
||||||
|
|
||||||
keep_archive: bool = True
|
keep_archive: bool = True
|
||||||
|
|
||||||
base_path: str = ''
|
base_path: str = ""
|
||||||
|
|
||||||
log_level: str = 'info'
|
log_level: str = "info"
|
||||||
|
|
||||||
allow_manifestless: bool = False
|
allow_manifestless: bool = False
|
||||||
|
|
||||||
|
|
@ -57,8 +57,8 @@ class Config:
|
||||||
|
|
||||||
started: int = 0
|
started: int = 0
|
||||||
|
|
||||||
streamer_vcodec = 'libx264'
|
streamer_vcodec = "libx264"
|
||||||
streamer_acodec = 'aac'
|
streamer_acodec = "aac"
|
||||||
|
|
||||||
auth_username: str = None
|
auth_username: str = None
|
||||||
auth_password: str = None
|
auth_password: str = None
|
||||||
|
|
@ -66,64 +66,87 @@ class Config:
|
||||||
ytdlp_version: str = YTDLP_VERSION
|
ytdlp_version: str = YTDLP_VERSION
|
||||||
tasks: list = []
|
tasks: list = []
|
||||||
presets: list = [
|
presets: list = [
|
||||||
{'name': 'default', 'format': 'default', 'postprocessors': [],
|
{"name": "default", "format": "default", "postprocessors": [], "args": {}},
|
||||||
'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]',
|
"name": "Video - 1080p h264/acc",
|
||||||
'format': 'bestaudio/best',
|
"format": "bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]",
|
||||||
'postprocessors': [
|
"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",
|
"key": "FFmpegExtractAudio",
|
||||||
"preferredcodec": "mp3",
|
"preferredcodec": "mp3",
|
||||||
"preferredquality": "5",
|
"preferredquality": "5",
|
||||||
"nopostoverwrites": False
|
"nopostoverwrites": False,
|
||||||
},
|
},
|
||||||
{
|
{"key": "FFmpegMetadata", "add_chapters": True, "add_metadata": True, "add_infojson": "if_exists"},
|
||||||
"key": "FFmpegMetadata",
|
{"key": "EmbedThumbnail", "already_have_thumbnail": False},
|
||||||
"add_chapters": True,
|
{"key": "FFmpegConcat", "only_multi_video": True, "when": "playlist"},
|
||||||
"add_metadata": True,
|
|
||||||
"add_infojson": "if_exists"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "EmbedThumbnail",
|
|
||||||
"already_have_thumbnail": False
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "FFmpegConcat",
|
|
||||||
"only_multi_video": True,
|
|
||||||
"when": "playlist"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
'args': {
|
"args": {
|
||||||
"outtmpl": {
|
"outtmpl": {"pl_thumbnail": ""},
|
||||||
"pl_thumbnail": ""
|
|
||||||
},
|
|
||||||
"ignoreerrors": "only_download",
|
"ignoreerrors": "only_download",
|
||||||
"retries": 10,
|
"retries": 10,
|
||||||
"fragment_retries": 10,
|
"fragment_retries": 10,
|
||||||
"writethumbnail": True,
|
"writethumbnail": True,
|
||||||
"extract_flat": "discard_in_playlist",
|
"extract_flat": "discard_in_playlist",
|
||||||
"final_ext": "mp3",
|
"final_ext": "mp3",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
_manual_vars: tuple = ('temp_path', 'config_path', 'download_path',)
|
_manual_vars: tuple = (
|
||||||
|
"temp_path",
|
||||||
|
"config_path",
|
||||||
|
"download_path",
|
||||||
|
)
|
||||||
_immutable: tuple = (
|
_immutable: tuple = (
|
||||||
'version', '__instance', 'ytdl_options', 'tasks',
|
"version",
|
||||||
'new_version_available', 'ytdlp_version', 'started', 'presets',
|
"__instance",
|
||||||
|
"ytdl_options",
|
||||||
|
"tasks",
|
||||||
|
"new_version_available",
|
||||||
|
"ytdlp_version",
|
||||||
|
"started",
|
||||||
|
"presets",
|
||||||
)
|
)
|
||||||
|
|
||||||
_int_vars: tuple = ('port', 'max_workers', 'socket_timeout', 'extract_info_timeout', 'debugpy_port',)
|
_int_vars: tuple = (
|
||||||
_boolean_vars: tuple = ('keep_archive', 'ytdl_debug', 'debug', 'temp_keep', 'allow_manifestless',)
|
"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 = (
|
_frontend_vars: tuple = (
|
||||||
'download_path', 'keep_archive', 'output_template',
|
"download_path",
|
||||||
'ytdlp_version', 'version', 'url_host', 'started', 'url_prefix',
|
"keep_archive",
|
||||||
|
"output_template",
|
||||||
|
"ytdlp_version",
|
||||||
|
"version",
|
||||||
|
"url_host",
|
||||||
|
"started",
|
||||||
|
"url_prefix",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -140,20 +163,20 @@ class Config:
|
||||||
|
|
||||||
baseDefaultPath: str = str(Path(__file__).parent.parent.parent.absolute())
|
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.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.config_path = os.environ.get("YTP_CONFIG_PATH", None) or os.path.join(baseDefaultPath, "var", "config")
|
||||||
self.download_path = os.environ.get(
|
self.download_path = os.environ.get("YTP_DOWNLOAD_PATH", None) or os.path.join(
|
||||||
'YTP_DOWNLOAD_PATH', None) or os.path.join(
|
baseDefaultPath, "var", "downloads"
|
||||||
baseDefaultPath, 'var', 'downloads')
|
)
|
||||||
|
|
||||||
envFile: str = os.path.join(self.config_path, '.env')
|
envFile: str = os.path.join(self.config_path, ".env")
|
||||||
|
|
||||||
if os.path.exists(envFile):
|
if os.path.exists(envFile):
|
||||||
logging.info(f"Loading environment variables from '{envFile}'.")
|
logging.info(f"Loading environment variables from '{envFile}'.")
|
||||||
load_dotenv(envFile)
|
load_dotenv(envFile)
|
||||||
|
|
||||||
for k, v in self._getAttributes().items():
|
for k, v in self._getAttributes().items():
|
||||||
if k.startswith('_') or k in self._manual_vars:
|
if k.startswith("_") or k in self._manual_vars:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If the variable declared as immutable, set it to the default value.
|
# If the variable declared as immutable, set it to the default value.
|
||||||
|
|
@ -161,15 +184,15 @@ class Config:
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lookUpKey: str = f'YTP_{k}'.upper()
|
lookUpKey: str = f"YTP_{k}".upper()
|
||||||
setattr(self, k, os.environ[lookUpKey] if lookUpKey in os.environ else v)
|
setattr(self, k, os.environ[lookUpKey] if lookUpKey in os.environ else v)
|
||||||
|
|
||||||
for k, v in self.__dict__.items():
|
for k, v in self.__dict__.items():
|
||||||
if k.startswith('_') or k in self._immutable or k in self._manual_vars:
|
if k.startswith("_") or k in self._immutable or k in self._manual_vars:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(v, str) and '{' in v and '}' in v:
|
if isinstance(v, str) and "{" in v and "}" in v:
|
||||||
for key in re.findall(r'\{.*?\}', v):
|
for key in re.findall(r"\{.*?\}", v):
|
||||||
localKey: str = key[1:-1]
|
localKey: str = key[1:-1]
|
||||||
if localKey not in self.__dict__:
|
if localKey not in self.__dict__:
|
||||||
logging.error(f"Config variable '{k}' had non-existing config reference '{key}'.")
|
logging.error(f"Config variable '{k}' had non-existing config reference '{key}'.")
|
||||||
|
|
@ -180,32 +203,31 @@ class Config:
|
||||||
setattr(self, k, v)
|
setattr(self, k, v)
|
||||||
|
|
||||||
if k in self._boolean_vars:
|
if k in self._boolean_vars:
|
||||||
if str(v).lower() not in (True, False, 'true', 'false', 'on', 'off', '1', '0'):
|
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}'.")
|
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'))
|
setattr(self, k, str(v).lower() in (True, "true", "on", "1"))
|
||||||
|
|
||||||
if k in self._int_vars:
|
if k in self._int_vars:
|
||||||
setattr(self, k, int(v))
|
setattr(self, k, int(v))
|
||||||
|
|
||||||
if not self.url_prefix.endswith('/'):
|
if not self.url_prefix.endswith("/"):
|
||||||
self.url_prefix += '/'
|
self.url_prefix += "/"
|
||||||
|
|
||||||
numeric_level = getattr(logging, self.log_level.upper(), None)
|
numeric_level = getattr(logging, self.log_level.upper(), None)
|
||||||
if not isinstance(numeric_level, int):
|
if not isinstance(numeric_level, int):
|
||||||
raise ValueError(f"Invalid log level '{self.log_level}' specified.")
|
raise ValueError(f"Invalid log level '{self.log_level}' specified.")
|
||||||
|
|
||||||
coloredlogs.install(
|
coloredlogs.install(
|
||||||
level=numeric_level,
|
level=numeric_level, fmt="%(asctime)s [%(name)s] [%(levelname)-5.5s] %(message)s", datefmt="%H:%M:%S"
|
||||||
fmt="%(asctime)s [%(name)s] [%(levelname)-5.5s] %(message)s",
|
|
||||||
datefmt='%H:%M:%S'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG = logging.getLogger('config')
|
LOG = logging.getLogger("config")
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
try:
|
try:
|
||||||
import debugpy
|
import debugpy
|
||||||
|
|
||||||
debugpy.listen(("0.0.0.0", self.debugpy_port))
|
debugpy.listen(("0.0.0.0", self.debugpy_port))
|
||||||
LOG.info(f"starting debugpy server on '0.0.0.0:{self.debugpy_port}'.")
|
LOG.info(f"starting debugpy server on '0.0.0.0:{self.debugpy_port}'.")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -213,7 +235,7 @@ class Config:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Error starting debugpy server at '0.0.0.0:{self.debugpy_port}'. {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')
|
optsFile: str = os.path.join(self.config_path, "ytdlp.json")
|
||||||
if os.path.exists(optsFile) and os.path.getsize(optsFile) > 4:
|
if os.path.exists(optsFile) and os.path.getsize(optsFile) > 4:
|
||||||
LOG.info(f"Loading yt-dlp custom options from '{optsFile}'.")
|
LOG.info(f"Loading yt-dlp custom options from '{optsFile}'.")
|
||||||
|
|
||||||
|
|
@ -226,7 +248,7 @@ class Config:
|
||||||
else:
|
else:
|
||||||
LOG.info(f"No yt-dlp custom options found at '{optsFile}'.")
|
LOG.info(f"No yt-dlp custom options found at '{optsFile}'.")
|
||||||
|
|
||||||
tasksFile = os.path.join(self.config_path, 'tasks.json')
|
tasksFile = os.path.join(self.config_path, "tasks.json")
|
||||||
if os.path.exists(tasksFile) and os.path.getsize(tasksFile) > 0:
|
if os.path.exists(tasksFile) and os.path.getsize(tasksFile) > 0:
|
||||||
LOG.info(f"Loading tasks from '{tasksFile}'.")
|
LOG.info(f"Loading tasks from '{tasksFile}'.")
|
||||||
try:
|
try:
|
||||||
|
|
@ -235,10 +257,10 @@ class Config:
|
||||||
LOG.error(f"Could not load tasks file from '{tasksFile}'. '{error}'.")
|
LOG.error(f"Could not load tasks file from '{tasksFile}'. '{error}'.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
self.tasks.extend(tasks)
|
self.tasks.extend(tasks)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
presetsFile = os.path.join(self.config_path, 'presets.json')
|
presetsFile = os.path.join(self.config_path, "presets.json")
|
||||||
if os.path.exists(presetsFile) and os.path.getsize(presetsFile) > 0:
|
if os.path.exists(presetsFile) and os.path.getsize(presetsFile) > 0:
|
||||||
LOG.info(f"Loading extra presets from '{presetsFile}'.")
|
LOG.info(f"Loading extra presets from '{presetsFile}'.")
|
||||||
try:
|
try:
|
||||||
|
|
@ -248,32 +270,32 @@ class Config:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
for preset in presets:
|
for preset in presets:
|
||||||
if 'name' not in preset:
|
if "name" not in preset:
|
||||||
LOG.error(f"Missing 'name' key in preset '{preset}'.")
|
LOG.error(f"Missing 'name' key in preset '{preset}'.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if 'format' not in preset:
|
if "format" not in preset:
|
||||||
LOG.error(f"Missing 'format' key in preset '{preset}'.")
|
LOG.error(f"Missing 'format' key in preset '{preset}'.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if 'args' not in preset:
|
if "args" not in preset:
|
||||||
preset['args'] = {}
|
preset["args"] = {}
|
||||||
|
|
||||||
if 'postprocessors' not in preset:
|
if "postprocessors" not in preset:
|
||||||
preset['postprocessors'] = []
|
preset["postprocessors"] = []
|
||||||
|
|
||||||
self.presets.append(preset)
|
self.presets.append(preset)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.ytdl_options['socket_timeout'] = self.socket_timeout
|
self.ytdl_options["socket_timeout"] = self.socket_timeout
|
||||||
|
|
||||||
if self.keep_archive:
|
if self.keep_archive:
|
||||||
LOG.info(f"keep archive option is enabled.")
|
LOG.info("keep archive option is enabled.")
|
||||||
self.ytdl_options['download_archive'] = os.path.join(self.config_path, 'archive.log')
|
self.ytdl_options["download_archive"] = os.path.join(self.config_path, "archive.log")
|
||||||
|
|
||||||
if self.temp_keep:
|
if self.temp_keep:
|
||||||
LOG.info(f'Keep temp files option is enabled.')
|
LOG.info("Keep temp files option is enabled.")
|
||||||
|
|
||||||
if self.auth_password and self.auth_username:
|
if self.auth_password and self.auth_username:
|
||||||
LOG.warning(f"Basic authentication enabled with username '{self.auth_username}'.")
|
LOG.warning(f"Basic authentication enabled with username '{self.auth_username}'.")
|
||||||
|
|
@ -285,7 +307,7 @@ class Config:
|
||||||
vClass: str = self.__class__
|
vClass: str = self.__class__
|
||||||
|
|
||||||
for attribute in vClass.__dict__.keys():
|
for attribute in vClass.__dict__.keys():
|
||||||
if attribute.startswith('_'):
|
if attribute.startswith("_"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
value = getattr(vClass, attribute)
|
value = getattr(vClass, attribute)
|
||||||
|
|
|
||||||
47
app/main.py
47
app/main.py
|
|
@ -1,33 +1,34 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import logging
|
|
||||||
import caribou
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import magic
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from aiohttp import web
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from library.Emitter import Emitter
|
|
||||||
from library.config import Config
|
import caribou
|
||||||
from library.encoder import Encoder
|
import magic
|
||||||
from library.DownloadQueue import DownloadQueue
|
|
||||||
from library.Webhooks import Webhooks
|
|
||||||
from library.HttpSocket import HttpSocket
|
|
||||||
from library.HttpAPI import HttpAPI
|
|
||||||
from aiocron import crontab
|
from aiocron import crontab
|
||||||
|
from aiohttp import web
|
||||||
|
from library.config import Config
|
||||||
|
from library.DownloadQueue import DownloadQueue
|
||||||
|
from library.Emitter import Emitter
|
||||||
|
from library.encoder import Encoder
|
||||||
|
from library.HttpAPI import HttpAPI
|
||||||
|
from library.HttpSocket import HttpSocket
|
||||||
|
from library.Webhooks import Webhooks
|
||||||
|
|
||||||
LOG = logging.getLogger("app")
|
LOG = logging.getLogger("app")
|
||||||
MIME = magic.Magic(mime=True)
|
MIME = magic.Magic(mime=True)
|
||||||
|
|
||||||
|
|
||||||
class main:
|
class Main:
|
||||||
config: Config = None
|
config: Config
|
||||||
app: web.Application = None
|
app: web.Application
|
||||||
http: HttpAPI = None
|
http: HttpAPI
|
||||||
socket: HttpSocket = None
|
socket: HttpSocket
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = Config.get_instance()
|
self.config = Config.get_instance()
|
||||||
|
|
@ -61,9 +62,7 @@ class main:
|
||||||
LOG.info(f"Creating download folder at '{self.config.download_path}'.")
|
LOG.info(f"Creating download folder at '{self.config.download_path}'.")
|
||||||
os.makedirs(self.config.download_path, exist_ok=True)
|
os.makedirs(self.config.download_path, exist_ok=True)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
LOG.error(
|
LOG.error(f"Could not create download folder at '{self.config.download_path}'.")
|
||||||
f"Could not create download folder at '{self.config.download_path}'."
|
|
||||||
)
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -111,9 +110,7 @@ class main:
|
||||||
LOG.info(f"Completed 'Task: {taskName}' at '{timeNow}'.")
|
LOG.info(f"Completed 'Task: {taskName}' at '{timeNow}'.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
timeNow = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
timeNow = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
LOG.error(
|
LOG.error(f"Failed 'Task: {taskName}' at '{timeNow}'. Error message '{str(e)}'.")
|
||||||
f"Failed 'Task: {taskName}' at '{timeNow}'. Error message '{str(e)}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
def load_tasks(self):
|
def load_tasks(self):
|
||||||
for task in self.config.tasks:
|
for task in self.config.tasks:
|
||||||
|
|
@ -131,9 +128,7 @@ class main:
|
||||||
loop=asyncio.get_event_loop(),
|
loop=asyncio.get_event_loop(),
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(f"Added 'Task: {task.get('name', task.get('url'))}' to be executed every '{cron_timer}'.")
|
||||||
f"Added 'Task: {task.get('name', task.get('url'))}' to be executed every '{cron_timer}'."
|
|
||||||
)
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.socket.attach(self.app)
|
self.socket.attach(self.app)
|
||||||
|
|
@ -155,4 +150,4 @@ class main:
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
main().start()
|
Main().start()
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,80 @@
|
||||||
|
[tool.ruff]
|
||||||
|
# Exclude a variety of commonly ignored directories.
|
||||||
|
exclude = [
|
||||||
|
".bzr",
|
||||||
|
".direnv",
|
||||||
|
".eggs",
|
||||||
|
".git",
|
||||||
|
".git-rewrite",
|
||||||
|
".hg",
|
||||||
|
".mypy_cache",
|
||||||
|
".nox",
|
||||||
|
".pants.d",
|
||||||
|
".pyenv",
|
||||||
|
".pytest_cache",
|
||||||
|
".pytype",
|
||||||
|
".ruff_cache",
|
||||||
|
".svn",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
".vscode",
|
||||||
|
"__pypackages__",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"site-packages",
|
||||||
|
"venv",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Same as Black.
|
||||||
|
line-length = 120
|
||||||
|
indent-width = 4
|
||||||
|
|
||||||
|
# Assume Python 3.11
|
||||||
|
target-version = "py311"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||||
|
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||||
|
# McCabe complexity (`C901`) by default.
|
||||||
|
select = ["E4", "E7", "E9", "F", "G", "W"]
|
||||||
|
ignore = ["PTH", "G0", "G1", "G201"]
|
||||||
|
|
||||||
|
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||||
|
fixable = ["ALL"]
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
# Allow unused variables when underscore-prefixed.
|
||||||
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
# Like Black, use double quotes for strings.
|
||||||
|
quote-style = "double"
|
||||||
|
|
||||||
|
# Like Black, indent with spaces, rather than tabs.
|
||||||
|
indent-style = "space"
|
||||||
|
|
||||||
|
# Like Black, respect magic trailing commas.
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
|
||||||
|
# Like Black, automatically detect the appropriate line ending.
|
||||||
|
line-ending = "auto"
|
||||||
|
|
||||||
|
# Enable auto-formatting of code examples in docstrings. Markdown,
|
||||||
|
# reStructuredText code/literal blocks and doctests are all supported.
|
||||||
|
#
|
||||||
|
# This is currently disabled by default, but it is planned for this
|
||||||
|
# to be opt-out in the future.
|
||||||
|
docstring-code-format = false
|
||||||
|
|
||||||
|
# Set the line length limit used when formatting code snippets in
|
||||||
|
# docstrings.
|
||||||
|
#
|
||||||
|
# This only has an effect when the `docstring-code-format` setting is
|
||||||
|
# enabled.
|
||||||
|
docstring-code-line-length = "dynamic"
|
||||||
|
|
||||||
[tool.autopep8]
|
[tool.autopep8]
|
||||||
--max-line-length = 120
|
--max-line-length = 120
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue