2026-03-23 12:34:27 +03:00
|
|
|
import axios from 'axios'
|
2026-03-23 15:01:46 +03:00
|
|
|
import type { Album, Artist, SearchResult, Track, TrackDetail } from './types'
|
2026-03-19 17:31:09 +03:00
|
|
|
|
2026-04-08 14:51:52 +01:00
|
|
|
const FURUMI_API_BASE = import.meta.env.VITE_FURUMI_API_URL ?? ''
|
|
|
|
|
export const API_ROOT = `${FURUMI_API_BASE}/api`
|
2026-03-23 12:34:27 +03:00
|
|
|
|
|
|
|
|
export const furumiApi = axios.create({
|
|
|
|
|
baseURL: API_ROOT,
|
|
|
|
|
})
|
2026-03-19 17:31:09 +03:00
|
|
|
|
2026-04-08 14:51:52 +01:00
|
|
|
export function setAuthToken(token: string) {
|
|
|
|
|
furumiApi.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function clearAuthToken() {
|
|
|
|
|
delete furumiApi.defaults.headers.common['Authorization']
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 15:41:47 +01:00
|
|
|
async function refreshToken(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/auth/token', { credentials: 'include' })
|
|
|
|
|
if (!res.ok) return false
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
if (data.access_token) {
|
|
|
|
|
setAuthToken(data.access_token)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let refreshPromise: Promise<boolean> | null = null
|
|
|
|
|
|
|
|
|
|
furumiApi.interceptors.response.use(
|
|
|
|
|
(response) => response,
|
|
|
|
|
async (error) => {
|
|
|
|
|
const original = error.config
|
|
|
|
|
if (error.response?.status === 401 && !original._retried) {
|
|
|
|
|
original._retried = true
|
|
|
|
|
// Deduplicate concurrent refresh attempts
|
|
|
|
|
if (!refreshPromise) {
|
|
|
|
|
refreshPromise = refreshToken().finally(() => { refreshPromise = null })
|
|
|
|
|
}
|
|
|
|
|
const ok = await refreshPromise
|
|
|
|
|
if (ok) return furumiApi(original)
|
|
|
|
|
}
|
|
|
|
|
return Promise.reject(error)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-23 15:01:46 +03:00
|
|
|
export async function getArtists(): Promise<Artist[] | null> {
|
|
|
|
|
const res = await furumiApi.get<Artist[]>('/artists').catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 15:01:46 +03:00
|
|
|
export async function getArtistAlbums(artistSlug: string): Promise<Album[] | null> {
|
|
|
|
|
const res = await furumiApi.get<Album[]>(`/artists/${artistSlug}/albums`).catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 15:01:46 +03:00
|
|
|
export async function getAlbumTracks(albumSlug: string): Promise<Track[] | null> {
|
|
|
|
|
const res = await furumiApi.get<Track[]>(`/albums/${albumSlug}`).catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 15:01:46 +03:00
|
|
|
export async function getArtistTracks(artistSlug: string): Promise<Track[] | null> {
|
|
|
|
|
const res = await furumiApi.get<Track[]>(`/artists/${artistSlug}/tracks`).catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 15:01:46 +03:00
|
|
|
export async function searchTracks(query: string): Promise<SearchResult[] | null> {
|
|
|
|
|
const res = await furumiApi
|
|
|
|
|
.get<SearchResult[]>(`/search?q=${encodeURIComponent(query)}`)
|
|
|
|
|
.catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 15:01:46 +03:00
|
|
|
export async function getTrackInfo(trackSlug: string): Promise<TrackDetail | null> {
|
|
|
|
|
const res = await furumiApi.get<TrackDetail>(`/tracks/${trackSlug}`).catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 17:08:54 +01:00
|
|
|
export type RecentPlay = {
|
|
|
|
|
track_slug: string
|
|
|
|
|
track_title: string
|
|
|
|
|
artist_name: string
|
|
|
|
|
album_slug: string | null
|
|
|
|
|
played_at: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getRecentPlays(): Promise<RecentPlay[] | null> {
|
|
|
|
|
const res = await furumiApi.get<RecentPlay[]>('/me/recent').catch(() => null)
|
|
|
|
|
return res?.data ?? null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 16:51:53 +01:00
|
|
|
export async function recordPlay(trackSlug: string): Promise<void> {
|
|
|
|
|
await furumiApi.post(`/tracks/${trackSlug}/play`).catch(() => null)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 12:45:24 +03:00
|
|
|
export async function preloadStream(trackSlug: string) {
|
2026-04-02 00:29:21 +03:00
|
|
|
return await furumiApi.get(`/stream/${trackSlug}`, { responseType: 'blob' }).catch(() => null)
|
2026-03-23 12:45:24 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 15:39:14 +01:00
|
|
|
export async function fetchCoverBlob(trackSlug: string): Promise<string | null> {
|
|
|
|
|
const res = await furumiApi.get(`/tracks/${trackSlug}/cover`, { responseType: 'blob' }).catch(() => null)
|
|
|
|
|
if (!res?.data) return null
|
|
|
|
|
return URL.createObjectURL(res.data)
|
|
|
|
|
}
|
|
|
|
|
|