commit
0682f9d443
18 changed files with 189 additions and 93 deletions
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
|
|
@ -37,7 +37,8 @@
|
||||||
"YTP_LOG_LEVEL": "DEBUG",
|
"YTP_LOG_LEVEL": "DEBUG",
|
||||||
"YTP_DEBUG": "true",
|
"YTP_DEBUG": "true",
|
||||||
"YTP_YTDL_DEBUG": "true",
|
"YTP_YTDL_DEBUG": "true",
|
||||||
"YTP_MAX_WORKERS": "2"
|
"YTP_MAX_WORKERS": "2",
|
||||||
|
"YTP_IGNORE_UI": "true",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ Certain values can be set via environment variables, using the `-e` parameter on
|
||||||
* __YTP_EXTRACT_INFO_TIMEOUT__: The timeout for extracting video information. Defaults to `70`.
|
* __YTP_EXTRACT_INFO_TIMEOUT__: The timeout for extracting video information. Defaults to `70`.
|
||||||
* __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`.
|
* __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`.
|
||||||
* __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`.
|
* __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`.
|
||||||
|
* __YTP_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`.
|
||||||
|
|
||||||
## Running behind a reverse proxy
|
## Running behind a reverse proxy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,12 @@ class DownloadQueue:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
return {"status": "error", "msg": str(e)}
|
return {"status": "error", "msg": str(e)}
|
||||||
|
|
||||||
|
extras: dict = {}
|
||||||
|
fields: tuple = ("uploader", "channel", "thumbnail")
|
||||||
|
for field in fields:
|
||||||
|
if entry.get(field, None):
|
||||||
|
extras[field] = entry.get(field)
|
||||||
|
|
||||||
dl = ItemDTO(
|
dl = ItemDTO(
|
||||||
id=entry.get("id"),
|
id=entry.get("id"),
|
||||||
title=entry.get("title"),
|
title=entry.get("title"),
|
||||||
|
|
@ -153,6 +159,7 @@ class DownloadQueue:
|
||||||
is_live=is_live,
|
is_live=is_live,
|
||||||
live_in=live_in,
|
live_in=live_in,
|
||||||
options=options,
|
options=options,
|
||||||
|
extras=extras,
|
||||||
)
|
)
|
||||||
|
|
||||||
for property, value in entry.items():
|
for property, value in entry.items():
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,12 @@ class HttpAPI(common):
|
||||||
preloaded += 2
|
preloaded += 2
|
||||||
|
|
||||||
if preloaded < 1:
|
if preloaded < 1:
|
||||||
raise ValueError(f"Could not find the frontend UI static assets. '{staticDir}'.")
|
message = f"Could not find the frontend UI static assets. '{staticDir}'."
|
||||||
|
if self.config.ignore_ui:
|
||||||
|
LOG.warning(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
LOG.info(f"Preloaded '{preloaded}' static files.")
|
LOG.info(f"Preloaded '{preloaded}' static files.")
|
||||||
|
|
||||||
|
|
@ -301,9 +306,7 @@ class HttpAPI(common):
|
||||||
remove_file: bool = bool(post.get("remove_file", True))
|
remove_file: bool = bool(post.get("remove_file", True))
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data=await (
|
data=await (self.queue.cancel(ids) if where == "queue" else self.queue.clear(ids, remove_file=remove_file)),
|
||||||
self.queue.cancel(ids) if where == "queue" else self.queue.clear(ids, remove_file=remove_file)
|
|
||||||
),
|
|
||||||
status=web.HTTPOk.status_code,
|
status=web.HTTPOk.status_code,
|
||||||
dumps=self.encoder.encode,
|
dumps=self.encoder.encode,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class ItemDTO:
|
||||||
live_in: str | None = None
|
live_in: str | None = None
|
||||||
file_size: int | None = None
|
file_size: int | None = None
|
||||||
options: dict = field(default_factory=dict)
|
options: dict = field(default_factory=dict)
|
||||||
|
extras: dict = field(default_factory=dict)
|
||||||
|
|
||||||
# yt-dlp injected fields.
|
# yt-dlp injected fields.
|
||||||
tmpfilename: str | None = None
|
tmpfilename: str | None = None
|
||||||
|
|
@ -48,5 +49,21 @@ class ItemDTO:
|
||||||
speed: str | None = None
|
speed: str | None = None
|
||||||
eta: str | None = None
|
eta: str | None = None
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
deprecated: tuple = (
|
||||||
|
"thumbnail",
|
||||||
|
"quality",
|
||||||
|
"format",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.thumbnail and "thumbnail" not in self.extras:
|
||||||
|
self.extras["thumbnail"] = self.thumbnail
|
||||||
|
|
||||||
|
dump = self.__dict__.copy()
|
||||||
|
for f in deprecated:
|
||||||
|
dump.pop(f)
|
||||||
|
|
||||||
|
return dump
|
||||||
|
|
||||||
def json(self) -> str:
|
def json(self) -> str:
|
||||||
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
return json.dumps(self.serialize(), default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import posixpath
|
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ class Config:
|
||||||
|
|
||||||
db_file: str = "{config_path}/ytptube.db"
|
db_file: str = "{config_path}/ytptube.db"
|
||||||
manual_archive: str = "{config_path}/archive.manual.log"
|
manual_archive: str = "{config_path}/archive.manual.log"
|
||||||
|
ui_update_title: bool = True
|
||||||
|
|
||||||
# immutable config vars.
|
# immutable config vars.
|
||||||
version: str = APP_VERSION
|
version: str = APP_VERSION
|
||||||
|
|
@ -64,6 +65,7 @@ class Config:
|
||||||
new_version_available: bool = False
|
new_version_available: bool = False
|
||||||
ytdlp_version: str = YTDLP_VERSION
|
ytdlp_version: str = YTDLP_VERSION
|
||||||
started: int = 0
|
started: int = 0
|
||||||
|
ignore_ui: bool = False
|
||||||
presets: list = [
|
presets: list = [
|
||||||
{"name": "default", "format": "default", "postprocessors": [], "args": {}},
|
{"name": "default", "format": "default", "postprocessors": [], "args": {}},
|
||||||
{
|
{
|
||||||
|
|
@ -142,6 +144,8 @@ class Config:
|
||||||
"allow_manifestless",
|
"allow_manifestless",
|
||||||
"access_log",
|
"access_log",
|
||||||
"remove_files",
|
"remove_files",
|
||||||
|
"ignore_ui",
|
||||||
|
"ui_update_title",
|
||||||
)
|
)
|
||||||
|
|
||||||
_frontend_vars: tuple = (
|
_frontend_vars: tuple = (
|
||||||
|
|
@ -154,6 +158,7 @@ class Config:
|
||||||
"started",
|
"started",
|
||||||
"url_prefix",
|
"url_prefix",
|
||||||
"remove_files",
|
"remove_files",
|
||||||
|
"ui_update_title",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ class Encoder(json.JSONEncoder):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
|
if isinstance(o, object) and hasattr(o, "serialize"):
|
||||||
|
return o.serialize()
|
||||||
|
|
||||||
if isinstance(o, object) and hasattr(o, "__dict__"):
|
if isinstance(o, object) and hasattr(o, "__dict__"):
|
||||||
return o.__dict__
|
return o.__dict__
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,9 @@ class Main:
|
||||||
LOG.info(f"YTPTube v{self.config.version} - started on http://{self.config.host}:{self.config.port}")
|
LOG.info(f"YTPTube v{self.config.version} - started on http://{self.config.host}:{self.config.port}")
|
||||||
LOG.info("=" * 40)
|
LOG.info("=" * 40)
|
||||||
|
|
||||||
|
if self.config.access_log:
|
||||||
|
http_logger.addFilter(lambda record: "GET /ping" not in record.getMessage())
|
||||||
|
|
||||||
web.run_app(
|
web.run_app(
|
||||||
self.app,
|
self.app,
|
||||||
host=self.config.host,
|
host=self.config.host,
|
||||||
|
|
|
||||||
|
|
@ -213,3 +213,32 @@ hr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.play-overlay {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
background-image: url(/images/play-icon.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-icon:hover,
|
||||||
|
.play-icon:focus,
|
||||||
|
.play-active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,40 +96,21 @@
|
||||||
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail, }" /></span>
|
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail, }" /></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
<div v-if="false === hideThumbnail" class="card-image">
|
<div v-if="false === hideThumbnail" class="card-image">
|
||||||
<figure class="image is-3by1" v-if="item.thumbnail">
|
<figure class="image is-3by1">
|
||||||
<template v-if="item.status === 'finished'">
|
<span v-if="'finished' === item.status" @click="playVideo(item)" class="play-overlay">
|
||||||
<a v-tooltip="`Play: ${item.title}`" :href="makeDownload(config, item, 'm3u8')"
|
<div class="play-icon"></div>
|
||||||
@click.prevent="playVideo(item)">
|
<img
|
||||||
<img
|
:src="config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||||
:src="config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.thumbnail)"
|
:alt="item.title" v-if="item.extras?.thumbnail" />
|
||||||
:alt="item.title" />
|
<img v-else src="/images/placeholder.png" :alt="item.title" />
|
||||||
</a>
|
</span>
|
||||||
</template>
|
<NuxtLink v-else target="_blank" :href="item.url" v-tooltip="`Open: ${item.title} link`">
|
||||||
<template v-else>
|
<img :alt="item.title" v-if="item.extras?.thumbnail"
|
||||||
<NuxtLink target="_blank" :href="item.url" v-tooltip="`Open: ${item.title} link`">
|
:src="config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.extras.thumbnail)" />
|
||||||
<img
|
<img v-else src="/images/placeholder.png" :alt="item.title" />
|
||||||
:src="config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.thumbnail)"
|
</NuxtLink>
|
||||||
:alt="item.title" />
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
</figure>
|
|
||||||
<figure class="image is-3by1" v-else>
|
|
||||||
<template v-if="item.status === 'finished'">
|
|
||||||
<a v-tooltip="`Play: ${item.title}`" :href="makeDownload(config, item, 'm3u8')"
|
|
||||||
@click.prevent="playVideo(item)">
|
|
||||||
<img :src="config.app.url_host + config.app.url_prefix + 'images/placeholder.png'"
|
|
||||||
:alt="item.title" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<NuxtLink target="_blank" :href="item.url" v-tooltip="`Open: ${item.title} link`">
|
|
||||||
<img :src="config.app.url_host + config.app.url_prefix + 'images/placeholder.png'"
|
|
||||||
:alt="item.title" />
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
@ -253,7 +234,8 @@
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<VideoPlayer type="default" :link="video_link" :isMuted="false" autoplay="true" :isControls="true"
|
<VideoPlayer type="default" :link="video_link" :isMuted="false" autoplay="true" :isControls="true"
|
||||||
:title="video_title" class="is-fullwidth" @closeModel="video_link = ''; video_title = ''" />
|
:title="video_title" :thumbnail="video_thumbnail" :artist="video_artist" class="is-fullwidth"
|
||||||
|
@closeModel="video_link = ''; video_title = ''; video_thumbnail = ''; video_artist = '';" />
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close is-large" aria-label="close" @click="video_link = ''"></button>
|
<button class="modal-close is-large" aria-label="close" @click="video_link = ''"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -270,38 +252,51 @@ const config = useConfigStore()
|
||||||
const stateStore = useStateStore()
|
const stateStore = useStateStore()
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
|
|
||||||
const selectedElms = ref([]);
|
const selectedElms = ref([])
|
||||||
const masterSelectAll = ref(false);
|
const masterSelectAll = ref(false)
|
||||||
const showCompleted = useStorage('showCompleted', true)
|
const showCompleted = useStorage('showCompleted', true)
|
||||||
const hideThumbnail = useStorage('hideThumbnailHistory', false)
|
const hideThumbnail = useStorage('hideThumbnailHistory', false)
|
||||||
const direction = useStorage('sortCompleted', 'desc')
|
const direction = useStorage('sortCompleted', 'desc')
|
||||||
|
|
||||||
const video_link = ref('')
|
const video_link = ref('')
|
||||||
const video_title = ref('')
|
const video_title = ref('')
|
||||||
|
const video_thumbnail = ref('')
|
||||||
|
const video_artist = ref('')
|
||||||
|
|
||||||
const playVideo = item => {
|
const playVideo = item => {
|
||||||
video_link.value = makeDownload(config, item, 'm3u8');
|
video_thumbnail.value = '';
|
||||||
video_title.value = item.title;
|
video_artist.value = '';
|
||||||
|
video_link.value = makeDownload(config, item, 'm3u8')
|
||||||
|
video_title.value = item.title
|
||||||
|
if (item.extras?.thumbnail) {
|
||||||
|
video_thumbnail.value = config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.extras.thumbnail)
|
||||||
|
}
|
||||||
|
if (item.extras?.channel) {
|
||||||
|
video_artist.value = item.extras.channel
|
||||||
|
}
|
||||||
|
if (!video_artist.value && item.extras?.uploader) {
|
||||||
|
video_artist.value = item.extras.uploader
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(masterSelectAll, (value) => {
|
watch(masterSelectAll, (value) => {
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
const element = stateStore.history[key];
|
const element = stateStore.history[key]
|
||||||
if (value) {
|
if (value) {
|
||||||
selectedElms.value.push(element._id);
|
selectedElms.value.push(element._id)
|
||||||
} else {
|
} else {
|
||||||
selectedElms.value = [];
|
selectedElms.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const sortCompleted = computed(() => {
|
const sortCompleted = computed(() => {
|
||||||
const thisDirection = direction.value;
|
const thisDirection = direction.value
|
||||||
return Object.values(stateStore.history).sort((a, b) => {
|
return Object.values(stateStore.history).sort((a, b) => {
|
||||||
if ('asc' === thisDirection) {
|
if ('asc' === thisDirection) {
|
||||||
return new Date(a.datetime) - new Date(b.datetime);
|
return new Date(a.datetime) - new Date(b.datetime)
|
||||||
}
|
}
|
||||||
return new Date(b.datetime) - new Date(a.datetime);
|
return new Date(b.datetime) - new Date(a.datetime)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -313,80 +308,80 @@ const showMessage = (item) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return item.msg.length > 0;
|
return item.msg.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasIncomplete = computed(() => {
|
const hasIncomplete = computed(() => {
|
||||||
if (Object.keys(stateStore.history)?.length < 0) {
|
if (Object.keys(stateStore.history)?.length < 0) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
const element = stateStore.history[key];
|
const element = stateStore.history[key]
|
||||||
if (element.status !== 'finished') {
|
if (element.status !== 'finished') {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasCompleted = computed(() => {
|
const hasCompleted = computed(() => {
|
||||||
if (Object.keys(stateStore.history)?.length < 0) {
|
if (Object.keys(stateStore.history)?.length < 0) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
const element = stateStore.history[key];
|
const element = stateStore.history[key]
|
||||||
if (element.status === 'finished') {
|
if (element.status === 'finished') {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteSelectedItems = () => {
|
const deleteSelectedItems = () => {
|
||||||
if (selectedElms.value.length < 1) {
|
if (selectedElms.value.length < 1) {
|
||||||
toast.error('No items selected.');
|
toast.error('No items selected.')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = `Are you sure you want to delete '${selectedElms.value.length}' items?`
|
let msg = `Are you sure you want to delete '${selectedElms.value.length}' items?`
|
||||||
if (true === config.app.remove_files) {
|
if (true === config.app.remove_files) {
|
||||||
msg += '\nThis will delete the files from the server if they exist.';
|
msg += '\nThis will delete the files from the server if they exist.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === confirm(msg)) {
|
if (false === confirm(msg)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in selectedElms.value) {
|
for (const key in selectedElms.value) {
|
||||||
const item = stateStore.history[selectedElms.value[key]];
|
const item = stateStore.history[selectedElms.value[key]]
|
||||||
if ('finished' === item.status) {
|
if ('finished' === item.status) {
|
||||||
socket.emit('archive_item', item);
|
socket.emit('archive_item', item)
|
||||||
}
|
}
|
||||||
socket.emit('item_delete', {
|
socket.emit('item_delete', {
|
||||||
id: item._id,
|
id: item._id,
|
||||||
remove_file: config.app.remove_files,
|
remove_file: config.app.remove_files,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearCompleted = () => {
|
const clearCompleted = () => {
|
||||||
let msg = 'Are you sure you want to clear all completed downloads?';
|
let msg = 'Are you sure you want to clear all completed downloads?'
|
||||||
if (false === confirm(msg)) {
|
if (false === confirm(msg)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
if ('finished' === ag(stateStore.get('history', key, {}), 'status')) {
|
if ('finished' === ag(stateStore.get('history', key, {}), 'status')) {
|
||||||
socket.emit('item_delete', { id: stateStore.history[key]._id, remove_file: false, });
|
socket.emit('item_delete', { id: stateStore.history[key]._id, remove_file: false, })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearIncomplete = () => {
|
const clearIncomplete = () => {
|
||||||
if (false === confirm('Are you sure you want to clear all in-complete downloads?')) {
|
if (false === confirm('Are you sure you want to clear all in-complete downloads?')) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
|
|
@ -394,40 +389,40 @@ const clearIncomplete = () => {
|
||||||
socket.emit('item_delete', {
|
socket.emit('item_delete', {
|
||||||
id: stateStore.history[key]._id,
|
id: stateStore.history[key]._id,
|
||||||
remove_file: false,
|
remove_file: false,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setIcon = item => {
|
const setIcon = item => {
|
||||||
if (item.status === 'finished' && item.is_live) {
|
if (item.status === 'finished' && item.is_live) {
|
||||||
return 'fa-solid fa-globe';
|
return 'fa-solid fa-globe'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.status === 'finished') {
|
if (item.status === 'finished') {
|
||||||
return 'fa-solid fa-circle-check';
|
return 'fa-solid fa-circle-check'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.status === 'error') {
|
if (item.status === 'error') {
|
||||||
return 'fa-solid fa-circle-xmark';
|
return 'fa-solid fa-circle-xmark'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.status === 'canceled') {
|
if (item.status === 'canceled') {
|
||||||
return 'fa-solid fa-eject';
|
return 'fa-solid fa-eject'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'fa-solid fa-circle';
|
return 'fa-solid fa-circle'
|
||||||
}
|
}
|
||||||
|
|
||||||
const requeueIncomplete = () => {
|
const requeueIncomplete = () => {
|
||||||
if (false === confirm('Are you sure you want to re-queue all incomplete downloads?')) {
|
if (false === confirm('Are you sure you want to re-queue all incomplete downloads?')) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in stateStore.history) {
|
for (const key in stateStore.history) {
|
||||||
const item = stateStore.get('history', key, {});
|
const item = stateStore.get('history', key, {})
|
||||||
if ('finished' === item.status) {
|
if ('finished' === item.status) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
reQueueItem(item)
|
reQueueItem(item)
|
||||||
}
|
}
|
||||||
|
|
@ -437,12 +432,12 @@ const archiveItem = item => {
|
||||||
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
|
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socket.emit('archive_item', item);
|
socket.emit('archive_item', item)
|
||||||
socket.emit('item_delete', { id: item._id, remove_file: false });
|
socket.emit('item_delete', { id: item._id, remove_file: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeItem = item => {
|
const removeItem = item => {
|
||||||
const msg = `Remove '${item.title ?? item.id ?? item.url ?? '??'}'?\n this will delete the file from the server.`;
|
const msg = `Remove '${item.title ?? item.id ?? item.url ?? '??'}'?\n this will delete the file from the server.`
|
||||||
if (config.app.remove_files && !confirm(msg)) {
|
if (config.app.remove_files && !confirm(msg)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -450,7 +445,7 @@ const removeItem = item => {
|
||||||
socket.emit('item_delete', {
|
socket.emit('item_delete', {
|
||||||
id: item._id,
|
id: item._id,
|
||||||
remove_file: config.app.remove_files
|
remove_file: config.app.remove_files
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const reQueueItem = item => {
|
const reQueueItem = item => {
|
||||||
|
|
@ -462,6 +457,6 @@ const reQueueItem = item => {
|
||||||
ytdlp_config: item.ytdlp_config,
|
ytdlp_config: item.ytdlp_config,
|
||||||
ytdlp_cookies: item.ytdlp_cookies,
|
ytdlp_cookies: item.ytdlp_cookies,
|
||||||
output_template: item.output_template,
|
output_template: item.output_template,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="false === hideThumbnail" class="card-image">
|
<div v-if="false === hideThumbnail" class="card-image">
|
||||||
<figure class="image is-3by1" v-if="item.thumbnail">
|
<figure class="image is-3by1" v-if="item.extras?.thumbnail">
|
||||||
<NuxtLink v-tooltip="item.title" :href="item.url" target="_blank">
|
<NuxtLink v-tooltip="item.title" :href="item.url" target="_blank">
|
||||||
<img :src="config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.thumbnail)"
|
<img :src="config.app.url_host + config.app.url_prefix + 'thumbnail?url=' + encodePath(item.extras.thumbnail)"
|
||||||
:alt="item.title" />
|
:alt="item.title" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
thumbnail: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
artist: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
isControls: {
|
isControls: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
|
@ -85,10 +93,22 @@ onUnmounted(() => {
|
||||||
if (props.title) {
|
if (props.title) {
|
||||||
window.document.title = 'YTPTube'
|
window.document.title = 'YTPTube'
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const prepareVideoPlayer = () => {
|
const prepareVideoPlayer = () => {
|
||||||
|
let mediaMetadata = {
|
||||||
|
title: props.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.thumbnail) {
|
||||||
|
mediaMetadata['artwork'] = [
|
||||||
|
{ src: props.thumbnail, sizes: '1920x1080', type: 'image/jpeg' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (props.artist) {
|
||||||
|
mediaMetadata['artist'] = props.artist
|
||||||
|
}
|
||||||
|
|
||||||
player = new Plyr(video.value, {
|
player = new Plyr(video.value, {
|
||||||
debug: false,
|
debug: false,
|
||||||
clickToPlay: true,
|
clickToPlay: true,
|
||||||
|
|
@ -106,9 +126,7 @@ const prepareVideoPlayer = () => {
|
||||||
key: 'plyr'
|
key: 'plyr'
|
||||||
},
|
},
|
||||||
title: props.title,
|
title: props.title,
|
||||||
mediaMetadata: {
|
mediaMetadata: mediaMetadata,
|
||||||
title: props.title
|
|
||||||
},
|
|
||||||
captions: {
|
captions: {
|
||||||
update: true,
|
update: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item" v-if="config.tasks.length > 0">
|
<div class="navbar-item" v-if="config.tasks.length > 0">
|
||||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/tasks">
|
<NuxtLink class="button is-dark has-tooltip-bottom" to="/tasks">
|
||||||
<span class="icon"><i class="fa-solid fa-tasks" /></span>
|
<span class="icon"><i class="fa-solid fa-tasks" /></span>
|
||||||
<span class="is-hidden-mobile">Tasks</span>
|
<span class="is-hidden-mobile">Tasks</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
useHead({ title: 'Index' })
|
const config = useConfigStore()
|
||||||
|
useHead({ title: 'YTPTube' })
|
||||||
|
|
||||||
|
watch(() => config.app.ui_update_title, value => {
|
||||||
|
if (true !== value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = useStateStore()
|
||||||
|
useHead({ title: `YTPTube: ( ${Object.keys(s.queue).length || 0} | ${Object.keys(s.history).length || 0} )` })
|
||||||
|
watch([s.queue, s.history], () => {
|
||||||
|
const title = `YTPTube: ( ${Object.keys(s.queue).length || 0} | ${Object.keys(s.history).length || 0} )`
|
||||||
|
useHead({ title })
|
||||||
|
})
|
||||||
|
|
||||||
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { parseExpression } from 'cron-parser'
|
import { parseExpression } from 'cron-parser'
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
console.log(config.tasks)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
BIN
ui/public/images/play-icon.png
Normal file
BIN
ui/public/images/play-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -4,6 +4,7 @@ const CONFIG_KEYS = {
|
||||||
download_path: '/downloads',
|
download_path: '/downloads',
|
||||||
keep_archive: false,
|
keep_archive: false,
|
||||||
remove_files: false,
|
remove_files: false,
|
||||||
|
ui_update_title: true,
|
||||||
output_template: '',
|
output_template: '',
|
||||||
ytdlp_version: '',
|
ytdlp_version: '',
|
||||||
version: '',
|
version: '',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue