diff --git a/.vscode/launch.json b/.vscode/launch.json
index 000b4eda..d6dcc102 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -37,7 +37,8 @@
"YTP_LOG_LEVEL": "DEBUG",
"YTP_DEBUG": "true",
"YTP_YTDL_DEBUG": "true",
- "YTP_MAX_WORKERS": "2"
+ "YTP_MAX_WORKERS": "2",
+ "YTP_IGNORE_UI": "true",
}
},
{
diff --git a/README.md b/README.md
index 436edf2b..1420b88e 100644
--- a/README.md
+++ b/README.md
@@ -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_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_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`.
## Running behind a reverse proxy
diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py
index 1158cabd..fbd5320d 100644
--- a/app/library/DownloadQueue.py
+++ b/app/library/DownloadQueue.py
@@ -135,6 +135,12 @@ class DownloadQueue:
LOG.exception(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(
id=entry.get("id"),
title=entry.get("title"),
@@ -153,6 +159,7 @@ class DownloadQueue:
is_live=is_live,
live_in=live_in,
options=options,
+ extras=extras,
)
for property, value in entry.items():
diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py
index f1bece6e..96b487c7 100644
--- a/app/library/HttpAPI.py
+++ b/app/library/HttpAPI.py
@@ -123,7 +123,12 @@ class HttpAPI(common):
preloaded += 2
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.")
@@ -301,9 +306,7 @@ class HttpAPI(common):
remove_file: bool = bool(post.get("remove_file", True))
return web.json_response(
- data=await (
- self.queue.cancel(ids) if where == "queue" else self.queue.clear(ids, remove_file=remove_file)
- ),
+ data=await (self.queue.cancel(ids) if where == "queue" else self.queue.clear(ids, remove_file=remove_file)),
status=web.HTTPOk.status_code,
dumps=self.encoder.encode,
)
diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py
index f4aee17a..48f4803b 100644
--- a/app/library/ItemDTO.py
+++ b/app/library/ItemDTO.py
@@ -36,6 +36,7 @@ class ItemDTO:
live_in: str | None = None
file_size: int | None = None
options: dict = field(default_factory=dict)
+ extras: dict = field(default_factory=dict)
# yt-dlp injected fields.
tmpfilename: str | None = None
@@ -48,5 +49,21 @@ class ItemDTO:
speed: 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:
- 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)
diff --git a/app/library/Utils.py b/app/library/Utils.py
index 61a40b2d..52cc7776 100644
--- a/app/library/Utils.py
+++ b/app/library/Utils.py
@@ -4,7 +4,6 @@ import json
import logging
import os
import pathlib
-import posixpath
import re
import socket
import uuid
diff --git a/app/library/config.py b/app/library/config.py
index aefcfe3e..43b16537 100644
--- a/app/library/config.py
+++ b/app/library/config.py
@@ -55,6 +55,7 @@ class Config:
db_file: str = "{config_path}/ytptube.db"
manual_archive: str = "{config_path}/archive.manual.log"
+ ui_update_title: bool = True
# immutable config vars.
version: str = APP_VERSION
@@ -64,6 +65,7 @@ class Config:
new_version_available: bool = False
ytdlp_version: str = YTDLP_VERSION
started: int = 0
+ ignore_ui: bool = False
presets: list = [
{"name": "default", "format": "default", "postprocessors": [], "args": {}},
{
@@ -142,6 +144,8 @@ class Config:
"allow_manifestless",
"access_log",
"remove_files",
+ "ignore_ui",
+ "ui_update_title",
)
_frontend_vars: tuple = (
@@ -154,6 +158,7 @@ class Config:
"started",
"url_prefix",
"remove_files",
+ "ui_update_title",
)
@staticmethod
diff --git a/app/library/encoder.py b/app/library/encoder.py
index d78668b1..2a788649 100644
--- a/app/library/encoder.py
+++ b/app/library/encoder.py
@@ -9,6 +9,9 @@ class Encoder(json.JSONEncoder):
"""
def default(self, o):
+ if isinstance(o, object) and hasattr(o, "serialize"):
+ return o.serialize()
+
if isinstance(o, object) and hasattr(o, "__dict__"):
return o.__dict__
diff --git a/app/main.py b/app/main.py
index 9f82e14d..1511b0e9 100644
--- a/app/main.py
+++ b/app/main.py
@@ -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("=" * 40)
+ if self.config.access_log:
+ http_logger.addFilter(lambda record: "GET /ping" not in record.getMessage())
+
web.run_app(
self.app,
host=self.config.host,
diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css
index e655e006..634eff5d 100644
--- a/ui/assets/css/style.css
+++ b/ui/assets/css/style.css
@@ -213,3 +213,32 @@ hr {
cursor: pointer;
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;
+}
diff --git a/ui/components/History.vue b/ui/components/History.vue
index 1bc77fc7..82868752 100644
--- a/ui/components/History.vue
+++ b/ui/components/History.vue
@@ -96,40 +96,21 @@
:class="{ 'fa-arrow-down': hideThumbnail, 'fa-arrow-up': !hideThumbnail, }" />
-
@@ -253,7 +234,8 @@
+ :title="video_title" :thumbnail="video_thumbnail" :artist="video_artist" class="is-fullwidth"
+ @closeModel="video_link = ''; video_title = ''; video_thumbnail = ''; video_artist = '';" />
@@ -270,38 +252,51 @@ const config = useConfigStore()
const stateStore = useStateStore()
const socket = useSocketStore()
-const selectedElms = ref([]);
-const masterSelectAll = ref(false);
+const selectedElms = ref([])
+const masterSelectAll = ref(false)
const showCompleted = useStorage('showCompleted', true)
const hideThumbnail = useStorage('hideThumbnailHistory', false)
const direction = useStorage('sortCompleted', 'desc')
const video_link = ref('')
const video_title = ref('')
+const video_thumbnail = ref('')
+const video_artist = ref('')
const playVideo = item => {
- video_link.value = makeDownload(config, item, 'm3u8');
- video_title.value = item.title;
+ video_thumbnail.value = '';
+ 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) => {
for (const key in stateStore.history) {
- const element = stateStore.history[key];
+ const element = stateStore.history[key]
if (value) {
- selectedElms.value.push(element._id);
+ selectedElms.value.push(element._id)
} else {
- selectedElms.value = [];
+ selectedElms.value = []
}
}
})
const sortCompleted = computed(() => {
- const thisDirection = direction.value;
+ const thisDirection = direction.value
return Object.values(stateStore.history).sort((a, b) => {
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 item.msg.length > 0;
+ return item.msg.length > 0
}
const hasIncomplete = computed(() => {
if (Object.keys(stateStore.history)?.length < 0) {
- return false;
+ return false
}
for (const key in stateStore.history) {
- const element = stateStore.history[key];
+ const element = stateStore.history[key]
if (element.status !== 'finished') {
- return true;
+ return true
}
}
- return false;
+ return false
})
const hasCompleted = computed(() => {
if (Object.keys(stateStore.history)?.length < 0) {
- return false;
+ return false
}
for (const key in stateStore.history) {
- const element = stateStore.history[key];
+ const element = stateStore.history[key]
if (element.status === 'finished') {
- return true;
+ return true
}
}
- return false;
+ return false
})
const deleteSelectedItems = () => {
if (selectedElms.value.length < 1) {
- toast.error('No items selected.');
- return;
+ toast.error('No items selected.')
+ return
}
let msg = `Are you sure you want to delete '${selectedElms.value.length}' items?`
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)) {
- return;
+ return
}
for (const key in selectedElms.value) {
- const item = stateStore.history[selectedElms.value[key]];
+ const item = stateStore.history[selectedElms.value[key]]
if ('finished' === item.status) {
- socket.emit('archive_item', item);
+ socket.emit('archive_item', item)
}
socket.emit('item_delete', {
id: item._id,
remove_file: config.app.remove_files,
- });
+ })
}
}
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)) {
- return;
+ return
}
for (const key in stateStore.history) {
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 = () => {
if (false === confirm('Are you sure you want to clear all in-complete downloads?')) {
- return;
+ return
}
for (const key in stateStore.history) {
@@ -394,40 +389,40 @@ const clearIncomplete = () => {
socket.emit('item_delete', {
id: stateStore.history[key]._id,
remove_file: false,
- });
+ })
}
}
}
const setIcon = item => {
if (item.status === 'finished' && item.is_live) {
- return 'fa-solid fa-globe';
+ return 'fa-solid fa-globe'
}
if (item.status === 'finished') {
- return 'fa-solid fa-circle-check';
+ return 'fa-solid fa-circle-check'
}
if (item.status === 'error') {
- return 'fa-solid fa-circle-xmark';
+ return 'fa-solid fa-circle-xmark'
}
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 = () => {
if (false === confirm('Are you sure you want to re-queue all incomplete downloads?')) {
- return false;
+ return false
}
for (const key in stateStore.history) {
- const item = stateStore.get('history', key, {});
+ const item = stateStore.get('history', key, {})
if ('finished' === item.status) {
- continue;
+ continue
}
reQueueItem(item)
}
@@ -437,12 +432,12 @@ const archiveItem = item => {
if (!confirm(`Archive '${item.title ?? item.id ?? item.url ?? '??'}'?`)) {
return
}
- socket.emit('archive_item', item);
- socket.emit('item_delete', { id: item._id, remove_file: false });
+ socket.emit('archive_item', item)
+ socket.emit('item_delete', { id: item._id, remove_file: false })
}
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)) {
return false
}
@@ -450,7 +445,7 @@ const removeItem = item => {
socket.emit('item_delete', {
id: item._id,
remove_file: config.app.remove_files
- });
+ })
}
const reQueueItem = item => {
@@ -462,6 +457,6 @@ const reQueueItem = item => {
ytdlp_config: item.ytdlp_config,
ytdlp_cookies: item.ytdlp_cookies,
output_template: item.output_template,
- });
+ })
}
diff --git a/ui/components/Queue.vue b/ui/components/Queue.vue
index 135a56d8..3bc0e254 100644
--- a/ui/components/Queue.vue
+++ b/ui/components/Queue.vue
@@ -50,9 +50,9 @@
-
+
-
diff --git a/ui/components/VideoPlayer.vue b/ui/components/VideoPlayer.vue
index 633e5226..a2083625 100644
--- a/ui/components/VideoPlayer.vue
+++ b/ui/components/VideoPlayer.vue
@@ -43,6 +43,14 @@ const props = defineProps({
type: String,
default: ''
},
+ thumbnail: {
+ type: String,
+ default: ''
+ },
+ artist: {
+ type: String,
+ default: ''
+ },
isControls: {
type: Boolean,
default: true
@@ -85,10 +93,22 @@ onUnmounted(() => {
if (props.title) {
window.document.title = 'YTPTube'
}
-
})
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, {
debug: false,
clickToPlay: true,
@@ -106,9 +126,7 @@ const prepareVideoPlayer = () => {
key: 'plyr'
},
title: props.title,
- mediaMetadata: {
- title: props.title
- },
+ mediaMetadata: mediaMetadata,
captions: {
update: true,
}
diff --git a/ui/layouts/default.vue b/ui/layouts/default.vue
index 4c5cf870..9b9d03c6 100644
--- a/ui/layouts/default.vue
+++ b/ui/layouts/default.vue
@@ -31,7 +31,7 @@
-
+
Tasks
diff --git a/ui/pages/index.vue b/ui/pages/index.vue
index bb14d8f3..c1c62e22 100644
--- a/ui/pages/index.vue
+++ b/ui/pages/index.vue
@@ -6,5 +6,20 @@
diff --git a/ui/pages/tasks.vue b/ui/pages/tasks.vue
index 53f522df..67215bab 100644
--- a/ui/pages/tasks.vue
+++ b/ui/pages/tasks.vue
@@ -54,7 +54,6 @@
import moment from 'moment'
import { parseExpression } from 'cron-parser'
const config = useConfigStore()
-console.log(config.tasks)