Added Console view for yt-dlp command.
This commit is contained in:
parent
7a519bf16c
commit
0f95ea405c
10 changed files with 1259 additions and 1148 deletions
35
app/main.py
35
app/main.py
|
|
@ -5,6 +5,7 @@ from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import selectors
|
||||||
import time
|
import time
|
||||||
from Config import Config
|
from Config import Config
|
||||||
from DownloadQueue import DownloadQueue
|
from DownloadQueue import DownloadQueue
|
||||||
|
|
@ -492,6 +493,40 @@ class Main:
|
||||||
|
|
||||||
return web.Response(text=self.appLoader, content_type='text/html', charset='utf-8', status=200)
|
return web.Response(text=self.appLoader, content_type='text/html', charset='utf-8', status=200)
|
||||||
|
|
||||||
|
@self.sio.event()
|
||||||
|
async def cli_post(sid, data):
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _read_stream(streamType, stream):
|
||||||
|
while True:
|
||||||
|
line = await stream.readline()
|
||||||
|
if line:
|
||||||
|
await self.sio.emit('cli_output', {
|
||||||
|
'type': streamType,
|
||||||
|
'line': line.decode('utf-8')
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
proc = await asyncio.subprocess.create_subprocess_exec(
|
||||||
|
'yt-dlp', data,
|
||||||
|
cwd=self.config.download_path,
|
||||||
|
stdin=asyncio.subprocess.DEVNULL,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
_read_stream('stdout', proc.stdout),
|
||||||
|
_read_stream('stderr', proc.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
while proc.returncode is None:
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
await self.sio.emit('cli_close', {'exitcode': proc.returncode})
|
||||||
|
|
||||||
@self.sio.event()
|
@self.sio.event()
|
||||||
async def connect(sid, environ):
|
async def connect(sid, environ):
|
||||||
await self.connect(sid, environ)
|
await self.connect(sid, environ)
|
||||||
|
|
|
||||||
2191
frontend/package-lock.json
generated
2191
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -21,7 +21,7 @@
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"plyr": "^3.7.8",
|
"plyr": "^3.7.8",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "^4.7.2",
|
||||||
"vue": "^3.3.9",
|
"vue": "^3.4.27",
|
||||||
"vue-toastification": "^2.0.0-rc.5"
|
"vue-toastification": "^2.0.0-rc.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<PageHeader :config="config" @toggleForm="addForm = !addForm" @toggleTasks="showTasks = !showTasks"
|
<PageHeader :config="config" @toggleForm="addForm = !addForm" @toggleTasks="showTasks = !showTasks"
|
||||||
@reload="reloadWindow" />
|
@toggleConsole="showConsole = !showConsole" @reload="reloadWindow" />
|
||||||
<formAdd v-if="addForm" :config="config" @addItem="addItem" />
|
|
||||||
<pageTasks v-if="showTasks" :tasks="config.tasks" />
|
<CliConsole v-if="showConsole" @runCommand="runCommand" :cli_output="cli_output" :isLoading="cli_isLoading"
|
||||||
<DownloadingList :config="config" :queue="downloading" @deleteItem="deleteItem" />
|
@cli_clear="cli_output = []" />
|
||||||
<PageCompleted :config="config" :completed="completed" @deleteItem="deleteItem" @addItem="addItem"
|
<template v-else>
|
||||||
@playItem="playItem" />
|
<formAdd v-if="addForm" :config="config" @addItem="addItem" />
|
||||||
|
<pageTasks v-if="showTasks" :tasks="config.tasks" />
|
||||||
|
<DownloadingList :config="config" :queue="downloading" @deleteItem="deleteItem" />
|
||||||
|
<PageCompleted :config="config" :completed="completed" @deleteItem="deleteItem" @addItem="addItem"
|
||||||
|
@playItem="playItem" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="modal is-active" v-if="video_link">
|
<div class="modal is-active" v-if="video_link">
|
||||||
<div class="modal-background"></div>
|
<div class="modal-background"></div>
|
||||||
|
|
@ -31,31 +36,43 @@ import VideoPlayer from './components/Video-Player'
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { useStorage, useEventBus } from '@vueuse/core'
|
import { useStorage, useEventBus } from '@vueuse/core'
|
||||||
|
import CliConsole from './components/CLI-Console.vue'
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast()
|
||||||
const bus = useEventBus('item_added', 'show_form', 'task_edit');
|
const bus = useEventBus('item_added', 'show_form', 'task_edit')
|
||||||
|
|
||||||
const config = reactive({
|
const config = reactive({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
app: {},
|
app: {},
|
||||||
tasks: [],
|
tasks: [],
|
||||||
});
|
})
|
||||||
|
|
||||||
const downloading = reactive({});
|
const socket = ref()
|
||||||
const completed = reactive({});
|
const downloading = reactive({})
|
||||||
const video_link = ref('');
|
const completed = reactive({})
|
||||||
|
const video_link = ref('')
|
||||||
const addForm = useStorage('addForm', true)
|
const addForm = useStorage('addForm', true)
|
||||||
const showTasks = useStorage('showTasks', false)
|
const showTasks = useStorage('showTasks', false)
|
||||||
|
const cli_output = ref([])
|
||||||
|
const cli_isLoading = ref(false)
|
||||||
|
const showConsole = ref(false)
|
||||||
|
|
||||||
|
const runCommand = (args) => {
|
||||||
|
cli_output.value = [];
|
||||||
|
cli_isLoading.value = true;
|
||||||
|
console.log(args)
|
||||||
|
socket.value.emit('cli_post', args);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const socket = io(process.env.VUE_APP_BASE_URL, {
|
socket.value = io(process.env.VUE_APP_BASE_URL, {
|
||||||
path: document.location.pathname + 'socket.io',
|
path: document.location.pathname + 'socket.io',
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on('connect', () => config.isConnected = true);
|
socket.value.on('connect', () => config.isConnected = true);
|
||||||
socket.on('disconnect', () => config.isConnected = false);
|
socket.value.on('disconnect', () => config.isConnected = false);
|
||||||
|
|
||||||
socket.on('initial_data', stream => {
|
socket.value.on('initial_data', stream => {
|
||||||
const initialData = JSON.parse(stream);
|
const initialData = JSON.parse(stream);
|
||||||
config.app = initialData['config'];
|
config.app = initialData['config'];
|
||||||
config.tasks = initialData['tasks'];
|
config.tasks = initialData['tasks'];
|
||||||
|
|
@ -67,20 +84,20 @@ onMounted(() => {
|
||||||
for (const id in initialData['done']) {
|
for (const id in initialData['done']) {
|
||||||
completed[id] = initialData['done'][id];
|
completed[id] = initialData['done'][id];
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on('added', stream => {
|
socket.value.on('added', stream => {
|
||||||
const item = JSON.parse(stream);
|
const item = JSON.parse(stream);
|
||||||
downloading[item._id] = item;
|
downloading[item._id] = item;
|
||||||
toast.success(`Item queued successfully: ${downloading[item._id]?.title}`);
|
toast.success(`Item queued successfully: ${downloading[item._id]?.title}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', stream => {
|
socket.value.on('error', stream => {
|
||||||
const [item, error] = JSON.parse(stream);
|
const [item, error] = JSON.parse(stream);
|
||||||
toast.error(`${item?.id}: Error: ${error}`);
|
toast.error(`${item?.id}: Error: ${error}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('completed', stream => {
|
socket.value.on('completed', stream => {
|
||||||
const item = JSON.parse(stream);
|
const item = JSON.parse(stream);
|
||||||
if (item._id in downloading) {
|
if (item._id in downloading) {
|
||||||
delete downloading[item._id];
|
delete downloading[item._id];
|
||||||
|
|
@ -88,7 +105,7 @@ onMounted(() => {
|
||||||
completed[item._id] = item;
|
completed[item._id] = item;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('canceled', stream => {
|
socket.value.on('canceled', stream => {
|
||||||
const id = JSON.parse(stream);
|
const id = JSON.parse(stream);
|
||||||
if (false === (id in downloading)) {
|
if (false === (id in downloading)) {
|
||||||
return
|
return
|
||||||
|
|
@ -98,7 +115,7 @@ onMounted(() => {
|
||||||
delete downloading[id];
|
delete downloading[id];
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('cleared', stream => {
|
socket.value.on('cleared', stream => {
|
||||||
const id = JSON.parse(stream);
|
const id = JSON.parse(stream);
|
||||||
if (false === (id in completed)) {
|
if (false === (id in completed)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -107,14 +124,14 @@ onMounted(() => {
|
||||||
delete completed[id];
|
delete completed[id];
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("updated", stream => {
|
socket.value.on("updated", stream => {
|
||||||
const data = JSON.parse(stream);
|
const data = JSON.parse(stream);
|
||||||
let dl = downloading[data._id] ?? {};
|
let dl = downloading[data._id] ?? {};
|
||||||
data.deleting = dl?.deleting;
|
data.deleting = dl?.deleting;
|
||||||
downloading[data._id] = data;
|
downloading[data._id] = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("update", stream => {
|
socket.value.on("update", stream => {
|
||||||
const data = JSON.parse(stream);
|
const data = JSON.parse(stream);
|
||||||
if (false === (data._id in completed)) {
|
if (false === (data._id in completed)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -122,6 +139,11 @@ onMounted(() => {
|
||||||
completed[data._id] = data;
|
completed[data._id] = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.value.on('cli_close', () => cli_isLoading.value = false);
|
||||||
|
socket.value.on('cli_output', stream => {
|
||||||
|
console.log(stream)
|
||||||
|
cli_output.value.push(stream)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteItem = (type, item) => {
|
const deleteItem = (type, item) => {
|
||||||
|
|
|
||||||
69
frontend/src/components/CLI-Console.vue
Normal file
69
frontend/src/components/CLI-Console.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div class="mt-1 columns is-multiline">
|
||||||
|
<div class="column is-12 is-clearfix">
|
||||||
|
<h1 class="title is-4">Console</h1>
|
||||||
|
<div class="subtitle is-6">
|
||||||
|
You can execute <strong>non-interactive</strong> commands here. The interface jailed to the <code>yt-dlp</code>
|
||||||
|
command.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-12">
|
||||||
|
<form @submit.prevent="$emit('runCommand', command)">
|
||||||
|
<div class="field">
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<p class="control is-expanded has-icons-left">
|
||||||
|
<input type="text" class="input" v-model="command" placeholder="--help" autocomplete="off" autofocus
|
||||||
|
:disabled="props.isLoading">
|
||||||
|
<span class="icon is-left">
|
||||||
|
<font-awesome-icon icon="fa-solid fa-terminal" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-primary" type="submit" :disabled="props.isLoading"
|
||||||
|
:class="{ 'is-loading': props.isLoading }">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-server" /></span>
|
||||||
|
<span>Run</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<button class="button is-info" type="button" v-tooltip="'Clear output'" @click="$emit('cli_clear')">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-broom" /></span>
|
||||||
|
<span>Clear</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="column is-12">
|
||||||
|
<pre ref="outputConsole"
|
||||||
|
style="min-height: 60vh;max-height:65vh; overflow-y: scroll"><code><div v-for="(item, index) in props.cli_output" :key="'log_line-' + index" :class="{ 'has-text-danger': 'stderr' === item.type }">{{ item.line }}</div></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
cli_output: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['runCommand','cli_clear']);
|
||||||
|
|
||||||
|
const command = ref('')
|
||||||
|
</script>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showCompleted = !showCompleted">
|
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showCompleted = !showCompleted">
|
||||||
<span class="icon-text">
|
<span class="icon-text title is-4">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<font-awesome-icon :icon="showCompleted ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
|
<font-awesome-icon :icon="showCompleted ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showQueue = !showQueue">
|
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showQueue = !showQueue">
|
||||||
<span class="icon-text">
|
<span class="icon-text title is-4">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<font-awesome-icon :icon="showQueue ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
|
<font-awesome-icon :icon="showQueue ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<button v-tooltip="'Show/Hide Add Form'" class="button is-dark has-tooltip-bottom" @click="$emit('toggleForm')">
|
<button v-tooltip="'Toggle Console'" class="button is-dark has-tooltip-bottom"
|
||||||
|
@click="$emit('toggleConsole')">
|
||||||
|
<font-awesome-icon icon="fa-solid fa-terminal" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-item">
|
||||||
|
<button v-tooltip="'Toggle Add Form'" class="button is-dark has-tooltip-bottom" @click="$emit('toggleForm')">
|
||||||
<font-awesome-icon icon="fa-solid fa-plus" />
|
<font-awesome-icon icon="fa-solid fa-plus" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-item" v-if="config.tasks.length > 0">
|
<div class="navbar-item" v-if="config.tasks.length > 0">
|
||||||
<button v-tooltip="'Show/Hide Tasks'" class="button is-dark has-tooltip-bottom" @click="$emit('toggleTasks')">
|
<button v-tooltip="'Toggle Tasks'" class="button is-dark has-tooltip-bottom" @click="$emit('toggleTasks')">
|
||||||
<font-awesome-icon icon="fa-solid fa-tasks" />
|
<font-awesome-icon icon="fa-solid fa-tasks" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-3 is-unselectable">
|
<h1 class="mt-3 is-size-3 is-clickable is-unselectable">
|
||||||
<p class="title is-3">
|
<span class="icon-text title is-4">
|
||||||
<span class="icon-text">
|
<span class="icon"><font-awesome-icon icon="fa-solid fa-tasks" /></span>
|
||||||
<span class="icon">
|
<span>Tasks</span>
|
||||||
<font-awesome-icon icon="fa-solid fa-tasks" />
|
</span>
|
||||||
</span>
|
</h1>
|
||||||
<span>Tasks</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="column is-12">
|
<div class="column is-12">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import {
|
import {
|
||||||
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
||||||
faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ, faArrowDownAZ, faEject, faGlobe, faMoon, faSun
|
faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ, faArrowDownAZ, faEject, faGlobe, faMoon, faSun,
|
||||||
|
faTerminal,
|
||||||
|
faBroom,
|
||||||
|
faServer,
|
||||||
|
faPowerOff
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
@ -18,7 +22,7 @@ import 'floating-vue/dist/style.css'
|
||||||
|
|
||||||
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload,
|
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload,
|
||||||
faUpRightFromSquare, faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ,
|
faUpRightFromSquare, faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks, faCalendar, faArrowUpAZ,
|
||||||
faArrowDownAZ, faEject, faGlobe, faMoon, faSun)
|
faArrowDownAZ, faEject, faGlobe, faMoon, faSun, faTerminal, faBroom, faServer, faPowerOff)
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue