Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ebd20d7
Basic engine idea
Mar 16, 2026
f44e6a5
Audio
Mar 16, 2026
9034992
Library and Storage
Mar 16, 2026
a5e4d58
So much moreeeeeee
Mar 16, 2026
838ffec
A few things that needed to be fixed
Mar 16, 2026
88690dd
more fixes
Mar 16, 2026
ffa6e91
Movement
Mar 17, 2026
b4c5ed6
owwwwwwwww
Mar 19, 2026
4a5abec
More stuff
Mar 20, 2026
59458ae
Helpers go bye byeeeeee (most of em)
Mar 20, 2026
5f488ed
one more fileeee (hook time)
Mar 20, 2026
918be77
make-pretty
Mar 20, 2026
9c81a2d
hooks
Mar 20, 2026
f0d9542
Tiny stuff
Mar 20, 2026
7e2c1d6
more stuff
Mar 20, 2026
0902010
move
Mar 28, 2026
2298d76
bunch of changes
Mar 29, 2026
e269acc
SATORIII
Mar 29, 2026
27df322
bunch of stuff (i forgot to commit this from a week ago oops)
Mar 31, 2026
62e90ea
Some tiny state fixes
Apr 8, 2026
cdae3fd
Gasp i almost deleted this folder I thought I'd commited these
NellowTCS Sep 14, 2026
d72f153
Visualizers
NellowTCS Sep 21, 2026
4fc6545
Cursor
NellowTCS Sep 22, 2026
c5454ae
Various fixes
NellowTCS Sep 23, 2026
c3140f0
Visualizers
NellowTCS Sep 23, 2026
7d50977
Updates
NellowTCS Sep 23, 2026
3e60188
Fix
NellowTCS Sep 23, 2026
bf5721e
Many changes
NellowTCS Sep 25, 2026
b3fa7db
More fixes
NellowTCS Sep 25, 2026
c9042e0
Massive changes to everything
NellowTCS Sep 28, 2026
7a85b8d
Fixes
NellowTCS Sep 29, 2026
54fafed
Review comments and changes
NellowTCS Sep 30, 2026
5a0dc42
Review comments
NellowTCS Oct 1, 2026
d52c8e0
WASM stuff and fixes
NellowTCS Oct 2, 2026
89d56c1
Many changes
NellowTCS Oct 5, 2026
1843396
temporary vendor of music-metadata = CAF working (also the other dura…
NellowTCS Oct 6, 2026
39055db
netlify
NellowTCS Oct 6, 2026
2788279
oop
NellowTCS Oct 6, 2026
4b7279d
Fix CI
NellowTCS Oct 6, 2026
18521ed
Enable everything on safari to see what breaks
NellowTCS Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Gasp i almost deleted this folder I thought I'd commited these
  • Loading branch information
NellowTCS committed Sep 14, 2026
commit cdae3fd9568d9d220f9fbfa3a8345c6e3d1a0aa0
1,449 changes: 745 additions & 704 deletions Build/package-lock.json

Large diffs are not rendered by default.

65 changes: 46 additions & 19 deletions Build/src/core/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class KomorebiEngine {
private currentTime = 0;
private duration = 0;
private currentError: EngineError | null = null;
private currentPlaylist: Playlist | null = null;

private timeUpdateInterval: number | null = null;
private scheduledTransitionId: number | null = null;
Expand Down Expand Up @@ -126,11 +127,25 @@ export class KomorebiEngine {

if (playlist) {
this.queue.setPlaylist(playlist);
this.currentPlaylist = playlist;
} else if (
this.queue.getTracks().findIndex((t) => t.id === track.id) < 0
) {
const solo: Playlist = {
id: `solo:${track.id}`,
name: track.title,
songs: [track],
};
this.queue.setPlaylist(solo);
this.currentPlaylist = solo;
}

const previousTrack = this.queue.getCurrentTrack();

this.queue.setCurrentIndex(
this.queue.getTracks().findIndex((t) => t.id === track.id),
);
this.emitTrackChange(previousTrack, track);

this.stateMachine.transition("loading");
this.currentError = null;
Expand Down Expand Up @@ -353,14 +368,15 @@ export class KomorebiEngine {

setPlaylist(playlist: Playlist): void {
this.queue.setPlaylist(playlist, false);
this.currentPlaylist = playlist;
this.events.emit("queuechange", { queue: this.queue.getState() });
}

getState(): EngineState {
return {
state: this.stateMachine.getState(),
currentTrack: this.queue.getCurrentTrack(),
currentPlaylist: null,
currentPlaylist: this.currentPlaylist,
queue: this.queue.getState(),
settings: { ...this.settings },
currentTime: this.currentTime,
Expand Down Expand Up @@ -422,32 +438,39 @@ export class KomorebiEngine {
timeout = 5000,
): Promise<void> {
return new Promise((resolve, reject) => {
const checkState = () => {
if (this.stateMachine.getState() === targetState) {
resolve();
return true;
}
if (this.stateMachine.hasError()) {
reject(new Error("State transition failed"));
return true;
}
return false;
};

if (checkState()) return;

let settled = false;
const timeoutId = setTimeout(() => {
this.events.off("statechange", stateHandler);
if (settled) return;
settled = true;
this.events.off("statechange", onStateChange);
reject(new Error(`Timeout waiting for state: ${targetState}`));
}, timeout);

const stateHandler = () => {
if (checkState()) {
const onStateChange = () => {
if (settled) return;
if (this.stateMachine.getState() === targetState) {
settled = true;
clearTimeout(timeoutId);
this.events.off("statechange", onStateChange);
resolve();
} else if (this.stateMachine.hasError()) {
settled = true;
clearTimeout(timeoutId);
this.events.off("statechange", onStateChange);
reject(new Error("State transition failed"));
}
};

this.events.on("statechange", stateHandler);
if (this.stateMachine.getState() === targetState) {
onStateChange();
return;
}
if (this.stateMachine.hasError()) {
onStateChange();
return;
}

this.events.on("statechange", onStateChange);
});
}

Expand Down Expand Up @@ -529,6 +552,10 @@ export class KomorebiEngine {
return;
}

if (this.stateMachine.isTransitioning()) {
return;
}

if (this.settings.autoPlayNext) {
this.next();
} else {
Expand Down
13 changes: 13 additions & 0 deletions Build/src/helpers/addSong.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Track } from "../core/engine/types";
import { trackStorage } from "../platform/storage/trackStorage";

export async function prepareAndStoreSong(
song: Track,
file: File,
): Promise<Track> {
song.url = URL.createObjectURL(file);
song.hasStoredAudio = true;
const arrayBuffer = await file.arrayBuffer();
await trackStorage.saveTrack(song, arrayBuffer);
return song;
}
4 changes: 2 additions & 2 deletions Build/src/hooks/useFileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ const logger = createLogger("useFileHandler");

export function useFileHandler(
addSong: (song: Track, file?: File) => Promise<void>,
t: any,
t: (key: string, options?: Record<string, unknown>) => string,
importFiles: (
files: File[],
addSong: (song: Track, file?: File) => Promise<void>,
t: any,
t: (key: string, options?: Record<string, unknown>) => string,
) => Promise<void>,
isInitialized?: boolean,
) {
Expand Down
117 changes: 88 additions & 29 deletions Build/src/hooks/useKomorebi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ const DEFAULT_QUEUE: QueueState = {
shuffleOrder: [],
};

const LOAD_WAIT_TIMEOUT_MS = 5000;

function createInitialState(): EngineState {
return {
state: "idle",
Expand Down Expand Up @@ -125,6 +127,7 @@ export function useKomorebi(
const backendRef = useRef<IAudioBackend | null>(null);
const engineRef = useRef<KomorebiEngine | null>(null);
const libraryRef = useRef<LibraryManager | null>(null);
const playlistsSaveTimerRef = useRef<number | null>(null);
const settingsRef = useRef<SettingsManager | null>(null);
const initializedRef = useRef(false);

Expand Down Expand Up @@ -170,35 +173,34 @@ export function useKomorebi(
setCurrentTime(e.currentTime);
setDuration(e.duration);
};
const handleEnded = async () => {
const currentState = engine.getState();
if (currentState.settings.autoPlayNext) {
await engine.next();
}
};
const handleError = (e: { error: { message: string } }) => {
setError(e.error.message);
};
const handleLoading = () => setIsLoading(true);
const handleReady = () => setIsLoading(false);

engine.on("statechange", handleStateChange);
engine.on("volumechange", handleStateChange);
engine.on("queuechange", handleStateChange);
engine.on("settingschange", handleStateChange);
engine.on("durationchange", handleStateChange);
engine.on("trackchange", handleTrackChange);
engine.on("timeupdate", handleTimeUpdate);
engine.on("ended", handleEnded);
engine.on("error", handleError);
engine.on("loading", handleLoading);
engine.on("ready", handleReady);

const handleLibraryChange = () => {
console.log("[handleLibraryChange] called, songs:", library.getState().songs.length);
setSongs([...library.getState().songs]);
setState(engine.getState());
};
library.on("songadded", handleLibraryChange);
library.on("songremoved", handleLibraryChange);
library.on("songupdated", handleLibraryChange);
library.on("playlistadded", handleLibraryChange);
library.on("playlistremoved", handleLibraryChange);
library.on("playlistupdated", handleLibraryChange);
library.on("playlistsupdated", handleLibraryChange);
library.on("favoritechanged", handleLibraryChange);

const loadLibrary = async () => {
Expand All @@ -209,11 +211,7 @@ export function useKomorebi(
const reconstructed = await trackStorage.reconstructUrl(song);
library.addSong(reconstructed);
}
savedLibrary.playlists.forEach((playlist) => {
if ("songs" in playlist) {
library.addPlaylist(playlist);
}
});
library.seedPlaylists(savedLibrary.playlists);
savedLibrary.favorites.forEach((id) => {
const song = library.getSong(id);
if (song) library.toggleFavorite(id);
Expand All @@ -226,23 +224,28 @@ export function useKomorebi(

loadLibrary().then(() => {
setIsReady(true);
console.log("[loadLibrary] setting songs:", library.getState().songs.length);
setSongs([...library.getState().songs]);
setState(engine.getState());
});

return () => {
engine.off("statechange", handleStateChange);
engine.off("volumechange", handleStateChange);
engine.off("queuechange", handleStateChange);
engine.off("settingschange", handleStateChange);
engine.off("durationchange", handleStateChange);
engine.off("trackchange", handleTrackChange);
engine.off("timeupdate", handleTimeUpdate);
engine.off("ended", handleEnded);
engine.off("error", handleError);
engine.off("loading", handleLoading);
engine.off("ready", handleReady);
library.off("songadded", handleLibraryChange);
library.off("songremoved", handleLibraryChange);
library.off("songupdated", handleLibraryChange);
library.off("playlistadded", handleLibraryChange);
library.off("playlistremoved", handleLibraryChange);
library.off("playlistupdated", handleLibraryChange);
library.off("playlistsupdated", handleLibraryChange);
library.off("favoritechanged", handleLibraryChange);
backendRef.current?.dispose();
};
Expand All @@ -264,6 +267,23 @@ export function useKomorebi(
}
};

const handlePlaylistsChange = () => {
if (playlistsSaveTimerRef.current !== null) {
window.clearTimeout(playlistsSaveTimerRef.current);
}
playlistsSaveTimerRef.current = window.setTimeout(() => {
playlistsSaveTimerRef.current = null;
const current = libraryRef.current;
if (current) {
libraryPersistence
.savePlaylists(current.getState().playlists)
.catch((error: unknown) => {
logger.error("Failed to save playlists:", { error: String(error) });
});
}
}, 300);
};

const library = libraryRef.current;
library.on(
"songadded",
Expand All @@ -277,8 +297,27 @@ export function useKomorebi(
"favoritechanged",
handleFavoritesChange as Parameters<typeof library.on>[1],
);
library.on(
"playlistadded",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
library.on(
"playlistremoved",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
library.on(
"playlistupdated",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
library.on(
"playlistsupdated",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);

return () => {
if (playlistsSaveTimerRef.current !== null) {
window.clearTimeout(playlistsSaveTimerRef.current);
}
library.off(
"songadded",
handleSongAdded as Parameters<typeof library.on>[1],
Expand All @@ -291,6 +330,22 @@ export function useKomorebi(
"favoritechanged",
handleFavoritesChange as Parameters<typeof library.on>[1],
);
library.off(
"playlistadded",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
library.off(
"playlistremoved",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
library.off(
"playlistupdated",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
library.off(
"playlistsupdated",
handlePlaylistsChange as Parameters<typeof library.on>[1],
);
};
}, [options.persistLibrary, isReady]);

Expand Down Expand Up @@ -322,34 +377,39 @@ export function useKomorebi(
}, []);

const playSong = useCallback(async (song: Track, playlist?: Playlist) => {
console.log("[playSong] called", { song: song?.title, songUrl: song?.url, playlist: playlist?.name });
setError(null);
const engine = engineRef.current;
console.log("[playSong] engine:", engine);
if (!engine) {
console.log("[playSong] no engine!");
return;
}
if (!engine) return;

const trackToPlay = await trackStorage.reconstructUrl(song);
console.log("[playSong] reconstructed url:", trackToPlay.url);

if (playlist) {
engine.setPlaylist(playlist);
}
engine.load(trackToPlay, playlist);

await new Promise<void>((resolve) => {
const handleReady = () => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
engine.off("ready", handleReady);
engine.off("error", handleError);
resolve();
};
const timeoutId = setTimeout(finish, LOAD_WAIT_TIMEOUT_MS);
const handleReady = () => finish();
const handleError = () => finish();

engine.on("ready", handleReady);
engine.on("error", handleError);
});

console.log("[playSong] calling engine.play()");
await engine.play();
console.log("[playSong] play() completed");

const playerState = engine.getState().state;
if (playerState === "ready" || playerState === "paused") {
await engine.play();
}
}, []);

const seek = useCallback((time: number) => {
Expand Down Expand Up @@ -396,7 +456,6 @@ export function useKomorebi(
}, []);

const addSong = useCallback((song: Track) => {
console.log("[addSong] called", { songId: song.id, title: song.title });
libraryRef.current?.addSong(song);
}, []);

Expand Down
Loading
Loading