Feat: Implement Settings Panel to update yt-dlp core library
This commit is contained in:
parent
a41b9d2143
commit
701b146dc2
5 changed files with 157 additions and 10 deletions
38
backend/api/endpoints/settings.py
Normal file
38
backend/api/endpoints/settings.py
Normal 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))
|
||||
|
|
@ -1,28 +1,33 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, APIRouter # Added APIRouter
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
import os
|
||||
|
||||
from backend.core.config import settings
|
||||
from backend.api.endpoints import playlists, search, stream, lyrics
|
||||
from backend.core.config import settings # Renamed to settings_config to avoid conflict
|
||||
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
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.BACKEND_CORS_ORIGINS,
|
||||
allow_origins=settings.BACKEND_CORS_ORIGINS, # Used settings_config
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include Routers
|
||||
app.include_router(playlists.router, prefix=f"{settings.API_V1_STR}", tags=["playlists"])
|
||||
app.include_router(search.router, prefix=f"{settings.API_V1_STR}", tags=["search"])
|
||||
app.include_router(stream.router, prefix=f"{settings.API_V1_STR}", tags=["stream"])
|
||||
app.include_router(lyrics.router, prefix=f"{settings.API_V1_STR}", tags=["lyrics"])
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(playlists.router, prefix="/playlists", tags=["playlists"])
|
||||
api_router.include_router(search.router, tags=["search"])
|
||||
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)
|
||||
if settings.CACHE_DIR.parent.name == "backend":
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { Home, Search, Library } from "lucide-react";
|
||||
import { Home, Search, Library, Settings } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
|
||||
export default function MobileNav() {
|
||||
const pathname = usePathname();
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
const isActive = (path: string) => pathname === path;
|
||||
|
||||
|
|
@ -25,6 +28,11 @@ export default function MobileNav() {
|
|||
<Library size={24} />
|
||||
<span className="text-[10px]">Library</span>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
83
frontend/components/SettingsModal.tsx
Normal file
83
frontend/components/SettingsModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
"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 { usePlayer } from "@/context/PlayerContext";
|
||||
import { useState } from "react";
|
||||
import CreatePlaylistModal from "./CreatePlaylistModal";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
import { dbService } from "@/services/db";
|
||||
import { useLibrary } from "@/context/LibraryContext";
|
||||
import Logo from "./Logo";
|
||||
|
|
@ -14,6 +15,7 @@ export default function Sidebar() {
|
|||
const { likedTracks } = usePlayer();
|
||||
const { userPlaylists, libraryItems, refreshLibrary: refresh, activeFilter, setActiveFilter } = useLibrary();
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
const handleCreatePlaylist = async (name: string) => {
|
||||
await dbService.createPlaylist(name);
|
||||
|
|
@ -49,6 +51,13 @@ export default function Sidebar() {
|
|||
<Search className="w-6 h-6" />
|
||||
<span className="font-bold">Search</span>
|
||||
</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 className="bg-[#121212] rounded-lg flex-1 flex flex-col overflow-hidden">
|
||||
|
|
@ -185,6 +194,10 @@ export default function Sidebar() {
|
|||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onCreate={handleCreatePlaylist}
|
||||
/>
|
||||
<SettingsModal
|
||||
isOpen={isSettingsOpen}
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue