openplugdj 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,337 @@
1
+ # OpenPlugDJ
2
+
3
+ [![npm version](https://img.shields.io/npm/v/openplugdj.svg)](https://www.npmjs.com/package/openplugdj)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A lightweight, event-driven DJ queue and room engine inspired by plug.dj. Build collaborative music listening experiences with real-time queue management, voting, and multiple room support.
7
+
8
+ ## Features
9
+
10
+ - ðŸŽĩ **Queue Management** - Add, remove, and reorder tracks in a DJ queue
11
+ - 🏠 **Multi-Room Support** - Create and manage multiple music rooms
12
+ - ðŸ‘Ĩ **User Management** - Handle users with different roles (listener, DJ, admin)
13
+ - ðŸ—ģïļ **Voting System** - Built-in upvote/downvote functionality for tracks
14
+ - ðŸ“Ķ **Flexible Storage** - Memory or Redis-backed storage adapters
15
+ - 🔌 **Event-Driven** - React to track changes, votes, and queue updates
16
+ - ðŸŽŊ **TypeScript First** - Full type safety and IntelliSense support
17
+ - ðŸŠķ **Lightweight** - Minimal dependencies, maximum performance
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install openplugdj
23
+ ```
24
+
25
+ For Redis support:
26
+ ```bash
27
+ npm install openplugdj ioredis
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```typescript
33
+ import { DJ, Room, Track, DJEntry } from "openplugdj";
34
+
35
+ // Create a custom stream service (implement your audio logic)
36
+ const streamService = {
37
+ async play(track: Track) {
38
+ console.log(`Playing: ${track.title}`);
39
+ // Your audio streaming logic here
40
+ await new Promise(resolve => setTimeout(resolve, track.duration));
41
+ },
42
+ async stop() {
43
+ console.log("Stopped playback");
44
+ }
45
+ };
46
+
47
+ // Initialize the DJ engine
48
+ const dj = new DJ(streamService);
49
+
50
+ // Create a room
51
+ const room = dj.createRoom("room-1");
52
+
53
+ // Add a user
54
+ room.addUser({
55
+ id: "user-1",
56
+ username: "DJ Cool",
57
+ role: "dj",
58
+ joinedAt: Date.now()
59
+ });
60
+
61
+ // Add a track to the queue
62
+ const entry: DJEntry = {
63
+ id: "entry-1",
64
+ track: {
65
+ id: "track-1",
66
+ title: "Awesome Song",
67
+ url: "https://example.com/song.mp3",
68
+ duration: 180000, // 3 minutes in milliseconds
69
+ addedBy: "user-1",
70
+ createdAt: Date.now()
71
+ },
72
+ userId: "user-1",
73
+ position: 0,
74
+ createdAt: Date.now()
75
+ };
76
+
77
+ room.addEntry(entry);
78
+
79
+ // Listen to events
80
+ room.on("trackStart", (track) => {
81
+ console.log(`Now playing: ${track.title}`);
82
+ });
83
+
84
+ room.on("trackEnd", (track) => {
85
+ console.log(`Finished: ${track.title}`);
86
+ });
87
+
88
+ room.on("voteUpdated", (votes) => {
89
+ console.log(`Votes: ${votes.up} up, ${votes.down} down`);
90
+ });
91
+
92
+ // Start playback
93
+ room.start();
94
+ ```
95
+
96
+ ## Core Concepts
97
+
98
+ ### DJ
99
+
100
+ The main engine that manages multiple rooms.
101
+
102
+ ```typescript
103
+ import { DJ } from "openplugdj";
104
+
105
+ const dj = new DJ(streamService);
106
+
107
+ // Create rooms
108
+ const room1 = dj.createRoom("rock-room");
109
+ const room2 = dj.createRoom("jazz-room");
110
+
111
+ // Get a room
112
+ const room = dj.getRoom("rock-room");
113
+
114
+ // Delete a room
115
+ dj.deleteRoom("rock-room");
116
+
117
+ // Listen to room events
118
+ dj.on("roomCreated", (room) => {
119
+ console.log(`Room created: ${room.id}`);
120
+ });
121
+ ```
122
+
123
+ ### Room
124
+
125
+ A room contains a queue, player, and manages users.
126
+
127
+ ```typescript
128
+ // Add users
129
+ room.addUser(user);
130
+ room.removeUser(userId);
131
+
132
+ // Access the queue
133
+ room.queue.add(entry);
134
+ room.queue.remove(entryId);
135
+ room.queue.reorder(0, 2); // Move from index 0 to index 2
136
+ const allEntries = room.queue.getAll();
137
+
138
+ // Player controls
139
+ room.player.skip(); // Skip current track
140
+ room.player.vote(userId, "up"); // Vote on current track
141
+ const current = room.player.getCurrent();
142
+
143
+ // Events
144
+ room.on("userJoined", (user) => {});
145
+ room.on("userLeft", (user) => {});
146
+ room.on("queueUpdated", (entries) => {});
147
+ room.on("trackStart", (track) => {});
148
+ room.on("trackEnd", (track) => {});
149
+ room.on("voteUpdated", (results) => {});
150
+ room.on("queueEmpty", () => {});
151
+ ```
152
+
153
+ ### StreamService
154
+
155
+ Implement this interface to connect your audio streaming logic:
156
+
157
+ ```typescript
158
+ import { StreamService, Track } from "openplugdj";
159
+
160
+ class MyStreamService implements StreamService {
161
+ async play(track: Track): Promise<void> {
162
+ // Start streaming the track
163
+ // Wait for track.duration milliseconds or until stop() is called
164
+ }
165
+
166
+ async stop(): Promise<void> {
167
+ // Stop current playback
168
+ }
169
+ }
170
+ ```
171
+
172
+ ### Storage Adapters
173
+
174
+ Choose between in-memory or Redis storage:
175
+
176
+ ```typescript
177
+ import { MemoryStore, RedisStore } from "openplugdj";
178
+
179
+ // In-memory (default)
180
+ const memoryStore = new MemoryStore<Room>();
181
+
182
+ // Redis
183
+ const redisStore = new RedisStore<Room>(
184
+ "redis://localhost:6379",
185
+ "rooms" // namespace
186
+ );
187
+
188
+ // Use with custom storage
189
+ await redisStore.set("room-1", room);
190
+ const room = await redisStore.get("room-1");
191
+ ```
192
+
193
+ ## API Reference
194
+
195
+ ### Types
196
+
197
+ #### Track
198
+ ```typescript
199
+ interface Track {
200
+ id: string;
201
+ title: string;
202
+ url: string;
203
+ duration: number; // milliseconds
204
+ addedBy: string;
205
+ createdAt: number;
206
+ }
207
+ ```
208
+
209
+ #### User
210
+ ```typescript
211
+ type UserRole = "listener" | "dj" | "admin";
212
+
213
+ interface User {
214
+ id: string;
215
+ username: string;
216
+ role: UserRole;
217
+ joinedAt: number;
218
+ }
219
+ ```
220
+
221
+ #### DJEntry
222
+ ```typescript
223
+ interface DJEntry {
224
+ id: string;
225
+ track: Track;
226
+ userId: string;
227
+ position: number;
228
+ createdAt: number;
229
+ }
230
+ ```
231
+
232
+ ## Advanced Usage
233
+
234
+ ### Using Redis for Persistence
235
+
236
+ ```typescript
237
+ import { DJ, RedisStore, Room } from "openplugdj";
238
+
239
+ const dj = new DJ(streamService);
240
+ const roomStore = new RedisStore<Room>("redis://localhost:6379", "rooms");
241
+
242
+ // Your rooms persist across restarts
243
+ const room = dj.createRoom("persistent-room");
244
+ await roomStore.set(room.id, room);
245
+ ```
246
+
247
+ ### Vote Management
248
+
249
+ ```typescript
250
+ // Users can vote on the current track
251
+ room.player.vote("user-1", "up");
252
+ room.player.vote("user-2", "down");
253
+
254
+ room.on("voteUpdated", (results) => {
255
+ console.log(`Score: ${results.up - results.down}`);
256
+
257
+ // Auto-skip if too many downvotes
258
+ if (results.down > 5) {
259
+ room.player.skip();
260
+ }
261
+ });
262
+ ```
263
+
264
+ ### Queue Management
265
+
266
+ ```typescript
267
+ // Add multiple tracks
268
+ const entries = [entry1, entry2, entry3];
269
+ entries.forEach(entry => room.queue.add(entry));
270
+
271
+ // Reorder queue (drag and drop)
272
+ room.queue.reorder(0, 2);
273
+
274
+ // Remove a track
275
+ room.queue.remove(entryId);
276
+
277
+ // Clear entire queue
278
+ room.queue.clear();
279
+
280
+ // Get queue info
281
+ console.log(`Queue size: ${room.queue.size()}`);
282
+ ```
283
+
284
+ ## Examples
285
+
286
+ Check out these example implementations:
287
+
288
+ - **Simple Bot** - Basic DJ bot with preset playlist
289
+ - **Web Server** - Express + Socket.IO integration
290
+ - **Discord Bot** - Voice channel music bot
291
+ - **REST API** - Full-featured API backend
292
+
293
+ See the `/examples` directory in the repository.
294
+
295
+ ## Contributing
296
+
297
+ Contributions are welcome! Please feel free to submit a Pull Request.
298
+
299
+ 1. Fork the repository
300
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
301
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
302
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
303
+ 5. Open a Pull Request
304
+
305
+ ## License
306
+
307
+ MIT ÂĐ [bulondra](https://github.com/bulondra)
308
+
309
+ ## Links
310
+
311
+ - [GitHub Repository](https://github.com/bulondra/openplugdj)
312
+ - [Issue Tracker](https://github.com/bulondra/openplugdj/issues)
313
+ - [npm Package](https://www.npmjs.com/package/openplugdj)
314
+
315
+ ## Support
316
+
317
+ If you find this project helpful, please consider giving it a ⭐ on GitHub!
318
+
319
+ ### Error Handling
320
+
321
+ All errors are emitted via the `error` event:
322
+
323
+ ```typescript
324
+ room.on("error", (error) => {
325
+ console.error(`[${error.type}]:`, error.error.message);
326
+
327
+ if (error.track) {
328
+ console.error(`Track: ${error.track.title}`);
329
+ }
330
+
331
+ // Error types:
332
+ // - "stream": Audio streaming failed
333
+ // - "vote": Voting failed
334
+ // - "skip": Skip operation failed
335
+ // - "metadata": Metadata fetching failed
336
+ // - "storage": Storage operation failed
337
+ });
@@ -8,9 +8,11 @@ export declare class Player extends EventEmitter {
8
8
  private current;
9
9
  private voteService;
10
10
  private playing;
11
+ private abortController;
11
12
  constructor(queue: Queue, streamService: StreamService);
12
13
  playNext(): Promise<void>;
13
14
  vote(userId: string, type: "up" | "down"): void;
14
- skip(): void;
15
+ skip(): Promise<void>;
15
16
  getCurrent(): DJEntry | null;
17
+ isPlaying(): boolean;
16
18
  }
@@ -11,6 +11,7 @@ class Player extends events_1.EventEmitter {
11
11
  this.current = null;
12
12
  this.voteService = new VoteService_1.VoteService();
13
13
  this.playing = false;
14
+ this.abortController = null;
14
15
  }
15
16
  async playNext() {
16
17
  if (this.playing)
@@ -23,23 +24,68 @@ class Player extends events_1.EventEmitter {
23
24
  this.current = entry;
24
25
  this.voteService.clear();
25
26
  this.playing = true;
27
+ this.abortController = new AbortController();
26
28
  this.emit("trackStart", entry.track);
27
- await this.streamService.play(entry.track);
28
- this.emit("trackEnd", entry.track);
29
- this.playing = false;
29
+ try {
30
+ await this.streamService.play(entry.track);
31
+ // Check if we were cancelled during playback
32
+ if (this.abortController.signal.aborted) {
33
+ return;
34
+ }
35
+ this.emit("trackEnd", entry.track);
36
+ }
37
+ catch (error) {
38
+ this.emit("error", {
39
+ type: "stream",
40
+ track: entry.track,
41
+ error: error instanceof Error ? error : new Error(String(error))
42
+ });
43
+ }
44
+ finally {
45
+ this.playing = false;
46
+ this.abortController = null;
47
+ }
48
+ // Continue to next track (recursive call happens after cleanup)
30
49
  this.playNext();
31
50
  }
32
51
  vote(userId, type) {
33
- this.voteService.vote(userId, type);
34
- this.emit("voteUpdated", this.voteService.getResults());
52
+ try {
53
+ this.voteService.vote(userId, type);
54
+ this.emit("voteUpdated", this.voteService.getResults());
55
+ }
56
+ catch (error) {
57
+ this.emit("error", {
58
+ type: "vote",
59
+ error: error instanceof Error ? error : new Error(String(error))
60
+ });
61
+ }
35
62
  }
36
- skip() {
37
- this.streamService.stop();
63
+ async skip() {
64
+ if (!this.playing)
65
+ return;
66
+ // Signal cancellation
67
+ if (this.abortController) {
68
+ this.abortController.abort();
69
+ }
70
+ try {
71
+ await this.streamService.stop();
72
+ }
73
+ catch (error) {
74
+ this.emit("error", {
75
+ type: "skip",
76
+ error: error instanceof Error ? error : new Error(String(error))
77
+ });
78
+ }
38
79
  this.playing = false;
80
+ this.abortController = null;
81
+ // Continue to next track
39
82
  this.playNext();
40
83
  }
41
84
  getCurrent() {
42
85
  return this.current;
43
86
  }
87
+ isPlaying() {
88
+ return this.playing;
89
+ }
44
90
  }
45
91
  exports.Player = Player;
@@ -15,6 +15,16 @@ class Queue {
15
15
  this.entries = this.entries.filter(e => e.id !== entryId);
16
16
  }
17
17
  reorder(from, to) {
18
+ // Validate bounds
19
+ if (from < 0 || from >= this.entries.length) {
20
+ throw new Error(`Invalid 'from' index: ${from}. Queue length: ${this.entries.length}`);
21
+ }
22
+ if (to < 0 || to >= this.entries.length) {
23
+ throw new Error(`Invalid 'to' index: ${to}. Queue length: ${this.entries.length}`);
24
+ }
25
+ if (from === to) {
26
+ return; // No-op
27
+ }
18
28
  const [item] = this.entries.splice(from, 1);
19
29
  this.entries.splice(to, 0, item);
20
30
  }
@@ -14,4 +14,5 @@ export declare class Room extends EventEmitter {
14
14
  removeUser(userId: string): void;
15
15
  addEntry(entry: DJEntry): void;
16
16
  start(): void;
17
+ getUsers(): User[];
17
18
  }
package/dist/core/Room.js CHANGED
@@ -15,6 +15,7 @@ class Room extends events_1.EventEmitter {
15
15
  this.player.on("trackEnd", t => this.emit("trackEnd", t));
16
16
  this.player.on("voteUpdated", v => this.emit("voteUpdated", v));
17
17
  this.player.on("queueEmpty", () => this.emit("queueEmpty"));
18
+ this.player.on("error", e => this.emit("error", e));
18
19
  }
19
20
  addUser(user) {
20
21
  this.users.set(user.id, user);
@@ -28,11 +29,22 @@ class Room extends events_1.EventEmitter {
28
29
  this.emit("userLeft", user);
29
30
  }
30
31
  addEntry(entry) {
31
- this.queue.add(entry);
32
- this.emit("queueUpdated", this.queue.getAll());
32
+ try {
33
+ this.queue.add(entry);
34
+ this.emit("queueUpdated", this.queue.getAll());
35
+ }
36
+ catch (error) {
37
+ this.emit("error", {
38
+ type: "storage",
39
+ error: error instanceof Error ? error : new Error(String(error))
40
+ });
41
+ }
33
42
  }
34
43
  start() {
35
44
  this.player.playNext();
36
45
  }
46
+ getUsers() {
47
+ return Array.from(this.users.values());
48
+ }
37
49
  }
38
50
  exports.Room = Room;
@@ -10,6 +10,12 @@ export interface RoomEvents {
10
10
  voteUpdated: (votes: {
11
11
  up: number;
12
12
  down: number;
13
+ total: number;
13
14
  }) => void;
14
15
  queueEmpty: () => void;
16
+ error: (error: {
17
+ type: "stream" | "vote" | "skip" | "metadata" | "storage";
18
+ error: Error;
19
+ track?: Track;
20
+ }) => void;
15
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openplugdj",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "A lightweight DJ queue and room engine inspired by plug.dj",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",