@students-dev/audify-js 1.0.0 → 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/LICENSE +21 -0
- package/README.md +93 -310
- package/dist/AudioEngine.js +232 -0
- package/dist/cjs/index.js +1497 -1392
- 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 +1490 -1389
- 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/AudioEngine.d.ts +44 -1
- 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 -13
- 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 +17 -0
- 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 +14 -0
- 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 +19 -9
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Player } from './engine/Player';
|
|
2
|
+
import { Filters } from './engine/Filters';
|
|
3
|
+
import { Queue } from './queue/Queue';
|
|
4
|
+
import { EventBus } from './events/EventBus';
|
|
5
|
+
import { ProviderRegistry } from './providers/ProviderRegistry';
|
|
6
|
+
import { PluginManager } from './plugins/PluginManager';
|
|
7
|
+
import { LoopMode } from './constants';
|
|
8
|
+
import { IAudioEngine, IProvider, ITrack } from './interfaces';
|
|
9
|
+
/**
|
|
10
|
+
* Main audio engine class
|
|
11
|
+
*/
|
|
12
|
+
export declare class AudioEngine implements IAudioEngine {
|
|
13
|
+
options: any;
|
|
14
|
+
player: Player;
|
|
15
|
+
filters: Filters;
|
|
16
|
+
queue: Queue;
|
|
17
|
+
eventBus: EventBus;
|
|
18
|
+
providers: ProviderRegistry;
|
|
19
|
+
plugins: PluginManager;
|
|
20
|
+
isReady: boolean;
|
|
21
|
+
constructor(options?: any);
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the audio engine
|
|
24
|
+
*/
|
|
25
|
+
initialize(): Promise<void>;
|
|
26
|
+
registerProvider(provider: IProvider): Promise<void>;
|
|
27
|
+
getProvider(name: string): IProvider | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Play track or resume playback
|
|
30
|
+
* @param track - Track to play or track identifier
|
|
31
|
+
*/
|
|
32
|
+
play(track?: ITrack | string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Pause playback
|
|
35
|
+
*/
|
|
36
|
+
pause(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Stop playback
|
|
39
|
+
*/
|
|
40
|
+
stop(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Seek to position
|
|
43
|
+
* @param time - Time in seconds
|
|
44
|
+
*/
|
|
45
|
+
seek(time: number): void;
|
|
46
|
+
/**
|
|
47
|
+
* Set volume
|
|
48
|
+
* @param volume - Volume level (0-1)
|
|
49
|
+
*/
|
|
50
|
+
setVolume(volume: number): void;
|
|
51
|
+
/**
|
|
52
|
+
* Add track(s) to queue
|
|
53
|
+
* @param tracks - Track(s) to add
|
|
54
|
+
*/
|
|
55
|
+
add(tracks: ITrack | ITrack[] | string | string[]): void;
|
|
56
|
+
/**
|
|
57
|
+
* Remove track from queue
|
|
58
|
+
* @param identifier - Track index or ID
|
|
59
|
+
*/
|
|
60
|
+
remove(identifier: number | string): ITrack | null;
|
|
61
|
+
/**
|
|
62
|
+
* Skip to next track
|
|
63
|
+
*/
|
|
64
|
+
next(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Go to previous track
|
|
67
|
+
*/
|
|
68
|
+
previous(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Shuffle queue
|
|
71
|
+
*/
|
|
72
|
+
shuffle(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Clear queue
|
|
75
|
+
*/
|
|
76
|
+
clear(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Jump to track in queue
|
|
79
|
+
* @param index - Track index
|
|
80
|
+
*/
|
|
81
|
+
jump(index: number): void;
|
|
82
|
+
/**
|
|
83
|
+
* Apply audio filter
|
|
84
|
+
* @param type - Filter type
|
|
85
|
+
* @param options - Filter options
|
|
86
|
+
*/
|
|
87
|
+
applyFilter(type: any, options: any): void;
|
|
88
|
+
/**
|
|
89
|
+
* Remove audio filter
|
|
90
|
+
* @param type - Filter type
|
|
91
|
+
*/
|
|
92
|
+
removeFilter(type: any): void;
|
|
93
|
+
/**
|
|
94
|
+
* Set loop mode
|
|
95
|
+
* @param mode - Loop mode
|
|
96
|
+
*/
|
|
97
|
+
setLoopMode(mode: LoopMode): void;
|
|
98
|
+
/**
|
|
99
|
+
* Get current state
|
|
100
|
+
* @returns Engine state
|
|
101
|
+
*/
|
|
102
|
+
getState(): any;
|
|
103
|
+
/**
|
|
104
|
+
* Destroy the engine
|
|
105
|
+
*/
|
|
106
|
+
destroy(): void;
|
|
107
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const EVENTS: {
|
|
2
|
+
readonly READY: "ready";
|
|
3
|
+
readonly ERROR: "error";
|
|
4
|
+
readonly PLAY: "play";
|
|
5
|
+
readonly PAUSE: "pause";
|
|
6
|
+
readonly STOP: "stop";
|
|
7
|
+
readonly TRACK_START: "trackStart";
|
|
8
|
+
readonly TRACK_END: "trackEnd";
|
|
9
|
+
readonly TRACK_ADD: "trackAdd";
|
|
10
|
+
readonly TRACK_REMOVE: "trackRemove";
|
|
11
|
+
readonly QUEUE_UPDATE: "queueUpdate";
|
|
12
|
+
readonly FILTER_APPLIED: "filterApplied";
|
|
13
|
+
readonly VOLUME_CHANGE: "volumeChange";
|
|
14
|
+
readonly SEEK: "seek";
|
|
15
|
+
};
|
|
16
|
+
export type EventType = typeof EVENTS[keyof typeof EVENTS];
|
|
17
|
+
export declare const LOOP_MODES: {
|
|
18
|
+
readonly OFF: "off";
|
|
19
|
+
readonly TRACK: "track";
|
|
20
|
+
readonly QUEUE: "queue";
|
|
21
|
+
};
|
|
22
|
+
export type LoopMode = typeof LOOP_MODES[keyof typeof LOOP_MODES];
|
|
23
|
+
export declare const PLAYER_STATES: {
|
|
24
|
+
readonly IDLE: "idle";
|
|
25
|
+
readonly PLAYING: "playing";
|
|
26
|
+
readonly PAUSED: "paused";
|
|
27
|
+
readonly BUFFERING: "buffering";
|
|
28
|
+
};
|
|
29
|
+
export type PlayerState = typeof PLAYER_STATES[keyof typeof PLAYER_STATES];
|
|
30
|
+
export declare const FILTER_TYPES: {
|
|
31
|
+
readonly BASSBOOST: "bassboost";
|
|
32
|
+
readonly NIGHTCORE: "nightcore";
|
|
33
|
+
readonly VAPORWAVE: "vaporwave";
|
|
34
|
+
readonly ROTATE_8D: "8d";
|
|
35
|
+
readonly PITCH: "pitch";
|
|
36
|
+
readonly SPEED: "speed";
|
|
37
|
+
readonly REVERB: "reverb";
|
|
38
|
+
};
|
|
39
|
+
export type FilterType = typeof FILTER_TYPES[keyof typeof FILTER_TYPES];
|