feat: add rename to history items.

This commit is contained in:
arabcoders 2026-05-04 20:29:34 +03:00
parent 80316df952
commit 285e661667
8 changed files with 384 additions and 73 deletions

38
API.md
View file

@ -24,6 +24,7 @@ This document describes the available endpoints and their usage. All endpoints r
- [DELETE /api/history](#delete-apihistory)
- [POST /api/history/{id}](#post-apihistoryid)
- [GET /api/history/{id}](#get-apihistoryid)
- [POST /api/history/{id}/rename](#post-apihistoryidrename)
- [GET /api/history](#get-apihistory)
- [GET /api/history/live](#get-apihistorylive)
- [POST /api/history/start](#post-apihistorystart)
@ -56,7 +57,7 @@ This document describes the available endpoints and their usage. All endpoints r
- [GET /api/player/segments/{segment}/{file:.\*}.ts](#get-apiplayersegmentssegmentfilets)
- [GET /api/player/subtitle/{file:.\*}.vtt](#get-apiplayersubtitlefilevtt)
- [GET /api/player/subtitles/manifest/{file:.\*}](#get-apiplayersubtitlesmanifestfile)
- [GET /api/player/subtitles/{source_format}/{file:.\*}](#get-apiplayersubtitlessource_formatfile)
- [GET /api/player/subtitles/{source\_format}/{file:.\*}](#get-apiplayersubtitlessource_formatfile)
- [GET /api/thumbnail](#get-apithumbnail)
- [GET /api/file/ffprobe/{file:.\*}](#get-apifileffprobefile)
- [GET /api/file/info/{file:.\*}](#get-apifileinfofile)
@ -532,6 +533,41 @@ or an error:
---
### POST /api/history/{id}/rename
**Purpose**: Rename a downloaded history file and its sidecars.
**Path Parameter**:
- `id` = Unique history item ID.
**Body**:
```json
{
"new_name": "renamed_video.mp4"
}
```
**Response**:
```json
{
"_id": "<uuid>",
"title": "Video Title",
"url": "https://youtube.com/watch?v=...",
....
}
```
**Error Responses**:
- `400 Bad Request` if `id` or `new_name` is missing, or the item has no downloaded file.
- `404 Not Found` if the item does not exist.
- `409 Conflict` if the rename destination already exists.
- `500 Internal Server Error` if the filesystem rename fails unexpectedly.
**Notes**:
- Uses the same file rename behavior as the file browser actions.
- Renames matching sidecar files together with the main media file.
---
### GET /api/history
**Purpose**: Returns the download queue and/or download history with optional pagination support.

View file

@ -256,7 +256,7 @@ class HttpAPI:
response: Response = await handler(request)
contentType: str = response.headers.get("content-type", "")
contentType: str = str(response.headers.get("content-type", ""))
if contentType.startswith("text/html") and getattr(response, "_path", None):
rewrite_path: str = base_path.rstrip("/")
async with await anyio.open_file(response._path, "rb") as f:

View file

@ -1,5 +1,6 @@
import asyncio
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any
from aiohttp import web
@ -10,10 +11,12 @@ from app.features.presets.service import Presets
from app.library.config import Config
from app.library.DataStore import StoreType
from app.library.downloads import Download, DownloadQueue
from app.library.downloads.utils import safe_relative_path
from app.library.encoder import Encoder
from app.library.Events import EventBus, Events
from app.library.ItemDTO import Item
from app.library.router import route
from app.library.Utils import calc_download_path, get_file_sidecar, rename_file
if TYPE_CHECKING:
from library.downloads import Download
@ -287,9 +290,9 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
}
if "finished" == item.info.status and (filename := item.info.get_file()):
from app.features.streaming.library.ffprobe import ffprobe
try:
from app.features.streaming.library.ffprobe import ffprobe
info["ffprobe"] = await ffprobe(filename)
except Exception:
pass
@ -302,6 +305,70 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
return web.json_response(data=info, status=web.HTTPOk.status_code, dumps=encoder.encode)
@route("POST", r"api/history/{id}/rename", "history.item.rename")
async def item_rename(
request: Request,
queue: DownloadQueue,
encoder: Encoder,
notify: EventBus,
config: Config,
) -> Response:
if not (id := request.match_info.get("id")):
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
item: Download | None = await queue.done.get_by_id(id)
if not item:
return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code)
try:
post: dict = await request.json()
if not post:
return web.json_response(data={"error": "no data provided."}, status=web.HTTPBadRequest.status_code)
except Exception:
return web.json_response(data={"error": "invalid JSON body."}, status=web.HTTPBadRequest.status_code)
new_name: str = str(post.get("new_name") or "").strip()
if not new_name:
return web.json_response(data={"error": "new_name is required."}, status=web.HTTPBadRequest.status_code)
filepath: Path | None = item.info.get_file(download_path=Path(config.download_path))
if not filepath:
return web.json_response(data={"error": "item has no downloaded file."}, status=web.HTTPBadRequest.status_code)
try:
renamed, sidecar_renamed = rename_file(filepath, new_name)
except ValueError as e:
return web.json_response(data={"error": str(e)}, status=web.HTTPConflict.status_code)
except OSError as e:
LOG.exception(e)
return web.json_response(data={"error": str(e)}, status=web.HTTPInternalServerError.status_code)
item_dir: Path = (
Path(item.info.download_dir)
if item.info.download_dir
else Path(calc_download_path(base_path=config.download_path, folder=item.info.folder, create_path=False))
)
item.info.filename = safe_relative_path(renamed, item_dir, Path(config.download_path))
try:
item.info.sidecar = get_file_sidecar(renamed)
except Exception:
item.info.sidecar = {}
await queue.done.put(item, no_notify=True)
notify.emit(Events.ITEM_UPDATED, data=item.info)
sidecar_count: int = len(sidecar_renamed)
sidecar_msg: str = f" and {sidecar_count} sidecar file/s" if sidecar_count > 0 else ""
LOG.info(f"Renamed file '{filepath}' to '{renamed}'{sidecar_msg}")
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

@ -1,12 +1,16 @@
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, Mock
import pytest
from app.library.encoder import Encoder
from app.library.DataStore import StoreType
from app.routes.api.history import items_delete
from app.library.ItemDTO import ItemDTO
from app.library.encoder import Encoder
from app.routes.api.history import item_rename, items_delete
from app.tests.helpers import temporary_test_dir
class _FakeRequest:
@ -14,11 +18,39 @@ class _FakeRequest:
self._payload = payload or {}
self.query: dict[str, str] = {}
self.match_info: dict[str, str] = {}
self.body_exists = bool(payload)
async def json(self) -> dict[str, Any]:
return self._payload
def _make_download(
*,
filename: str | None = None,
folder: str = "",
download_dir: str | None = None,
status: str = "finished",
) -> SimpleNamespace:
base_dir = download_dir or "/downloads"
original_post_init = ItemDTO.__post_init__
ItemDTO.__post_init__ = lambda self: None
try:
item = ItemDTO(
id="test-id",
title="Test Video",
url="https://example.com/watch?v=test-id",
folder=folder,
status=status,
filename=filename,
download_dir=base_dir,
)
finally:
ItemDTO.__post_init__ = original_post_init
return SimpleNamespace(info=item)
@pytest.mark.asyncio
async def test_items_delete_uses_bulk_history_status_clear() -> None:
request = _FakeRequest(payload={"type": StoreType.HISTORY.value, "status": "finished,skip", "remove_file": False})
@ -47,3 +79,112 @@ async def test_items_delete_uses_bulk_history_id_clear() -> None:
queue.clear_bulk.assert_awaited_once_with(["a", "b"], remove_file=False)
body = json.loads(response.body.decode("utf-8"))
assert body == {"items": {}, "deleted": 2}
@pytest.mark.asyncio
async def test_item_rename_requires_new_name() -> None:
request = _FakeRequest(payload={})
request.match_info["id"] = "item-1"
queue = SimpleNamespace(
done=SimpleNamespace(get_by_id=AsyncMock(return_value=_make_download(filename="video.mp4")))
)
encoder = Encoder()
notify = Mock()
config = SimpleNamespace(download_path="/downloads")
response = await item_rename(request, queue, encoder, notify, config)
assert response.status == 400
body = json.loads(response.body.decode("utf-8"))
assert body == {"error": "no data provided."}
@pytest.mark.asyncio
async def test_item_rename_returns_not_found_when_item_missing() -> None:
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
request.match_info["id"] = "missing"
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=None)))
encoder = Encoder()
notify = Mock()
config = SimpleNamespace(download_path="/downloads")
response = await item_rename(request, queue, encoder, notify, config)
assert response.status == 404
body = json.loads(response.body.decode("utf-8"))
assert body == {"error": "item 'missing' not found."}
@pytest.mark.asyncio
async def test_item_rename_requires_existing_downloaded_file() -> None:
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
request.match_info["id"] = "item-1"
item = _make_download(filename="video.mp4")
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=item)))
encoder = Encoder()
notify = Mock()
config = SimpleNamespace(download_path="/downloads")
item.info.get_file = lambda download_path=None: None
response = await item_rename(request, queue, encoder, notify, config)
assert response.status == 400
body = json.loads(response.body.decode("utf-8"))
assert body == {"error": "item has no downloaded file."}
@pytest.mark.asyncio
async def test_item_rename_renames_file_and_sidecars() -> None:
with temporary_test_dir("history-rename") as temp_dir:
media = temp_dir / "video.mp4"
subtitle = temp_dir / "video.en.srt"
media.write_text("video")
subtitle.write_text("subtitle")
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
request.match_info["id"] = "item-1"
item = _make_download(filename="video.mp4", download_dir=str(temp_dir))
item.info._id = "item-1"
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=item), put=AsyncMock()))
encoder = Encoder()
notify = Mock()
config = SimpleNamespace(download_path=str(temp_dir))
response = await item_rename(request, queue, encoder, notify, config)
assert response.status == 200
body = json.loads(response.body.decode("utf-8"))
assert body["filename"] == "renamed.mp4"
assert item.info.filename == "renamed.mp4"
assert (temp_dir / "renamed.mp4").exists()
assert (temp_dir / "renamed.en.srt").exists()
assert not media.exists()
assert not subtitle.exists()
queue.done.put.assert_awaited_once_with(item, no_notify=True)
notify.emit.assert_called_once()
@pytest.mark.asyncio
async def test_item_rename_returns_conflict_on_collision() -> None:
with temporary_test_dir("history-rename-conflict") as temp_dir:
media = temp_dir / "video.mp4"
conflict = temp_dir / "renamed.mp4"
media.write_text("video")
conflict.write_text("existing")
request = _FakeRequest(payload={"new_name": "renamed.mp4"})
request.match_info["id"] = "item-1"
item = _make_download(filename="video.mp4", download_dir=str(temp_dir))
queue = SimpleNamespace(done=SimpleNamespace(get_by_id=AsyncMock(return_value=item), put=AsyncMock()))
encoder = Encoder()
notify = Mock()
config = SimpleNamespace(download_path=str(temp_dir))
response = await item_rename(request, queue, encoder, notify, config)
assert response.status == 409
body = json.loads(response.body.decode("utf-8"))
assert body == {"error": "Destination 'renamed.mp4' already exists"}
queue.done.put.assert_not_awaited()
notify.emit.assert_not_called()

View file

@ -32,7 +32,7 @@
:loading="isRefreshing"
:disabled="isRefreshing"
:square="isMobile"
@click="() => void refreshQueue()"
@click="() => refreshQueue()"
>
<span v-if="!isMobile">Reload Queue</span>
</UButton>
@ -329,7 +329,7 @@
variant="soft"
size="xs"
icon="i-lucide-play-circle"
@click="void stateStore.startItems([item._id])"
@click="() => stateStore.startItems([item._id])"
>
Start
</UButton>
@ -340,7 +340,7 @@
variant="soft"
size="xs"
icon="i-lucide-pause"
@click="void stateStore.pauseItems([item._id])"
@click="() => stateStore.pauseItems([item._id])"
>
Pause
</UButton>
@ -350,7 +350,7 @@
variant="outline"
size="xs"
icon="i-lucide-x"
@click="void stateStore.cancelItems([item._id])"
@click="() => stateStore.cancelItems([item._id])"
>
{{ item.is_live ? 'Stop' : 'Cancel' }}
</UButton>
@ -400,7 +400,7 @@
:sibling-count="0"
@update:page="
(page) =>
loadHistory(page, {
load(page, {
order: 'DESC',
perPage: configStore.app.default_pagination,
})
@ -415,7 +415,7 @@
:loading="historyIsLoading"
:disabled="historyIsLoading"
@click="
void reloadHistory({ order: 'DESC', perPage: configStore.app.default_pagination })
() => reload({ order: 'DESC', perPage: configStore.app.default_pagination })
"
>
Reload
@ -542,7 +542,7 @@
variant="soft"
size="xs"
icon="i-lucide-rotate-cw"
@click="() => void requeueItem(item)"
@click="() => requeueItem(item)"
>
Requeue
</UButton>
@ -552,7 +552,7 @@
variant="outline"
size="xs"
icon="i-lucide-trash"
@click="() => void deleteHistoryItem(item)"
@click="() => deleteHistoryItem(item)"
>
Delete
</UButton>
@ -589,7 +589,7 @@
:sibling-count="0"
@update:page="
(page) =>
loadHistory(page, {
load(page, {
order: 'DESC',
perPage: configStore.app.default_pagination,
})
@ -711,10 +711,10 @@ const {
items: historyItems,
pagination,
isLoading,
loadHistory,
reloadHistory,
deleteHistoryItems,
historyMoveHandler,
load,
reload,
remove,
moveHandler,
} = useHistoryState();
const embedUrl = ref('');
@ -1261,20 +1261,18 @@ const requeueItem = async (item: StoreItem): Promise<void> => {
payload.extras = JSON.parse(JSON.stringify(item.extras));
}
await deleteHistoryItems({ ids: [item._id], removeFile: false });
await reloadHistory({ order: 'DESC', perPage: configStore.app.default_pagination });
await remove({ ids: [item._id], removeFile: false });
await reload({ order: 'DESC', perPage: configStore.app.default_pagination });
await stateStore.addDownload(payload);
};
const deleteHistoryItem = async (item: StoreItem): Promise<void> => {
await deleteHistoryItems({ ids: [item._id], removeFile: app.value.remove_files });
await reloadHistory({ order: 'DESC', perPage: configStore.app.default_pagination });
await remove({ ids: [item._id], removeFile: app.value.remove_files });
await reload({ order: 'DESC', perPage: configStore.app.default_pagination });
toast.info('Removed from history queue.');
};
const handleHistoryItemMoved = historyMoveHandler(
() => simpleMode.value && historyInitialized.value,
);
const handleHistoryItemMoved = moveHandler(() => simpleMode.value && historyInitialized.value);
const showMessage = (item: StoreItem): boolean => {
if (!item?.msg || item.msg === item?.error) {
@ -1303,7 +1301,7 @@ onMounted(async () => {
await normalizeSimpleRoute();
historyInitialized.value = true;
socketStore.on('item_moved', handleHistoryItemMoved);
await loadHistory(1, { order: 'DESC', perPage: configStore.app.default_pagination });
await load(1, { order: 'DESC', perPage: configStore.app.default_pagination });
if (!socketStore.isConnected && autoRefreshEnabled.value) {
startAutoRefresh();
@ -1389,7 +1387,7 @@ watch(
return;
}
void loadHistory(1, { order: 'DESC', perPage: configStore.app.default_pagination });
void load(1, { order: 'DESC', perPage: configStore.app.default_pagination });
},
);
</script>

View file

@ -77,7 +77,7 @@ const buildQuery = (
return params.toString();
};
const loadHistory = async (page: number = 1, options: HistoryLoadOptions = {}): Promise<void> => {
const load = async (page: number = 1, options: HistoryLoadOptions = {}): Promise<void> => {
const { order = 'DESC', status, perPage = pageSize.value } = options;
isLoading.value = true;
@ -104,12 +104,12 @@ const loadHistory = async (page: number = 1, options: HistoryLoadOptions = {}):
}
};
const reloadHistory = async (options: HistoryLoadOptions = {}): Promise<void> => {
const reload = async (options: HistoryLoadOptions = {}): Promise<void> => {
const targetPage = isLoaded.value ? pagination.value.page : 1;
await loadHistory(targetPage, options);
await load(targetPage, options);
};
const deleteHistoryItems = async (
const remove = async (
options: {
ids?: string[];
status?: string;
@ -155,7 +155,39 @@ const deleteHistoryItems = async (
}
};
const resetHistory = (): void => {
const rename = async (item: StoreItem, newName: string): Promise<boolean> => {
const trimmedName = newName.trim();
if (!trimmedName || trimmedName === item.filename?.split('/').pop()) {
return false;
}
try {
const response = await request(`/api/history/${item._id}/rename`, {
method: 'POST',
body: JSON.stringify({ new_name: trimmedName }),
});
await ensureSuccess(response);
const updated = (await response.json()) as StoreItem;
const index = items.value.findIndex((entry) => entry._id === updated._id);
if (index !== -1) {
items.value = [...items.value.slice(0, index), updated, ...items.value.slice(index + 1)];
}
lastError.value = null;
return true;
} catch (error) {
handleError(error);
if (throwInstead.value) {
throw error;
}
return false;
}
};
const reset = (): void => {
items.value = [];
pagination.value = {
page: 1,
@ -169,7 +201,7 @@ const resetHistory = (): void => {
lastError.value = null;
};
const addHistoryItem = (item: StoreItem): void => {
const upsert = (item: StoreItem): void => {
const existingIndex = items.value.findIndex((existing) => existing._id === item._id);
if (existingIndex !== -1) {
@ -195,7 +227,7 @@ const addHistoryItem = (item: StoreItem): void => {
pagination.value.has_next = pagination.value.page < pagination.value.total_pages;
};
const historyMoveHandler = (
const moveHandler = (
shouldHandle: () => boolean = () => isLoaded.value,
): ((payload: WSEP['item_moved']) => void) => {
return (payload: WSEP['item_moved']): void => {
@ -203,7 +235,7 @@ const historyMoveHandler = (
return;
}
addHistoryItem(payload.data.item);
upsert(payload.data.item);
};
};
@ -215,11 +247,12 @@ export const useHistoryState = () => {
isLoaded,
lastError,
pageSize,
loadHistory,
reloadHistory,
deleteHistoryItems,
resetHistory,
upsertHistoryItem: addHistoryItem,
historyMoveHandler,
load,
reload,
remove,
rename,
reset,
upsert,
moveHandler,
};
};

View file

@ -50,7 +50,7 @@
icon="i-lucide-refresh-cw"
:loading="isLoading"
:disabled="isLoading"
@click="void reloadHistory({ order: 'DESC', perPage: config.app.default_pagination })"
@click="() => reload({ order: 'DESC', perPage: config.app.default_pagination })"
>
<span>Reload</span>
</UButton>
@ -78,7 +78,7 @@
show-edges
:sibling-count="0"
@update:page="
(page) => loadHistory(page, { order: 'DESC', perPage: config.app.default_pagination })
(page) => load(page, { order: 'DESC', perPage: config.app.default_pagination })
"
/>
</div>
@ -314,7 +314,7 @@
variant="outline"
size="xs"
icon="i-lucide-rotate-cw"
@click="void retryItem(item, true)"
@click="() => retryItem(item, true)"
>
Retry
</UButton>
@ -337,7 +337,7 @@
variant="outline"
size="xs"
icon="i-lucide-trash"
@click="void removeItem(item)"
@click="() => removeItem(item)"
>
Remove
</UButton>
@ -608,7 +608,7 @@
variant="outline"
icon="i-lucide-rotate-cw"
class="w-full justify-center"
@click="void retryItem(item, false)"
@click="() => retryItem(item, false)"
>
Retry
</UButton>
@ -631,7 +631,7 @@
variant="outline"
icon="i-lucide-trash"
class="w-full justify-center"
@click="void removeItem(item)"
@click="() => removeItem(item)"
>
{{ config.app.remove_files ? 'Remove' : 'Clear' }}
</UButton>
@ -703,7 +703,7 @@
show-edges
:sibling-count="0"
@update:page="
(page) => loadHistory(page, { order: 'DESC', perPage: config.app.default_pagination })
(page) => load(page, { order: 'DESC', perPage: config.app.default_pagination })
"
/>
</div>
@ -786,7 +786,7 @@ const stateStore = useQueueState();
const socketStore = useAppSocket();
const toast = useNotification();
const box = useConfirm();
const { confirmDialog } = useDialog();
const { confirmDialog, promptDialog } = useDialog();
const { toggleExpand, expandClass } = useExpandableMeta();
const pendingDownloadFormItem = useState<item_request | Record<string, never>>(
'pending-download-form-item',
@ -797,10 +797,11 @@ const {
pagination,
isLoading,
isLoaded,
loadHistory,
reloadHistory,
deleteHistoryItems,
historyMoveHandler,
load,
reload,
remove,
rename,
moveHandler,
} = useHistoryState();
const show_thumbnail = useStorage<boolean>('show_thumbnail', true);
@ -837,11 +838,11 @@ const paginationInfo = computed(() => ({
isLoaded: isLoaded.value,
}));
const handleHistoryItemMoved = historyMoveHandler();
const handleHistoryItemMoved = moveHandler();
onMounted(async () => {
socketStore.on('item_moved', handleHistoryItemMoved);
await loadHistory(1, { order: 'DESC', perPage: config.app.default_pagination });
await load(1, { order: 'DESC', perPage: config.app.default_pagination });
});
onBeforeUnmount(() => {
@ -1048,6 +1049,12 @@ const itemActionGroups = (item: StoreItem): Array<Array<Record<string, unknown>>
icon: 'i-lucide-file-code-2',
onSelect: () => void generateNfo(item),
});
mediaActions.push({
label: 'Rename file',
icon: 'i-lucide-pencil',
onSelect: () => void renameFile(item),
});
} else if (isEmbedable(item.url)) {
mediaActions.push({
label: 'Play video',
@ -1127,9 +1134,9 @@ const deleteSelectedItems = async (): Promise<void> => {
return;
}
await deleteHistoryItems({ ids: [...selectedElms.value], removeFile: config.app.remove_files });
await remove({ ids: [...selectedElms.value], removeFile: config.app.remove_files });
selectedElms.value = [];
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
};
const clearCompleted = async (): Promise<void> => {
@ -1141,9 +1148,9 @@ const clearCompleted = async (): Promise<void> => {
selectedElms.value = [];
await deleteHistoryItems({ status: 'finished,skip', removeFile: false });
await remove({ status: 'finished,skip', removeFile: false });
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
};
const clearIncomplete = async (): Promise<void> => {
@ -1152,8 +1159,8 @@ const clearIncomplete = async (): Promise<void> => {
}
selectedElms.value = [];
await deleteHistoryItems({ status: '!finished', removeFile: false });
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await remove({ status: '!finished', removeFile: false });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
};
const setIcon = (item: StoreItem): string => {
@ -1307,10 +1314,10 @@ const archiveItem = async (item: StoreItem, opts = {}): Promise<void> => {
}
if ((opts as { remove_history?: boolean })?.remove_history) {
await deleteHistoryItems({ ids: [item._id], removeFile: false });
await remove({ ids: [item._id], removeFile: false });
}
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
};
const removeItem = async (item: StoreItem): Promise<void> => {
@ -1324,13 +1331,13 @@ const removeItem = async (item: StoreItem): Promise<void> => {
return;
}
await deleteHistoryItems({ ids: [item._id], removeFile: config.app.remove_files });
await remove({ ids: [item._id], removeFile: config.app.remove_files });
if (selectedElms.value.includes(item._id || '')) {
selectedElms.value = selectedElms.value.filter((entry) => entry !== item._id);
}
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
};
const retryItem = async (
@ -1349,13 +1356,13 @@ const retryItem = async (
auto_start: item.auto_start,
};
await deleteHistoryItems({ ids: [item._id], removeFile: remove_file });
await remove({ ids: [item._id], removeFile: remove_file });
if (selectedElms.value.includes(item._id || '')) {
selectedElms.value = selectedElms.value.filter((entry) => entry !== item._id);
}
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
if (true === re_add) {
toast.info('Cleared the item from history, and added it to the new download form.');
@ -1532,10 +1539,10 @@ const removeFromArchive = async (
}
if (opts?.remove_history) {
await deleteHistoryItems({ ids: [item._id], removeFile: file_delete });
await remove({ ids: [item._id], removeFile: file_delete });
}
await reloadHistory({ order: 'DESC', perPage: config.app.default_pagination });
await reload({ order: 'DESC', perPage: config.app.default_pagination });
};
const isQueuedAnimation = (item: StoreItem): string => {
@ -1565,4 +1572,33 @@ const generateNfo = async (item: StoreItem): Promise<void> => {
toast.error(`Error: ${error.message}`);
}
};
const renameFile = async (item: StoreItem): Promise<void> => {
if (!item.filename) {
return;
}
const currentName = item.filename.split('/').pop() || item.filename;
const { status, value: newName } = await promptDialog({
title: 'Rename File',
message: `Enter new name for '${currentName}' (and its sidecars):`,
initial: currentName,
confirmText: 'Rename',
cancelText: 'Cancel',
});
if (!status) {
return;
}
const trimmedName = (newName || '').trim();
if (!trimmedName || trimmedName === currentName) {
return;
}
const success = await rename(item, trimmedName);
if (success) {
toast.success(`Renamed '${currentName}'.`);
}
};
</script>

View file

@ -1405,11 +1405,11 @@ wheels = [
[[package]]
name = "pytz"
version = "2026.1.post1"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
]
[[package]]