@students-dev/audify-js 1.0.1 → 1.0.2
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 +92 -441
- package/dist/AudioEngine.js +232 -0
- package/dist/cjs/index.js +1478 -1746
- package/dist/cjs/index.js.map +1 -1
- package/dist/constants/index.js +35 -0
- package/dist/engine/Filters.js +137 -0
- package/dist/engine/MockAudioContext.js +53 -0
- package/dist/engine/Player.js +209 -0
- package/dist/esm/index.js +1474 -1744
- package/dist/esm/index.js.map +1 -1
- package/dist/events/EventBus.js +61 -0
- package/dist/index.js +18 -0
- package/dist/interfaces/index.js +1 -0
- package/dist/plugins/Plugin.js +27 -0
- package/dist/plugins/PluginManager.js +106 -0
- package/dist/providers/LavalinkProvider.js +81 -0
- package/dist/providers/LocalProvider.js +70 -0
- package/dist/providers/ProviderRegistry.js +20 -0
- package/dist/providers/SpotifyProvider.js +59 -0
- package/dist/providers/YouTubeProvider.js +48 -0
- package/dist/queue/Queue.js +186 -0
- package/dist/queue/Track.js +54 -0
- package/dist/types/AudioEngine.d.ts +107 -0
- package/dist/types/constants/index.d.ts +39 -0
- package/dist/types/engine/Filters.d.ts +25 -24
- package/dist/types/engine/MockAudioContext.d.ts +43 -0
- package/dist/types/engine/Player.d.ts +25 -21
- package/dist/types/events/EventBus.d.ts +17 -15
- package/dist/types/index.d.ts +17 -15
- package/dist/types/interfaces/index.d.ts +31 -0
- package/dist/types/plugins/Plugin.d.ts +11 -43
- package/dist/types/plugins/PluginManager.d.ts +19 -19
- package/dist/types/providers/LavalinkProvider.d.ts +15 -46
- package/dist/types/providers/LocalProvider.d.ts +11 -22
- package/dist/types/providers/ProviderRegistry.d.ts +10 -0
- package/dist/types/providers/SpotifyProvider.d.ts +12 -52
- package/dist/types/providers/YouTubeProvider.d.ts +11 -28
- package/dist/types/queue/Queue.d.ts +28 -22
- package/dist/types/queue/Track.d.ts +18 -16
- package/dist/types/utils/Logger.d.ts +12 -16
- package/dist/types/utils/Metadata.d.ts +16 -15
- package/dist/types/utils/Probe.d.ts +7 -7
- package/dist/types/utils/Time.d.ts +9 -9
- package/dist/utils/Logger.js +59 -0
- package/dist/utils/Metadata.js +90 -0
- package/dist/utils/Probe.js +59 -0
- package/dist/utils/Time.js +54 -0
- package/package.json +14 -8
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin manager for loading and managing plugins
|
|
3
|
+
*/
|
|
4
|
+
export class PluginManager {
|
|
5
|
+
constructor(engine) {
|
|
6
|
+
this.engine = engine;
|
|
7
|
+
this.plugins = new Map();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Load a plugin
|
|
11
|
+
* @param plugin - Plugin instance
|
|
12
|
+
*/
|
|
13
|
+
load(plugin) {
|
|
14
|
+
if (!plugin || typeof plugin.onLoad !== 'function') {
|
|
15
|
+
throw new Error('Invalid plugin instance');
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
plugin.onLoad(this.engine);
|
|
19
|
+
this.plugins.set(plugin.name, plugin);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error(`Failed to load plugin ${plugin.name}:`, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Enable a plugin
|
|
27
|
+
* @param name - Plugin name
|
|
28
|
+
*/
|
|
29
|
+
enable(name) {
|
|
30
|
+
const plugin = this.plugins.get(name);
|
|
31
|
+
if (plugin && !plugin.onEnable)
|
|
32
|
+
return; // Should have onEnable from interface
|
|
33
|
+
// Check if we track enabled state in plugin interface?
|
|
34
|
+
// Interface has onEnable methods.
|
|
35
|
+
// Plugin implementations usually track their own state or we assume onEnable does it.
|
|
36
|
+
// Check if plugin object has 'enabled' property (generic check)
|
|
37
|
+
if (plugin && plugin.enabled === false) {
|
|
38
|
+
plugin.onEnable();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Disable a plugin
|
|
43
|
+
* @param name - Plugin name
|
|
44
|
+
*/
|
|
45
|
+
disable(name) {
|
|
46
|
+
const plugin = this.plugins.get(name);
|
|
47
|
+
if (plugin) {
|
|
48
|
+
plugin.onDisable();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Unload a plugin
|
|
53
|
+
* @param name - Plugin name
|
|
54
|
+
*/
|
|
55
|
+
unload(name) {
|
|
56
|
+
const plugin = this.plugins.get(name);
|
|
57
|
+
if (plugin) {
|
|
58
|
+
plugin.onUnload();
|
|
59
|
+
this.plugins.delete(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get plugin by name
|
|
64
|
+
* @param name - Plugin name
|
|
65
|
+
* @returns Plugin instance
|
|
66
|
+
*/
|
|
67
|
+
get(name) {
|
|
68
|
+
return this.plugins.get(name);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get all plugins
|
|
72
|
+
* @returns Map of plugins
|
|
73
|
+
*/
|
|
74
|
+
getAll() {
|
|
75
|
+
return new Map(this.plugins);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get enabled plugins
|
|
79
|
+
* @returns Array of enabled plugins
|
|
80
|
+
*/
|
|
81
|
+
getEnabled() {
|
|
82
|
+
// We assume plugins that are loaded are potential candidates,
|
|
83
|
+
// but the IPlugin interface doesn't enforce an 'enabled' property reading.
|
|
84
|
+
// However, the Base Plugin class does.
|
|
85
|
+
// We'll filter by checking 'enabled' property if it exists, or assume true?
|
|
86
|
+
// Safer to check property.
|
|
87
|
+
return Array.from(this.plugins.values()).filter(plugin => plugin.enabled === true);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Call hook on all enabled plugins
|
|
91
|
+
* @param hook - Hook name
|
|
92
|
+
* @param args - Arguments to pass
|
|
93
|
+
*/
|
|
94
|
+
callHook(hook, ...args) {
|
|
95
|
+
this.getEnabled().forEach(plugin => {
|
|
96
|
+
if (typeof plugin[hook] === 'function') {
|
|
97
|
+
try {
|
|
98
|
+
plugin[hook](...args);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error(`Error in plugin ${plugin.name} hook ${hook}:`, error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Track } from '../queue/Track';
|
|
2
|
+
import { LavalinkManager } from 'lavalink-client';
|
|
3
|
+
export class LavalinkProvider {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.name = 'lavalink';
|
|
6
|
+
this.version = '1.0.0';
|
|
7
|
+
this.engine = null;
|
|
8
|
+
this.manager = null;
|
|
9
|
+
this.node = null;
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
async initialize(engine) {
|
|
13
|
+
this.engine = engine;
|
|
14
|
+
this.manager = new LavalinkManager({
|
|
15
|
+
nodes: [{
|
|
16
|
+
host: this.options.host || 'localhost',
|
|
17
|
+
port: this.options.port || 2333,
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
password: this.options.password || 'youshallnotpass',
|
|
20
|
+
secure: this.options.secure || false,
|
|
21
|
+
id: 'main'
|
|
22
|
+
}],
|
|
23
|
+
sendToShard: (guildId, payload) => {
|
|
24
|
+
// Mock
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
if (this.manager.connect)
|
|
29
|
+
await this.manager.connect();
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
this.node = this.manager.nodes ? this.manager.nodes.get('main') : this.manager.node;
|
|
32
|
+
}
|
|
33
|
+
async resolve(identifier) {
|
|
34
|
+
if (!this.node)
|
|
35
|
+
throw new Error('Lavalink not connected');
|
|
36
|
+
const result = await this.node.rest.loadTracks(identifier);
|
|
37
|
+
if (result.loadType === 'TRACK_LOADED') {
|
|
38
|
+
return this._formatTrack(result.tracks[0]);
|
|
39
|
+
}
|
|
40
|
+
else if (result.loadType === 'PLAYLIST_LOADED' || result.loadType === 'SEARCH_RESULT') {
|
|
41
|
+
return result.tracks.map((t) => this._formatTrack(t));
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
async play(track) {
|
|
46
|
+
throw new Error('Lavalink play() requires guild/channel context. Use createPlayer() directly.');
|
|
47
|
+
}
|
|
48
|
+
createPlayer(guildId, channelId) {
|
|
49
|
+
if (!this.manager)
|
|
50
|
+
throw new Error('Not initialized');
|
|
51
|
+
return this.manager.createPlayer({
|
|
52
|
+
guildId,
|
|
53
|
+
voiceChannelId: channelId
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async stop() {
|
|
57
|
+
// Stop all?
|
|
58
|
+
}
|
|
59
|
+
destroy() {
|
|
60
|
+
if (this.manager) {
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
if (this.manager.destroy)
|
|
63
|
+
this.manager.destroy();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
_formatTrack(lavalinkTrack) {
|
|
67
|
+
const info = lavalinkTrack.info;
|
|
68
|
+
return new Track(info.uri, {
|
|
69
|
+
id: lavalinkTrack.track,
|
|
70
|
+
title: info.title,
|
|
71
|
+
artist: info.author,
|
|
72
|
+
duration: Math.floor(info.length / 1000),
|
|
73
|
+
thumbnail: info.artworkUrl,
|
|
74
|
+
source: 'lavalink',
|
|
75
|
+
metadata: {
|
|
76
|
+
lavalinkTrack: lavalinkTrack.track,
|
|
77
|
+
identifier: info.identifier
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { extname } from 'path';
|
|
3
|
+
import { Track } from '../queue/Track';
|
|
4
|
+
export class LocalProvider {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'local';
|
|
7
|
+
this.version = '1.0.0';
|
|
8
|
+
this.engine = null;
|
|
9
|
+
}
|
|
10
|
+
async initialize(engine) {
|
|
11
|
+
this.engine = engine;
|
|
12
|
+
}
|
|
13
|
+
async resolve(path) {
|
|
14
|
+
if (!await this.exists(path)) {
|
|
15
|
+
throw new Error('File not found');
|
|
16
|
+
}
|
|
17
|
+
// Node.js specific checks
|
|
18
|
+
const stats = await fs.stat(path);
|
|
19
|
+
if (!stats.isFile()) {
|
|
20
|
+
throw new Error('Path is not a file');
|
|
21
|
+
}
|
|
22
|
+
const track = new Track(`file://${path}`, {
|
|
23
|
+
title: path.split('/').pop()?.replace(extname(path), '') || 'Unknown',
|
|
24
|
+
source: 'local',
|
|
25
|
+
metadata: {
|
|
26
|
+
size: stats.size,
|
|
27
|
+
modified: stats.mtime
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return track;
|
|
31
|
+
}
|
|
32
|
+
async play(track) {
|
|
33
|
+
if (!this.engine)
|
|
34
|
+
throw new Error('Provider not initialized');
|
|
35
|
+
// For local files, we assume the player can handle file:// URLs or we might need to read it into a buffer here?
|
|
36
|
+
// The previous Player implementation used fetch(url). fetch supports file:// in some envs but not all.
|
|
37
|
+
// However, given the hybrid nature, we'll assume the engine's player handles the URL.
|
|
38
|
+
// Actually, Player.ts uses fetch(). fetch('file://...') might fail in Node if not polyfilled or configured.
|
|
39
|
+
// But let's stick to the architecture: Provider calls engine.player.load(track).
|
|
40
|
+
// Wait, AudioEngine.ts in JS called `player.play(track)`.
|
|
41
|
+
// So the Provider.play just needs to confirm it CAN play or do setup?
|
|
42
|
+
// If AudioEngine delegates to Provider, then Provider MUST do the work.
|
|
43
|
+
// "AudioEngine calls provider.play(track)" -> Provider must make sound happen.
|
|
44
|
+
// So LocalProvider should call this.engine.player.play(track).
|
|
45
|
+
// BUT checking for infinite loop: Engine calls Provider.play -> Provider calls Engine.player.play?
|
|
46
|
+
// Engine needs to know NOT to call Provider again.
|
|
47
|
+
// Engine.play(track) -> check provider -> provider.play(track)
|
|
48
|
+
// Provider.play(track) -> engine.player.loadSource(track.url) -> source.start()
|
|
49
|
+
// We need to expose `loadSource` or similar on engine/player.
|
|
50
|
+
// For now, I'll assume engine.player has low-level methods.
|
|
51
|
+
// Let's assume the Player has a `playStream(url)` method.
|
|
52
|
+
// I'll type cast engine.player for now.
|
|
53
|
+
await this.engine.player.playStream(track);
|
|
54
|
+
}
|
|
55
|
+
async stop() {
|
|
56
|
+
// Local provider doesn't manage state separate from engine
|
|
57
|
+
}
|
|
58
|
+
destroy() {
|
|
59
|
+
// No cleanup needed
|
|
60
|
+
}
|
|
61
|
+
async exists(path) {
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(path);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class ProviderRegistry {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.providers = new Map();
|
|
4
|
+
}
|
|
5
|
+
register(provider) {
|
|
6
|
+
this.providers.set(provider.name, provider);
|
|
7
|
+
}
|
|
8
|
+
unregister(name) {
|
|
9
|
+
this.providers.delete(name);
|
|
10
|
+
}
|
|
11
|
+
get(name) {
|
|
12
|
+
return this.providers.get(name);
|
|
13
|
+
}
|
|
14
|
+
getAll() {
|
|
15
|
+
return Array.from(this.providers.values());
|
|
16
|
+
}
|
|
17
|
+
has(name) {
|
|
18
|
+
return this.providers.has(name);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Track } from '../queue/Track';
|
|
2
|
+
import SpotifyWebApi from 'spotify-web-api-node';
|
|
3
|
+
export class SpotifyProvider {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.name = 'spotify';
|
|
6
|
+
this.version = '1.0.0';
|
|
7
|
+
this.engine = null;
|
|
8
|
+
this.spotifyApi = new SpotifyWebApi({
|
|
9
|
+
clientId: options.clientId,
|
|
10
|
+
clientSecret: options.clientSecret,
|
|
11
|
+
redirectUri: options.redirectUri,
|
|
12
|
+
accessToken: options.accessToken,
|
|
13
|
+
refreshToken: options.refreshToken
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async initialize(engine) {
|
|
17
|
+
this.engine = engine;
|
|
18
|
+
}
|
|
19
|
+
async resolve(query) {
|
|
20
|
+
// Check if query is ID or URL or Search
|
|
21
|
+
if (query.includes('spotify.com/track/')) {
|
|
22
|
+
const id = query.split('track/')[1].split('?')[0];
|
|
23
|
+
const data = await this.spotifyApi.getTrack(id);
|
|
24
|
+
return this._formatTrack(data.body);
|
|
25
|
+
}
|
|
26
|
+
// Default to search
|
|
27
|
+
const data = await this.spotifyApi.searchTracks(query);
|
|
28
|
+
return data.body.tracks?.items.map(t => this._formatTrack(t)) || [];
|
|
29
|
+
}
|
|
30
|
+
async play(track) {
|
|
31
|
+
if (!this.engine)
|
|
32
|
+
throw new Error('Provider not initialized');
|
|
33
|
+
// Spotify playback usually requires Web SDK or resolving to another source
|
|
34
|
+
// Here we can throw or try to resolve if Preview URL is available
|
|
35
|
+
if (track.metadata.preview_url) {
|
|
36
|
+
await this.engine.player.playStream({ ...track, url: track.metadata.preview_url });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error('Spotify full playback not supported in this provider version (preview only)');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async stop() { }
|
|
43
|
+
destroy() { }
|
|
44
|
+
_formatTrack(spotifyTrack) {
|
|
45
|
+
return new Track(spotifyTrack.external_urls.spotify, {
|
|
46
|
+
id: spotifyTrack.id,
|
|
47
|
+
title: spotifyTrack.name,
|
|
48
|
+
artist: spotifyTrack.artists.map((a) => a.name).join(', '),
|
|
49
|
+
duration: Math.floor(spotifyTrack.duration_ms / 1000),
|
|
50
|
+
thumbnail: spotifyTrack.album.images[0]?.url,
|
|
51
|
+
source: 'spotify',
|
|
52
|
+
metadata: {
|
|
53
|
+
spotifyId: spotifyTrack.id,
|
|
54
|
+
preview_url: spotifyTrack.preview_url,
|
|
55
|
+
popularity: spotifyTrack.popularity
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Track } from '../queue/Track';
|
|
2
|
+
export class YouTubeProvider {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.name = 'youtube';
|
|
5
|
+
this.version = '1.0.0';
|
|
6
|
+
this.engine = null;
|
|
7
|
+
}
|
|
8
|
+
async initialize(engine) {
|
|
9
|
+
this.engine = engine;
|
|
10
|
+
}
|
|
11
|
+
async resolve(query) {
|
|
12
|
+
if (query.includes('youtube.com') || query.includes('youtu.be')) {
|
|
13
|
+
const videoId = this.extractVideoId(query);
|
|
14
|
+
if (!videoId)
|
|
15
|
+
throw new Error('Invalid YouTube URL');
|
|
16
|
+
return new Track(query, {
|
|
17
|
+
title: `YouTube Video ${videoId}`,
|
|
18
|
+
thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
|
|
19
|
+
source: 'youtube',
|
|
20
|
+
metadata: { videoId }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Search not implemented in this mock
|
|
24
|
+
throw new Error('Search not implemented');
|
|
25
|
+
}
|
|
26
|
+
async play(track) {
|
|
27
|
+
if (!this.engine)
|
|
28
|
+
throw new Error('Provider not initialized');
|
|
29
|
+
// In a real app, resolve stream URL here (e.g. ytdl-core)
|
|
30
|
+
// const streamUrl = await ytdl(track.url);
|
|
31
|
+
// await this.engine.player.playStream(streamUrl);
|
|
32
|
+
throw new Error('Stream URL extraction requires additional dependencies (ytdl-core)');
|
|
33
|
+
}
|
|
34
|
+
async stop() { }
|
|
35
|
+
destroy() { }
|
|
36
|
+
extractVideoId(url) {
|
|
37
|
+
try {
|
|
38
|
+
const urlObj = new URL(url);
|
|
39
|
+
if (urlObj.hostname === 'youtu.be') {
|
|
40
|
+
return urlObj.pathname.slice(1);
|
|
41
|
+
}
|
|
42
|
+
return urlObj.searchParams.get('v');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Track } from './Track';
|
|
2
|
+
import { EventBus } from '../events/EventBus';
|
|
3
|
+
import { EVENTS } from '../constants';
|
|
4
|
+
/**
|
|
5
|
+
* Audio queue management
|
|
6
|
+
*/
|
|
7
|
+
export class Queue {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.tracks = [];
|
|
10
|
+
this.currentIndex = -1;
|
|
11
|
+
this.eventBus = new EventBus();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Add track(s) to queue
|
|
15
|
+
* @param tracks - Track(s) to add
|
|
16
|
+
* @param position - Position to insert (optional)
|
|
17
|
+
*/
|
|
18
|
+
add(tracks, position) {
|
|
19
|
+
const trackArray = Array.isArray(tracks) ? tracks : [tracks];
|
|
20
|
+
const processedTracks = trackArray.map(track => {
|
|
21
|
+
if (typeof track === 'string') {
|
|
22
|
+
return new Track(track);
|
|
23
|
+
}
|
|
24
|
+
if (track instanceof Track) {
|
|
25
|
+
return track;
|
|
26
|
+
}
|
|
27
|
+
// It's ITrack or similar object
|
|
28
|
+
return new Track(track.url, track);
|
|
29
|
+
});
|
|
30
|
+
if (position !== undefined && position >= 0 && position <= this.tracks.length) {
|
|
31
|
+
this.tracks.splice(position, 0, ...processedTracks);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
this.tracks.push(...processedTracks);
|
|
35
|
+
}
|
|
36
|
+
processedTracks.forEach(track => {
|
|
37
|
+
this.eventBus.emit(EVENTS.TRACK_ADD, track);
|
|
38
|
+
});
|
|
39
|
+
this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Remove track from queue
|
|
43
|
+
* @param identifier - Track index or ID
|
|
44
|
+
* @returns Removed track
|
|
45
|
+
*/
|
|
46
|
+
remove(identifier) {
|
|
47
|
+
let index;
|
|
48
|
+
if (typeof identifier === 'number') {
|
|
49
|
+
index = identifier;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
index = this.tracks.findIndex(track => track.id === identifier);
|
|
53
|
+
}
|
|
54
|
+
if (index < 0 || index >= this.tracks.length)
|
|
55
|
+
return null;
|
|
56
|
+
const removed = this.tracks.splice(index, 1)[0];
|
|
57
|
+
if (this.currentIndex > index) {
|
|
58
|
+
this.currentIndex--;
|
|
59
|
+
}
|
|
60
|
+
else if (this.currentIndex === index) {
|
|
61
|
+
this.currentIndex = -1;
|
|
62
|
+
}
|
|
63
|
+
this.eventBus.emit(EVENTS.TRACK_REMOVE, removed);
|
|
64
|
+
this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
|
|
65
|
+
return removed;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Shuffle the queue
|
|
69
|
+
*/
|
|
70
|
+
shuffle() {
|
|
71
|
+
if (this.tracks.length <= 1)
|
|
72
|
+
return;
|
|
73
|
+
let currentTrack = null;
|
|
74
|
+
if (this.currentIndex >= 0) {
|
|
75
|
+
currentTrack = this.tracks[this.currentIndex];
|
|
76
|
+
}
|
|
77
|
+
for (let i = this.tracks.length - 1; i > 0; i--) {
|
|
78
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
79
|
+
[this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]];
|
|
80
|
+
}
|
|
81
|
+
if (currentTrack) {
|
|
82
|
+
this.currentIndex = this.tracks.indexOf(currentTrack);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.currentIndex = -1;
|
|
86
|
+
}
|
|
87
|
+
this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Clear the queue
|
|
91
|
+
*/
|
|
92
|
+
clear() {
|
|
93
|
+
this.tracks = [];
|
|
94
|
+
this.currentIndex = -1;
|
|
95
|
+
this.eventBus.emit(EVENTS.QUEUE_UPDATE, this.tracks);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Jump to specific track
|
|
99
|
+
* @param index - Track index
|
|
100
|
+
* @returns Track at index
|
|
101
|
+
*/
|
|
102
|
+
jump(index) {
|
|
103
|
+
if (index < 0 || index >= this.tracks.length)
|
|
104
|
+
return null;
|
|
105
|
+
this.currentIndex = index;
|
|
106
|
+
return this.tracks[index];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get current track
|
|
110
|
+
* @returns Current track
|
|
111
|
+
*/
|
|
112
|
+
getCurrent() {
|
|
113
|
+
return this.currentIndex >= 0 ? this.tracks[this.currentIndex] : null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get next track
|
|
117
|
+
* Moves cursor forward
|
|
118
|
+
* @param loop - Whether to loop back to start
|
|
119
|
+
* @returns Next track
|
|
120
|
+
*/
|
|
121
|
+
next(loop = false) {
|
|
122
|
+
if (this.tracks.length === 0)
|
|
123
|
+
return null;
|
|
124
|
+
let nextIndex = this.currentIndex + 1;
|
|
125
|
+
if (nextIndex >= this.tracks.length) {
|
|
126
|
+
if (loop) {
|
|
127
|
+
nextIndex = 0;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.currentIndex = nextIndex;
|
|
134
|
+
return this.tracks[this.currentIndex];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get previous track
|
|
138
|
+
* Moves cursor backward
|
|
139
|
+
* @param loop - Whether to loop to end
|
|
140
|
+
* @returns Previous track
|
|
141
|
+
*/
|
|
142
|
+
previous(loop = false) {
|
|
143
|
+
if (this.tracks.length === 0)
|
|
144
|
+
return null;
|
|
145
|
+
let prevIndex = this.currentIndex - 1;
|
|
146
|
+
if (prevIndex < 0) {
|
|
147
|
+
if (loop) {
|
|
148
|
+
prevIndex = this.tracks.length - 1;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this.currentIndex = prevIndex;
|
|
155
|
+
return this.tracks[this.currentIndex];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get all tracks
|
|
159
|
+
* @returns Array of tracks
|
|
160
|
+
*/
|
|
161
|
+
getTracks() {
|
|
162
|
+
return [...this.tracks];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get queue size
|
|
166
|
+
* @returns Number of tracks
|
|
167
|
+
*/
|
|
168
|
+
size() {
|
|
169
|
+
return this.tracks.length;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if queue is empty
|
|
173
|
+
* @returns Is empty
|
|
174
|
+
*/
|
|
175
|
+
isEmpty() {
|
|
176
|
+
return this.tracks.length === 0;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get track at index
|
|
180
|
+
* @param index - Track index
|
|
181
|
+
* @returns Track at index
|
|
182
|
+
*/
|
|
183
|
+
getTrack(index) {
|
|
184
|
+
return index >= 0 && index < this.tracks.length ? this.tracks[index] : null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MetadataUtils } from '../utils/Metadata';
|
|
2
|
+
/**
|
|
3
|
+
* Represents an audio track
|
|
4
|
+
*/
|
|
5
|
+
export class Track {
|
|
6
|
+
/**
|
|
7
|
+
* @param url - Track URL or file path
|
|
8
|
+
* @param options - Additional options
|
|
9
|
+
*/
|
|
10
|
+
constructor(url, options = {}) {
|
|
11
|
+
const extracted = MetadataUtils.extract(url);
|
|
12
|
+
this.url = url;
|
|
13
|
+
this.title = options.title || extracted.title || 'Unknown Title';
|
|
14
|
+
this.artist = options.artist || extracted.artist;
|
|
15
|
+
this.duration = options.duration || extracted.duration;
|
|
16
|
+
this.thumbnail = options.thumbnail || extracted.thumbnail;
|
|
17
|
+
this.source = options.source || extracted.source || 'unknown';
|
|
18
|
+
this.metadata = options.metadata || {};
|
|
19
|
+
this.id = options.id || Math.random().toString(36).substr(2, 9);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get track info
|
|
23
|
+
* @returns Track information
|
|
24
|
+
*/
|
|
25
|
+
getInfo() {
|
|
26
|
+
return {
|
|
27
|
+
id: this.id,
|
|
28
|
+
url: this.url,
|
|
29
|
+
title: this.title,
|
|
30
|
+
artist: this.artist,
|
|
31
|
+
duration: this.duration,
|
|
32
|
+
thumbnail: this.thumbnail,
|
|
33
|
+
source: this.source,
|
|
34
|
+
metadata: this.metadata
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Update track metadata
|
|
39
|
+
* @param metadata - New metadata
|
|
40
|
+
*/
|
|
41
|
+
updateMetadata(metadata) {
|
|
42
|
+
Object.assign(this, metadata);
|
|
43
|
+
if (metadata.metadata) {
|
|
44
|
+
Object.assign(this.metadata, metadata.metadata);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if track is valid
|
|
49
|
+
* @returns Is valid
|
|
50
|
+
*/
|
|
51
|
+
isValid() {
|
|
52
|
+
return !!(this.url && this.title);
|
|
53
|
+
}
|
|
54
|
+
}
|