Added Console view for yt-dlp command.

This commit is contained in:
ArabCoders 2024-05-16 16:12:08 +03:00
parent 7a519bf16c
commit 0f95ea405c
10 changed files with 1259 additions and 1148 deletions

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -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": {

View file

@ -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) => {

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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);