Skip to content

Commit 9034992

Browse files
author
NellowTCS
committed
Library and Storage
1 parent f44e6a5 commit 9034992

12 files changed

Lines changed: 1774 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./types";
2+
export * from "./library";
3+
export * from "./persistence";
4+
export * from "./scoring";
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import type { Track, Playlist, PlaylistFolder } from "../../core/engine/types";
2+
import type {
3+
LibraryState,
4+
LibraryEventMap,
5+
LibraryActions,
6+
} from "./types";
7+
8+
type EventCallback<T> = (data: T) => void;
9+
10+
export class LibraryManager implements LibraryActions {
11+
private state: LibraryState = {
12+
songs: [],
13+
playlists: [],
14+
favorites: [],
15+
searchQuery: "",
16+
};
17+
18+
private listeners: Map<keyof LibraryEventMap, Set<EventCallback<unknown>>> = new Map();
19+
20+
constructor(initialState?: Partial<LibraryState>) {
21+
if (initialState) {
22+
this.state = { ...this.state, ...initialState };
23+
}
24+
}
25+
26+
getState(): LibraryState {
27+
return {
28+
...this.state,
29+
songs: [...this.state.songs],
30+
playlists: [...this.state.playlists],
31+
favorites: [...this.state.favorites],
32+
};
33+
}
34+
35+
addSong(song: Track): void {
36+
const exists = this.state.songs.some((s) => s.id === song.id);
37+
if (exists) return;
38+
39+
this.state.songs.push(song);
40+
this.emit("songadded", song);
41+
}
42+
43+
removeSong(songId: string): void {
44+
const index = this.state.songs.findIndex((s) => s.id === songId);
45+
if (index === -1) return;
46+
47+
this.state.songs.splice(index, 1);
48+
this.state.favorites = this.state.favorites.filter((id) => id !== songId);
49+
50+
for (const item of this.state.playlists) {
51+
if ("songs" in item) {
52+
item.songs = item.songs.filter((s) => s.id !== songId);
53+
}
54+
}
55+
56+
this.emit("songremoved", songId);
57+
}
58+
59+
updateSong(songId: string, updates: Partial<Track>): void {
60+
const index = this.state.songs.findIndex((s) => s.id === songId);
61+
if (index === -1) return;
62+
63+
this.state.songs[index] = { ...this.state.songs[index], ...updates };
64+
}
65+
66+
getSong(songId: string): Track | undefined {
67+
return this.state.songs.find((s) => s.id === songId);
68+
}
69+
70+
addPlaylist(playlist: Playlist): void {
71+
const exists = this.state.playlists.some((p) => "id" in p && p.id === playlist.id);
72+
if (exists) return;
73+
74+
this.state.playlists.push(playlist);
75+
this.emit("playlistadded", playlist);
76+
}
77+
78+
removePlaylist(playlistId: string): void {
79+
const index = this.state.playlists.findIndex((p) => "id" in p && p.id === playlistId);
80+
if (index === -1) return;
81+
82+
this.state.playlists.splice(index, 1);
83+
this.emit("playlistremoved", playlistId);
84+
}
85+
86+
updatePlaylist(playlistId: string, updates: Partial<Playlist>): void {
87+
const index = this.state.playlists.findIndex((p) => "id" in p && p.id === playlistId);
88+
if (index === -1) return;
89+
90+
const playlist = this.state.playlists[index];
91+
if ("songs" in playlist) {
92+
this.state.playlists[index] = { ...playlist, ...updates } as Playlist;
93+
}
94+
}
95+
96+
getPlaylist(playlistId: string): Playlist | undefined {
97+
const item = this.state.playlists.find((p) => "id" in p && p.id === playlistId);
98+
if (item && "songs" in item) {
99+
return item;
100+
}
101+
return undefined;
102+
}
103+
104+
addToPlaylist(playlistId: string, song: Track): void {
105+
const playlist = this.getPlaylist(playlistId);
106+
if (!playlist) return;
107+
108+
const exists = playlist.songs.some((s) => s.id === song.id);
109+
if (exists) return;
110+
111+
playlist.songs.push(song);
112+
}
113+
114+
removeFromPlaylist(playlistId: string, songId: string): void {
115+
const playlist = this.getPlaylist(playlistId);
116+
if (!playlist) return;
117+
118+
playlist.songs = playlist.songs.filter((s) => s.id !== songId);
119+
}
120+
121+
reorderPlaylistSongs(playlistId: string, songs: Track[]): void {
122+
const playlist = this.getPlaylist(playlistId);
123+
if (!playlist) return;
124+
125+
playlist.songs = songs;
126+
}
127+
128+
createFolder(name: string): PlaylistFolder {
129+
const folder: PlaylistFolder = {
130+
id: this.generateId(),
131+
name,
132+
children: [],
133+
};
134+
this.state.playlists.push(folder);
135+
return folder;
136+
}
137+
138+
moveToFolder(playlistId: string, folderId: string): void {
139+
const playlistIndex = this.state.playlists.findIndex(
140+
(p) => "id" in p && p.id === playlistId
141+
);
142+
if (playlistIndex === -1) return;
143+
144+
const playlist = this.state.playlists[playlistIndex];
145+
146+
if (folderId === "root") {
147+
return;
148+
}
149+
150+
const folderIndex = this.state.playlists.findIndex(
151+
(p) => "children" in p && "id" in p && p.id === folderId
152+
);
153+
if (folderIndex === -1) return;
154+
155+
const folder = this.state.playlists[folderIndex];
156+
if (!("children" in folder)) return;
157+
158+
folder.children.push(playlist as Playlist);
159+
this.state.playlists.splice(playlistIndex, 1);
160+
}
161+
162+
toggleFavorite(songId: string): void {
163+
const index = this.state.favorites.indexOf(songId);
164+
if (index === -1) {
165+
this.state.favorites.push(songId);
166+
this.emit("favoritechanged", { songId, isFavorite: true });
167+
} else {
168+
this.state.favorites.splice(index, 1);
169+
this.emit("favoritechanged", { songId, isFavorite: false });
170+
}
171+
}
172+
173+
isFavorite(songId: string): boolean {
174+
return this.state.favorites.includes(songId);
175+
}
176+
177+
setSearchQuery(query: string): void {
178+
this.state.searchQuery = query;
179+
}
180+
181+
search(query: string): Track[] {
182+
if (!query.trim()) {
183+
return [...this.state.songs];
184+
}
185+
186+
const lowerQuery = query.toLowerCase();
187+
return this.state.songs.filter(
188+
(song) =>
189+
song.title.toLowerCase().includes(lowerQuery) ||
190+
song.artist.toLowerCase().includes(lowerQuery) ||
191+
song.album.toLowerCase().includes(lowerQuery)
192+
);
193+
}
194+
195+
getSongsByArtist(artist: string): Track[] {
196+
return this.state.songs.filter(
197+
(song) => song.artist.toLowerCase() === artist.toLowerCase()
198+
);
199+
}
200+
201+
getSongsByAlbum(album: string): Track[] {
202+
return this.state.songs.filter(
203+
(song) => song.album.toLowerCase() === album.toLowerCase()
204+
);
205+
}
206+
207+
getFavoriteSongs(): Track[] {
208+
return this.state.songs.filter((song) =>
209+
this.state.favorites.includes(song.id)
210+
);
211+
}
212+
213+
getAllArtists(): string[] {
214+
const artists = new Set<string>();
215+
for (const song of this.state.songs) {
216+
artists.add(song.artist);
217+
}
218+
return Array.from(artists).sort();
219+
}
220+
221+
getAllAlbums(): string[] {
222+
const albums = new Set<string>();
223+
for (const song of this.state.songs) {
224+
albums.add(song.album);
225+
}
226+
return Array.from(albums).sort();
227+
}
228+
229+
clearLibrary(): void {
230+
this.state = {
231+
songs: [],
232+
playlists: [],
233+
favorites: [],
234+
searchQuery: "",
235+
};
236+
this.emit("librarycleared", null);
237+
}
238+
239+
on<K extends keyof LibraryEventMap>(
240+
event: K,
241+
callback: EventCallback<LibraryEventMap[K]>
242+
): void {
243+
if (!this.listeners.has(event)) {
244+
this.listeners.set(event, new Set());
245+
}
246+
this.listeners.get(event)!.add(callback as EventCallback<unknown>);
247+
}
248+
249+
off<K extends keyof LibraryEventMap>(
250+
event: K,
251+
callback: EventCallback<LibraryEventMap[K]>
252+
): void {
253+
const callbacks = this.listeners.get(event);
254+
if (callbacks) {
255+
callbacks.delete(callback as EventCallback<unknown>);
256+
}
257+
}
258+
259+
private emit<K extends keyof LibraryEventMap>(
260+
event: K,
261+
data: LibraryEventMap[K]
262+
): void {
263+
const callbacks = this.listeners.get(event);
264+
if (callbacks) {
265+
callbacks.forEach((cb) => cb(data));
266+
}
267+
}
268+
269+
private generateId(): string {
270+
return `lib-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
271+
}
272+
}

0 commit comments

Comments
 (0)