port desktop's search engine

This commit is contained in:
Boof2015
2026-06-28 09:46:08 -04:00
parent 3ec3f5ecf2
commit dcdddb350d
19 changed files with 2944 additions and 223 deletions
+172
View File
@@ -0,0 +1,172 @@
// Last.fm / scrobbling data model — ported VERBATIM from desktop
// `src/types/lastFm.ts`. Pure types + URL/protocol helpers, no Node deps, so the
// scrobble service (src/services/lastfm) shares the exact same contract as desktop.
export const LASTFM_OFFICIAL_API_BASE_URL = 'https://ws.audioscrobbler.com/2.0/';
export const LASTFM_OFFICIAL_PROFILE_ID = 'official-lastfm';
export type LastFmProfileKind = 'official' | 'custom';
export type LastFmScrobbleProtocol = 'lastfm2' | 'audioscrobbler' | 'listenbrainz';
export interface LastFmPendingScrobble {
id: string;
trackPath: string | null;
track: string;
artist: string;
artistNames?: string[];
album: string | null;
albumArtist: string | null;
durationSeconds: number | null;
timestamp: number;
queuedAt: number;
retryCount: number;
nextRetryAt: number;
}
export interface LastFmProfileConfig {
id: string;
kind: LastFmProfileKind;
protocol: LastFmScrobbleProtocol;
name: string;
apiBaseUrl: string;
enabled: boolean;
sessionKey: string | null;
username: string | null;
pendingScrobbles: LastFmPendingScrobble[];
}
export interface LastFmProfileStatus {
id: string;
kind: LastFmProfileKind;
protocol: LastFmScrobbleProtocol;
protocolLabel: string;
name: string;
apiBaseUrl: string;
enabled: boolean;
username: string | null;
connected: boolean;
active: boolean;
pendingScrobbles: number;
canDelete: boolean;
requiresApiCredentials: boolean;
lastError: string | null;
}
export interface LastFmServiceConfig {
enabled: boolean;
activeProfileId: string;
profiles: LastFmProfileConfig[];
}
export interface LastFmStatus {
enabled: boolean;
connected: boolean;
username: string | null;
apiBaseUrl: string;
usingCustomEndpoint: boolean;
activeProfileId: string;
activeProfile: LastFmProfileStatus;
profiles: LastFmProfileStatus[];
authPending: boolean;
authPendingProfileId: string | null;
pendingScrobbles: number;
hasApiCredentials: boolean;
activeProfileRequiresApiCredentials: boolean;
statusMessage: string;
lastError: string | null;
}
export interface LastFmAuthStartResult {
ok: boolean;
authPending: boolean;
message: string;
authUrl?: string;
}
export interface LastFmAuthFinishResult {
ok: boolean;
connected: boolean;
username: string | null;
message: string;
}
export interface LastFmCustomProfileInput {
protocol?: LastFmScrobbleProtocol;
name: string;
apiBaseUrl: string;
username?: string | null;
sessionKey?: string | null;
}
function parseHttpUrl(value: unknown): URL | null {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
if (!trimmed) return null;
try {
const parsed = new URL(trimmed);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null;
parsed.search = '';
parsed.hash = '';
return parsed;
} catch {
return null;
}
}
export function normalizeLastFmScrobbleProtocol(value: unknown): LastFmScrobbleProtocol {
return value === 'audioscrobbler' || value === 'listenbrainz' ? value : 'lastfm2';
}
export function getLastFmProtocolLabel(
protocol: LastFmScrobbleProtocol,
kind: LastFmProfileKind
): string {
if (kind === 'official') return 'Official Last.fm';
if (protocol === 'audioscrobbler') return 'AudioScrobbler';
if (protocol === 'listenbrainz') return 'ListenBrainz';
return 'Last.fm 2.0';
}
export function lastFmProfileRequiresApiCredentials(
profile: Pick<LastFmProfileConfig, 'kind' | 'protocol'>
): boolean {
return profile.kind === 'official' && profile.protocol === 'lastfm2';
}
export function parseLastFmApiBaseUrl(value: unknown): string | null {
const parsed = parseHttpUrl(value);
if (!parsed) return null;
const normalized = parsed.toString();
if (normalized === 'https://ws.audioscrobbler.com/2.0') {
return LASTFM_OFFICIAL_API_BASE_URL;
}
return normalized;
}
export function normalizeLastFmApiBaseUrl(value: unknown): string {
return parseLastFmApiBaseUrl(value) ?? LASTFM_OFFICIAL_API_BASE_URL;
}
export function isLastFmCustomEndpoint(apiBaseUrl: string): boolean {
return normalizeLastFmApiBaseUrl(apiBaseUrl) !== LASTFM_OFFICIAL_API_BASE_URL;
}
export function parseListenBrainzApiBaseUrl(value: unknown): string | null {
const parsed = parseHttpUrl(value);
if (!parsed) return null;
const submitSuffix = '/1/submit-listens';
const normalizedPath = parsed.pathname.replace(/\/+$/, '');
if (normalizedPath.endsWith(submitSuffix)) {
const basePath = normalizedPath.slice(0, -submitSuffix.length);
parsed.pathname = basePath.length > 0 ? basePath : '/';
}
return parsed.toString();
}
export function normalizeListenBrainzApiBaseUrl(value: unknown): string | null {
return parseListenBrainzApiBaseUrl(value);
}