openplugdj 1.0.1 â 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 +337 -0
- package/dist/core/Player.d.ts +3 -1
- package/dist/core/Player.js +53 -7
- package/dist/core/Queue.js +10 -0
- package/dist/core/Room.d.ts +1 -0
- package/dist/core/Room.js +14 -2
- package/dist/events/EventTypes.d.ts +6 -0
- package/package.json +24 -5
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# OpenPlugDJ
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/openplugdj)
|
|
4
|
+
[](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
|
+
});
|
package/dist/core/Player.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/Player.js
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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.
|
|
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;
|
package/dist/core/Queue.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/Room.d.ts
CHANGED
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
|
-
|
|
32
|
-
|
|
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,19 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openplugdj",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/index.js"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
11
16
|
"scripts": {
|
|
12
17
|
"build": "tsc",
|
|
13
18
|
"dev": "tsc -w",
|
|
14
19
|
"prepublishOnly": "npm run build"
|
|
15
20
|
},
|
|
16
|
-
"keywords": [
|
|
21
|
+
"keywords": [
|
|
22
|
+
"dj",
|
|
23
|
+
"queue",
|
|
24
|
+
"music",
|
|
25
|
+
"room",
|
|
26
|
+
"realtime"
|
|
27
|
+
],
|
|
17
28
|
"author": "bulondra",
|
|
18
29
|
"license": "MIT",
|
|
19
30
|
"dependencies": {
|
|
@@ -23,5 +34,13 @@
|
|
|
23
34
|
"@types/node": "^25.2.3",
|
|
24
35
|
"typescript": "^5.5.3"
|
|
25
36
|
},
|
|
26
|
-
"private": false
|
|
27
|
-
|
|
37
|
+
"private": false,
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/bulondra/openplugdj.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/bulondra/openplugdj/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/bulondra/openplugdj#readme"
|
|
46
|
+
}
|