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