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)}')
|
f'Invalid cookies: was provided for {self.info.title} - {str(e)}')
|
||||||
|
|
||||||
log.info(
|
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])
|
ret = yt_dlp.YoutubeDL(params=params).download([self.info.url])
|
||||||
|
|
||||||
self.status_queue.put(
|
self.status_queue.put(
|
||||||
|
|
@ -141,10 +141,10 @@ class Download:
|
||||||
'msg': str(exc),
|
'msg': str(exc),
|
||||||
'error': str(exc)
|
'error': str(exc)
|
||||||
})
|
})
|
||||||
|
finally:
|
||||||
if self.tempPath and self.info._id and os.path.exists(self.tempPath):
|
if self.tempPath and self.info._id and os.path.exists(self.tempPath):
|
||||||
log.debug(f'Deleting Temp directory: {self.tempPath}')
|
log.debug(f'Deleting Temp directory: {self.tempPath}')
|
||||||
shutil.rmtree(self.tempPath, ignore_errors=True)
|
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:
|
||||||
|
|
@ -153,11 +153,12 @@ class Download:
|
||||||
self.status_queue = self.manager.Queue()
|
self.status_queue = self.manager.Queue()
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.notifier = notifier
|
self.notifier = notifier
|
||||||
|
|
||||||
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'
|
||||||
await self.notifier.updated(self.info)
|
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)
|
return await self.loop.run_in_executor(None, self.proc.join)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
|
|
@ -179,10 +180,20 @@ class Download:
|
||||||
def started(self):
|
def started(self):
|
||||||
return self.proc is not None
|
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:
|
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 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
|
return
|
||||||
|
|
||||||
self.tmpfilename = status.get('tmpfilename')
|
self.tmpfilename = status.get('tmpfilename')
|
||||||
|
|
@ -196,9 +207,13 @@ class Download:
|
||||||
self.info.filename = re.sub(
|
self.info.filename = re.sub(
|
||||||
r'\.webm$', '.jpg', self.info.filename)
|
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')
|
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:
|
if 'downloaded_bytes' in status:
|
||||||
total = status.get('total_bytes') or status.get(
|
total = status.get('total_bytes') or status.get(
|
||||||
'total_bytes_estimate')
|
'total_bytes_estimate')
|
||||||
|
|
@ -210,4 +225,7 @@ class Download:
|
||||||
self.info.speed = status.get('speed')
|
self.info.speed = status.get('speed')
|
||||||
self.info.eta = status.get('eta')
|
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)
|
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 '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'):
|
||||||
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'))
|
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['msg'] if 'msg' in entry else None
|
error = entry.get('msg', None)
|
||||||
|
|
||||||
etype = entry.get('_type') or 'video'
|
etype = entry.get('_type') or 'video'
|
||||||
if etype == 'playlist':
|
if etype == 'playlist':
|
||||||
|
|
@ -73,12 +70,12 @@ class DownloadQueue:
|
||||||
playlist_index_digits = len(str(len(entries)))
|
playlist_index_digits = len(str(len(entries)))
|
||||||
results = []
|
results = []
|
||||||
for index, etr in enumerate(entries, start=1):
|
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(
|
etr['playlist_index'] = '{{0:0{0:d}d}}'.format(
|
||||||
playlist_index_digits).format(index)
|
playlist_index_digits).format(index)
|
||||||
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[property]
|
etr[f'playlist_{property}'] = entry.get(property)
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
await self.__add_entry(
|
await self.__add_entry(
|
||||||
|
|
@ -98,11 +95,10 @@ class DownloadQueue:
|
||||||
|
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
elif (etype == 'video' or etype.startswith('url')) and 'id' in entry and 'title' in entry:
|
elif (etype == 'video' or etype.startswith('url')) and 'id' in entry and 'title' in entry:
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
f"entry: {entry.get('id', None)} - {entry.get('webpage_url', None)} - {entry.get('url', None) }")
|
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(
|
item = self.done.get(key=entry['id'], url=entry.get(
|
||||||
'webpage_url') or entry['url'])
|
'webpage_url') or entry['url'])
|
||||||
|
|
||||||
|
|
@ -111,15 +107,15 @@ class DownloadQueue:
|
||||||
|
|
||||||
await self.clear([item.info._id])
|
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(
|
log.info(
|
||||||
f'Item [{item.info.title}] already in download queue')
|
f'Item [{item.info.title}] already in download queue')
|
||||||
return {'status': 'error', 'msg': 'Link already queued for downloading.'}
|
return {'status': 'error', 'msg': 'Link already queued for downloading.'}
|
||||||
|
|
||||||
dl = ItemDTO(
|
dl = ItemDTO(
|
||||||
id=entry['id'],
|
id=entry.get('id'),
|
||||||
title=entry['title'],
|
title=entry.get('title'),
|
||||||
url=entry.get('webpage_url') or entry['url'],
|
url=entry.get('webpage_url') or entry.get('url'),
|
||||||
quality=quality,
|
quality=quality,
|
||||||
format=format,
|
format=format,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
|
|
@ -128,7 +124,7 @@ class DownloadQueue:
|
||||||
output_template=output_template if output_template else self.config.output_template,
|
output_template=output_template if output_template else self.config.output_template,
|
||||||
datetime=formatdate(time.time()),
|
datetime=formatdate(time.time()),
|
||||||
error=error,
|
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,
|
live_in=live_in,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -164,7 +160,7 @@ class DownloadQueue:
|
||||||
}
|
}
|
||||||
elif etype.startswith('url'):
|
elif etype.startswith('url'):
|
||||||
return await self.add(
|
return await self.add(
|
||||||
entry=entry['url'],
|
entry=entry.get('url'),
|
||||||
quality=quality,
|
quality=quality,
|
||||||
format=format,
|
format=format,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
|
|
@ -193,7 +189,7 @@ class DownloadQueue:
|
||||||
ytdlp_config = ytdlp_config if ytdlp_config else {}
|
ytdlp_config = ytdlp_config if ytdlp_config else {}
|
||||||
|
|
||||||
log.info(
|
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
|
already = set() if already is None else already
|
||||||
if url in already:
|
if url in already:
|
||||||
|
|
@ -232,7 +228,7 @@ 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 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
|
continue
|
||||||
|
|
||||||
item = self.queue.get(key=id)
|
item = self.queue.get(key=id)
|
||||||
|
|
@ -242,7 +238,7 @@ class DownloadQueue:
|
||||||
self.queue.get(id).cancel()
|
self.queue.get(id).cancel()
|
||||||
else:
|
else:
|
||||||
self.queue.delete(id)
|
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)
|
await self.notifier.canceled(id)
|
||||||
|
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
|
@ -250,10 +246,10 @@ class DownloadQueue:
|
||||||
async def clear(self, ids):
|
async def clear(self, ids):
|
||||||
for id in ids:
|
for id in ids:
|
||||||
if not self.done.exists(key=id):
|
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
|
continue
|
||||||
item = self.done.get(key=id)
|
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)
|
self.done.delete(id)
|
||||||
await self.notifier.cleared(id)
|
await self.notifier.cleared(id)
|
||||||
|
|
||||||
|
|
@ -287,7 +283,7 @@ class DownloadQueue:
|
||||||
|
|
||||||
id, entry = self.queue.next()
|
id, entry = self.queue.next()
|
||||||
log.info(
|
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)
|
await entry.start(self.notifier)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class ItemDTO:
|
||||||
is_live: bool = None
|
is_live: bool = None
|
||||||
datetime: str = field(default_factory=lambda: str(formatdate(time.time())))
|
datetime: str = field(default_factory=lambda: str(formatdate(time.time())))
|
||||||
live_in: str = None
|
live_in: str = None
|
||||||
|
file_size: int = None
|
||||||
|
|
||||||
# yt-dlp injected fields.
|
# yt-dlp injected fields.
|
||||||
tmpfilename: str = None
|
tmpfilename: str = None
|
||||||
|
|
|
||||||
13
app/Utils.py
13
app/Utils.py
|
|
@ -286,20 +286,23 @@ class Notifier:
|
||||||
self.sio = sio
|
self.sio = sio
|
||||||
self.serializer = serializer
|
self.serializer = serializer
|
||||||
|
|
||||||
async def added(self, dl):
|
async def added(self, dl: dict):
|
||||||
await self.emit('added', dl)
|
await self.emit('added', dl)
|
||||||
|
|
||||||
async def updated(self, dl):
|
async def updated(self, dl: dict):
|
||||||
await self.emit('updated', dl)
|
await self.emit('updated', dl)
|
||||||
|
|
||||||
async def completed(self, dl):
|
async def completed(self, dl: dict):
|
||||||
await self.emit('completed', dl)
|
await self.emit('completed', dl)
|
||||||
|
|
||||||
async def canceled(self, id):
|
async def canceled(self, id: str):
|
||||||
await self.emit('canceled', id)
|
await self.emit('canceled', id)
|
||||||
|
|
||||||
async def cleared(self, id):
|
async def cleared(self, id: str):
|
||||||
await self.emit('cleared', id)
|
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):
|
async def emit(self, event: str, data):
|
||||||
await self.sio.emit(event, self.serializer.encode(data))
|
await self.sio.emit(event, self.serializer.encode(data))
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ class Main:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def connect(self, sid, _):
|
async def connect(self, sid, _):
|
||||||
self.logger.info(f'Config [{self.config.__dict__}].')
|
|
||||||
data: dict = {
|
data: dict = {
|
||||||
**self.dqueue.get(),
|
**self.dqueue.get(),
|
||||||
"config": self.config,
|
"config": self.config,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,11 @@ onMounted(() => {
|
||||||
toast.success(`Item queued successfully: ${downloading[item._id]?.title}`);
|
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 => {
|
socket.on('completed', stream => {
|
||||||
const item = JSON.parse(stream);
|
const item = JSON.parse(stream);
|
||||||
if (item._id in downloading) {
|
if (item._id in downloading) {
|
||||||
|
|
|
||||||
|
|
@ -79,34 +79,52 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="columns is-multiline">
|
<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">
|
<div class="column is-12" v-if="item.error">
|
||||||
<span class="has-text-danger">{{ item.error }}</span>
|
<span class="has-text-danger">{{ item.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-12" v-if="showMessage(item)">
|
<div class="column is-12" v-if="showMessage(item)">
|
||||||
<span class="has-text-danger">{{ item.msg }}</span>
|
<span class="has-text-danger">{{ item.msg }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-4 has-text-centered" v-if="!item.live_in">
|
<div class="column has-text-centered">
|
||||||
<span class="icon-text">
|
<span v-if="!item.live_in">
|
||||||
<span class="icon"
|
<span class="icon-text">
|
||||||
:class="{ 'has-text-success': item.status === 'finished', 'has-text-danger': item.status !== 'finished' }">
|
<span class="icon"
|
||||||
<font-awesome-icon
|
:class="{ 'has-text-success': item.status === 'finished', 'has-text-danger': item.status !== 'finished' }">
|
||||||
:icon="item.status == 'finished' ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark'" />
|
<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>
|
||||||
<span>{{ capitalize(item.status) }}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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')">
|
<span :date-datetime="item.datetime" :data-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
|
||||||
{{ moment(item.datetime).fromNow() }}
|
{{ moment(item.datetime).fromNow() }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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"
|
<span :date-datetime="item.datetime"
|
||||||
:data-tooltip="'Will start at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')">
|
:data-tooltip="'Will start at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')">
|
||||||
{{ moment(item.live_in).fromNow() }}
|
{{ moment(item.live_in).fromNow() }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<label class="checkbox is-block">
|
||||||
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
|
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
|
||||||
:value="item._id">
|
:value="item._id">
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import {
|
import {
|
||||||
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
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'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-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';
|
import '@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css';
|
||||||
|
|
||||||
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
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);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
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);
|
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, {
|
app.use(Toast, {
|
||||||
transition: "Vue-Toastification__bounce",
|
transition: "Vue-Toastification__bounce",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue