minor design update
This commit is contained in:
parent
fb51fc3b1f
commit
a478e8e58d
13 changed files with 320 additions and 256 deletions
|
|
@ -1,40 +0,0 @@
|
|||
import logging
|
||||
|
||||
from .config import Config
|
||||
from .DownloadQueue import DownloadQueue
|
||||
from .encoder import Encoder
|
||||
from .ItemDTO import Item
|
||||
|
||||
LOG = logging.getLogger("common")
|
||||
|
||||
|
||||
class Common:
|
||||
"""
|
||||
This class is used to share common methods between the socket and the API gateways.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queue: DownloadQueue | None = None,
|
||||
encoder: Encoder | None = None,
|
||||
config: Config | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.queue: DownloadQueue = queue or DownloadQueue.get_instance()
|
||||
self.encoder: Encoder = encoder or Encoder()
|
||||
config: Config = config or Config.get_instance()
|
||||
self.default_preset: str = config.default_preset
|
||||
|
||||
async def add(self, item: Item) -> dict[str, str]:
|
||||
"""
|
||||
Add an item to the download queue.
|
||||
|
||||
Args:
|
||||
item (Item): The item to be added to the queue.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: The status of the download.
|
||||
{ "status": "text" }
|
||||
|
||||
"""
|
||||
return await self.queue.add(item=item)
|
||||
8
ui/@types/logs.ts
Normal file
8
ui/@types/logs.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
type log_line = {
|
||||
id: number,
|
||||
line: string,
|
||||
datetime?: string,
|
||||
}
|
||||
|
||||
|
||||
export type { log_line };
|
||||
17
ui/@types/tasks.ts
Normal file
17
ui/@types/tasks.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
type task_item = {
|
||||
id: string,
|
||||
name: string,
|
||||
url: string,
|
||||
preset?: string,
|
||||
folder?: string,
|
||||
template?: string,
|
||||
cli?: string,
|
||||
timer?: string,
|
||||
in_progress?: boolean,
|
||||
}
|
||||
|
||||
type exported_task = task_item & { _type: string, _version: string }
|
||||
|
||||
type error_response = { error: string }
|
||||
|
||||
export type { task_item, exported_task, error_response };
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
width: 80vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<h1 class="has-text-white">Not downloaded yet.</h1>
|
||||
|
|
@ -14,10 +15,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
@ -26,12 +25,13 @@ const props = defineProps({
|
|||
|
||||
const emitter = defineEmits(['closeModel'])
|
||||
|
||||
const eventFunc = e => {
|
||||
if (e.key === 'Escape') {
|
||||
emitter('closeModel')
|
||||
const handle_event = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
emitter('closeModel')
|
||||
}
|
||||
|
||||
onMounted(async () => window.addEventListener('keydown', eventFunc))
|
||||
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||
onMounted(() => document.addEventListener('keydown', handle_event))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,47 +1,50 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="modal is-active" v-if="false === externalModel">
|
||||
<div class="modal-background" @click="closeModal"></div>
|
||||
<div class="modal-background" @click="emitter('closeModel')"></div>
|
||||
<div class="modal-content" style="width:60vw;">
|
||||
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
|
||||
<i class="fas fa-circle-notch fa-spin"></i>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="p-0 m-0" style="position: relative">
|
||||
<div class="content" style="white-space: pre;">
|
||||
<code class="p-4 is-block" style="overflow: scroll;" v-text="data" />
|
||||
<button class="button is-small m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
|
||||
<div class="content p-0 m-0" style="position: relative">
|
||||
<pre><code class="p-4 is-block" v-text="data" />
|
||||
<button class="button m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
|
||||
style="position: absolute; top:0; right:0;">
|
||||
<span class="icon"><i class="fas fa-copy"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
|
||||
<button class="modal-close is-large" aria-label="close" @click="emitter('closeModel')"></button>
|
||||
</div>
|
||||
<div style="width:70vw; height: 80vh;" v-else>
|
||||
<div class="p-0 m-0" style="position: relative">
|
||||
<div class="content" style="white-space: pre;">
|
||||
<code class="p-4 is-block" v-text="data" />
|
||||
<button class="button is-small m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
|
||||
style="position: absolute; top:0; right:0;">
|
||||
<span class="icon"><i class="fas fa-copy"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="content p-0 m-0" style="position: relative">
|
||||
<pre><code class="p-4 is-block" v-text="data" /></pre>
|
||||
<button class="button m-4" @click="() => copyText(JSON.stringify(data, null, 4))"
|
||||
style="position: absolute; top:0; right:0;">
|
||||
<span class="icon"><i class="fas fa-copy"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<style scoped>
|
||||
code {
|
||||
color: var(--bulma-code) !important
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const emitter = defineEmits(['closeModel'])
|
||||
const isLoading = ref(false)
|
||||
const data = ref({})
|
||||
const toast = useNotification()
|
||||
|
||||
const emitter = defineEmits(['closeModel'])
|
||||
const isLoading = ref<Boolean>(false)
|
||||
const data = ref<any>({})
|
||||
|
||||
const props = defineProps({
|
||||
link: {
|
||||
type: String,
|
||||
|
|
@ -60,16 +63,15 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const closeModal = () => emitter('closeModel')
|
||||
|
||||
const eventFunc = e => {
|
||||
if (e.key === 'Escape') {
|
||||
emitter('closeModel')
|
||||
const handle_event = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
emitter('closeModel')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', eventFunc)
|
||||
document.addEventListener('keydown', handle_event)
|
||||
|
||||
const url = props.useUrl ? props.link : '/api/yt-dlp/url/info?url=' + encodePath(props.link)
|
||||
|
||||
|
|
@ -84,7 +86,7 @@ onMounted(async () => {
|
|||
data.value = body
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
toast.error(`Error: ${e.message}`)
|
||||
} finally {
|
||||
|
|
@ -92,5 +94,5 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
<style>
|
||||
.is-image {
|
||||
<style scoped>
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="font-size:30vh; width: 99%" class="has-text-centered" v-if="isLoading">
|
||||
<i class="fas fa-circle-notch fa-spin"></i>
|
||||
</div>
|
||||
<div v-else>
|
||||
<img :src="image" class="is-image">
|
||||
<img :src="image">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const emitter = defineEmits(['closeModel'])
|
||||
const isLoading = ref(false)
|
||||
const data = ref({})
|
||||
const toast = useNotification()
|
||||
const image = ref('')
|
||||
const emitter = defineEmits(['closeModel'])
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
const image = ref<string>('')
|
||||
|
||||
const props = defineProps({
|
||||
link: {
|
||||
|
|
@ -33,15 +34,15 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const eventFunc = e => {
|
||||
if ('Escape' === e.key) {
|
||||
emitter('closeModel')
|
||||
const handle_event = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
emitter('closeModel')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', eventFunc)
|
||||
|
||||
document.addEventListener('keydown', handle_event)
|
||||
|
||||
const url = props.link.startsWith('/') ? props.link : '/api/thumbnail?url=' + encodePath(props.link)
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ onMounted(async () => {
|
|||
}
|
||||
|
||||
image.value = URL.createObjectURL(await imgRequest.blob())
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
toast.error(`Error: ${e.message}`)
|
||||
} finally {
|
||||
|
|
@ -62,5 +63,5 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@
|
|||
<div>
|
||||
<div class="modal is-active">
|
||||
<div class="model-title" v-if="title" />
|
||||
<div class="modal-background" @click="closeModal"></div>
|
||||
<div class="modal-background" @click="emitter('close')"></div>
|
||||
<div class="modal-content" style="width:70vw;">
|
||||
<slot />
|
||||
</div>
|
||||
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
|
||||
<button class="modal-close is-large" aria-label="close" @click="emitter('close')"></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
const emitter = defineEmits(['close'])
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
|
@ -22,14 +22,13 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const closeModal = () => emitter('close')
|
||||
|
||||
const eventFunc = e => {
|
||||
if (e.key === 'Escape') {
|
||||
emitter('close')
|
||||
const handle_event = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
emitter('close')
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', eventFunc))
|
||||
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
|
||||
onMounted(() => document.addEventListener('keydown', handle_event))
|
||||
onBeforeUnmount(() => document.removeEventListener('keydown', handle_event))
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
</style>
|
||||
<template>
|
||||
<div v-if="infoLoaded">
|
||||
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls crossorigin="anonymous"
|
||||
preload="auto" autoplay>
|
||||
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls
|
||||
crossorigin="anonymous" preload="auto" autoplay>
|
||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||
:type="source.type" />
|
||||
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
||||
|
|
@ -25,11 +25,35 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { onMounted, onUpdated, ref, onUnmounted } from 'vue'
|
||||
import Hls from 'hls.js'
|
||||
import { makeDownload } from '~/utils/index'
|
||||
|
||||
type video_track_element = {
|
||||
file: string,
|
||||
kind: string,
|
||||
label: string,
|
||||
lang: string,
|
||||
}
|
||||
|
||||
type video_source_element = {
|
||||
src: string,
|
||||
type: string,
|
||||
onerror: (e: Event) => void,
|
||||
}
|
||||
|
||||
type file_info = {
|
||||
title: string,
|
||||
mimetype: string,
|
||||
sidecar?: {
|
||||
image?: Array<{ file: string }>,
|
||||
text?: Array<{ file: string }>,
|
||||
subtitle?: Array<{ name: string, lang: string, file: string }>,
|
||||
}
|
||||
error?: string,
|
||||
}
|
||||
|
||||
const config = useConfigStore()
|
||||
const toast = useNotification()
|
||||
|
||||
|
|
@ -42,9 +66,9 @@ const props = defineProps({
|
|||
|
||||
const emitter = defineEmits(['closeModel'])
|
||||
|
||||
const video = ref(null)
|
||||
const tracks = ref([])
|
||||
const sources = ref([])
|
||||
const video = useTemplateRef<HTMLMediaElement>('video')
|
||||
const tracks = ref<Array<video_track_element>>([])
|
||||
const sources = ref<Array<video_source_element>>([])
|
||||
|
||||
const thumbnail = ref('/images/placeholder.png')
|
||||
const artist = ref('')
|
||||
|
|
@ -54,19 +78,39 @@ const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)
|
|||
const volume = useStorage('player_volume', 1)
|
||||
const notFirefox = !navigator.userAgent.toLowerCase().includes('firefox')
|
||||
const infoLoaded = ref(false)
|
||||
const destroyed = ref(false)
|
||||
|
||||
let hls = null
|
||||
let hls: Hls | null = null
|
||||
|
||||
const eventFunc = e => {
|
||||
if ('Escape' === e.key) {
|
||||
emitter('closeModel')
|
||||
const handle_event = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
emitter('closeModel')
|
||||
}
|
||||
|
||||
const volume_change_handler = () => {
|
||||
if (!video.value) {
|
||||
return
|
||||
}
|
||||
|
||||
volume.value = video.value.volume
|
||||
|
||||
if (false === ("mediaSession" in navigator)) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.mediaSession.setPositionState({
|
||||
duration: video.value.duration,
|
||||
playbackRate: video.value.playbackRate,
|
||||
position: video.value.currentTime,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const req = await request(makeDownload(config, props.item, 'api/file/info'))
|
||||
|
||||
const response = await req.json()
|
||||
const response: file_info = await req.json()
|
||||
|
||||
if (!req.ok) {
|
||||
toast.error(`Failed to fetch video info. ${response?.error}`)
|
||||
|
|
@ -91,13 +135,13 @@ onMounted(async () => {
|
|||
sources.value.push({
|
||||
src: makeDownload(config, props.item, allowedCodec ? 'api/download' : 'm3u8'),
|
||||
type: allowedCodec ? response.mimetype : 'application/x-mpegURL',
|
||||
onerror: e => src_error(e),
|
||||
onerror: (e: Event) => src_error(),
|
||||
})
|
||||
} else {
|
||||
sources.value.push({
|
||||
src: makeDownload(config, props.item, 'api/download'),
|
||||
type: response.mimetype,
|
||||
onerror: e => src_error(e),
|
||||
onerror: e => src_error(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +166,7 @@ onMounted(async () => {
|
|||
isAudio.value = true
|
||||
}
|
||||
|
||||
response?.sidecar?.subtitle?.forEach((cap, id) => {
|
||||
response.sidecar?.subtitle?.forEach((cap, _) => {
|
||||
tracks.value.push({
|
||||
kind: "captions",
|
||||
label: cap.name,
|
||||
|
|
@ -139,30 +183,35 @@ onMounted(async () => {
|
|||
await nextTick()
|
||||
|
||||
prepareVideoPlayer()
|
||||
window.addEventListener('keydown', eventFunc)
|
||||
document.addEventListener('keydown', handle_event)
|
||||
})
|
||||
|
||||
onUpdated(() => prepareVideoPlayer())
|
||||
|
||||
onUnmounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
if (hls) {
|
||||
hls.destroy()
|
||||
}
|
||||
|
||||
window.removeEventListener('keydown', eventFunc)
|
||||
document.removeEventListener('keydown', handle_event)
|
||||
|
||||
if (title.value) {
|
||||
window.document.title = 'YTPTube'
|
||||
}
|
||||
if (video.value) {
|
||||
try {
|
||||
video.value.pause()
|
||||
video.value.querySelectorAll("source").forEach(source => source.removeAttribute("src"))
|
||||
video.value.load()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
if (!video.value) {
|
||||
return
|
||||
}
|
||||
destroyed.value = true
|
||||
|
||||
try {
|
||||
video.value.pause()
|
||||
video.value.querySelectorAll("source").forEach(source => source.removeAttribute("src"))
|
||||
video.value.removeEventListener('volumechange', volume_change_handler)
|
||||
video.value.load()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -175,9 +224,7 @@ const prepareVideoPlayer = () => {
|
|||
return
|
||||
}
|
||||
|
||||
let mediaMetadata = {
|
||||
title: title.value,
|
||||
}
|
||||
let mediaMetadata: MediaMetadataInit = { title: title.value }
|
||||
|
||||
if (thumbnail.value) {
|
||||
mediaMetadata.artwork = [{ src: thumbnail.value, sizes: '1920x1080', type: 'image/jpeg' }]
|
||||
|
|
@ -192,22 +239,31 @@ const prepareVideoPlayer = () => {
|
|||
window.document.title = `YTPTube - Playing: ${title.value}`
|
||||
}
|
||||
|
||||
video.value.volume = volume.value
|
||||
if (!video.value) {
|
||||
return
|
||||
}
|
||||
|
||||
video.value.addEventListener('volumechange', () => {
|
||||
volume.value = video.value.volume
|
||||
})
|
||||
video.value.volume = volume.value
|
||||
video.value.addEventListener('volumechange', volume_change_handler)
|
||||
}
|
||||
|
||||
const src_error = () => {
|
||||
const src_error = async () => {
|
||||
if (hls) {
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
if (destroyed.value) {
|
||||
return
|
||||
}
|
||||
console.warn('Direct play failed, trying HLS.')
|
||||
attach_hls(makeDownload(config, props.item, 'm3u8'))
|
||||
}
|
||||
|
||||
const attach_hls = link => {
|
||||
const attach_hls = (link: string) => {
|
||||
if (!video.value) {
|
||||
return
|
||||
}
|
||||
|
||||
hls = new Hls({
|
||||
debug: false,
|
||||
enableWorker: true,
|
||||
|
|
|
|||
|
|
@ -60,17 +60,17 @@
|
|||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<NuxtLink class="navbar-item" to="/console" @click.native="e => changeRoute(e)"
|
||||
v-if="config.app.console_enabled">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>Terminal</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/logs" @click.native="e => changeRoute(e)"
|
||||
v-if="config.app.file_logging">
|
||||
<span class="icon"><i class="fa-solid fa-file-lines" /></span>
|
||||
<span>Logs</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/console" @click.native="e => changeRoute(e)"
|
||||
v-if="config.app.console_enabled">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>Console</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ const reloadContent = async (dir = '/', fromMounted = false) => {
|
|||
}
|
||||
}
|
||||
|
||||
const popstateHandler = e => {
|
||||
const event_handler = e => {
|
||||
|
||||
if (!e.state) {
|
||||
return
|
||||
|
|
@ -358,12 +358,10 @@ onMounted(async () => {
|
|||
await reloadContent(path.value, true)
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', popstateHandler)
|
||||
document.addEventListener('popstate', event_handler)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('popstate', popstateHandler)
|
||||
})
|
||||
onBeforeUnmount(() => document.removeEventListener('popstate', event_handler))
|
||||
|
||||
const makeBreadCrumb = path => {
|
||||
const baseLink = '/'
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@
|
|||
<h1 class="title is-4">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>Terminal</span>
|
||||
<span>Console</span>
|
||||
</span>
|
||||
</h1>
|
||||
<div class="subtitle is-6 is-unselectable">
|
||||
You can use this terminal window to execute non-interactive commands. The interface is jailed to the
|
||||
You can use this console window to execute non-interactive commands. The interface is jailed to the
|
||||
<code>yt-dlp</code>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -25,8 +25,9 @@
|
|||
<span class="icon"><i class="fa-solid fa-terminal" /></span> Console Output
|
||||
</p>
|
||||
<p class="card-header-icon">
|
||||
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput">
|
||||
<i class="fa-solid fa-broom" /></span>
|
||||
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput()">
|
||||
<i class="fa-solid fa-broom" />
|
||||
</span>
|
||||
</p>
|
||||
</header>
|
||||
<section class="card-content p-0 m-0">
|
||||
|
|
@ -34,12 +35,6 @@
|
|||
</section>
|
||||
<section class="card-content p-1 m-1">
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<span class="icon-text input is-unselectable">
|
||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||
<span>yt-dlp</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input type="text" class="input" v-model="command" placeholder="--help" autocomplete="off"
|
||||
ref="command_input" @keydown.enter="runCommand" :disabled="isLoading" id="command">
|
||||
|
|
@ -60,22 +55,24 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import "@xterm/xterm/css/xterm.css"
|
||||
import { Terminal } from "@xterm/xterm"
|
||||
import { FitAddon } from "@xterm/addon-fit"
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const terminal = ref()
|
||||
const terminalFit = ref()
|
||||
const command = ref('')
|
||||
const terminal_window = ref()
|
||||
const command_input = ref()
|
||||
const isLoading = ref(false)
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const bg_enable = useStorage('random_bg', true)
|
||||
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||
|
||||
const bg_enable = useStorage<boolean>('random_bg', true)
|
||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.85)
|
||||
|
||||
const terminal = ref<Terminal>()
|
||||
const terminalFit = ref<FitAddon>()
|
||||
const command = ref<string>('')
|
||||
const terminal_window = useTemplateRef<HTMLDivElement>('terminal_window')
|
||||
const command_input = useTemplateRef<HTMLInputElement>('command_input')
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
watch(() => isLoading.value, async value => {
|
||||
if (value) {
|
||||
|
|
@ -100,11 +97,11 @@ watch(() => config.app.console_enabled, async () => {
|
|||
await navigateTo('/')
|
||||
})
|
||||
|
||||
const reSizeTerminal = () => {
|
||||
const handle_event = () => {
|
||||
if (!terminal.value) {
|
||||
return
|
||||
}
|
||||
terminalFit.value.fit()
|
||||
terminalFit.value?.fit()
|
||||
}
|
||||
|
||||
const runCommand = async () => {
|
||||
|
|
@ -128,13 +125,14 @@ const runCommand = async () => {
|
|||
cursorStyle: 'underline',
|
||||
cols: 108,
|
||||
rows: 10,
|
||||
buffer: 1000,
|
||||
disableStdin: true,
|
||||
scrollback: 1000,
|
||||
})
|
||||
terminalFit.value = new FitAddon()
|
||||
terminal.value.loadAddon(terminalFit.value)
|
||||
terminal.value.open(terminal_window.value)
|
||||
if (terminal_window.value) {
|
||||
terminal.value.open(terminal_window.value)
|
||||
}
|
||||
terminalFit.value.fit();
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +147,7 @@ const runCommand = async () => {
|
|||
terminal.value.writeln(`$ yt-dlp ${command.value}`)
|
||||
}
|
||||
|
||||
const clearOutput = async (withCommand = false) => {
|
||||
const clearOutput = async (withCommand: boolean = false) => {
|
||||
if (terminal.value) {
|
||||
terminal.value.clear()
|
||||
}
|
||||
|
|
@ -168,7 +166,7 @@ const focusInput = () => {
|
|||
command_input.value.focus()
|
||||
}
|
||||
|
||||
const writer = s => {
|
||||
const writer = (s: string) => {
|
||||
if (!terminal.value) {
|
||||
return
|
||||
}
|
||||
|
|
@ -188,23 +186,23 @@ watch(() => config.app.basic_mode, async () => {
|
|||
})
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', reSizeTerminal);
|
||||
document.addEventListener('resize', handle_event);
|
||||
focusInput()
|
||||
socket.off('cli_close', loader)
|
||||
socket.off('cli_output', writer)
|
||||
socket.on('cli_close', loader)
|
||||
socket.on('cli_output', writer)
|
||||
if (bg_enable.value) {
|
||||
document.querySelector('body').setAttribute("style", `opacity: 1.0`)
|
||||
document.querySelector('body')?.setAttribute("style", `opacity: 1.0`)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
socket.off('cli_close', loader)
|
||||
socket.off('cli_output', writer)
|
||||
window.removeEventListener('resize', reSizeTerminal)
|
||||
document.removeEventListener('resize', handle_event)
|
||||
if (bg_enable.value) {
|
||||
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
|
||||
document.querySelector('body')?.setAttribute("style", `opacity: ${bg_opacity.value}`)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -89,19 +89,19 @@ code {
|
|||
</span>
|
||||
<span v-for="log in filteredItems" :key="log.id" class="is-block">
|
||||
<template v-if="log?.datetime">[<span class="has-tooltip" :title="log.datetime">{{ moment(log.datetime).format('HH:mm:ss') }}</span>]</template>
|
||||
{{ log.line }}
|
||||
</span>
|
||||
<span class="is-block" v-if="filteredItems.length < 1">
|
||||
<span class="is-block m-0 notification is-warning is-dark has-text-centered" v-if="query">
|
||||
<span class="notification-title is-danger">
|
||||
<span class="icon"><i class="fas fa-filter"/></span>
|
||||
No logs match this query: <u>{{ query }}</u>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="has-text-danger">No logs available</span></span>
|
||||
</span>
|
||||
</code>
|
||||
{{ log.line }}
|
||||
</span>
|
||||
<span class="is-block" v-if="filteredItems.length < 1">
|
||||
<span class="is-block m-0 notification is-warning is-dark has-text-centered" v-if="query">
|
||||
<span class="notification-title is-danger">
|
||||
<span class="icon"><i class="fas fa-filter" /></span>
|
||||
No logs match this query: <u>{{ query }}</u>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="has-text-danger">No logs available</span></span>
|
||||
</span>
|
||||
</code>
|
||||
<div ref="bottomMarker"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -109,30 +109,45 @@ code {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import moment from 'moment'
|
||||
import { request } from '~/utils/index'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { log_line } from '~/@types/logs'
|
||||
|
||||
let scrollTimeout = null
|
||||
let scrollTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const toast = useNotification()
|
||||
const socket = useSocketStore()
|
||||
const config = useConfigStore()
|
||||
const route = useRoute()
|
||||
|
||||
const logs = ref([])
|
||||
const offset = ref(0)
|
||||
const loading = ref(false)
|
||||
const logContainer = ref(null)
|
||||
const bottomMarker = ref(null)
|
||||
const autoScroll = ref(true)
|
||||
const textWrap = ref(true)
|
||||
const reachedEnd = ref(false)
|
||||
const bg_enable = useStorage('random_bg', true)
|
||||
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||
const logs = ref<Array<log_line>>([])
|
||||
const offset = ref<number>(0)
|
||||
const loading = ref<boolean>(false)
|
||||
const logContainer = useTemplateRef<HTMLDivElement>('logContainer')
|
||||
const bottomMarker = useTemplateRef<HTMLDivElement>('bottomMarker')
|
||||
const autoScroll = ref<boolean>(true)
|
||||
const textWrap = ref<boolean>(true)
|
||||
const reachedEnd = ref<boolean>(false)
|
||||
const bg_enable = useStorage<boolean>('random_bg', true)
|
||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.85)
|
||||
|
||||
const query = ref<string>((() => {
|
||||
const filter = route.query.filter ?? ''
|
||||
if (!filter) {
|
||||
return ''
|
||||
}
|
||||
if (typeof filter === 'string') {
|
||||
return filter.trim()
|
||||
}
|
||||
if (Array.isArray(filter) && filter.length > 0) {
|
||||
return filter[0]?.trim() ?? ''
|
||||
}
|
||||
return ''
|
||||
})())
|
||||
|
||||
const query = ref(useRoute().query.filter ?? '')
|
||||
const toggleFilter = ref(false)
|
||||
watch(toggleFilter, () => {
|
||||
if (!toggleFilter.value) {
|
||||
|
|
@ -161,9 +176,11 @@ const filteredItems = computed(() => {
|
|||
const context = contextMatch ? parseInt(contextMatch[1], 10) : 0
|
||||
const searchTerm = raw.replace(/context:\d+/, '').trim()
|
||||
|
||||
if (!searchTerm) return logs.value
|
||||
if (!searchTerm) {
|
||||
return logs.value
|
||||
}
|
||||
|
||||
const result = []
|
||||
const result: Array<log_line> = []
|
||||
const matchedIndexes = new Set()
|
||||
|
||||
logs.value.forEach((log, i) => {
|
||||
|
|
@ -174,7 +191,7 @@ const filteredItems = computed(() => {
|
|||
}
|
||||
})
|
||||
|
||||
Array.from(matchedIndexes).sort((a, b) => a - b).forEach(index => {
|
||||
Array.from(matchedIndexes).sort((a: any, b: any) => a - b).forEach((index: any) => {
|
||||
result.push(logs.value[index])
|
||||
})
|
||||
|
||||
|
|
@ -251,11 +268,11 @@ const handleScroll = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const scrollToBottom = (fast=false) => {
|
||||
const scrollToBottom = (fast = false) => {
|
||||
autoScroll.value = true
|
||||
nextTick(() => {
|
||||
if (bottomMarker.value) {
|
||||
bottomMarker.value.scrollIntoView({ behavior: fast ? 'auto': 'smooth' })
|
||||
bottomMarker.value.scrollIntoView({ behavior: fast ? 'auto' : 'smooth' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -263,7 +280,8 @@ const scrollToBottom = (fast=false) => {
|
|||
onMounted(async () => {
|
||||
await fetchLogs()
|
||||
socket.emit('subscribe', 'log_lines')
|
||||
socket.on('log_lines', data => {
|
||||
|
||||
socket.on('log_lines', (data: any) => {
|
||||
logs.value.push(data)
|
||||
|
||||
nextTick(() => {
|
||||
|
|
@ -271,17 +289,19 @@ onMounted(async () => {
|
|||
bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
if (bg_enable.value) {
|
||||
document.querySelector('body').setAttribute("style", `opacity: 1.0`)
|
||||
document.querySelector('body')?.setAttribute("style", `opacity: 1.0`)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
socket.emit('unsubscribe', 'log_lines')
|
||||
socket.off('log_lines')
|
||||
if (bg_enable.value) {
|
||||
document.querySelector('body').setAttribute("style", `opacity: ${bg_opacity.value}`)
|
||||
document.querySelector('body')?.setAttribute("style", `opacity: ${bg_opacity.value}`)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ div.is-centered {
|
|||
</p>
|
||||
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="reloadContent" :class="{ 'is-loading': isLoading }"
|
||||
<button class="button is-info" @click="reloadContent()" :class="{ 'is-loading': isLoading }"
|
||||
:disabled="!socket.isConnected || isLoading" v-if="tasks && tasks.length > 0">
|
||||
<span class="icon"><i class="fas fa-refresh" /></span>
|
||||
</button>
|
||||
|
|
@ -97,9 +97,8 @@ div.is-centered {
|
|||
<span v-if="item.timer" class="has-tooltip" v-tooltip="item.timer">
|
||||
{{ tryParse(item.timer) }}
|
||||
</span>
|
||||
<span v-else class="has-text-danger">
|
||||
<span class="icon"><i class="fa-solid fa-exclamation-triangle" /></span>
|
||||
<span>No timer is set</span>
|
||||
<span v-else class="has-text-warning">
|
||||
No timer is set
|
||||
</span>
|
||||
</td>
|
||||
<td class="is-vcentered is-items-center">
|
||||
|
|
@ -155,9 +154,16 @@ div.is-centered {
|
|||
<div class="card-content is-flex-grow-1">
|
||||
<div class="content">
|
||||
<p class="is-text-overflow">
|
||||
<span class="icon"><i class="fa-solid fa-clock" /></span>
|
||||
<span v-if="item.timer">{{ tryParse(item.timer) }} - {{ item.timer }}</span>
|
||||
<span v-else>No timer is set</span>
|
||||
<span class="icon">
|
||||
<i class="fa-solid"
|
||||
:class="{ 'fa-clock': item.timer, 'has-text-danger fa-exclamation-triangle': !item.timer }" />
|
||||
</span>
|
||||
<span v-if="item.timer">
|
||||
<NuxtLink target="_blank" :to="`https://crontab.guru/#${item.timer.replace(/ /g, '_')}`">
|
||||
{{ item.timer }} - {{ tryParse(item.timer) }}
|
||||
</NuxtLink>
|
||||
</span>
|
||||
<span class="has-text-warning" v-else>No timer is set</span>
|
||||
</p>
|
||||
<p class="is-text-overflow" v-if="item.folder">
|
||||
<span class="icon"><i class="fa-solid fa-folder" /></span>
|
||||
|
|
@ -214,25 +220,26 @@ div.is-centered {
|
|||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import moment from 'moment'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
import { request } from '~/utils/index'
|
||||
import type { task_item, exported_task, error_response } from '~/@types/tasks'
|
||||
|
||||
const box = useConfirm()
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const box = useConfirm()
|
||||
|
||||
const tasks = ref([])
|
||||
const task = ref({})
|
||||
const taskRef = ref('')
|
||||
const toggleForm = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const initialLoad = ref(true)
|
||||
const addInProgress = ref(false)
|
||||
const display_style = useStorage("tasks_display_style", "cards")
|
||||
const tasks = ref<Array<task_item>>([])
|
||||
const task = ref<task_item | Object>({})
|
||||
const taskRef = ref<string>('')
|
||||
const toggleForm = ref<boolean>(false)
|
||||
const isLoading = ref<boolean>(true)
|
||||
const initialLoad = ref<boolean>(true)
|
||||
const addInProgress = ref<boolean>(false)
|
||||
const display_style = useStorage<string>("tasks_display_style", "cards")
|
||||
const remove_keys = ['in_progress']
|
||||
|
||||
watch(() => config.app.basic_mode, async () => {
|
||||
|
|
@ -250,7 +257,7 @@ watch(() => socket.isConnected, async () => {
|
|||
}
|
||||
})
|
||||
|
||||
const reloadContent = async (fromMounted = false) => {
|
||||
const reloadContent = async (fromMounted: boolean = false) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await request('/api/tasks')
|
||||
|
|
@ -276,18 +283,16 @@ const reloadContent = async (fromMounted = false) => {
|
|||
}
|
||||
}
|
||||
|
||||
const resetForm = (closeForm = false) => {
|
||||
const resetForm = (closeForm: boolean = false) => {
|
||||
task.value = {}
|
||||
taskRef.value = null
|
||||
taskRef.value = ''
|
||||
addInProgress.value = false
|
||||
if (closeForm) {
|
||||
toggleForm.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateTasks = async items => {
|
||||
let data = {}
|
||||
|
||||
const updateTasks = async (items: Array<task_item>) => {
|
||||
try {
|
||||
addInProgress.value = true
|
||||
|
||||
|
|
@ -299,24 +304,24 @@ const updateTasks = async items => {
|
|||
body: JSON.stringify(items.map(item => cleanObject(toRaw(item), remove_keys))),
|
||||
})
|
||||
|
||||
data = await response.json()
|
||||
const data: Array<task_item> | error_response = await response.json()
|
||||
|
||||
if (200 !== response.status) {
|
||||
toast.error(`Failed to update task. ${data.error}`);
|
||||
if ("error" in data) {
|
||||
toast.error(`Failed to update tasks. ${data.error}`);
|
||||
return false
|
||||
}
|
||||
|
||||
tasks.value = data
|
||||
resetForm(true)
|
||||
return true
|
||||
} catch (e) {
|
||||
toast.error(`Failed to update task. ${data?.error ?? e.message}`);
|
||||
} catch (e: any) {
|
||||
toast.error(`Failed to update tasks. ${e.message}`);
|
||||
} finally {
|
||||
addInProgress.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteItem = async item => {
|
||||
const deleteItem = async (item: task_item) => {
|
||||
if (true !== box.confirm(`Delete '${item.name}' task?`, true)) {
|
||||
return
|
||||
}
|
||||
|
|
@ -338,7 +343,7 @@ const deleteItem = async item => {
|
|||
toast.success('Task deleted.')
|
||||
}
|
||||
|
||||
const updateItem = async ({ reference, task }) => {
|
||||
const updateItem = async ({ reference, task }: { reference?: string, task: task_item }) => {
|
||||
if (reference) {
|
||||
// -- find the task index.
|
||||
const index = tasks.value.findIndex((t) => t?.id === reference)
|
||||
|
|
@ -354,17 +359,17 @@ const updateItem = async ({ reference, task }) => {
|
|||
return
|
||||
}
|
||||
|
||||
toast.success('Task updated')
|
||||
toast.success('Task updated.')
|
||||
resetForm(true)
|
||||
}
|
||||
|
||||
const editItem = item => {
|
||||
const editItem = (item: task_item) => {
|
||||
task.value = item
|
||||
taskRef.value = item.id
|
||||
toggleForm.value = true
|
||||
}
|
||||
|
||||
const calcPath = path => {
|
||||
const calcPath = (path: string) => {
|
||||
const loc = config.app.download_path || '/downloads'
|
||||
|
||||
if (path) {
|
||||
|
|
@ -382,7 +387,7 @@ onMounted(async () => {
|
|||
await reloadContent(true)
|
||||
});
|
||||
|
||||
const tryParse = expression => {
|
||||
const tryParse = (expression: string) => {
|
||||
try {
|
||||
return moment(CronExpressionParser.parse(expression).next().toISOString()).fromNow()
|
||||
} catch (e) {
|
||||
|
|
@ -390,7 +395,7 @@ const tryParse = expression => {
|
|||
}
|
||||
}
|
||||
|
||||
const runNow = async item => {
|
||||
const runNow = async (item: task_item) => {
|
||||
if (true !== box.confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) {
|
||||
return
|
||||
}
|
||||
|
|
@ -400,7 +405,7 @@ const runNow = async item => {
|
|||
let data = {
|
||||
url: item.url,
|
||||
preset: item.preset,
|
||||
}
|
||||
} as task_item
|
||||
|
||||
if (item.folder) {
|
||||
data.folder = item.folder
|
||||
|
|
@ -422,9 +427,9 @@ const runNow = async item => {
|
|||
}, 500)
|
||||
}
|
||||
|
||||
onUnmounted(() => socket.off('status', statusHandler))
|
||||
onBeforeUnmount(() => socket.off('status', statusHandler))
|
||||
|
||||
const statusHandler = async stream => {
|
||||
const statusHandler = async (stream: string) => {
|
||||
const { status, msg } = JSON.parse(stream)
|
||||
|
||||
if ('error' === status) {
|
||||
|
|
@ -433,7 +438,7 @@ const statusHandler = async stream => {
|
|||
}
|
||||
}
|
||||
|
||||
const exportItem = async item => {
|
||||
const exportItem = async (item: task_item) => {
|
||||
const info = JSON.parse(JSON.stringify(item))
|
||||
|
||||
let data = {
|
||||
|
|
@ -442,7 +447,7 @@ const exportItem = async item => {
|
|||
preset: info.preset,
|
||||
timer: info.timer,
|
||||
folder: info.folder,
|
||||
}
|
||||
} as exported_task
|
||||
|
||||
if (info.template) {
|
||||
data.template = info.template
|
||||
|
|
@ -452,8 +457,8 @@ const exportItem = async item => {
|
|||
data.cli = info.cli
|
||||
}
|
||||
|
||||
data['_type'] = 'task'
|
||||
data['_version'] = '2.0'
|
||||
data._type = 'task'
|
||||
data._version = '2.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue