Merge pull request #52 from arabcoders/dev

Added final file size & Separated live_in from error messages.
This commit is contained in:
Abdulmohsen 2024-01-03 18:45:51 +03:00 committed by GitHub
commit 2a6660b08b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 98 additions and 47 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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))

View file

@ -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,

View file

@ -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) {

View file

@ -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">

View file

@ -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",