Merge pull request #84 from arabcoders/dev
Cleanup after download finish.
This commit is contained in:
commit
38febbfe57
4 changed files with 110 additions and 54 deletions
|
|
@ -7,7 +7,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from AsyncPool import Terminator
|
||||||
from Utils import Notifier, get_format, get_opts, jsonCookie, mergeConfig
|
from Utils import Notifier, get_format, get_opts, jsonCookie, mergeConfig
|
||||||
from ItemDTO import ItemDTO
|
from ItemDTO import ItemDTO
|
||||||
from Config import Config
|
from Config import Config
|
||||||
|
|
@ -32,10 +32,12 @@ class Download:
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
tempPath: str = None
|
tempPath: str = None
|
||||||
notifier: Notifier = None
|
notifier: Notifier = None
|
||||||
|
manager: multiprocessing.Manager = None
|
||||||
canceled: bool = False
|
canceled: bool = False
|
||||||
is_live: bool = False
|
is_live: bool = False
|
||||||
info_dict: dict = None
|
info_dict: dict = None
|
||||||
"yt-dlp metadata dict."
|
"yt-dlp metadata dict."
|
||||||
|
update_task = None
|
||||||
|
|
||||||
bad_live_options: list = [
|
bad_live_options: list = [
|
||||||
"concurrent_fragment_downloads",
|
"concurrent_fragment_downloads",
|
||||||
|
|
@ -73,12 +75,10 @@ class Download:
|
||||||
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 = debug
|
||||||
|
|
||||||
self.canceled = False
|
self.canceled = False
|
||||||
self.tmpfilename = None
|
self.tmpfilename = None
|
||||||
self.status_queue = None
|
self.status_queue = None
|
||||||
self.proc = None
|
self.proc = None
|
||||||
self.loop = None
|
|
||||||
self.notifier = None
|
self.notifier = None
|
||||||
self.max_workers = int(config.max_workers)
|
self.max_workers = int(config.max_workers)
|
||||||
self.tempKeep = bool(config.temp_keep)
|
self.tempKeep = bool(config.temp_keep)
|
||||||
|
|
@ -183,9 +183,9 @@ class Download:
|
||||||
LOG.info(f'Finished {os.getpid()=} id="{self.info.id}" title="{self.info.title}".')
|
LOG.info(f'Finished {os.getpid()=} id="{self.info.id}" title="{self.info.title}".')
|
||||||
|
|
||||||
async def start(self, notifier: Notifier):
|
async def start(self, notifier: Notifier):
|
||||||
self.status_queue = multiprocessing.Manager().Queue()
|
self.manager = multiprocessing.Manager()
|
||||||
self.loop = asyncio.get_running_loop()
|
|
||||||
self.notifier = notifier
|
self.notifier = notifier
|
||||||
|
self.status_queue = self.manager.Queue()
|
||||||
|
|
||||||
# Create temp dir for each download.
|
# Create temp dir for each download.
|
||||||
self.tempPath = os.path.join(self.temp_dir, hashlib.shake_256(self.info.id.encode('utf-8')).hexdigest(5))
|
self.tempPath = os.path.join(self.temp_dir, hashlib.shake_256(self.info.id.encode('utf-8')).hexdigest(5))
|
||||||
|
|
@ -193,14 +193,14 @@ class Download:
|
||||||
if not os.path.exists(self.tempPath):
|
if not os.path.exists(self.tempPath):
|
||||||
os.makedirs(self.tempPath, exist_ok=True)
|
os.makedirs(self.tempPath, exist_ok=True)
|
||||||
|
|
||||||
self.proc = multiprocessing.Process(target=self._download)
|
self.proc = multiprocessing.Process(name=f"download-{self.id}", target=self._download)
|
||||||
self.proc.start()
|
self.proc.start()
|
||||||
self.info.status = 'preparing'
|
self.info.status = 'preparing'
|
||||||
|
|
||||||
asyncio.create_task(self.notifier.updated(self.info))
|
asyncio.create_task(self.notifier.updated(self.info), name=f"notifier-{self.id}")
|
||||||
asyncio.create_task(self.progress_update())
|
asyncio.create_task(self.progress_update(), name=f"update-{self.id}")
|
||||||
|
|
||||||
return await self.loop.run_in_executor(None, self.proc.join)
|
return await asyncio.get_running_loop().run_in_executor(None, self.proc.join)
|
||||||
|
|
||||||
def started(self) -> bool:
|
def started(self) -> bool:
|
||||||
return self.proc is not None
|
return self.proc is not None
|
||||||
|
|
@ -209,22 +209,54 @@ class Download:
|
||||||
if not self.started():
|
if not self.started():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.kill():
|
self.canceled = True
|
||||||
self.canceled = True
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def close(self) -> bool:
|
async def close(self) -> bool:
|
||||||
|
"""
|
||||||
|
Close download process.
|
||||||
|
This method MUST be called to clear resources.
|
||||||
|
"""
|
||||||
if not self.started():
|
if not self.started():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
procId = self.proc.ident
|
||||||
try:
|
try:
|
||||||
LOG.info(f"Closing download process: '{self.proc.ident}'.")
|
LOG.info(f"Closing download process: '{procId}'.")
|
||||||
self.proc.close()
|
try:
|
||||||
|
if 'update_task' in self.__dict__ and self.update_task.done() is False:
|
||||||
|
self.status_queue.put(Terminator(), timeout=3)
|
||||||
|
LOG.debug(f"Closed status queue: '{procId}'.")
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Failed to close status queue: '{procId}'. {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.kill()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
if self.proc.is_alive():
|
||||||
|
tasks.append(loop.run_in_executor(None, self.proc.join))
|
||||||
|
|
||||||
|
if self.manager:
|
||||||
|
tasks.append(loop.run_in_executor(None, self.manager.shutdown))
|
||||||
|
|
||||||
|
if len(tasks) > 0:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
if self.proc:
|
||||||
|
self.proc.close()
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
self.delete_temp()
|
self.delete_temp()
|
||||||
|
|
||||||
|
LOG.debug(f"Closed download process: '{procId}'.")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to close process: '{self.proc.ident}'. {e}")
|
LOG.error(f"Failed to close process: '{procId}'. {e}")
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -238,7 +270,7 @@ class Download:
|
||||||
return self.canceled
|
return self.canceled
|
||||||
|
|
||||||
def kill(self) -> bool:
|
def kill(self) -> bool:
|
||||||
if not self.started():
|
if not self.running():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -275,16 +307,23 @@ class Download:
|
||||||
Update status of download task and notify the client.
|
Update status of download task and notify the client.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
status = await self.loop.run_in_executor(None, self.status_queue.get)
|
self.update_task = asyncio.get_running_loop().run_in_executor(None, self.status_queue.get)
|
||||||
if status.get('id') != self.id or len(status) < 2:
|
|
||||||
|
status = await self.update_task
|
||||||
|
|
||||||
|
if status is None or status.__class__ is Terminator:
|
||||||
|
LOG.debug(f'Closing progress update for: {self.info._id=}.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if status.get('id') != self.id or len(status) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
LOG.debug(f'Status Update: {self.info._id=} {status=}')
|
LOG.debug(f'Status Update: {self.info._id=} {status=}')
|
||||||
|
|
||||||
if isinstance(status, str):
|
if isinstance(status, str):
|
||||||
asyncio.create_task(self.notifier.updated(self.info))
|
asyncio.create_task(self.notifier.updated(self.info), name=f"notifier-u-{self.id}")
|
||||||
return
|
continue
|
||||||
|
|
||||||
self.tmpfilename = status.get('tmpfilename')
|
self.tmpfilename = status.get('tmpfilename')
|
||||||
|
|
||||||
|
|
@ -303,7 +342,7 @@ class Download:
|
||||||
|
|
||||||
if self.info.status == 'error' and 'error' in status:
|
if self.info.status == 'error' and 'error' in status:
|
||||||
self.info.error = status.get('error')
|
self.info.error = status.get('error')
|
||||||
asyncio.create_task(self.notifier.error(self.info, self.info.error))
|
asyncio.create_task(self.notifier.error(self.info, self.info.error), name=f"notifier-e-{self.id}")
|
||||||
|
|
||||||
if 'downloaded_bytes' in status:
|
if 'downloaded_bytes' in status:
|
||||||
total = status.get('total_bytes') or status.get('total_bytes_estimate')
|
total = status.get('total_bytes') or status.get('total_bytes_estimate')
|
||||||
|
|
@ -322,4 +361,4 @@ class Download:
|
||||||
self.info.file_size = None
|
self.info.file_size = None
|
||||||
pass
|
pass
|
||||||
|
|
||||||
asyncio.create_task(self.notifier.updated(self.info))
|
asyncio.create_task(self.notifier.updated(self.info), name=f"notifier-u-{self.id}")
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ class DownloadQueue:
|
||||||
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'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def __add_entry(
|
async def __add_entry(
|
||||||
|
|
@ -172,7 +173,9 @@ class DownloadQueue:
|
||||||
itemDownload = self.queue.put(dlInfo)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
self.event.set()
|
self.event.set()
|
||||||
|
|
||||||
asyncio.create_task(self.notifier.emit(NotifyEvent, itemDownload.info))
|
asyncio.create_task(
|
||||||
|
self.notifier.emit(NotifyEvent, itemDownload.info),
|
||||||
|
name=f'notifier-{NotifyEvent}-{itemDownload.info.id}')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'ok'
|
'status': 'ok'
|
||||||
|
|
@ -227,16 +230,15 @@ class DownloadQueue:
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
LOG.debug(f'extract_info: checking {url=}')
|
LOG.debug(f'extract_info: checking {url=}')
|
||||||
|
|
||||||
with ThreadPoolExecutor(1) as executor:
|
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(
|
None,
|
||||||
executor,
|
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.'}
|
||||||
|
|
@ -277,17 +279,18 @@ class DownloadQueue:
|
||||||
|
|
||||||
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
itemMessage = f"{id=} {item.info.id=} {item.info.title=}"
|
||||||
|
|
||||||
if item.started() is True:
|
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()
|
||||||
else:
|
else:
|
||||||
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.notifier.canceled(id))
|
asyncio.create_task(self.notifier.canceled(id), name=f'notifier-c-{id}')
|
||||||
self.done.put(item)
|
self.done.put(item)
|
||||||
asyncio.create_task(self.notifier.completed(item))
|
asyncio.create_task(self.notifier.completed(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'
|
||||||
|
|
@ -309,7 +312,7 @@ class DownloadQueue:
|
||||||
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.notifier.cleared(id))
|
asyncio.create_task(self.notifier.cleared(id), 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'
|
||||||
|
|
||||||
|
|
@ -388,31 +391,32 @@ class DownloadQueue:
|
||||||
async def __downloadFile(self, id: str, entry: Download):
|
async def __downloadFile(self, id: str, entry: Download):
|
||||||
LOG.info(f'Queuing {id=} - {entry.info.title=} - {entry.info.url=} - {entry.info.folder=}.')
|
LOG.info(f'Queuing {id=} - {entry.info.title=} - {entry.info.url=} - {entry.info.folder=}.')
|
||||||
|
|
||||||
await entry.start(self.notifier)
|
try:
|
||||||
|
await entry.start(self.notifier)
|
||||||
|
|
||||||
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:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
entry.info.status = 'error'
|
entry.info.status = 'error'
|
||||||
|
finally:
|
||||||
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.notifier.canceled(id))
|
asyncio.create_task(self.notifier.canceled(id), 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.notifier.completed(entry.info))
|
asyncio.create_task(self.notifier.completed(entry.info), name=f'notifier-d-{id}')
|
||||||
|
|
||||||
self.event.set()
|
self.event.set()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ class Main:
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
start: str = f'YTPTube v{self.config.version} - listening on http://{self.config.host}:{self.config.port}'
|
||||||
web.run_app(
|
web.run_app(
|
||||||
self.app,
|
self.app,
|
||||||
host=self.config.host,
|
host=self.config.host,
|
||||||
|
|
@ -113,8 +114,7 @@ class Main:
|
||||||
reuse_port=True,
|
reuse_port=True,
|
||||||
loop=self.loop,
|
loop=self.loop,
|
||||||
access_log=None,
|
access_log=None,
|
||||||
print=lambda _: print(
|
print=lambda _: LOG.info(start)
|
||||||
f'YTPTube v{self.config.version} - listening on http://{self.config.host}:{self.config.port}'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def connect(self, sid, _):
|
async def connect(self, sid, _):
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,19 @@ const deleteItem = (type, item) => {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
}).then(resp => resp.json()).then(json => {
|
||||||
|
for (const key in items) {
|
||||||
|
const itemId = items[key];
|
||||||
|
if (itemId in json && json[itemId] === 'ok') {
|
||||||
|
if (true === (itemId in completed)) {
|
||||||
|
delete completed[itemId];
|
||||||
|
}
|
||||||
|
if (true === (itemId in downloading)) {
|
||||||
|
toast.info('Download canceled: ' + downloading[itemId]?.title);
|
||||||
|
delete downloading[itemId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
toast.error('Failed to delete/cancel item/s. ' + error);
|
toast.error('Failed to delete/cancel item/s. ' + error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue