Add Jellyfin NFO sidecar generation for downloaded videos

Writes a Kodi/Jellyfin-compatible .nfo XML file next to each .mp4 on
download completion, deletes it when the download record is removed, and
exposes POST /api/downloads/nfo/generate to backfill NFOs for existing
downloads. Frontend adds a "Generate NFO" button in the Downloads header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 00:03:58 +02:00
parent 65bc199366
commit 001d2ddcf0
3 changed files with 90 additions and 1 deletions

View File

@@ -167,3 +167,4 @@ export const fetchChannelPlaylists = (channelId) => api.post(`/playlists/channel
export const getPlaylistVideos = (playlistId, offset = 0, limit = 60) =>
api.get(`/playlists/${playlistId}/videos`, { params: { offset, limit } });
export const indexPlaylist = (playlistId) => api.post(`/playlists/${playlistId}/index`);
export const generateNfoFiles = () => api.post("/downloads/nfo/generate");

View File

@@ -1,7 +1,7 @@
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getDownloads, deleteDownload, deleteAllDownloads, restoreDownload, getActiveTasks } from "../api";
import { getDownloads, deleteDownload, deleteAllDownloads, restoreDownload, getActiveTasks, generateNfoFiles } from "../api";
import SortPicker from "../components/SortPicker";
const HISTORY_SORTS = [
@@ -80,6 +80,12 @@ export default function DownloadsPage() {
},
});
const [nfoResult, setNfoResult] = useState(null);
const nfoMut = useMutation({
mutationFn: generateNfoFiles,
onSuccess: (r) => setNfoResult(r.data.generated),
});
if (isLoading) {
return (
<div className="flex items-center justify-center py-16">
@@ -104,6 +110,15 @@ export default function DownloadsPage() {
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<h1 className="font-display font-bold text-2xl text-zinc-100">Downloads</h1>
<div className="flex items-center gap-3">
<button
onClick={() => { setNfoResult(null); nfoMut.mutate(); }}
disabled={nfoMut.isPending}
title="Generate Jellyfin .nfo sidecar files for all completed downloads"
className="text-sm text-zinc-500 hover:text-zinc-300 disabled:opacity-50 transition-colors"
>
{nfoMut.isPending ? "Generating…" : nfoResult != null ? `Generated ${nfoResult} NFO` : "Generate NFO"}
</button>
{hasRemovable && (
confirmClear ? (
<div className="flex items-center gap-2">
@@ -131,6 +146,7 @@ export default function DownloadsPage() {
</button>
)
)}
</div>
</div>
{activeTasks.length > 0 && (