import { BASE_WEBAMP_OPTIONS, cleanBufferOnSkinLoad, closeEqualizer, createM3uPlaylist, enabledMilkdrop, getMetadataProvider, getWebampElement, loadButterchurnPreset, loadMilkdropWhenNeeded, MAIN_WINDOW, PLAYLIST_WINDOW, setSkinData, tracksFromPlaylist, updateWebampPosition, } from "components/apps/Webamp/functions"; import type { SkinData, WebampCI } from "components/apps/Webamp/types"; import useFileDrop from "components/system/Files/FileManager/useFileDrop"; import useWindowActions from "components/system/Window/Titlebar/useWindowActions"; import { useFileSystem } from "contexts/fileSystem"; import { useProcesses } from "contexts/process"; import processDirectory from "contexts/process/directory"; import { useSession } from "contexts/session"; import { basename, dirname, extname } from "path"; import { useCallback, useRef } from "react"; import { AUDIO_PLAYLIST_EXTENSIONS, DESKTOP_PATH, HIGH_PRIORITY_REQUEST, MILLISECONDS_IN_SECOND, SAVE_PATH, TRANSITIONS_IN_MILLISECONDS, } from "utils/constants"; import { haltEvent } from "utils/functions"; import type { Options, Track, URLTrack } from "webamp"; type Webamp = { initWebamp: (containerElement: HTMLDivElement, options: Options) => void; webampCI?: WebampCI; }; const SKIN_DATA_PATH = `${SAVE_PATH}/webampSkinData.json`; const useWebamp = (id: string): Webamp => { const { onClose, onMinimize } = useWindowActions(id); const { setWindowStates, windowStates: { [id]: windowState } = {} } = useSession(); const { position } = windowState || {}; const { linkElement, processes: { [id]: process }, title, } = useProcesses(); const { componentWindow } = process || {}; const webampCI = useRef(); const { createPath, deletePath, exists, readFile, mkdirRecursive, updateFolder, writeFile, } = useFileSystem(); const { onDrop } = useFileDrop({ id }); const metadataProviderRef = useRef(); const windowPositionDebounceRef = useRef(); const initWebamp = useCallback( ( containerElement: HTMLDivElement, { initialSkin, initialTracks }: Options ) => { const handleUrl = async (): Promise => { // eslint-disable-next-line no-alert const externalUrl = prompt( "Enter an Internet location to open here:\nFor example: https://server.com/playlist.m3u" ); if (externalUrl) { const playlistExtension = extname(externalUrl).toLowerCase(); if (AUDIO_PLAYLIST_EXTENSIONS.has(playlistExtension)) { return tracksFromPlaylist( await (await fetch(externalUrl, HIGH_PRIORITY_REQUEST)).text(), playlistExtension ); } return [ { duration: 0, url: externalUrl, }, ]; } return []; }; const webamp = new window.Webamp({ ...BASE_WEBAMP_OPTIONS, handleAddUrlEvent: handleUrl, handleLoadListEvent: handleUrl, handleSaveListEvent: (tracks: URLTrack[]) => { createPath( "playlist.m3u", DESKTOP_PATH, Buffer.from(createM3uPlaylist(tracks)) ).then((saveName) => updateFolder(DESKTOP_PATH, saveName)); }, initialSkin, initialTracks, } as Options) as WebampCI; const setupElements = (): void => { const webampElement = getWebampElement(); if (webampElement) { const mainWindow = webampElement.querySelector(MAIN_WINDOW); const playlistWindow = webampElement.querySelector(PLAYLIST_WINDOW); [mainWindow, playlistWindow].forEach((element) => { element?.addEventListener("drop", onDrop); element?.addEventListener("dragover", haltEvent); }); if (process && !componentWindow && mainWindow) { linkElement(id, "componentWindow", containerElement); linkElement(id, "peekElement", mainWindow); } if (!initialSkin && !process.url?.endsWith(".wsz")) { exists(SKIN_DATA_PATH).then(async (skinExists) => { if (skinExists) { setSkinData( webamp, JSON.parse( (await readFile(SKIN_DATA_PATH)).toString() ) as SkinData ); } }); } containerElement.append(webampElement); } }; const updatePosition = (): void => { window.clearInterval(windowPositionDebounceRef.current); windowPositionDebounceRef.current = window.setTimeout(() => { const mainWindow = getWebampElement()?.querySelector(MAIN_WINDOW); const { x = 0, y = 0 } = mainWindow?.getBoundingClientRect() || {}; setWindowStates((currentWindowStates) => ({ ...currentWindowStates, [id]: { position: { x, y }, }, })); }, TRANSITIONS_IN_MILLISECONDS.WINDOW); }; const subscriptions = [ webamp.onWillClose((cancel) => { cancel(); onClose(); window.setTimeout(() => { subscriptions.forEach((unsubscribe) => unsubscribe()); webamp.close(); }, TRANSITIONS_IN_MILLISECONDS.WINDOW); window.clearInterval(metadataProviderRef.current); window.clearInterval(windowPositionDebounceRef.current); }), webamp.onMinimize(() => onMinimize()), webamp.onTrackDidChange((track) => { const { milkdrop, windows } = webamp.store.getState(); if (windows?.genWindows?.milkdrop?.open && milkdrop?.butterchurn) { loadButterchurnPreset(webamp); } window.clearInterval(metadataProviderRef.current); if (track?.url) { const getMetadata = getMetadataProvider(track.url); if (getMetadata) { const updateTrackInfo = async (): Promise => { const { display: { closed = false } = {}, playlist: { currentTrack = -1 } = {}, tracks, } = webamp.store.getState() || {}; if (closed) { window.clearInterval(metadataProviderRef.current); } else if (tracks[currentTrack]) { const metaData = await getMetadata?.(); if (metaData) { webamp.store.dispatch({ type: "SET_MEDIA_TAGS", ...tracks[currentTrack], ...metaData, }); title(id, `${metaData.artist} - ${metaData.title}`); } } }; updateTrackInfo(); metadataProviderRef.current = window.setInterval( updateTrackInfo, 30 * MILLISECONDS_IN_SECOND ); } else { const { playlist: { currentTrack = -1 } = {}, tracks } = webamp.store.getState() || {}; if (tracks[currentTrack]) { const { artist, title: trackTitle } = tracks[currentTrack]; let newTitle = ""; if (trackTitle && artist) { newTitle = `${artist} - ${trackTitle}`; } else if (trackTitle || artist) { newTitle = trackTitle || artist; } if (newTitle) title(id, newTitle); } } } else { title(id, processDirectory.Webamp.title); } }), webamp._actionEmitter.on("SET_SKIN_DATA", async ({ data }) => { if (!(await exists(SAVE_PATH))) { await mkdirRecursive(SAVE_PATH); updateFolder(dirname(SAVE_PATH)); } writeFile(SKIN_DATA_PATH, JSON.stringify(data), true); updateFolder(SAVE_PATH, basename(SKIN_DATA_PATH)); }), webamp._actionEmitter.on("LOAD_DEFAULT_SKIN", () => { deletePath(SKIN_DATA_PATH); }), webamp._actionEmitter.on("UPDATE_WINDOW_POSITIONS", updatePosition), ]; if (initialSkin) cleanBufferOnSkinLoad(webamp, initialSkin.url); webamp.renderWhenReady(containerElement).then(() => { closeEqualizer(webamp); enabledMilkdrop(webamp); loadMilkdropWhenNeeded(webamp); updateWebampPosition(webamp, position); setupElements(); if (initialTracks) webamp.play(); }); window.WebampGlobal = webamp; webampCI.current = webamp; }, [ componentWindow, createPath, deletePath, exists, id, linkElement, mkdirRecursive, onClose, onDrop, onMinimize, position, process, readFile, setWindowStates, title, updateFolder, writeFile, ] ); return { initWebamp, webampCI: webampCI.current, }; }; export default useWebamp;