Merge pull request #308 from arabcoders/dev

Make it possible to view local item info
This commit is contained in:
Abdulmohsen 2025-06-20 21:03:13 +03:00 committed by GitHub
commit 3f3f63f0e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 108 additions and 16 deletions

29
API.md
View file

@ -23,6 +23,7 @@ This document describes the available endpoints and their usage. All endpoints r
- [POST /api/history](#post-apihistory)
- [DELETE /api/history](#delete-apihistory)
- [POST /api/history/{id}](#post-apihistoryid)
- [GET /api/history/{id}](#get-apihistoryid)
- [GET /api/history](#get-apihistory)
- [GET /api/tasks](#get-apitasks)
- [PUT /api/tasks](#put-apitasks)
@ -296,6 +297,34 @@ or an error:
---
### GET /api/history/{id}
**Purpose**: View details of a specific item in the database.
**Path Parameter**:
- `id` = Unique item ID.
**Response**:
```json
{
"_id": "<uuid>",
"title": "Video Title",
"url": "https://youtube.com/watch?v=...",
....
}
```
or an error:
```json
{
"error": "text"
}
```
- `200 OK` If the item exists and is returned.
- `404 Not Found` if the item doesnt exist.
- `400 Bad Request` if id is missing.
---
### GET /api/history
**Purpose**: Returns the download queue and the download history.

View file

@ -6,6 +6,8 @@ from dataclasses import dataclass, field
from email.utils import formatdate
from typing import Any
from app.library.Utils import clean_item
LOG = logging.getLogger("ItemDTO")
@ -224,7 +226,8 @@ class ItemDTO:
eta: str | None = None
def serialize(self) -> dict:
return self.__dict__.copy()
item, _ = clean_item(self.__dict__.copy(), ItemDTO.removed_fields())
return item
def json(self) -> str:
return json.dumps(self.serialize(), default=lambda o: o.__dict__, sort_keys=True, indent=4)

View file

@ -69,6 +69,32 @@ async def item_delete(request: Request, queue: DownloadQueue, encoder: Encoder)
)
@route("GET", "api/history/{id}", "item_view")
async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) -> Response:
"""
Update an item in the history.
Args:
request (Request): The request object.
queue (DownloadQueue): The download queue instance.
encoder (Encoder): The encoder instance.
notify (EventBus): The event bus instance.
Returns:
Response: The response object.
"""
id: str = request.match_info.get("id")
if not id:
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
item: Download | None = queue.done.get_by_id(id) or queue.queue.get_by_id(id)
if not item:
return web.json_response(data={"error": "item not found."}, status=web.HTTPNotFound.status_code)
return web.json_response(data=item.info, status=web.HTTPOk.status_code, dumps=encoder.encode)
@route("POST", "api/history/{id}", "item_update")
async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder, notify: EventBus) -> Response:
"""

View file

@ -190,7 +190,12 @@
</template>
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Information</span>
<span>yt-dlp Information</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)">
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span>
</NuxtLink>
<template v-if="item.status != 'finished' || !item.filename">
@ -311,7 +316,8 @@
</div>
<div class="column is-half-mobile has-text-centered is-text-overflow is-unselectable"
v-if="item.file_size">
{{ formatBytes(item.file_size) }}
<span class="has-tooltip" v-tooltip="`Path: ${makePath(item)}`">{{
formatBytes(item.file_size) }}</span>
</div>
</div>
<div class="columns is-mobile is-multiline">
@ -361,7 +367,12 @@
<NuxtLink class="dropdown-item" @click="emitter('getInfo', item.url)">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Information</span>
<span>yt-dlp Information</span>
</NuxtLink>
<NuxtLink class="dropdown-item" @click="emitter('getItemInfo', item._id)">
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
<span>Local Information</span>
</NuxtLink>
<template v-if="item.status != 'finished' || !item.filename">
@ -435,7 +446,7 @@ import { getEmbedable, isEmbedable } from '~/utils/embedable'
import { formatBytes, makeDownload, uri } from '~/utils/index'
import Dropdown from './Dropdown.vue'
const emitter = defineEmits(['getInfo', 'add_new'])
const emitter = defineEmits(['getInfo', 'add_new', 'getItemInfo'])
const props = defineProps({
thumbnails: {
@ -862,4 +873,14 @@ const is_queued = item => {
return item.live_in || item.extras?.live_in || item.extras?.release_in ? 'fa-spin fa-spin-10' : ''
}
const makePath = item => {
if (!item?.filename) {
return ''
}
const real_path = eTrim(item.download_dir, '/') + '/' + sTrim(item.filename, '/')
return real_path.replace(config.app.download_path, '').replace(/^\//, '')
}
</script>

View file

@ -49,13 +49,15 @@
<NewDownload v-if="config.showForm || config.app.basic_mode" @getInfo="url => get_info = url" :item="item_form"
@clear_form="item_form = {}" @remove_archive="" />
<Queue @getInfo="url => get_info = url" :thumbnails="show_thumbnail" />
<History @getInfo="url => get_info = url" @add_new="item => toNewDownload(item)" :thumbnails="show_thumbnail" />
<GetInfo v-if="get_info" :link="get_info" @closeModel="get_info = ''" />
<Queue @getInfo="(url: string) => view_info(url, false)" :thumbnails="show_thumbnail"
@getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" />
<History @getInfo="(url: string) => view_info(url, false)" @add_new="item => toNewDownload(item)"
:thumbnails="show_thumbnail" @getItemInfo="(id: string) => view_info(`/api/history/${id}`, true)" />
<GetInfo v-if="get_info" :link="get_info" :useUrl="get_info_use_url" @closeModel="close_info()" />
</div>
</template>
<script setup>
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
const config = useConfigStore()
@ -63,11 +65,12 @@ const stateStore = useStateStore()
const socket = useSocketStore()
const box = useConfirm()
const get_info = ref('')
const bg_enable = useStorage('random_bg', true)
const bg_opacity = useStorage('random_bg_opacity', 0.85)
const display_style = useStorage('display_style', 'cards')
const show_thumbnail = useStorage('show_thumbnail', true)
const get_info = ref<string>('')
const get_info_use_url = ref<boolean>(false)
const bg_enable = useStorage<boolean>('random_bg', true)
const bg_opacity = useStorage<number>('random_bg_opacity', 0.85)
const display_style = useStorage<string>('display_style', 'cards')
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const item_form = ref({})
@ -101,17 +104,27 @@ const pauseDownload = () => {
socket.emit('pause', {})
}
const close_info = () => {
get_info.value = ''
get_info_use_url.value = false
}
const view_info = (url: string, useUrl: boolean = false) => {
get_info.value = url
get_info_use_url.value = useUrl
}
watch(get_info, v => {
if (!bg_enable.value) {
return
}
document.querySelector('body').setAttribute("style", `opacity: ${v ? 1 : bg_opacity.value}`)
document.querySelector('body')?.setAttribute("style", `opacity: ${v ? 1 : bg_opacity.value}`)
})
const changeDisplay = () => display_style.value = display_style.value === 'cards' ? 'list' : 'cards'
const toNewDownload = async (item) => {
const toNewDownload = async (item: any) => {
if (!item) {
return
}