feat: add full screen audio player and radio station management
- Implemented FullScreenPlayer component for enhanced audio playback experience. - Added functionality to toggle full screen mode in AudioPlayer. - Introduced NavidromeConfigContext for managing Navidrome server configurations. - Created RadioStationsPage for managing internet radio stations, including adding, deleting, and playing stations. - Enhanced SettingsPage to configure Navidrome server connection with validation and feedback. - Updated NavidromeAPI to support fetching and managing radio stations. - Integrated lyrics fetching and display in FullScreenPlayer using LrcLibClient.
This commit is contained in:
108
lib/lrclib.ts
Normal file
108
lib/lrclib.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
interface LrcLibTrack {
|
||||
id: number;
|
||||
name: string;
|
||||
trackName: string;
|
||||
artistName: string;
|
||||
albumName: string;
|
||||
duration: number;
|
||||
instrumental: boolean;
|
||||
plainLyrics: string | null;
|
||||
syncedLyrics: string | null;
|
||||
}
|
||||
|
||||
interface LyricLine {
|
||||
time: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class LrcLibClient {
|
||||
private baseUrl = 'https://lrclib.net/api';
|
||||
|
||||
async searchTrack(artist: string, track: string, album?: string, duration?: number): Promise<LrcLibTrack | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
artist_name: artist,
|
||||
track_name: track,
|
||||
});
|
||||
|
||||
if (album) {
|
||||
params.append('album_name', album);
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
params.append('duration', Math.round(duration).toString());
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/search?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const results: LrcLibTrack[] = await response.json();
|
||||
|
||||
// Return the best match (first result is usually the best)
|
||||
return results.length > 0 ? results[0] : null;
|
||||
} catch (error) {
|
||||
console.error('Failed to search lyrics:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getTrackById(id: number): Promise<LrcLibTrack | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/get/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Failed to get track by ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
parseSyncedLyrics(syncedLyrics: string): LyricLine[] {
|
||||
if (!syncedLyrics) return [];
|
||||
|
||||
const lines = syncedLyrics.split('\n');
|
||||
const lyricLines: LyricLine[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2})\](.*)/);
|
||||
if (match) {
|
||||
const minutes = parseInt(match[1], 10);
|
||||
const seconds = parseInt(match[2], 10);
|
||||
const centiseconds = parseInt(match[3], 10);
|
||||
const text = match[4].trim();
|
||||
|
||||
const time = minutes * 60 + seconds + centiseconds / 100;
|
||||
lyricLines.push({ time, text });
|
||||
}
|
||||
}
|
||||
|
||||
return lyricLines.sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
getCurrentLyricIndex(lyricLines: LyricLine[], currentTime: number): number {
|
||||
if (lyricLines.length === 0) return -1;
|
||||
|
||||
for (let i = lyricLines.length - 1; i >= 0; i--) {
|
||||
if (currentTime >= lyricLines[i].time) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export const lrcLibClient = new LrcLibClient();
|
||||
@@ -82,6 +82,13 @@ export interface Playlist {
|
||||
coverArt?: string;
|
||||
}
|
||||
|
||||
export interface RadioStation {
|
||||
id: string;
|
||||
streamUrl: string;
|
||||
name: string;
|
||||
homePageUrl?: string;
|
||||
}
|
||||
|
||||
class NavidromeAPI {
|
||||
private config: NavidromeConfig;
|
||||
private clientName = 'stillnavidrome';
|
||||
@@ -326,27 +333,90 @@ class NavidromeAPI {
|
||||
const searchData = response.searchResult3 as { song?: Song[] };
|
||||
return searchData?.song || [];
|
||||
}
|
||||
|
||||
async getRadioStations(): Promise<RadioStation[]> {
|
||||
const response = await this.makeRequest('getRadioStations');
|
||||
const radioStationsData = response.radioStations as { radioStation?: RadioStation[] };
|
||||
return radioStationsData?.radioStation || [];
|
||||
}
|
||||
|
||||
async getRadioStation(stationId: string): Promise<RadioStation> {
|
||||
const response = await this.makeRequest('getRadioStation', { id: stationId });
|
||||
return response.radioStation as RadioStation;
|
||||
}
|
||||
|
||||
async getInternetRadioStations(): Promise<RadioStation[]> {
|
||||
try {
|
||||
const response = await this.makeRequest('getInternetRadioStations');
|
||||
const radioData = response.internetRadioStations as { internetRadioStation?: RadioStation[] };
|
||||
return radioData?.internetRadioStation || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to get internet radio stations:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async createInternetRadioStation(name: string, streamUrl: string, homePageUrl?: string): Promise<void> {
|
||||
const params: Record<string, string> = { name, streamUrl };
|
||||
if (homePageUrl) params.homePageUrl = homePageUrl;
|
||||
await this.makeRequest('createInternetRadioStation', params);
|
||||
}
|
||||
|
||||
async deleteInternetRadioStation(id: string): Promise<void> {
|
||||
await this.makeRequest('deleteInternetRadioStation', { id });
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance management
|
||||
let navidromeInstance: NavidromeAPI | null = null;
|
||||
|
||||
export function getNavidromeAPI(): NavidromeAPI {
|
||||
if (!navidromeInstance) {
|
||||
const config: NavidromeConfig = {
|
||||
serverUrl: process.env.NEXT_PUBLIC_NAVIDROME_URL || '',
|
||||
username: process.env.NEXT_PUBLIC_NAVIDROME_USERNAME || '',
|
||||
password: process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD || ''
|
||||
};
|
||||
|
||||
if (!config.serverUrl || !config.username || !config.password) {
|
||||
throw new Error('Navidrome configuration is incomplete. Please check environment variables.');
|
||||
export function getNavidromeAPI(customConfig?: NavidromeConfig): NavidromeAPI {
|
||||
let config: NavidromeConfig;
|
||||
|
||||
if (customConfig) {
|
||||
config = customConfig;
|
||||
} else {
|
||||
// Try to get config from localStorage first (client-side)
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedConfig = localStorage.getItem('navidrome-config');
|
||||
if (savedConfig) {
|
||||
try {
|
||||
config = JSON.parse(savedConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse saved Navidrome config:', error);
|
||||
config = getEnvConfig();
|
||||
}
|
||||
} else {
|
||||
config = getEnvConfig();
|
||||
}
|
||||
} else {
|
||||
// Server-side: use environment variables
|
||||
config = getEnvConfig();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!config.serverUrl || !config.username || !config.password) {
|
||||
throw new Error('Navidrome configuration is incomplete. Please configure in settings or check environment variables.');
|
||||
}
|
||||
|
||||
// Always create a new instance if config is provided or if no instance exists
|
||||
if (customConfig || !navidromeInstance) {
|
||||
navidromeInstance = new NavidromeAPI(config);
|
||||
}
|
||||
|
||||
return navidromeInstance;
|
||||
}
|
||||
|
||||
function getEnvConfig(): NavidromeConfig {
|
||||
return {
|
||||
serverUrl: process.env.NEXT_PUBLIC_NAVIDROME_URL || '',
|
||||
username: process.env.NEXT_PUBLIC_NAVIDROME_USERNAME || '',
|
||||
password: process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD || ''
|
||||
};
|
||||
}
|
||||
|
||||
export function resetNavidromeAPI(): void {
|
||||
navidromeInstance = null;
|
||||
}
|
||||
|
||||
export default NavidromeAPI;
|
||||
|
||||
Reference in New Issue
Block a user