Merge pull request #52 from arabcoders/dev
Added final file size & Separated live_in from error messages.
This commit is contained in:
commit
2a6660b08b
8 changed files with 98 additions and 47 deletions
|
|
@ -129,7 +129,7 @@ class Download:
|
|||
f'Invalid cookies: was provided for {self.info.title} - {str(e)}')
|
||||
|
||||
log.info(
|
||||
f'Downloading {self.info._id=} {self.info.title=}... {params=}')
|
||||
f'Downloading id="{self.info.id}" title="{self.info.title}".')
|
||||
ret = yt_dlp.YoutubeDL(params=params).download([self.info.url])
|
||||
|
||||
self.status_queue.put(
|
||||
|
|
@ -141,10 +141,10 @@ class Download:
|
|||
'msg': str(exc),
|
||||
'error': str(exc)
|
||||
})
|
||||
|
||||
if self.tempPath and self.info._id and os.path.exists(self.tempPath):
|
||||
log.debug(f'Deleting Temp directory: {self.tempPath}')
|
||||
shutil.rmtree(self.tempPath, ignore_errors=True)
|
||||
finally:
|
||||
if self.tempPath and self.info._id and os.path.exists(self.tempPath):
|
||||
log.debug(f'Deleting Temp directory: {self.tempPath}')
|
||||
shutil.rmtree(self.tempPath, ignore_errors=True)
|
||||
|
||||
async def start(self, notifier: Notifier):
|
||||
if self.manager is None:
|
||||
|
|
@ -153,11 +153,12 @@ class Download:
|
|||
self.status_queue = self.manager.Queue()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.notifier = notifier
|
||||
|
||||
self.proc = multiprocessing.Process(target=self._download)
|
||||
self.proc.start()
|
||||
self.info.status = 'preparing'
|
||||
await self.notifier.updated(self.info)
|
||||
asyncio.create_task(self.update_status())
|
||||
asyncio.create_task(self.progress_update())
|
||||
return await self.loop.run_in_executor(None, self.proc.join)
|
||||
|
||||
def cancel(self):
|
||||
|
|
@ -179,10 +180,20 @@ class Download:
|
|||
def started(self):
|
||||
return self.proc is not None
|
||||
|
||||
async def update_status(self):
|
||||
async def progress_update(self):
|
||||
"""
|
||||
Update status of download task and notify the client.
|
||||
"""
|
||||
while True:
|
||||
status = await self.loop.run_in_executor(None, self.status_queue.get)
|
||||
if status is None:
|
||||
if not status:
|
||||
return
|
||||
|
||||
if self.debug:
|
||||
log.debug(f'Status Update: {self.info._id=} {status=}')
|
||||
|
||||
if isinstance(status, str):
|
||||
await self.notifier.updated(self.info)
|
||||
return
|
||||
|
||||
self.tmpfilename = status.get('tmpfilename')
|
||||
|
|
@ -196,9 +207,13 @@ class Download:
|
|||
self.info.filename = re.sub(
|
||||
r'\.webm$', '.jpg', self.info.filename)
|
||||
|
||||
self.info.status = status['status']
|
||||
self.info.status = status.get('status', self.info.status)
|
||||
self.info.msg = status.get('msg')
|
||||
|
||||
if self.info.status == 'error' and 'error' in status:
|
||||
self.info.error = status.get('error')
|
||||
await self.notifier.error(self.info, self.info.error)
|
||||
|
||||
if 'downloaded_bytes' in status:
|
||||
total = status.get('total_bytes') or status.get(
|
||||
'total_bytes_estimate')
|
||||
|
|
@ -210,4 +225,7 @@ class Download:
|
|||
self.info.speed = status.get('speed')
|
||||
self.info.eta = status.get('eta')
|
||||
|
||||
if self.info.status == 'finished' and 'filename' in status and self.info.format != 'thumbnail':
|
||||
self.info.file_size = os.path.getsize(status.get('filename'))
|
||||
|
||||
await self.notifier.updated(self.info)
|
||||
|
|
|
|||
|
|
@ -57,14 +57,11 @@ class DownloadQueue:
|
|||
|
||||
if 'live_status' in entry and entry.get('live_status') == 'is_upcoming':
|
||||
if 'release_timestamp' in entry and entry.get('release_timestamp'):
|
||||
dt_ts = datetime.fromtimestamp(
|
||||
entry.get('release_timestamp'), tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S %z')
|
||||
error = f"Live stream is scheduled to start at {dt_ts}"
|
||||
live_in = formatdate(entry.get('release_timestamp'))
|
||||
else:
|
||||
error = 'Live stream not yet started. And no date is set.'
|
||||
else:
|
||||
error = entry['msg'] if 'msg' in entry else None
|
||||
error = entry.get('msg', None)
|
||||
|
||||
etype = entry.get('_type') or 'video'
|
||||
if etype == 'playlist':
|
||||
|
|
@ -73,12 +70,12 @@ class DownloadQueue:
|
|||
playlist_index_digits = len(str(len(entries)))
|
||||
results = []
|
||||
for index, etr in enumerate(entries, start=1):
|
||||
etr['playlist'] = entry['id']
|
||||
etr['playlist'] = entry.get('id')
|
||||
etr['playlist_index'] = '{{0:0{0:d}d}}'.format(
|
||||
playlist_index_digits).format(index)
|
||||
for property in ('id', 'title', 'uploader', 'uploader_id'):
|
||||
if property in entry:
|
||||
etr[f'playlist_{property}'] = entry[property]
|
||||
etr[f'playlist_{property}'] = entry.get(property)
|
||||
|
||||
results.append(
|
||||
await self.__add_entry(
|
||||
|
|
@ -98,11 +95,10 @@ class DownloadQueue:
|
|||
|
||||
return {'status': 'ok'}
|
||||
elif (etype == 'video' or etype.startswith('url')) and 'id' in entry and 'title' in entry:
|
||||
|
||||
log.debug(
|
||||
f"entry: {entry.get('id', None)} - {entry.get('webpage_url', None)} - {entry.get('url', None) }")
|
||||
|
||||
if self.done.exists(key=entry['id'], url=entry.get('webpage_url') or entry['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'])
|
||||
|
||||
|
|
@ -111,15 +107,15 @@ class DownloadQueue:
|
|||
|
||||
await self.clear([item.info._id])
|
||||
|
||||
if self.queue.exists(key=entry['id'], url=entry.get('webpage_url') or entry['url']):
|
||||
if self.queue.exists(key=entry.get('id'), url=entry.get('webpage_url') or entry.get('url')):
|
||||
log.info(
|
||||
f'Item [{item.info.title}] already in download queue')
|
||||
return {'status': 'error', 'msg': 'Link already queued for downloading.'}
|
||||
|
||||
dl = ItemDTO(
|
||||
id=entry['id'],
|
||||
title=entry['title'],
|
||||
url=entry.get('webpage_url') or entry['url'],
|
||||
id=entry.get('id'),
|
||||
title=entry.get('title'),
|
||||
url=entry.get('webpage_url') or entry.get('url'),
|
||||
quality=quality,
|
||||
format=format,
|
||||
folder=folder,
|
||||
|
|
@ -128,7 +124,7 @@ class DownloadQueue:
|
|||
output_template=output_template if output_template else self.config.output_template,
|
||||
datetime=formatdate(time.time()),
|
||||
error=error,
|
||||
is_live=entry['is_live'] if 'is_live' in entry else None,
|
||||
is_live=entry.get('is_live', None),
|
||||
live_in=live_in,
|
||||
)
|
||||
|
||||
|
|
@ -164,7 +160,7 @@ class DownloadQueue:
|
|||
}
|
||||
elif etype.startswith('url'):
|
||||
return await self.add(
|
||||
entry=entry['url'],
|
||||
entry=entry.get('url'),
|
||||
quality=quality,
|
||||
format=format,
|
||||
folder=folder,
|
||||
|
|
@ -193,7 +189,7 @@ class DownloadQueue:
|
|||
ytdlp_config = ytdlp_config if ytdlp_config else {}
|
||||
|
||||
log.info(
|
||||
f'adding {url}: {quality=} {format=} {folder=} {output_template=} {ytdlp_cookies=} {ytdlp_config=}')
|
||||
f'Adding {url=} {quality=} {format=} {folder=} {output_template=} {ytdlp_cookies=} {ytdlp_config=}')
|
||||
|
||||
already = set() if already is None else already
|
||||
if url in already:
|
||||
|
|
@ -232,7 +228,7 @@ class DownloadQueue:
|
|||
async def cancel(self, ids):
|
||||
for id in ids:
|
||||
if not self.queue.exists(id):
|
||||
log.warn(f'requested cancel for non-existent download {id}')
|
||||
log.warn(f'Requested cancel for non-existent download {id=}')
|
||||
continue
|
||||
|
||||
item = self.queue.get(key=id)
|
||||
|
|
@ -242,7 +238,7 @@ class DownloadQueue:
|
|||
self.queue.get(id).cancel()
|
||||
else:
|
||||
self.queue.delete(id)
|
||||
log.info(f'deleting {id=} {item.info.title=}')
|
||||
log.info(f'Deleting {id=} {item.info.title=}')
|
||||
await self.notifier.canceled(id)
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
|
@ -250,10 +246,10 @@ class DownloadQueue:
|
|||
async def clear(self, ids):
|
||||
for id in ids:
|
||||
if not self.done.exists(key=id):
|
||||
log.warn(f'requested delete for non-existent download {id}')
|
||||
log.warn(f'Requested delete for non-existent download {id}')
|
||||
continue
|
||||
item = self.done.get(key=id)
|
||||
log.info(f'deleting {id=} {item.info.title=}')
|
||||
log.info(f'Deleting {id=} {item.info.title=}')
|
||||
self.done.delete(id)
|
||||
await self.notifier.cleared(id)
|
||||
|
||||
|
|
@ -287,7 +283,7 @@ class DownloadQueue:
|
|||
|
||||
id, entry = self.queue.next()
|
||||
log.info(
|
||||
f'downloading {id=} - {entry.info.title=} - {entry.info.url=} - {entry.info.folder=}')
|
||||
f'Queuing {id=} - {entry.info.title=} - {entry.info.url=} - {entry.info.folder=}.')
|
||||
|
||||
await entry.start(self.notifier)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class ItemDTO:
|
|||
is_live: bool = None
|
||||
datetime: str = field(default_factory=lambda: str(formatdate(time.time())))
|
||||
live_in: str = None
|
||||
file_size: int = None
|
||||
|
||||
# yt-dlp injected fields.
|
||||
tmpfilename: str = None
|
||||
|
|
|
|||
13
app/Utils.py
13
app/Utils.py
|
|
@ -286,20 +286,23 @@ class Notifier:
|
|||
self.sio = sio
|
||||
self.serializer = serializer
|
||||
|
||||
async def added(self, dl):
|
||||
async def added(self, dl: dict):
|
||||
await self.emit('added', dl)
|
||||
|
||||
async def updated(self, dl):
|
||||
async def updated(self, dl: dict):
|
||||
await self.emit('updated', dl)
|
||||
|
||||
async def completed(self, dl):
|
||||
async def completed(self, dl: dict):
|
||||
await self.emit('completed', dl)
|
||||
|
||||
async def canceled(self, id):
|
||||
async def canceled(self, id: str):
|
||||
await self.emit('canceled', id)
|
||||
|
||||
async def cleared(self, id):
|
||||
async def cleared(self, id: str):
|
||||
await self.emit('cleared', id)
|
||||
|
||||
async def error(self, dl: dict, message: str):
|
||||
await self.emit('error', (dl, message))
|
||||
|
||||
async def emit(self, event: str, data):
|
||||
await self.sio.emit(event, self.serializer.encode(data))
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ class Main:
|
|||
)
|
||||
|
||||
async def connect(self, sid, _):
|
||||
self.logger.info(f'Config [{self.config.__dict__}].')
|
||||
data: dict = {
|
||||
**self.dqueue.get(),
|
||||
"config": self.config,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ onMounted(() => {
|
|||
toast.success(`Item queued successfully: ${downloading[item._id]?.title}`);
|
||||
});
|
||||
|
||||
socket.on('error', stream => {
|
||||
const [item, error] = JSON.parse(stream);
|
||||
toast.error(`${item?.id}: Error: ${error}`);
|
||||
});
|
||||
|
||||
socket.on('completed', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
if (item._id in downloading) {
|
||||
|
|
|
|||
|
|
@ -79,34 +79,52 @@
|
|||
</header>
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12" v-if="item.status === 'error' && item.live_in">
|
||||
<span class="has-text-info">
|
||||
LIVE stream is scheduled to start at {{ moment(item.datetime).format() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-12" v-if="item.error">
|
||||
<span class="has-text-danger">{{ item.error }}</span>
|
||||
</div>
|
||||
<div class="column is-12" v-if="showMessage(item)">
|
||||
<span class="has-text-danger">{{ item.msg }}</span>
|
||||
</div>
|
||||
<div class="column is-4 has-text-centered" v-if="!item.live_in">
|
||||
<span class="icon-text">
|
||||
<span class="icon"
|
||||
:class="{ 'has-text-success': item.status === 'finished', 'has-text-danger': item.status !== 'finished' }">
|
||||
<font-awesome-icon
|
||||
:icon="item.status == 'finished' ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark'" />
|
||||
<div class="column has-text-centered">
|
||||
<span v-if="!item.live_in">
|
||||
<span class="icon-text">
|
||||
<span class="icon"
|
||||
:class="{ 'has-text-success': item.status === 'finished', 'has-text-danger': item.status !== 'finished' }">
|
||||
<font-awesome-icon
|
||||
:icon="item.status == 'finished' ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark'" />
|
||||
</span>
|
||||
<span>{{ capitalize(item.status) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="icon-text">
|
||||
<span class="icon has-text-info">
|
||||
<font-awesome-icon icon="fa-solid fa-calendar" />
|
||||
</span>
|
||||
<span>Live Stream</span>
|
||||
</span>
|
||||
<span>{{ capitalize(item.status) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-4 has-text-centered">
|
||||
<div class="column has-text-centered">
|
||||
<span :date-datetime="item.datetime" :data-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
|
||||
{{ moment(item.datetime).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-4 has-text-centered" v-if="item.live_in && item.status != 'finished'">
|
||||
<div class="column has-text-centered" v-if="item.live_in && item.status != 'finished'">
|
||||
<span :date-datetime="item.datetime"
|
||||
:data-tooltip="'Will start at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')">
|
||||
{{ moment(item.live_in).fromNow() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-4 has-text-centered">
|
||||
<div class="column has-text-centered" v-if="item.file_size">
|
||||
{{ formatBytes(item.file_size) }}
|
||||
</div>
|
||||
<div class="column has-text-centered">
|
||||
<label class="checkbox is-block">
|
||||
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
|
||||
:value="item._id">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import {
|
||||
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
||||
faSpinner, faArrowUp, faArrowDown, faTasks
|
||||
faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
||||
|
|
@ -17,7 +17,7 @@ import './assets/css/style.css'
|
|||
import '@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css';
|
||||
|
||||
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
||||
faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks,)
|
||||
faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar)
|
||||
const app = createApp(App);
|
||||
|
||||
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
||||
|
|
@ -31,6 +31,17 @@ app.config.globalProperties.makeDownload = (config, item, base = 'download') =>
|
|||
|
||||
return config.app.url_host + config.app.url_prefix + baseDir + encodeURIComponent(item.filename);
|
||||
}
|
||||
app.config.globalProperties.formatBytes = (bytes, decimals = 2) => {
|
||||
if (!+bytes) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
app.use(Toast, {
|
||||
transition: "Vue-Toastification__bounce",
|
||||
|
|
|
|||
Loading…
Reference in a new issue