Add filesystem Library Scan (movies/tv/music) and a full Music library

- New admin Library Scan page walks the NAS mounts directly for movies,
  TV, and music not already in Radarr/Sonarr/local storage, and imports
  them with storage_type=scan (fully integrated: streamable, transcodable,
  listed in Admin) — Radarr/Sonarr import flows are untouched.
- New Music section (nav tab, album-grid browse, per-album track list,
  persistent bottom player with queue/seek) backed by a Track model and
  range-request audio streaming endpoint.
- MusicBrainz + Cover Art Archive integration (free, no API key) to fill
  in album art for scanned tracks, paced to their 1 req/sec rate limit.
- Mounted the NAS music share into CT104 (new mp2 LXC mountpoint,
  CIFS read-only) alongside the existing video mount.
- Optional Bluetooth/output-device picker on the player bar via the Audio
  Output Devices API (setSinkId) for already-paired speakers — Chrome/Edge
  only, since browsers can't pair new BT devices directly.
This commit is contained in:
Myron Blair
2026-07-26 20:25:41 -05:00
parent f7626ced2e
commit 514cbad4ab
15 changed files with 975 additions and 40 deletions
+38 -27
View File
@@ -2,9 +2,11 @@ import { BrowserRouter, Routes, Route, Navigate, useLocation } from "react-route
import { Toaster } from "sonner";
import { AuthProvider, useAuth } from "./lib/auth";
import { ProfileProvider, useProfile } from "./lib/profile";
import { MusicPlayerProvider } from "./lib/musicPlayer";
import Navbar from "./components/Navbar";
import GrainOverlay from "./components/GrainOverlay";
import ServiceStatus from "./components/ServiceStatus";
import MusicPlayerBar from "./components/MusicPlayerBar";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Browse from "./pages/Browse";
@@ -23,6 +25,9 @@ import EpisodePlayer from "./pages/EpisodePlayer";
import SonarrImport from "./pages/SonarrImport";
import AdminShowEpisodes from "./pages/AdminShowEpisodes";
import Users from "./pages/Users";
import Music from "./pages/Music";
import AlbumDetail from "./pages/AlbumDetail";
import LibraryScan from "./pages/LibraryScan";
const ProfileGate = ({ children }) => {
const { user, loading: authLoading } = useAuth();
@@ -53,6 +58,7 @@ const Shell = ({ children }) => {
{!hideChrome && user && active && <Navbar />}
{!hideChrome && user?.is_admin && active && <ServiceStatus />}
{children}
{!hideChrome && user && active && <MusicPlayerBar />}
<GrainOverlay />
</>
);
@@ -72,33 +78,38 @@ function App() {
<BrowserRouter>
<AuthProvider>
<ProfileProvider>
<Shell>
<Routes>
<Route path="/" element={<RootRedirect />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/browse" element={<ProfileGate><Browse /></ProfileGate>} />
<Route path="/shows" element={<ProfileGate><Shows /></ProfileGate>} />
<Route path="/show/:id" element={<ProfileGate><ShowDetail /></ProfileGate>} />
<Route path="/watch/episode/:id" element={<ProfileGate><EpisodePlayer /></ProfileGate>} />
<Route path="/my-list" element={<ProfileGate><MyList /></ProfileGate>} />
<Route path="/search" element={<ProfileGate><Search /></ProfileGate>} />
<Route path="/watch/:id" element={<ProfileGate><Player /></ProfileGate>} />
<Route path="/requests" element={<ProfileGate><Requests /></ProfileGate>} />
<Route path="/settings" element={<ProfileGate><Settings /></ProfileGate>} />
<Route path="/admin" element={<AdminGate><Admin /></AdminGate>} />
<Route path="/admin/users" element={<AdminGate><Users /></AdminGate>} />
<Route path="/admin/upload" element={<AdminGate><AdminUpload /></AdminGate>} />
<Route path="/admin/radarr" element={<AdminGate><RadarrImport /></AdminGate>} />
<Route path="/admin/sonarr" element={<AdminGate><SonarrImport /></AdminGate>} />
<Route path="/admin/shows/:id/episodes" element={<AdminGate><AdminShowEpisodes /></AdminGate>} />
<Route path="/admin/queue" element={<AdminGate><TranscodeQueue /></AdminGate>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Shell>
<Toaster position="bottom-right" theme="dark" toastOptions={{
style: { background: "#0F0F0F", border: "1px solid #222", color: "#F2F2F2" },
}} />
<MusicPlayerProvider>
<Shell>
<Routes>
<Route path="/" element={<RootRedirect />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/browse" element={<ProfileGate><Browse /></ProfileGate>} />
<Route path="/shows" element={<ProfileGate><Shows /></ProfileGate>} />
<Route path="/show/:id" element={<ProfileGate><ShowDetail /></ProfileGate>} />
<Route path="/watch/episode/:id" element={<ProfileGate><EpisodePlayer /></ProfileGate>} />
<Route path="/my-list" element={<ProfileGate><MyList /></ProfileGate>} />
<Route path="/search" element={<ProfileGate><Search /></ProfileGate>} />
<Route path="/watch/:id" element={<ProfileGate><Player /></ProfileGate>} />
<Route path="/requests" element={<ProfileGate><Requests /></ProfileGate>} />
<Route path="/settings" element={<ProfileGate><Settings /></ProfileGate>} />
<Route path="/music" element={<ProfileGate><Music /></ProfileGate>} />
<Route path="/music/album/:artist/:album" element={<ProfileGate><AlbumDetail /></ProfileGate>} />
<Route path="/admin" element={<AdminGate><Admin /></AdminGate>} />
<Route path="/admin/users" element={<AdminGate><Users /></AdminGate>} />
<Route path="/admin/upload" element={<AdminGate><AdminUpload /></AdminGate>} />
<Route path="/admin/radarr" element={<AdminGate><RadarrImport /></AdminGate>} />
<Route path="/admin/sonarr" element={<AdminGate><SonarrImport /></AdminGate>} />
<Route path="/admin/scan" element={<AdminGate><LibraryScan /></AdminGate>} />
<Route path="/admin/shows/:id/episodes" element={<AdminGate><AdminShowEpisodes /></AdminGate>} />
<Route path="/admin/queue" element={<AdminGate><TranscodeQueue /></AdminGate>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Shell>
<Toaster position="bottom-right" theme="dark" toastOptions={{
style: { background: "#0F0F0F", border: "1px solid #222", color: "#F2F2F2" },
}} />
</MusicPlayerProvider>
</ProfileProvider>
</AuthProvider>
</BrowserRouter>