more changes to support multi downloads.

This commit is contained in:
ArabCoders 2024-01-09 00:17:26 +03:00
parent 1d95bbfec8
commit c932c8deb6
5 changed files with 76 additions and 52 deletions

3
.vscode/launch.json vendored
View file

@ -35,8 +35,9 @@
"YTP_YTDL_OPTIONS_FILE": "${workspaceFolder}/var/config/ytdlp.json", "YTP_YTDL_OPTIONS_FILE": "${workspaceFolder}/var/config/ytdlp.json",
"YTP_URL_HOST": "http://localhost:8081", "YTP_URL_HOST": "http://localhost:8081",
"YTP_YTDL_DEBUG": "false", "YTP_YTDL_DEBUG": "false",
"YTP_TEMP_KEEP": "true", "YTP_TEMP_KEEP": "false",
"YTP_KEEP_ARCHIVE": "false", "YTP_KEEP_ARCHIVE": "false",
"YTP_MAX_WORKERS": "0",
} }
} }
] ]

View file

@ -44,6 +44,7 @@ class Config:
'keep_archive', 'ytdl_debug', 'keep_archive', 'ytdl_debug',
'temp_keep', 'allow_manifestless', 'temp_keep', 'allow_manifestless',
) )
_int_vars: tuple = ('port', 'max_workers',)
_immutable: tuple = ('version', '__instance', 'ytdl_options',) _immutable: tuple = ('version', '__instance', 'ytdl_options',)
@staticmethod @staticmethod
@ -102,6 +103,9 @@ class Config:
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:
setattr(self, k, int(v))
if not self.url_prefix.endswith('/'): if not self.url_prefix.endswith('/'):
self.url_prefix += '/' self.url_prefix += '/'
@ -142,6 +146,8 @@ class Config:
self.ytdl_options['download_archive'] = os.path.join( self.ytdl_options['download_archive'] = os.path.join(
self.config_path, 'archive.log') self.config_path, 'archive.log')
log.info(f'Keep temp: {self.temp_keep}')
def _getAttributes(self) -> dict: def _getAttributes(self) -> dict:
attrs: dict = {} attrs: dict = {}
vclass: str = self.__class__ vclass: str = self.__class__

View file

@ -3,6 +3,7 @@ import copy
from datetime import datetime, timezone from datetime import datetime, timezone
from email.utils import formatdate from email.utils import formatdate
import json import json
import logging
import sqlite3 import sqlite3
from Utils import calcDownloadPath from Utils import calcDownloadPath
from Config import Config from Config import Config
@ -45,10 +46,9 @@ class DataStore:
if key and key in self.dict: if key and key in self.dict:
return True return True
if url: for i in self.dict:
for key in self.dict: if (key and self.dict[i].info._id == key) or (url and self.dict[i].info.url == url):
if self.dict[key].info.url == url: return True
return True
return False return False
@ -56,13 +56,9 @@ class DataStore:
if not key and not url: if not key and not url:
raise KeyError('key or url must be provided') raise KeyError('key or url must be provided')
if key and key in self.dict: for i in self.dict:
return self.dict[key] if (key and self.dict[i].info._id == key) or (url and self.dict[i].info.url == url):
return self.dict[i]
if url:
for key in self.dict:
if self.dict[key].info.url == url:
return self.dict[key]
raise KeyError('key or url not found') raise KeyError('key or url not found')
@ -91,10 +87,10 @@ class DataStore:
return items return items
def put(self, value: Download) -> Download: def put(self, value: Download) -> Download:
for key in self.dict: # for key in self.dict:
if self.dict[key].info.url == value.info.url: # if self.dict[key].info.url == value.info.url:
value.info._id = key # value.info._id = key
return # return
self.dict[value.info._id] = value self.dict[value.info._id] = value
self._updateStoreItem(self.type, value.info) self._updateStoreItem(self.type, value.info)
@ -124,6 +120,13 @@ class DataStore:
return False return False
def getNextDownload(self) -> Download:
for key in self.dict:
if self.dict[key].started() is False and self.dict[key].is_canceled() is False:
return self.dict[key]
return None
def _updateStoreItem(self, type: str, item: ItemDTO) -> None: def _updateStoreItem(self, type: str, item: ItemDTO) -> None:
sqlStatement = """ sqlStatement = """
INSERT INTO "history" ("id", "type", "url", "data") INSERT INTO "history" ("id", "type", "url", "data")

View file

@ -28,6 +28,8 @@ class Download:
debug: bool = False debug: bool = False
tempPath: str = None tempPath: str = None
notifier: Notifier = None notifier: Notifier = None
canceled: bool = False
_ytdlp_fields: tuple = ( _ytdlp_fields: tuple = (
'tmpfilename', 'tmpfilename',
'filename', 'filename',
@ -94,12 +96,6 @@ class Download:
'filename': filename 'filename': filename
}) })
# Create temp dir for each download.
self.tempPath = os.path.join(
self.temp_dir, self.info.id if self.info.id else self.info._id)
if not os.path.exists(self.tempPath):
os.makedirs(self.tempPath, exist_ok=True)
params: dict = { params: dict = {
'no_color': True, 'no_color': True,
'format': self.format, 'format': self.format,
@ -154,14 +150,6 @@ class Download:
'msg': str(exc), 'msg': str(exc),
'error': str(exc) 'error': str(exc)
}) })
finally:
if self.tempKeep is False and self.tempPath and os.path.exists(self.tempPath):
if self.tempPath == self.temp_dir:
log.warning(
f'Attempted to delete video temp directory: {self.tempPath}, but it is the same as main temp directory.')
else:
log.debug(f'Deleting Temp directory: {self.tempPath}')
shutil.rmtree(self.tempPath, ignore_errors=True)
async def start(self, notifier: Notifier): async def start(self, notifier: Notifier):
if self.manager is None: if self.manager is None:
@ -171,6 +159,11 @@ class Download:
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self.notifier = notifier self.notifier = notifier
# Create temp dir for each download.
self.tempPath = os.path.join(self.temp_dir, self.info._id)
if not os.path.exists(self.tempPath):
os.makedirs(self.tempPath, exist_ok=True)
self.proc = multiprocessing.Process(target=self._download) self.proc = multiprocessing.Process(target=self._download)
self.proc.start() self.proc.start()
self.info.status = 'preparing' self.info.status = 'preparing'
@ -178,25 +171,44 @@ class Download:
asyncio.create_task(self.progress_update()) asyncio.create_task(self.progress_update())
return await self.loop.run_in_executor(None, self.proc.join) return await self.loop.run_in_executor(None, self.proc.join)
def started(self) -> bool:
return self.proc is not None
def cancel(self): def cancel(self):
if self.running(): self.kill()
self.proc.kill()
self.canceled = True self.canceled = True
def close(self): def close(self):
if self.started(): if self.started():
self.proc.close() self.proc.close()
if self.max_workers < 1:
self.status_queue.put(None) self.delete_temp()
def running(self) -> bool: def running(self) -> bool:
try: return self.started() and self.proc.is_alive()
return self.proc is not None and self.proc.is_alive()
except ValueError:
return False
def started(self) -> bool: def is_canceled(self) -> bool:
return self.proc is not None return self.canceled
def kill(self):
if self.running():
log.info(f'Killing download process: {self.proc.ident}')
self.proc.kill()
def delete_temp(self):
if self.tempKeep is True or not self.tempPath:
return
if not os.path.exists(self.tempPath):
return
if self.tempPath == self.temp_dir:
log.warning(
f'Attempted to delete video temp directory: {self.tempPath}, but it is the same as main temp directory.')
return
log.info(f'Deleting Temp directory: {self.tempPath}')
shutil.rmtree(self.tempPath, ignore_errors=True)
async def progress_update(self): async def progress_update(self):
""" """
@ -204,7 +216,7 @@ class Download:
""" """
while True: while True:
status = await self.loop.run_in_executor(None, self.status_queue.get) status = await self.loop.run_in_executor(None, self.status_queue.get)
if not status or status.get('id') != self.id: if status.get('id') != self.id or len(status) < 2:
return return
if self.debug: if self.debug:

View file

@ -257,17 +257,18 @@ class DownloadQueue:
async def cancel(self, ids): async def cancel(self, ids):
for id in ids: for id in ids:
if not self.queue.exists(id): if self.queue.exists(key=id) is False:
log.warn(f'Requested cancel for non-existent download {id=}') log.warn(f'Requested cancel for non-existent download {id=}')
continue continue
item = self.queue.get(key=id) item = self.queue.get(key=id)
if item.started(): if item.started() is True:
log.info(f'Canceling {id=} {item.info.title=}') log.info(f'Canceling {id=} {item.info.title=}')
item.cancel() item.cancel()
else: else:
self.queue.delete(id) self.queue.delete(id)
item.close()
log.info(f'Deleting {id=} {item.info.title=}') log.info(f'Deleting {id=} {item.info.title=}')
await self.notifier.canceled(id) await self.notifier.canceled(id)
@ -326,11 +327,12 @@ class DownloadQueue:
else: else:
break break
for id in self.queue.dict: entry = self.queue.getNextDownload()
entry = self.queue.get(key=id) await asyncio.sleep(0.2)
if entry.started() is False and entry.canceled is False:
await executor.push(id=id, entry=entry) if entry.started() is False and entry.is_canceled() is False:
await asyncio.sleep(1) await executor.push(id=entry.info._id, entry=entry)
await asyncio.sleep(1)
async def __downloadFile(self, id: str, entry: Download): async def __downloadFile(self, id: str, entry: Download):
log.info( log.info(
@ -349,12 +351,12 @@ class DownloadQueue:
entry.close() entry.close()
if self.queue.exists(id): if self.queue.exists(key=id):
self.queue.delete(id) self.queue.delete(key=id)
if entry.canceled: if entry.is_canceled() is True:
await self.notifier.canceled(id) await self.notifier.canceled(id)
else: else:
self.done.put(entry) self.done.put(value=entry)
await self.notifier.completed(entry.info) await self.notifier.completed(entry.info)
async def __download(self): async def __download(self):