feat: add audio extraction management interface and services

- Implemented ExtractionsPage component for managing audio extractions.
- Added ExtractionsService for handling extraction API calls.
- Created Playlist component for displaying audio tracks.
- Introduced ScrollArea component for better UI scrolling experience.
- Developed FilesService for file download and thumbnail management.
- Added PlayerService for controlling audio playback and state.
- Updated API services index to include new services.
This commit is contained in:
JSC
2025-08-03 20:43:42 +02:00
parent b42b802c37
commit 6cbf0e5e6d
11 changed files with 1388 additions and 6 deletions

View File

@@ -0,0 +1,52 @@
import { apiClient } from '../client'
export interface ExtractionInfo {
id: number
url: string
status: 'pending' | 'processing' | 'completed' | 'failed'
title?: string
service?: string
service_id?: string
sound_id?: number
user_id: number
error?: string
created_at: string
updated_at: string
}
export interface CreateExtractionResponse {
message: string
extraction: ExtractionInfo
}
export interface GetExtractionsResponse {
extractions: ExtractionInfo[]
}
export class ExtractionsService {
/**
* Create a new extraction job
*/
async createExtraction(url: string): Promise<CreateExtractionResponse> {
const response = await apiClient.post<CreateExtractionResponse>(`/api/v1/extractions/?url=${encodeURIComponent(url)}`)
return response
}
/**
* Get extraction by ID
*/
async getExtraction(extractionId: number): Promise<ExtractionInfo> {
const response = await apiClient.get<ExtractionInfo>(`/api/v1/extractions/${extractionId}`)
return response
}
/**
* Get user's extractions
*/
async getUserExtractions(): Promise<ExtractionInfo[]> {
const response = await apiClient.get<GetExtractionsResponse>('/api/v1/extractions/')
return response.extractions
}
}
export const extractionsService = new ExtractionsService()

View File

@@ -0,0 +1,86 @@
import { apiClient } from '../client'
import { API_CONFIG } from '../config'
export class FilesService {
/**
* Download a sound file
*/
async downloadSound(soundId: number): Promise<void> {
try {
// Use fetch directly to handle file download
const response = await fetch(`${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/download`, {
method: 'GET',
credentials: 'include',
})
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`)
}
// Get filename from Content-Disposition header or use default
const contentDisposition = response.headers.get('Content-Disposition')
let filename = `sound_${soundId}.mp3`
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/)
if (filenameMatch) {
filename = filenameMatch[1]
}
}
// Create blob and download
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
// Create temporary download link
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
// Cleanup
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('Failed to download sound:', error)
throw error
}
}
/**
* Get thumbnail URL for a sound
*/
getThumbnailUrl(soundId: number): string {
return `${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/thumbnail`
}
/**
* Check if a sound has a thumbnail
*/
async hasThumbnail(soundId: number): Promise<boolean> {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/v1/files/sounds/${soundId}/thumbnail`, {
method: 'HEAD', // Only check headers, don't download
credentials: 'include',
})
return response.ok
} catch {
return false
}
}
/**
* Preload a thumbnail image
*/
async preloadThumbnail(soundId: number): Promise<boolean> {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(true)
img.onerror = () => resolve(false)
img.src = this.getThumbnailUrl(soundId)
})
}
}
export const filesService = new FilesService()

View File

@@ -1,2 +1,5 @@
export * from './auth'
export * from './sounds'
export * from './sounds'
export * from './player'
export * from './files'
export * from './extractions'

View File

@@ -0,0 +1,132 @@
import { apiClient } from '../client'
export type PlayerStatus = 'playing' | 'paused' | 'stopped'
export type PlayerMode = 'continuous' | 'loop' | 'loop_one' | 'random' | 'single'
export interface PlayerSound {
id: number
name: string
filename: string
duration: number
size: number
type: string
thumbnail?: string
play_count: number
extract_url?: string
}
export interface PlayerPlaylist {
id: number
name: string
length: number
duration: number
sounds: PlayerSound[]
}
export interface PlayerState {
status: PlayerStatus
mode: PlayerMode
volume: number
position: number
duration?: number
index?: number
current_sound?: PlayerSound
playlist?: PlayerPlaylist
}
export interface PlayerSeekRequest {
position: number
}
export interface PlayerVolumeRequest {
volume: number
}
export interface PlayerModeRequest {
mode: PlayerMode
}
export interface MessageResponse {
message: string
}
export class PlayerService {
/**
* Play current sound
*/
async play(): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/play')
}
/**
* Play sound at specific index
*/
async playAtIndex(index: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>(`/api/v1/player/play/${index}`)
}
/**
* Pause playback
*/
async pause(): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/pause')
}
/**
* Stop playback
*/
async stop(): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/stop')
}
/**
* Skip to next track
*/
async next(): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/next')
}
/**
* Go to previous track
*/
async previous(): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/previous')
}
/**
* Seek to specific position
*/
async seek(position: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/seek', { position })
}
/**
* Set playback volume
*/
async setVolume(volume: number): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/volume', { volume })
}
/**
* Set playback mode
*/
async setMode(mode: PlayerMode): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/mode', { mode })
}
/**
* Reload current playlist
*/
async reloadPlaylist(): Promise<MessageResponse> {
return apiClient.post<MessageResponse>('/api/v1/player/reload-playlist')
}
/**
* Get current player state
*/
async getState(): Promise<PlayerState> {
return apiClient.get<PlayerState>('/api/v1/player/state')
}
}
export const playerService = new PlayerService()