Feat: Implement Settings Panel to update yt-dlp core library

This commit is contained in:
Your Name 2026-01-01 13:36:51 +07:00
parent a41b9d2143
commit 701b146dc2
5 changed files with 157 additions and 10 deletions

View file

@ -0,0 +1,38 @@
import subprocess
import os
import sys
from fastapi import APIRouter, HTTPException, BackgroundTasks
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
def restart_server():
"""Restarts the server by killing the current process."""
logger.info("Restarting server...")
# This works in Docker if a restart policy is set (e.g., restart: always)
os.kill(os.getpid(), 15) # SIGTERM
@router.post("/update-ytdlp")
async def update_ytdlp(background_tasks: BackgroundTasks):
try:
# Run pip install to upgrade yt-dlp to master
logger.info("Starting yt-dlp update...")
process = subprocess.run(
[sys.executable, "-m", "pip", "install", "--upgrade", "--force-reinstall", "git+https://github.com/yt-dlp/yt-dlp.git@master"],
capture_output=True,
text=True,
check=True
)
logger.info(f"Update Output: {process.stdout}")
# Schedule restart after a short delay to allow response to be sent
background_tasks.add_task(restart_server)
return {"status": "success", "message": "yt-dlp updated. Server restarting..."}
except subprocess.CalledProcessError as e:
logger.error(f"Update Failed: {e.stderr}")
raise HTTPException(status_code=500, detail=f"Update failed: {e.stderr}")
except Exception as e:
logger.error(f"Unexpected Error: {e}")
raise HTTPException(status_code=500, detail=str(e))

View file

@ -1,28 +1,33 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi import FastAPI, APIRouter # Added APIRouter
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
import os import os
from backend.core.config import settings from backend.core.config import settings # Renamed to settings_config to avoid conflict
from backend.api.endpoints import playlists, search, stream, lyrics from backend.api.endpoints import playlists, search, stream, lyrics, settings as settings_router # Aliased settings router
app = FastAPI(title=settings.APP_NAME) app = FastAPI(title=settings.APP_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json") # Used settings_config
# CORS setup # CORS setup
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.BACKEND_CORS_ORIGINS, allow_origins=settings.BACKEND_CORS_ORIGINS, # Used settings_config
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
# Include Routers # Include Routers
app.include_router(playlists.router, prefix=f"{settings.API_V1_STR}", tags=["playlists"]) api_router = APIRouter()
app.include_router(search.router, prefix=f"{settings.API_V1_STR}", tags=["search"]) api_router.include_router(playlists.router, prefix="/playlists", tags=["playlists"])
app.include_router(stream.router, prefix=f"{settings.API_V1_STR}", tags=["stream"]) api_router.include_router(search.router, tags=["search"])
app.include_router(lyrics.router, prefix=f"{settings.API_V1_STR}", tags=["lyrics"]) api_router.include_router(stream.router, tags=["stream"])
api_router.include_router(lyrics.router, tags=["lyrics"])
api_router.include_router(settings_router.router, prefix="/settings", tags=["settings"]) # Included settings_router
app.include_router(api_router, prefix=settings.API_V1_STR) # Corrected prefix and removed extra tags
# Serve Static Frontend (Production Mode) # Serve Static Frontend (Production Mode)
if settings.CACHE_DIR.parent.name == "backend": if settings.CACHE_DIR.parent.name == "backend":

View file

@ -1,11 +1,14 @@
"use client"; "use client";
import { Home, Search, Library } from "lucide-react"; import { Home, Search, Library, Settings } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useState } from "react";
import SettingsModal from "./SettingsModal";
export default function MobileNav() { export default function MobileNav() {
const pathname = usePathname(); const pathname = usePathname();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const isActive = (path: string) => pathname === path; const isActive = (path: string) => pathname === path;
@ -25,6 +28,11 @@ export default function MobileNav() {
<Library size={24} /> <Library size={24} />
<span className="text-[10px]">Library</span> <span className="text-[10px]">Library</span>
</Link> </Link>
<button onClick={() => setIsSettingsOpen(true)} className={`flex flex-col items-center gap-1 text-neutral-400 hover:text-white`}>
<Settings size={24} />
<span className="text-[10px]">Settings</span>
</button>
<SettingsModal isOpen={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
</div> </div>
); );
} }

View file

@ -0,0 +1,83 @@
"use client";
import { useState } from "react";
import { X, RefreshCw, CheckCircle, AlertCircle } from "lucide-react";
import { api } from "@/services/apiClient";
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
const [updating, setUpdating] = useState(false);
const [status, setStatus] = useState<{ type: "success" | "error" | null; message: string }>({ type: null, message: "" });
if (!isOpen) return null;
const handleUpdate = async () => {
setUpdating(true);
setStatus({ type: null, message: "" });
try {
await api.post("/settings/update-ytdlp", {});
setStatus({ type: "success", message: "Update successful! Server is restarting..." });
// Reload page after a delay to reflect restart
setTimeout(() => {
window.location.reload();
}, 5000);
} catch (e: any) {
setStatus({ type: "error", message: e.message || "Update failed" });
} finally {
setUpdating(false);
}
};
return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-[#1e1e1e] rounded-xl p-6 w-full max-w-md shadow-2xl border border-white/10">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold">Settings</h2>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full transition">
<X size={20} />
</button>
</div>
<div className="space-y-6">
<div className="bg-[#2a2a2a] p-4 rounded-lg">
<h3 className="font-semibold mb-2">Core Components</h3>
<p className="text-sm text-gray-400 mb-4">
Update the core playback library (yt-dlp) to the latest version to fix playback issues.
</p>
<button
onClick={handleUpdate}
disabled={updating}
className={`w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition ${updating ? "bg-blue-600/50 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-500"
}`}
>
{updating ? (
<>
<RefreshCw className="animate-spin" size={18} />
Updating...
</>
) : (
<>
<RefreshCw size={18} />
Update yt-dlp (Nightly)
</>
)}
</button>
</div>
{status.message && (
<div className={`p-4 rounded-lg flex items-start gap-3 ${status.type === "success" ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"
}`}>
{status.type === "success" ? <CheckCircle size={20} /> : <AlertCircle size={20} />}
<p className="text-sm">{status.message}</p>
</div>
)}
</div>
</div>
</div>
);
}

View file

@ -1,10 +1,11 @@
"use client"; "use client";
import { Home, Search, Library, Plus, Heart } from "lucide-react"; import { Home, Search, Library, Plus, Heart, Settings } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePlayer } from "@/context/PlayerContext"; import { usePlayer } from "@/context/PlayerContext";
import { useState } from "react"; import { useState } from "react";
import CreatePlaylistModal from "./CreatePlaylistModal"; import CreatePlaylistModal from "./CreatePlaylistModal";
import SettingsModal from "./SettingsModal";
import { dbService } from "@/services/db"; import { dbService } from "@/services/db";
import { useLibrary } from "@/context/LibraryContext"; import { useLibrary } from "@/context/LibraryContext";
import Logo from "./Logo"; import Logo from "./Logo";
@ -14,6 +15,7 @@ export default function Sidebar() {
const { likedTracks } = usePlayer(); const { likedTracks } = usePlayer();
const { userPlaylists, libraryItems, refreshLibrary: refresh, activeFilter, setActiveFilter } = useLibrary(); const { userPlaylists, libraryItems, refreshLibrary: refresh, activeFilter, setActiveFilter } = useLibrary();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const handleCreatePlaylist = async (name: string) => { const handleCreatePlaylist = async (name: string) => {
await dbService.createPlaylist(name); await dbService.createPlaylist(name);
@ -49,6 +51,13 @@ export default function Sidebar() {
<Search className="w-6 h-6" /> <Search className="w-6 h-6" />
<span className="font-bold">Search</span> <span className="font-bold">Search</span>
</Link> </Link>
<button
onClick={() => setIsSettingsOpen(true)}
className="flex items-center gap-4 text-spotify-text-muted hover:text-white transition cursor-pointer text-left"
>
<Settings className="w-6 h-6" />
<span className="font-bold">Settings</span>
</button>
</div> </div>
<div className="bg-[#121212] rounded-lg flex-1 flex flex-col overflow-hidden"> <div className="bg-[#121212] rounded-lg flex-1 flex flex-col overflow-hidden">
@ -185,6 +194,10 @@ export default function Sidebar() {
onClose={() => setIsCreateModalOpen(false)} onClose={() => setIsCreateModalOpen(false)}
onCreate={handleCreatePlaylist} onCreate={handleCreatePlaylist}
/> />
<SettingsModal
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
/>
</aside> </aside>
); );
} }