soulsync/webui/src/platform/workflows/album-workflows.ts
Antti Kettunen 577e4bdace
Migrate issue domain to React
- Mount a React-owned issue domain host and bridge report issue actions through it
- Add typed issue creation helpers, report payload types, and shared album workflow launchers
- Expand issue detail UI parity with metadata, links, track details, and admin actions
- Remove legacy static issue modal/list/detail code and update tests for the React bridge
2026-05-13 22:26:20 +03:00

194 lines
5.6 KiB
TypeScript

import { apiClient } from '@/app/api-client';
export interface AlbumWorkflowLaunchInput {
spotifyAlbumId?: string;
artistName?: string;
albumName?: string;
source?: string;
}
export interface DownloadMissingAlbumWorkflowInput {
virtualPlaylistId: string;
playlistName: string;
tracks: Array<Record<string, unknown>>;
album: Record<string, unknown>;
artist: Record<string, unknown>;
albumType: string;
forceDownload: boolean;
registerDownload?: boolean;
}
export interface WishlistAlbumWorkflowInput {
tracks: Array<Record<string, unknown>>;
album: Record<string, unknown>;
artist: Record<string, unknown>;
albumType: string;
}
interface AlbumSearchResult {
id?: string;
name?: string;
title?: string;
artist?: string;
}
interface AlbumApiResponse {
id?: string;
name?: string;
album_type?: string;
images?: Array<{ url?: string }>;
image_url?: string | null;
release_date?: string;
total_tracks?: number;
artists?: Array<{ id?: string | null; name?: string }>;
tracks?: Array<Record<string, unknown>>;
}
interface EnhancedSearchResponse {
spotify_albums?: AlbumSearchResult[];
itunes_albums?: AlbumSearchResult[];
}
function getWorkflowBridge() {
const bridge = window.SoulSyncWorkflowActions;
if (!bridge) {
throw new Error('Album workflow host is not ready yet');
}
return bridge;
}
function notify(message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') {
if (window.SoulSyncWorkflowActions?.notify) {
window.SoulSyncWorkflowActions.notify(message, type);
return;
}
window.showToast?.(message, type);
}
async function searchAlbum(input: AlbumWorkflowLaunchInput): Promise<AlbumSearchResult> {
const query = `${input.artistName || ''} ${input.albumName || ''}`.trim();
if (!query) {
throw new Error('No album ID or artist/album info available');
}
const searchData =
(await apiClient
.post('enhanced-search', {
json: { query },
})
.json<EnhancedSearchResponse>()) ?? {};
const foundAlbum = searchData.spotify_albums?.[0] ?? searchData.itunes_albums?.[0];
if (!foundAlbum?.id) {
throw new Error(
`Could not find "${input.albumName || 'album'}" by ${input.artistName || 'unknown artist'}`,
);
}
return foundAlbum;
}
async function fetchAlbum(input: AlbumWorkflowLaunchInput): Promise<AlbumApiResponse> {
let albumId = input.spotifyAlbumId || '';
let albumName = input.albumName || '';
let artistName = input.artistName || '';
if (!albumId) {
const foundAlbum = await searchAlbum(input);
albumId = foundAlbum.id || '';
albumName = foundAlbum.name || foundAlbum.title || albumName;
artistName = foundAlbum.artist || artistName;
}
const searchParams = new URLSearchParams({ name: albumName, artist: artistName });
try {
return (
(await apiClient
.get(`spotify/album/${encodeURIComponent(albumId)}`, { searchParams })
.json<AlbumApiResponse>()) ?? {}
);
} catch (error) {
if (!input.spotifyAlbumId || (!input.artistName && !input.albumName)) {
throw error;
}
const foundAlbum = await searchAlbum(input);
const fallbackParams = new URLSearchParams({
name: foundAlbum.name || foundAlbum.title || albumName,
artist: foundAlbum.artist || artistName,
});
return (
(await apiClient
.get(`spotify/album/${encodeURIComponent(foundAlbum.id || '')}`, {
searchParams: fallbackParams,
})
.json<AlbumApiResponse>()) ?? {}
);
}
}
async function resolveAlbumWorkflowData(input: AlbumWorkflowLaunchInput) {
const albumData = await fetchAlbum(input);
if (!albumData.tracks?.length) {
throw new Error(`No tracks available for "${input.albumName || albumData.name || 'album'}"`);
}
const albumArtists = albumData.artists?.length
? albumData.artists
: [{ name: input.artistName || 'Unknown Artist' }];
const artistName = input.artistName || albumArtists[0]?.name || 'Unknown Artist';
const albumType = albumData.album_type || 'album';
const album = {
name: albumData.name || input.albumName || 'Unknown Album',
id: albumData.id || input.spotifyAlbumId || '',
album_type: albumType,
images: albumData.images || [],
image_url: albumData.image_url || albumData.images?.[0]?.url || null,
release_date: albumData.release_date,
total_tracks: albumData.total_tracks,
artists: albumArtists,
};
const tracks = albumData.tracks.map((track) => ({
...track,
artists: albumArtists,
album,
}));
return {
album,
albumType,
artist: { id: `workflow_${artistName}`, name: artistName, image_url: '' },
artistName,
tracks,
};
}
export async function launchAlbumDownloadWorkflow(input: AlbumWorkflowLaunchInput) {
const bridge = getWorkflowBridge();
const { album, albumType, artist, artistName, tracks } = await resolveAlbumWorkflowData(input);
const resolvedAlbumId = String(album.id || input.spotifyAlbumId || Date.now());
const source = input.source || 'album';
await bridge.openDownloadMissingAlbum({
virtualPlaylistId: `${source}_download_${resolvedAlbumId}`,
playlistName: `[${artistName}] ${String(album.name || 'Unknown Album')}`,
tracks,
album,
artist,
albumType,
forceDownload: true,
registerDownload: true,
});
}
export async function launchAlbumWishlistWorkflow(input: AlbumWorkflowLaunchInput) {
const bridge = getWorkflowBridge();
const { album, albumType, artist, tracks } = await resolveAlbumWorkflowData(input);
await bridge.openAddToWishlistAlbum({
album,
artist,
tracks,
albumType,
});
notify('Wishlist workflow opened', 'success');
}