initial code to support adding tasks via WebGUI
This commit is contained in:
parent
4c6059b81c
commit
bbc57113b6
5 changed files with 168 additions and 36 deletions
43
app/main.py
43
app/main.py
|
|
@ -107,9 +107,21 @@ class Main:
|
|||
f'YTPTube v{self.config.version} - listening on http://{self.config.host}:{self.config.port}'),
|
||||
)
|
||||
|
||||
async def connect(self, sid, environ):
|
||||
await self.sio.emit('all', self.serializer.encode(self.dqueue.get()), to=sid)
|
||||
await self.sio.emit('configuration', self.serializer.encode(self.config), to=sid)
|
||||
async def connect(self, sid, _):
|
||||
data: dict = {
|
||||
**self.dqueue.get(),
|
||||
"config": self.config,
|
||||
"tasks": [],
|
||||
}
|
||||
|
||||
if os.path.exists(os.path.join(self.config.config_path, 'tasks.json')):
|
||||
try:
|
||||
with open(os.path.join(self.config.config_path, 'tasks.json'), 'r') as f:
|
||||
data['tasks'] = json.load(f)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
await self.sio.emit('initial_data', self.serializer.encode(data), to=sid)
|
||||
|
||||
def addTasks(self):
|
||||
tasks_file: str = os.path.join(self.config.config_path, 'tasks.json')
|
||||
|
|
@ -118,8 +130,13 @@ class Main:
|
|||
f'No tasks file found at {tasks_file}. Skipping Tasks.')
|
||||
return
|
||||
|
||||
with open(tasks_file, 'r') as f:
|
||||
tasks = json.load(f)
|
||||
try:
|
||||
with open(tasks_file, 'r') as f:
|
||||
tasks = json.load(f)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f'Could not load tasks file [{tasks_file}]. Error message [{str(e)}]. Skipping Tasks.')
|
||||
return
|
||||
|
||||
for task in tasks:
|
||||
if not task.get('url'):
|
||||
|
|
@ -188,6 +205,22 @@ class Main:
|
|||
|
||||
return web.Response(text=self.serializer.encode(status))
|
||||
|
||||
@self.routes.get(self.config.url_prefix + 'tasks')
|
||||
async def tasks(_: Request) -> Response:
|
||||
tasks_file: str = os.path.join(
|
||||
self.config.config_path, 'tasks.json')
|
||||
|
||||
if not os.path.exists(tasks_file):
|
||||
return web.json_response({"error": "No tasks defined."}, status=404)
|
||||
|
||||
try:
|
||||
with open(tasks_file, 'r') as f:
|
||||
tasks = json.load(f)
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
return web.json_response(tasks)
|
||||
|
||||
@self.routes.post(self.config.url_prefix + 'add_batch')
|
||||
async def add_batch(request: Request) -> Response:
|
||||
status = {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<PageHeader :config="config" @toggleForm="addForm = !addForm" />
|
||||
<PageHeader :config="config" @toggleForm="addForm = !addForm" @toggleTasks="showTasks = !showTasks" />
|
||||
<formAdd v-if="addForm" :config="config" @addItem="addItem" />
|
||||
<pageTasks v-if="showTasks" :tasks="config.tasks" @removeTask="deleteTask" />
|
||||
<DownloadingList :config="config" :queue="downloading" @deleteItem="deleteItem" />
|
||||
<PageCompleted :config="config" :completed="completed" @deleteItem="deleteItem" @addItem="addItem"
|
||||
@playItem="playItem" />
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
import { onMounted, reactive, ref } from 'vue'
|
||||
import PageHeader from './components/Page-Header'
|
||||
import formAdd from './components/Form-Add'
|
||||
import pageTasks from './components/Page-Tasks'
|
||||
import DownloadingList from './components/Page-Downloading'
|
||||
import PageCompleted from './components/Page-Completed'
|
||||
import PageFooter from './components/Page-Footer'
|
||||
|
|
@ -34,12 +36,14 @@ const bus = useEventBus('item_added');
|
|||
const config = reactive({
|
||||
isConnected: false,
|
||||
app: {},
|
||||
tasks: [],
|
||||
});
|
||||
|
||||
const downloading = reactive({});
|
||||
const completed = reactive({});
|
||||
const video_link = ref('');
|
||||
const addForm = useStorage('addForm', true)
|
||||
const showTasks = useStorage('showTasks', false)
|
||||
|
||||
onMounted(() => {
|
||||
const socket = io(process.env.VUE_APP_BASE_URL, {
|
||||
|
|
@ -49,19 +53,17 @@ onMounted(() => {
|
|||
socket.on('connect', () => config.isConnected = true);
|
||||
socket.on('disconnect', () => config.isConnected = false);
|
||||
|
||||
socket.on('all', stream => {
|
||||
socket.on('initial_data', stream => {
|
||||
const initialData = JSON.parse(stream);
|
||||
for (const key in initialData) {
|
||||
const element = initialData[key];
|
||||
if (key === 'queue') {
|
||||
for (const id in element) {
|
||||
downloading[id] = element[id];
|
||||
}
|
||||
} else if (key === 'done') {
|
||||
for (const id in element) {
|
||||
completed[id] = element[id];
|
||||
}
|
||||
}
|
||||
config.app = initialData['config'];
|
||||
config.tasks = initialData['tasks'];
|
||||
|
||||
for (const id in initialData['queue']) {
|
||||
downloading[id] = initialData['done'][id];
|
||||
}
|
||||
|
||||
for (const id in initialData['done']) {
|
||||
completed[id] = initialData['done'][id];
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -104,10 +106,6 @@ onMounted(() => {
|
|||
data.deleting = dl?.deleting;
|
||||
downloading[data._id] = data;
|
||||
});
|
||||
|
||||
socket.on('configuration', stream => {
|
||||
config.app = JSON.parse(stream);
|
||||
});
|
||||
});
|
||||
|
||||
const deleteItem = (type, item) => {
|
||||
|
|
@ -172,6 +170,14 @@ const playItem = (item) => {
|
|||
video_link.value = config.app.url_host + config.app.url_prefix + baseDir + encodeURIComponent(item.filename);
|
||||
};
|
||||
|
||||
const deleteTask = (indexNumber, item) => {
|
||||
if (!confirm(`Are you sure you want to delete this task ${item?.name || item.url}?`)) {
|
||||
return;
|
||||
}
|
||||
config.tasks = config.tasks.filter((_, index) => {
|
||||
return index !== indexNumber;
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
<template>
|
||||
<nav class="navbar is-mobile is-dark">
|
||||
<div class="navbar-brand pl-5">
|
||||
<a class="navbar-item" href="#">
|
||||
<a class="navbar-item has-tooltip-bottom" :class="config.isConnected ? 'has-text-success' : 'has-text-danger'"
|
||||
:data-tooltip="config.isConnected ? 'Connected' : 'Connecting'" href="javascript:void(0);">
|
||||
<b>YTPTube</b>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<button class="button is-dark has-tooltip-bottom" :data-tooltip="config.isConnected ? 'Connected' : 'Connecting'">
|
||||
<span class="icon-text" :class="config.isConnected ? 'has-text-success' : 'has-text-danger'">
|
||||
<span class="icon">
|
||||
<font-awesome-icon :icon="config.isConnected ? 'fa-solid fa-wifi' : 'fa-solid fa-signal'" />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<button data-tooltip="Show/Hide Add Form" class="button is-dark has-tooltip-bottom" @click="$emit('toggleForm')">
|
||||
<font-awesome-icon icon="fa-solid fa-plus" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<button data-tooltip="Show/Hide Tasks" class="button is-dark has-tooltip-bottom" @click="$emit('toggleTasks')">
|
||||
<font-awesome-icon icon="fa-solid fa-tasks" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<button data-tooltip="Switch to Light theme" class="button is-dark has-tooltip-bottom"
|
||||
@click="selectedTheme = 'light'" v-if="selectedTheme == 'dark'">🌞</button>
|
||||
|
|
@ -37,7 +33,7 @@ import { useStorage } from '@vueuse/core'
|
|||
|
||||
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')());
|
||||
|
||||
defineEmits(['toggleForm'])
|
||||
defineEmits(['toggleForm', 'toggleTasks'])
|
||||
|
||||
defineProps({
|
||||
config: {
|
||||
|
|
@ -115,6 +111,7 @@ watch(selectedTheme, (value) => {
|
|||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
|
|
|
|||
96
frontend/src/components/Page-Tasks.vue
Normal file
96
frontend/src/components/Page-Tasks.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div class="mt-3 is-unselectable">
|
||||
<p class="title is-3">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<font-awesome-icon icon="fa-solid fa-tasks" />
|
||||
</span>
|
||||
<span>Tasks</span>
|
||||
</span>
|
||||
</p>
|
||||
<p class="subtitle is-4 has-text-danger">
|
||||
This section is not working yet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12 has-text-right">
|
||||
<button class="button is-primary" @click="$emit('task_new')">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<font-awesome-icon icon="fa-solid fa-plus" />
|
||||
</span>
|
||||
<span>New Task</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-narrow is-hoverable is-striped is-fullwidth is-fixed">
|
||||
<thead class="has-text-centered">
|
||||
<tr>
|
||||
<th class="has-text-centered" width="20%">Name</th>
|
||||
<th class="has-text-centered" width="50%">URL</th>
|
||||
<th class="has-text-centered" width="20%">Timer</th>
|
||||
<th class="has-text-centered" width="10%">Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item, key in tasks" :key="key">
|
||||
<td class="is-text-overflow has-text-centered" v-if="item.name">{{ item.name }}</td>
|
||||
<td class="has-text-centered" v-else>Not set</td>
|
||||
<td class="is-text-overflow">{{ item.url }}</td>
|
||||
<td class="has-text-centered" v-if="item.timer">{{ item.timer }}</td>
|
||||
<td class="has-text-centered" v-else>once an hour</td>
|
||||
<td class="has-text-centered">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-small is-danger" @click="$emit('removeTask', key, item)"
|
||||
data-tooltip="Remove task">
|
||||
<span class="icon">
|
||||
<font-awesome-icon icon="fa-solid fa-trash" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-small is-info" data-tooltip="Modify task">
|
||||
<span class="icon">
|
||||
<font-awesome-icon icon="fa-solid fa-cog" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="tasks.length < 1">
|
||||
<td colspan="4" class="has-text-centered has-text-danger">
|
||||
No tasks are defined.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
|
||||
defineEmits(['task_new', 'task_add', 'removeTask', 'tasks_updated'])
|
||||
|
||||
defineProps({
|
||||
tasks: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table.is-fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import {
|
||||
faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
||||
faSpinner, faWifi, faSignal, faArrowUp, faArrowDown,
|
||||
faSpinner, faArrowUp, faArrowDown, faTasks
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { faSquare, faSquareCheck } from '@fortawesome/free-regular-svg-icons'
|
||||
|
|
@ -17,7 +17,7 @@ import './assets/css/style.css'
|
|||
import '@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css';
|
||||
|
||||
library.add(faCog, faTrash, faLink, faPlus, faTrashCan, faCircleXmark, faCircleCheck, faRotateRight, faDownload, faUpRightFromSquare,
|
||||
faSquare, faSquareCheck, faSpinner, faWifi, faSignal, faArrowUp, faArrowDown,)
|
||||
faSquare, faSquareCheck, faSpinner, faArrowUp, faArrowDown, faTasks,)
|
||||
const app = createApp(App);
|
||||
|
||||
app.config.globalProperties.capitalize = s => s && s[0].toUpperCase() + s.slice(1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue