diff --git a/backend/api/endpoints/settings.py b/backend/api/endpoints/settings.py new file mode 100644 index 0000000..83a0223 --- /dev/null +++ b/backend/api/endpoints/settings.py @@ -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)) diff --git a/backend/main.py b/backend/main.py index bd667fa..54de65b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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": diff --git a/frontend/components/MobileNav.tsx b/frontend/components/MobileNav.tsx index 242db2a..dceb4ff 100644 --- a/frontend/components/MobileNav.tsx +++ b/frontend/components/MobileNav.tsx @@ -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 + + setIsSettingsOpen(false)} /> ); } diff --git a/frontend/components/SettingsModal.tsx b/frontend/components/SettingsModal.tsx new file mode 100644 index 0000000..f579d03 --- /dev/null +++ b/frontend/components/SettingsModal.tsx @@ -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 ( +
+
+
+

Settings

+ +
+ +
+
+

Core Components

+

+ Update the core playback library (yt-dlp) to the latest version to fix playback issues. +

+ + +
+ + {status.message && ( +
+ {status.type === "success" ? : } +

{status.message}

+
+ )} +
+
+
+ ); +} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 91ab94a..7db000b 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -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 +
@@ -185,6 +194,10 @@ export default function Sidebar() { onClose={() => setIsCreateModalOpen(false)} onCreate={handleCreatePlaylist} /> + setIsSettingsOpen(false)} + /> ); }