Added the ability to add item in paused state
This commit is contained in:
parent
1239643f97
commit
9ff9735a2f
12 changed files with 344 additions and 25 deletions
1
API.md
1
API.md
|
|
@ -210,6 +210,7 @@ or an error:
|
||||||
"cookies": "...", // -- optional. If provided, it MUST BE in Netscape HTTP Cookie format.
|
"cookies": "...", // -- optional. If provided, it MUST BE in Netscape HTTP Cookie format.
|
||||||
"template": "%(title)s.%(ext)s", // -- optional. The filename template to use for this item.
|
"template": "%(title)s.%(ext)s", // -- optional. The filename template to use for this item.
|
||||||
"cli": "--write-subs --embed-subs", // -- optional. Additional command options for yt-dlp to apply to this item.
|
"cli": "--write-subs --embed-subs", // -- optional. Additional command options for yt-dlp to apply to this item.
|
||||||
|
"auto_start": true // -- optional. Whether to auto-start the download after adding it. Defaults to true.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Or multiple items (array of objects)
|
// Or multiple items (array of objects)
|
||||||
|
|
|
||||||
|
|
@ -147,12 +147,13 @@ class DataStore:
|
||||||
if 0 == len(self._dict):
|
if 0 == len(self._dict):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return any(self._dict[key].started() is False for key in self._dict)
|
return any(self._dict[key].info.auto_start and self._dict[key].started() is False for key in self._dict)
|
||||||
|
|
||||||
def get_next_download(self) -> Download:
|
def get_next_download(self) -> Download:
|
||||||
for key in self._dict:
|
for key in self._dict:
|
||||||
if self._dict[key].started() is False and self._dict[key].is_cancelled() is False:
|
ref = self._dict[key]
|
||||||
return self._dict[key]
|
if ref.info.auto_start and ref.started() is False and ref.is_cancelled() is False:
|
||||||
|
return ref
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,94 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
)
|
)
|
||||||
asyncio.create_task(self._download_pool(), name="download_pool")
|
asyncio.create_task(self._download_pool(), name="download_pool")
|
||||||
|
|
||||||
|
async def start_items(self, ids: list[str]) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Start one or more queued downloads that were added with auto_started=False.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids (list[str]): List of item IDs to start.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: Dictionary of per-ID results and overall status.
|
||||||
|
|
||||||
|
"""
|
||||||
|
status: dict[str, str] = {"status": "ok"}
|
||||||
|
started = False
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
for item_id in ids:
|
||||||
|
try:
|
||||||
|
item = self.queue.get(key=item_id)
|
||||||
|
except KeyError as e:
|
||||||
|
status[item_id] = f"not found: {e!s}"
|
||||||
|
status["status"] = "error"
|
||||||
|
LOG.warning(f"Start requested for non-existent item {item_id=}.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.info.auto_start:
|
||||||
|
status[item_id] = "already started"
|
||||||
|
LOG.debug(f"Item {item.info.name()} already started.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
item.info.auto_start = True
|
||||||
|
updated = self.queue.put(item)
|
||||||
|
tasks.append(self._notify.emit(Events.UPDATED, data=updated.info))
|
||||||
|
status[item_id] = "started"
|
||||||
|
started = True
|
||||||
|
LOG.debug(f"Item {item.info.name()} marked as started.")
|
||||||
|
|
||||||
|
if started:
|
||||||
|
self.event.set()
|
||||||
|
|
||||||
|
if len(tasks) > 0:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
async def pause_items(self, ids: list[str]) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Pause one or more queued downloads that were added with auto_started=True.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids (list[str]): List of item IDs to pause.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: Dictionary of per-ID results and overall status.
|
||||||
|
|
||||||
|
"""
|
||||||
|
status: dict[str, str] = {"status": "ok"}
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
for item_id in ids:
|
||||||
|
try:
|
||||||
|
item = self.queue.get(key=item_id)
|
||||||
|
except KeyError as e:
|
||||||
|
status[item_id] = f"not found: {e!s}"
|
||||||
|
status["status"] = "error"
|
||||||
|
LOG.warning(f"Start requested for non-existent item {item_id=}.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.started() or item.is_cancelled():
|
||||||
|
status[item_id] = "already started"
|
||||||
|
LOG.debug(f"Item {item.info.name()} already started.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.info.auto_start is False:
|
||||||
|
status[item_id] = "not started"
|
||||||
|
LOG.debug(f"Item {item.info.name()} is not set to auto-start.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
item.info.auto_start = False
|
||||||
|
updated = self.queue.put(item)
|
||||||
|
tasks.append(self._notify.emit(Events.UPDATED, data=updated.info))
|
||||||
|
status[item_id] = "paused"
|
||||||
|
LOG.debug(f"Item {item.info.name()} marked as paused.")
|
||||||
|
|
||||||
|
if len(tasks) > 0:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
def pause(self, shutdown: bool = False) -> bool:
|
def pause(self, shutdown: bool = False) -> bool:
|
||||||
"""
|
"""
|
||||||
Pause the download queue.
|
Pause the download queue.
|
||||||
|
|
@ -358,11 +446,12 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
live_in=live_in if live_in else item.extras.get("live_in", None),
|
live_in=live_in if live_in else item.extras.get("live_in", None),
|
||||||
options=options,
|
options=options,
|
||||||
cli=item.cli,
|
cli=item.cli,
|
||||||
|
auto_start=item.auto_start,
|
||||||
extras=item.extras,
|
extras=item.extras,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dlInfo: Download = Download(info=dl, info_dict=entry, logs=logs)
|
dlInfo: Download = Download(info=dl, info_dict=entry if item.auto_start else None, logs=logs)
|
||||||
|
|
||||||
text_logs: str = ""
|
text_logs: str = ""
|
||||||
if filtered_logs := extract_ytdlp_logs(logs):
|
if filtered_logs := extract_ytdlp_logs(logs):
|
||||||
|
|
@ -407,7 +496,10 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
if _requeue:
|
if _requeue:
|
||||||
NotifyEvent = Events.ADDED
|
NotifyEvent = Events.ADDED
|
||||||
itemDownload = self.queue.put(dlInfo)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
self.event.set()
|
if item.auto_start:
|
||||||
|
self.event.set()
|
||||||
|
else:
|
||||||
|
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
||||||
else:
|
else:
|
||||||
dlInfo.info.status = "not_live"
|
dlInfo.info.status = "not_live"
|
||||||
itemDownload = self.done.put(dlInfo)
|
itemDownload = self.done.put(dlInfo)
|
||||||
|
|
@ -419,7 +511,10 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
else:
|
else:
|
||||||
NotifyEvent = Events.ADDED
|
NotifyEvent = Events.ADDED
|
||||||
itemDownload = self.queue.put(dlInfo)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
self.event.set()
|
if item.auto_start:
|
||||||
|
self.event.set()
|
||||||
|
else:
|
||||||
|
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
||||||
|
|
||||||
await self._notify.emit(NotifyEvent, data=itemDownload.info.serialize())
|
await self._notify.emit(NotifyEvent, data=itemDownload.info.serialize())
|
||||||
|
|
||||||
|
|
@ -628,7 +723,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
self.queue.delete(id)
|
self.queue.delete(id)
|
||||||
await self._notify.emit(Events.CANCELLED, data=item.info.serialize())
|
await self._notify.emit(Events.CANCELLED, data=item.info.serialize())
|
||||||
item.info.status = "cancelled"
|
item.info.status = "cancelled"
|
||||||
item.info.error = "Cancelled by user."
|
# item.info.error = "Cancelled by user."
|
||||||
self.done.put(item)
|
self.done.put(item)
|
||||||
await self._notify.emit(Events.COMPLETED, data=item.info.serialize())
|
await self._notify.emit(Events.COMPLETED, data=item.info.serialize())
|
||||||
LOG.info(f"Deleted from queue {item_ref}")
|
LOG.info(f"Deleted from queue {item_ref}")
|
||||||
|
|
@ -751,7 +846,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
LOG.info("Download pool resumed downloading.")
|
LOG.info("Download pool resumed downloading.")
|
||||||
|
|
||||||
for _id, entry in list(self.queue.items()):
|
for _id, entry in list(self.queue.items()):
|
||||||
if entry.started() or entry.is_cancelled():
|
if entry.started() or entry.is_cancelled() or entry.info.auto_start is False:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if entry.is_live:
|
if entry.is_live:
|
||||||
|
|
@ -812,7 +907,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
if entry.is_cancelled() is True:
|
if entry.is_cancelled() is True:
|
||||||
await self._notify.emit(Events.CANCELLED, data=entry.info.serialize())
|
await self._notify.emit(Events.CANCELLED, data=entry.info.serialize())
|
||||||
entry.info.status = "cancelled"
|
entry.info.status = "cancelled"
|
||||||
entry.info.error = "Cancelled by user."
|
# entry.info.error = "Cancelled by user."
|
||||||
|
|
||||||
self.done.put(value=entry)
|
self.done.put(value=entry)
|
||||||
await self._notify.emit(Events.COMPLETED, data=entry.info.serialize())
|
await self._notify.emit(Events.COMPLETED, data=entry.info.serialize())
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ class Item:
|
||||||
requeued: bool = False
|
requeued: bool = False
|
||||||
"""If the item has been retried already via conditions."""
|
"""If the item has been retried already via conditions."""
|
||||||
|
|
||||||
|
auto_start: bool = True
|
||||||
|
"""If the item should be started automatically."""
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
return self.__dict__.copy()
|
return self.__dict__.copy()
|
||||||
|
|
||||||
|
|
@ -95,7 +98,7 @@ class Item:
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for k, v in self.serialize().items():
|
for k, v in self.serialize().items():
|
||||||
if not v:
|
if not v and k not in ("auto_start"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if k == "cli":
|
if k == "cli":
|
||||||
|
|
@ -157,6 +160,10 @@ class Item:
|
||||||
if item.get("template") and isinstance(item.get("template"), str):
|
if item.get("template") and isinstance(item.get("template"), str):
|
||||||
data["template"] = item.get("template")
|
data["template"] = item.get("template")
|
||||||
|
|
||||||
|
if "auto_start" in item and isinstance(item.get("auto_start"), bool):
|
||||||
|
LOG.info("Item '%s' auto_start is set to %s.", url, item.get("auto_start"))
|
||||||
|
data["auto_start"] = bool(item.get("auto_start"))
|
||||||
|
|
||||||
extras = item.get("extras")
|
extras = item.get("extras")
|
||||||
if extras and isinstance(extras, dict) and len(extras) > 0:
|
if extras and isinstance(extras, dict) and len(extras) > 0:
|
||||||
data["extras"] = extras
|
data["extras"] = extras
|
||||||
|
|
@ -213,6 +220,7 @@ class ItemDTO:
|
||||||
options: dict = field(default_factory=dict)
|
options: dict = field(default_factory=dict)
|
||||||
extras: dict = field(default_factory=dict)
|
extras: dict = field(default_factory=dict)
|
||||||
cli: str = ""
|
cli: str = ""
|
||||||
|
auto_start: bool = True
|
||||||
|
|
||||||
# yt-dlp injected fields.
|
# yt-dlp injected fields.
|
||||||
tmpfilename: str | None = None
|
tmpfilename: str | None = None
|
||||||
|
|
@ -239,7 +247,7 @@ class ItemDTO:
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return f'id="{self.id}", title="{self.title}"'
|
return f'id="{self.id}", title="{self.title}"'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def removed_fields() -> tuple:
|
def removed_fields() -> tuple:
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class Task:
|
||||||
timer: str = ""
|
timer: str = ""
|
||||||
template: str = ""
|
template: str = ""
|
||||||
cli: str = ""
|
cli: str = ""
|
||||||
|
auto_start: bool = True
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
return self.__dict__
|
return self.__dict__
|
||||||
|
|
|
||||||
|
|
@ -1131,6 +1131,7 @@ def get_archive_id(url: str) -> tuple[bool, dict[str | None, str | None, str | N
|
||||||
idDict["archive_id"] = YTDLP_INFO_CLS._make_archive_id(idDict)
|
idDict["archive_id"] = YTDLP_INFO_CLS._make_archive_id(idDict)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
LOG.error(f"Error getting archive ID: {e}")
|
LOG.error(f"Error getting archive ID: {e}")
|
||||||
|
|
||||||
return idDict
|
return idDict
|
||||||
|
|
|
||||||
|
|
@ -128,3 +128,27 @@ async def archive_item(config: Config, data: dict):
|
||||||
LOG.info(f"Archiving url '{data['url']}' with id '{idDict['archive_id']}'.")
|
LOG.info(f"Archiving url '{data['url']}' with id '{idDict['archive_id']}'.")
|
||||||
else:
|
else:
|
||||||
LOG.info(f"URL '{data['url']}' with id '{idDict['archive_id']}' already archived.")
|
LOG.info(f"URL '{data['url']}' with id '{idDict['archive_id']}' already archived.")
|
||||||
|
|
||||||
|
|
||||||
|
@route(RouteType.SOCKET, "item_start", "item_start")
|
||||||
|
async def item_start(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
||||||
|
if not data:
|
||||||
|
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = [data]
|
||||||
|
|
||||||
|
await queue.start_items(data)
|
||||||
|
|
||||||
|
|
||||||
|
@route(RouteType.SOCKET, "item_pause", "item_pause")
|
||||||
|
async def item_pause(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
||||||
|
if not data:
|
||||||
|
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = [data]
|
||||||
|
|
||||||
|
await queue.pause_items(data)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ type task_item = {
|
||||||
cli?: string,
|
cli?: string,
|
||||||
timer?: string,
|
timer?: string,
|
||||||
in_progress?: boolean,
|
in_progress?: boolean,
|
||||||
|
auto_start?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type exported_task = task_item & { _type: string, _version: string }
|
type exported_task = task_item & { _type: string, _version: string }
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,19 @@
|
||||||
<span class="icon"><i class="fa-solid fa-link" /></span>
|
<span class="icon"><i class="fa-solid fa-link" /></span>
|
||||||
URLs
|
URLs
|
||||||
</label>
|
</label>
|
||||||
<div class="control">
|
<div class="field is-grouped">
|
||||||
<input type="text" class="input" id="url" placeholder="Video or playlist link"
|
<div class="control is-expanded">
|
||||||
:disabled="!socket.isConnected || addInProgress" v-model="form.url">
|
<input type="text" class="input" id="url" placeholder="Video or playlist link"
|
||||||
|
:disabled="!socket.isConnected || addInProgress" v-model="form.url">
|
||||||
|
</div>
|
||||||
|
<div class="control is-align-content-space-around" v-if="!config.app.basic_mode"
|
||||||
|
v-tooltip="'Auto start the download after adding it.'">
|
||||||
|
<input id="auto_start" type="checkbox" v-model="auto_start" :disabled="addInProgress"
|
||||||
|
class="switch is-success" />
|
||||||
|
<label for="auto_start" class="is-unselectable">Start</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="help is-bold">
|
<span class="help is-bold is-unselectable">
|
||||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
<span>You can add multiple URLs separated by <code
|
<span>You can add multiple URLs separated by <code
|
||||||
class="is-bold">{{ getSeparatorsName(separator) }}</code>.</span>
|
class="is-bold">{{ getSeparatorsName(separator) }}</code>.</span>
|
||||||
|
|
@ -233,6 +241,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import 'assets/css/bulma-switch.css'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -264,6 +273,7 @@ const getSeparatorsName = (value) => {
|
||||||
|
|
||||||
const showAdvanced = useStorage('show_advanced', false)
|
const showAdvanced = useStorage('show_advanced', false)
|
||||||
const separator = useStorage('url_separator', separators[0].value)
|
const separator = useStorage('url_separator', separators[0].value)
|
||||||
|
const auto_start = useStorage('auto_start', true)
|
||||||
|
|
||||||
const addInProgress = ref(false)
|
const addInProgress = ref(false)
|
||||||
|
|
||||||
|
|
@ -300,6 +310,7 @@ const addDownload = async () => {
|
||||||
template: config.app.basic_mode ? null : form.value.template,
|
template: config.app.basic_mode ? null : form.value.template,
|
||||||
cookies: config.app.basic_mode ? '' : form.value.cookies,
|
cookies: config.app.basic_mode ? '' : form.value.cookies,
|
||||||
cli: config.app.basic_mode ? null : form.value.cli,
|
cli: config.app.basic_mode ? null : form.value.cli,
|
||||||
|
auto_start: config.app.basic_mode ? true : auto_start.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value?.extras && Object.keys(form.value.extras).length > 0) {
|
if (form.value?.extras && Object.keys(form.value.extras).length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,19 @@
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column is-half-mobile" v-if="hasManualStart">
|
||||||
|
<button type="button" class="button is-fullwidth is-success" :disabled="!hasSelected" @click="startItems">
|
||||||
|
<span class="icon"><i class="fa-solid fa-circle-play" /></span>
|
||||||
|
<span>Start</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half-mobile" v-if="hasPausable">
|
||||||
|
<button type="button" class="button is-fullwidth is-background-warning-85" :disabled="!hasSelected"
|
||||||
|
@click="pauseSelected">
|
||||||
|
<span class="icon"><i class="fa-solid fa-pause" /></span>
|
||||||
|
<span>Pause</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="column is-half-mobile" v-if="('cards' === display_style || hasSelected)">
|
<div class="column is-half-mobile" v-if="('cards' === display_style || hasSelected)">
|
||||||
<button type="button" class="button is-fullwidth is-warning" :disabled="!hasSelected" @click="cancelSelected">
|
<button type="button" class="button is-fullwidth is-warning" :disabled="!hasSelected" @click="cancelSelected">
|
||||||
<span class="icon"><i class="fa-solid fa-eject" /></span>
|
<span class="icon"><i class="fa-solid fa-eject" /></span>
|
||||||
|
|
@ -105,6 +118,14 @@
|
||||||
<td class="is-vcentered is-items-center">
|
<td class="is-vcentered is-items-center">
|
||||||
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s"
|
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s"
|
||||||
:button_classes="'is-small'" label="Actions">
|
:button_classes="'is-small'" label="Actions">
|
||||||
|
<template v-if="!item.auto_start">
|
||||||
|
<NuxtLink class="dropdown-item has-text-success" @click="startItem(item)">
|
||||||
|
<span class="icon"><i class="fa-solid fa-circle-play" /></span>
|
||||||
|
<span>Start Download</span>
|
||||||
|
</NuxtLink>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="isEmbedable(item.url)">
|
<template v-if="isEmbedable(item.url)">
|
||||||
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url)">
|
<NuxtLink class="dropdown-item has-text-danger" @click="embed_url = getEmbedable(item.url)">
|
||||||
<span class="icon"><i class="fa-solid fa-play" /></span>
|
<span class="icon"><i class="fa-solid fa-play" /></span>
|
||||||
|
|
@ -219,6 +240,18 @@
|
||||||
<span>Cancel</span>
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column is-half-mobile" v-if="!item.auto_start && !item.status">
|
||||||
|
<button class="button is-success is-fullwidth" @click="startItem(item)">
|
||||||
|
<span class="icon"><i class="fa-solid fa-circle-play" /></span>
|
||||||
|
<span>Start</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half-mobile" v-if="item.auto_start && !item.status">
|
||||||
|
<button class="button is-background-warning-85 is-fullwidth" @click="pauseItem(item)">
|
||||||
|
<span class="icon"><i class="fa-solid fa-pause" /></span>
|
||||||
|
<span>Pause</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="column is-half-mobile">
|
<div class="column is-half-mobile">
|
||||||
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s" label="Actions">
|
<Dropdown icons="fa-solid fa-cogs" @open_state="s => table_container = !s" label="Actions">
|
||||||
<template v-if="isEmbedable(item.url)">
|
<template v-if="isEmbedable(item.url)">
|
||||||
|
|
@ -324,8 +357,42 @@ const filteredItems = computed(() => {
|
||||||
|
|
||||||
const hasSelected = computed(() => selectedElms.value.length > 0)
|
const hasSelected = computed(() => selectedElms.value.length > 0)
|
||||||
const hasQueuedItems = computed(() => stateStore.count('queue') > 0)
|
const hasQueuedItems = computed(() => stateStore.count('queue') > 0)
|
||||||
|
const hasManualStart = computed(() => {
|
||||||
|
if (stateStore.count('queue') < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in stateStore.queue) {
|
||||||
|
const item = stateStore.queue[key]
|
||||||
|
if (!item.status && item.auto_start === false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasPausable = computed(() => {
|
||||||
|
if (stateStore.count('queue') < 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in stateStore.queue) {
|
||||||
|
const item = stateStore.queue[key]
|
||||||
|
if (!item.status && item.auto_start === true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const setIcon = item => {
|
const setIcon = item => {
|
||||||
|
if (!item.auto_start) {
|
||||||
|
return 'fa-hourglass-half'
|
||||||
|
}
|
||||||
|
|
||||||
if ('downloading' === item.status && item.is_live) {
|
if ('downloading' === item.status && item.is_live) {
|
||||||
return 'fa-globe fa-spin'
|
return 'fa-globe fa-spin'
|
||||||
}
|
}
|
||||||
|
|
@ -350,6 +417,10 @@ const setIcon = item => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setStatus = item => {
|
const setStatus = item => {
|
||||||
|
if (!item.auto_start) {
|
||||||
|
return 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
if (null === item.status && true === config.paused) {
|
if (null === item.status && true === config.paused) {
|
||||||
return 'Paused'
|
return 'Paused'
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +449,7 @@ const setIconColor = item => {
|
||||||
return 'has-text-info'
|
return 'has-text-info'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === item.status && true === config.paused) {
|
if (!item.auto_start || (null === item.status && true === config.paused)) {
|
||||||
return 'has-text-warning'
|
return 'has-text-warning'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -422,8 +493,12 @@ const percentPipe = value => {
|
||||||
const updateProgress = (item) => {
|
const updateProgress = (item) => {
|
||||||
let string = ''
|
let string = ''
|
||||||
|
|
||||||
|
if (!item.auto_start) {
|
||||||
|
return 'Manual start'
|
||||||
|
}
|
||||||
|
|
||||||
if (null === item.status && true === config.paused) {
|
if (null === item.status && true === config.paused) {
|
||||||
return 'Paused'
|
return 'Global Pause'
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('postprocessing' === item.status) {
|
if ('postprocessing' === item.status) {
|
||||||
|
|
@ -482,6 +557,65 @@ const cancelItems = item => {
|
||||||
items.forEach(id => socket.emit('item_cancel', id))
|
items.forEach(id => socket.emit('item_cancel', id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startItem = item => socket.emit('item_start', item._id)
|
||||||
|
const pauseItem = item => socket.emit('item_pause', item._id)
|
||||||
|
|
||||||
|
const startItems = () => {
|
||||||
|
if (selectedElms.value.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtered = []
|
||||||
|
|
||||||
|
selectedElms.value.forEach(id => {
|
||||||
|
const item = stateStore.get('queue', id)
|
||||||
|
if (item && !item.auto_start && !item.status) {
|
||||||
|
filtered.push(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
selectedElms.value = []
|
||||||
|
|
||||||
|
if (filtered.length < 1) {
|
||||||
|
toast.error('No eligible items to start.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true !== box.confirm(`Start '${filtered.length}' selected items?`)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.forEach(id => socket.emit('item_start', id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseSelected = () => {
|
||||||
|
if (selectedElms.value.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtered = []
|
||||||
|
|
||||||
|
selectedElms.value.forEach(id => {
|
||||||
|
const item = stateStore.get('queue', id)
|
||||||
|
if (item && item.auto_start && !item.status) {
|
||||||
|
filtered.push(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
selectedElms.value = []
|
||||||
|
|
||||||
|
if (filtered.length < 1) {
|
||||||
|
toast.error('No eligible items to pause.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true !== box.confirm(`Pause '${filtered.length}' selected items?`)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.forEach(id => socket.emit('item_pause', id))
|
||||||
|
}
|
||||||
|
|
||||||
const pImg = e => e.target.naturalHeight > e.target.naturalWidth ? e.target.classList.add('image-portrait') : null
|
const pImg = e => e.target.naturalHeight > e.target.naturalWidth ? e.target.classList.add('image-portrait') : null
|
||||||
|
|
||||||
watch(embed_url, v => {
|
watch(embed_url, v => {
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6-tablet is-12-mobile">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-inline" for="output_template">
|
||||||
|
<span class="icon"><i class="fa-solid fa-circle-play" /></span>
|
||||||
|
Auto Start
|
||||||
|
</label>
|
||||||
|
<div class="control is-unselectable">
|
||||||
|
<input id="auto_start" type="checkbox" v-model="form.auto_start" :disabled="addInProgress"
|
||||||
|
class="switch is-success" />
|
||||||
|
<label for="auto_start" class="is-unselectable">
|
||||||
|
{{ form.auto_start ? 'Yes' : 'No' }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span class="help">
|
||||||
|
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||||
|
<span>Whether to automatically start downloading or just queue them in paused state.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label is-inline" for="cli_options">
|
<label class="label is-inline" for="cli_options">
|
||||||
|
|
@ -269,6 +289,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import 'assets/css/bulma-switch.css'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { CronExpressionParser } from 'cron-parser'
|
import { CronExpressionParser } from 'cron-parser'
|
||||||
import { decode } from '~/utils/importer'
|
import { decode } from '~/utils/importer'
|
||||||
|
|
@ -392,6 +413,8 @@ const importItem = async () => {
|
||||||
form.cli = item.cli
|
form.cli = item.cli
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.auto_start = item?.auto_start ?? true
|
||||||
|
|
||||||
if (item.preset) {
|
if (item.preset) {
|
||||||
// -- check if the preset exists in config.presets
|
// -- check if the preset exists in config.presets
|
||||||
const preset = config.presets.find(p => p.name === item.preset)
|
const preset = config.presets.find(p => p.name === item.preset)
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,7 @@ div.is-centered {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-multiline is-mobile" v-if="!toggleForm && filteredTasks && filteredTasks.length > 0">
|
||||||
<div class="columns is-multiline is-mobile" v-if="filteredTasks && filteredTasks.length > 0">
|
|
||||||
<div class="column" v-if="'list' !== display_style">
|
<div class="column" v-if="'list' !== display_style">
|
||||||
<button type="button" class="button is-fullwidth is-ghost is-inverted"
|
<button type="button" class="button is-fullwidth is-ghost is-inverted"
|
||||||
@click="masterSelectAll = !masterSelectAll">
|
@click="masterSelectAll = !masterSelectAll">
|
||||||
|
|
@ -103,10 +102,8 @@ div.is-centered {
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="columns is-multiline" v-if="!isLoading && !toggleForm && filteredTasks && filteredTasks.length > 0">
|
<div class="columns is-multiline" v-if="!isLoading && !toggleForm && filteredTasks && filteredTasks.length > 0">
|
||||||
<template v-if="'list' === display_style">
|
<template v-if="'list' === display_style">
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
|
|
@ -152,9 +149,21 @@ div.is-centered {
|
||||||
{{ remove_tags(item.name) }}
|
{{ remove_tags(item.name) }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.preset">
|
<div>
|
||||||
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
<span class="icon-text">
|
||||||
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
<span class="icon">
|
||||||
|
<i class="fa-solid"
|
||||||
|
:class="{ 'fa-circle-pause': item.auto_start, 'fa-circle-play': !item.auto_start }" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ item.auto_start ? 'Auto' : 'Manual' }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="icon-text" v-if="item.preset">
|
||||||
|
<span class="icon"><i class="fa-solid fa-tv" /></span>
|
||||||
|
<span>{{ item.preset ?? config.app.default_preset }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="is-vcentered has-text-centered">
|
<td class="is-vcentered has-text-centered">
|
||||||
|
|
@ -218,7 +227,12 @@ div.is-centered {
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<span class="icon" v-tooltip="`${item.auto_start ? 'Auto' : 'Manual'} start`">
|
||||||
|
<i class="fa-solid"
|
||||||
|
:class="{ 'fa-circle-pause': item.auto_start, 'fa-circle-play': !item.auto_start }" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
||||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||||
|
|
@ -653,6 +667,10 @@ const runNow = async (item: task_item, mass: boolean = false) => {
|
||||||
data.cli = item.cli
|
data.cli = item.cli
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item?.auto_start !== undefined) {
|
||||||
|
data.auto_start = item.auto_start
|
||||||
|
}
|
||||||
|
|
||||||
socket.emit('add_url', data)
|
socket.emit('add_url', data)
|
||||||
|
|
||||||
if (true === mass) {
|
if (true === mass) {
|
||||||
|
|
@ -685,6 +703,7 @@ const exportItem = async (item: task_item) => {
|
||||||
preset: info.preset,
|
preset: info.preset,
|
||||||
timer: info.timer,
|
timer: info.timer,
|
||||||
folder: info.folder,
|
folder: info.folder,
|
||||||
|
auto_start: info?.auto_start ?? true,
|
||||||
} as exported_task
|
} as exported_task
|
||||||
|
|
||||||
if (info.template) {
|
if (info.template) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue