mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 11:04:00 +00:00
I don't know how we'll feasibly solve the multide of potential race conditons and async clean up memory leaks without something like rxjs. So, this is a trial to see the following: 1. How much does it increase bundle size? 2. How effective is typechecking? 3. Does it solve some of the difficult cancelation issues we have?
336 lines
9.3 KiB
TypeScript
336 lines
9.3 KiB
TypeScript
import { parser, creator } from "winamp-eqf";
|
|
import { BANDS, LOAD_STYLE } from "../constants";
|
|
|
|
import * as Utils from "../utils";
|
|
|
|
import {
|
|
promptForFileReferences,
|
|
genArrayBufferFromFileReference,
|
|
genMediaDuration,
|
|
} from "../fileUtils";
|
|
import skinParser from "../skinParser";
|
|
import {
|
|
getTracks,
|
|
getTrackIsVisibleFunction,
|
|
getEqfData,
|
|
getPlaylistURL,
|
|
} from "../selectors";
|
|
|
|
import {
|
|
ADD_TRACK_FROM_URL,
|
|
PLAY_TRACK,
|
|
BUFFER_TRACK,
|
|
SET_MEDIA_TAGS,
|
|
SET_MEDIA_DURATION,
|
|
SET_SKIN_DATA,
|
|
LOADED,
|
|
LOADING,
|
|
} from "../actionTypes";
|
|
import LoadQueue from "../loadQueue";
|
|
|
|
import { removeAllTracks } from "./playlist";
|
|
import { setPreamp, setEqBand } from "./equalizer";
|
|
import { LoadStyle, Thunk, Action, Track, EqfPreset, SkinData } from "../types";
|
|
|
|
// Lower is better
|
|
const DURATION_VISIBLE_PRIORITY = 5;
|
|
const META_DATA_VISIBLE_PRIORITY = 10;
|
|
const DURATION_PRIORITY = 15;
|
|
const META_DATA_PRIORITY = 20;
|
|
|
|
const loadQueue = new LoadQueue({ threads: 4 });
|
|
|
|
export function addTracksFromReferences(
|
|
fileReferences: FileList,
|
|
loadStyle: LoadStyle,
|
|
atIndex: number | undefined
|
|
): Thunk {
|
|
const tracks: Track[] = Array.from(fileReferences).map(file => ({
|
|
blob: file,
|
|
defaultName: file.name,
|
|
}));
|
|
return loadMediaFiles(tracks, loadStyle, atIndex);
|
|
}
|
|
|
|
const SKIN_FILENAME_MATCHER = new RegExp("(wsz|zip)$", "i");
|
|
const EQF_FILENAME_MATCHER = new RegExp("eqf$", "i");
|
|
export function loadFilesFromReferences(
|
|
fileReferences: FileList,
|
|
loadStyle: LoadStyle = LOAD_STYLE.PLAY,
|
|
atIndex: number | undefined = undefined
|
|
): Thunk {
|
|
return dispatch => {
|
|
if (fileReferences.length < 1) {
|
|
return;
|
|
} else if (fileReferences.length === 1) {
|
|
const fileReference = fileReferences[0];
|
|
if (SKIN_FILENAME_MATCHER.test(fileReference.name)) {
|
|
dispatch(setSkinFromBlob(fileReference));
|
|
return;
|
|
} else if (EQF_FILENAME_MATCHER.test(fileReference.name)) {
|
|
dispatch(setEqFromFileReference(fileReference));
|
|
return;
|
|
}
|
|
}
|
|
dispatch(addTracksFromReferences(fileReferences, loadStyle, atIndex));
|
|
};
|
|
}
|
|
|
|
export function setSkinFromBlob(blob: Blob | Promise<Blob>): Thunk {
|
|
return async (dispatch, getState, { requireJSZip }) => {
|
|
if (!requireJSZip) {
|
|
alert("Webamp has not been configured to support custom skins.");
|
|
return;
|
|
}
|
|
dispatch({ type: LOADING });
|
|
let JSZip;
|
|
try {
|
|
JSZip = await requireJSZip();
|
|
} catch (e) {
|
|
console.error(e);
|
|
dispatch({ type: LOADED });
|
|
alert("Failed to load the skin parser.");
|
|
return;
|
|
}
|
|
try {
|
|
const skinData = await skinParser(blob, JSZip);
|
|
dispatch({
|
|
type: SET_SKIN_DATA,
|
|
data: {
|
|
skinImages: skinData.images,
|
|
skinColors: skinData.colors,
|
|
skinPlaylistStyle: skinData.playlistStyle,
|
|
skinCursors: skinData.cursors,
|
|
skinRegion: skinData.region,
|
|
skinGenLetterWidths: skinData.genLetterWidths,
|
|
skinGenExColors: skinData.genExColors,
|
|
} as SkinData,
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
dispatch({ type: LOADED });
|
|
alert(`Failed to parse skin`);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function setSkinFromUrl(url: string): Thunk {
|
|
return async dispatch => {
|
|
dispatch({ type: LOADING });
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(response.statusText);
|
|
}
|
|
dispatch(setSkinFromBlob(response.blob()));
|
|
} catch (e) {
|
|
console.error(e);
|
|
dispatch({ type: LOADED });
|
|
alert(`Failed to download skin from ${url}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
// This function is private, since Winamp consumers can provide means for
|
|
// opening files via other methods. Only use the file type specific
|
|
// versions below, since they can defer to the user-defined behavior.
|
|
function _openFileDialog(accept: string | null): Thunk {
|
|
return async dispatch => {
|
|
const fileReferences = await promptForFileReferences({ accept });
|
|
dispatch(loadFilesFromReferences(fileReferences));
|
|
};
|
|
}
|
|
|
|
export function openEqfFileDialog(): Thunk {
|
|
return _openFileDialog(".eqf");
|
|
}
|
|
|
|
export function openMediaFileDialog(): Thunk {
|
|
return _openFileDialog(null);
|
|
}
|
|
|
|
export function openSkinFileDialog() {
|
|
return _openFileDialog(".zip, .wsz");
|
|
}
|
|
|
|
export function fetchMediaDuration(url: string, id: number): Thunk {
|
|
return (dispatch, getState) => {
|
|
loadQueue.push(
|
|
async () => {
|
|
try {
|
|
const duration = await genMediaDuration(url);
|
|
dispatch({ type: SET_MEDIA_DURATION, duration, id });
|
|
} catch (e) {
|
|
// TODO: Should we update the state to indicate that we don't know the length?
|
|
}
|
|
},
|
|
|
|
() => {
|
|
const trackIsVisible = getTrackIsVisibleFunction(getState());
|
|
return trackIsVisible(id)
|
|
? DURATION_VISIBLE_PRIORITY
|
|
: DURATION_PRIORITY;
|
|
}
|
|
);
|
|
};
|
|
}
|
|
|
|
export function loadMedia(
|
|
e: React.DragEvent<HTMLDivElement>,
|
|
loadStyle: LoadStyle = LOAD_STYLE.NONE,
|
|
atIndex = 0
|
|
): Thunk {
|
|
const { files } = e.dataTransfer;
|
|
return async (dispatch, getState, { handleTrackDropEvent }) => {
|
|
if (handleTrackDropEvent) {
|
|
const tracks = await handleTrackDropEvent(e);
|
|
|
|
if (tracks != null) {
|
|
dispatch(loadMediaFiles(tracks, loadStyle, atIndex));
|
|
return;
|
|
}
|
|
}
|
|
dispatch(loadFilesFromReferences(files, loadStyle, atIndex));
|
|
};
|
|
}
|
|
|
|
export function loadMediaFiles(
|
|
tracks: Track[],
|
|
loadStyle: LoadStyle = LOAD_STYLE.NONE,
|
|
atIndex = 0
|
|
): Thunk {
|
|
return dispatch => {
|
|
if (loadStyle === LOAD_STYLE.PLAY) {
|
|
// I'm the worst. It just so happens that in every case that we autoPlay,
|
|
// we should also clear all tracks.
|
|
dispatch(removeAllTracks());
|
|
}
|
|
tracks.forEach((track, i) => {
|
|
const priority = i === 0 ? loadStyle : LOAD_STYLE.NONE;
|
|
dispatch(loadMediaFile(track, priority, atIndex + i));
|
|
});
|
|
};
|
|
}
|
|
|
|
export function loadMediaFile(
|
|
track: Track,
|
|
priority: LoadStyle = LOAD_STYLE.NONE,
|
|
atIndex = 0
|
|
): Thunk {
|
|
return dispatch => {
|
|
const id = Utils.uniqueId();
|
|
const { defaultName, metaData, duration } = track;
|
|
let canonicalUrl: string;
|
|
if ("url" in track) {
|
|
canonicalUrl = track.url.toString();
|
|
} else if ("blob" in track) {
|
|
canonicalUrl = URL.createObjectURL(track.blob);
|
|
} else {
|
|
throw new Error("Expected track to have either a blob or a url");
|
|
}
|
|
|
|
dispatch({
|
|
type: ADD_TRACK_FROM_URL,
|
|
url: canonicalUrl,
|
|
duration: track.duration,
|
|
defaultName,
|
|
id,
|
|
atIndex,
|
|
});
|
|
switch (priority) {
|
|
case LOAD_STYLE.BUFFER:
|
|
dispatch({ type: BUFFER_TRACK, id });
|
|
break;
|
|
case LOAD_STYLE.PLAY:
|
|
dispatch({ type: PLAY_TRACK, id });
|
|
break;
|
|
case LOAD_STYLE.NONE:
|
|
default:
|
|
// If we're not going to load this right away,
|
|
// we should set duration on our own
|
|
if (duration != null) {
|
|
dispatch({ type: SET_MEDIA_DURATION, duration, id });
|
|
} else {
|
|
dispatch(fetchMediaDuration(canonicalUrl, id));
|
|
}
|
|
}
|
|
|
|
if (metaData != null) {
|
|
const { artist, title, album } = metaData;
|
|
dispatch({
|
|
type: SET_MEDIA_TAGS,
|
|
artist,
|
|
title,
|
|
album,
|
|
// For now, we lie about these next three things.
|
|
// TODO: Ideally we would leave these as null and force a media data
|
|
// fetch when the user starts playing.
|
|
sampleRate: 44000,
|
|
bitrate: 192000,
|
|
numberOfChannels: 2,
|
|
id,
|
|
});
|
|
} else if ("blob" in track) {
|
|
// Blobs can be loaded quickly
|
|
dispatch(fetchMediaTags(track.blob, id));
|
|
} else {
|
|
dispatch(queueFetchingMediaTags(id));
|
|
}
|
|
};
|
|
}
|
|
|
|
function queueFetchingMediaTags(id: number): Thunk {
|
|
return (dispatch, getState) => {
|
|
const track = getTracks(getState())[id];
|
|
loadQueue.push(
|
|
() => dispatch(fetchMediaTags(track.url, id)),
|
|
() => {
|
|
const trackIsVisible = getTrackIsVisibleFunction(getState());
|
|
return trackIsVisible(id)
|
|
? META_DATA_VISIBLE_PRIORITY
|
|
: META_DATA_PRIORITY;
|
|
}
|
|
);
|
|
};
|
|
}
|
|
|
|
export function fetchMediaTags(file: string | Blob, id: number): Action {
|
|
return { type: "FETCH_MEDIA_TAGS", file, id };
|
|
}
|
|
|
|
export function setEqFromFileReference(fileReference: File): Thunk {
|
|
return async dispatch => {
|
|
const arrayBuffer = await genArrayBufferFromFileReference(fileReference);
|
|
const eqf = parser(arrayBuffer);
|
|
const preset: EqfPreset = eqf.presets[0];
|
|
dispatch(setEqFromObject(preset));
|
|
};
|
|
}
|
|
|
|
export function setEqFromObject(preset: EqfPreset): Thunk {
|
|
return dispatch => {
|
|
dispatch(setPreamp(Utils.normalizeEqBand(preset.preamp)));
|
|
BANDS.forEach(band => {
|
|
// @ts-ignore band and EqfPreset align
|
|
dispatch(setEqBand(band, Utils.normalizeEqBand(preset[`hz${band}`])));
|
|
});
|
|
};
|
|
}
|
|
|
|
export function downloadPreset(): Thunk {
|
|
return (dispatch, getState) => {
|
|
const state = getState();
|
|
const data = getEqfData(state);
|
|
const arrayBuffer = creator(data);
|
|
const base64 = Utils.base64FromArrayBuffer(arrayBuffer);
|
|
const dataURI = `data:application/zip;base64,${base64}`;
|
|
Utils.downloadURI(dataURI, "entry.eqf");
|
|
};
|
|
}
|
|
|
|
export function downloadHtmlPlaylist(): Thunk {
|
|
return (dispatch, getState) => {
|
|
const uri = getPlaylistURL(getState());
|
|
Utils.downloadURI(uri, "Winamp Playlist.html");
|
|
};
|
|
}
|