@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.
Files changed (48) hide show
  1. package/README.md +92 -441
  2. package/dist/AudioEngine.js +232 -0
  3. package/dist/cjs/index.js +1478 -1746
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/constants/index.js +35 -0
  6. package/dist/engine/Filters.js +137 -0
  7. package/dist/engine/MockAudioContext.js +53 -0
  8. package/dist/engine/Player.js +209 -0
  9. package/dist/esm/index.js +1474 -1744
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/events/EventBus.js +61 -0
  12. package/dist/index.js +18 -0
  13. package/dist/interfaces/index.js +1 -0
  14. package/dist/plugins/Plugin.js +27 -0
  15. package/dist/plugins/PluginManager.js +106 -0
  16. package/dist/providers/LavalinkProvider.js +81 -0
  17. package/dist/providers/LocalProvider.js +70 -0
  18. package/dist/providers/ProviderRegistry.js +20 -0
  19. package/dist/providers/SpotifyProvider.js +59 -0
  20. package/dist/providers/YouTubeProvider.js +48 -0
  21. package/dist/queue/Queue.js +186 -0
  22. package/dist/queue/Track.js +54 -0
  23. package/dist/types/AudioEngine.d.ts +107 -0
  24. package/dist/types/constants/index.d.ts +39 -0
  25. package/dist/types/engine/Filters.d.ts +25 -24
  26. package/dist/types/engine/MockAudioContext.d.ts +43 -0
  27. package/dist/types/engine/Player.d.ts +25 -21
  28. package/dist/types/events/EventBus.d.ts +17 -15
  29. package/dist/types/index.d.ts +17 -15
  30. package/dist/types/interfaces/index.d.ts +31 -0
  31. package/dist/types/plugins/Plugin.d.ts +11 -43
  32. package/dist/types/plugins/PluginManager.d.ts +19 -19
  33. package/dist/types/providers/LavalinkProvider.d.ts +15 -46
  34. package/dist/types/providers/LocalProvider.d.ts +11 -22
  35. package/dist/types/providers/ProviderRegistry.d.ts +10 -0
  36. package/dist/types/providers/SpotifyProvider.d.ts +12 -52
  37. package/dist/types/providers/YouTubeProvider.d.ts +11 -28
  38. package/dist/types/queue/Queue.d.ts +28 -22
  39. package/dist/types/queue/Track.d.ts +18 -16
  40. package/dist/types/utils/Logger.d.ts +12 -16
  41. package/dist/types/utils/Metadata.d.ts +16 -15
  42. package/dist/types/utils/Probe.d.ts +7 -7
  43. package/dist/types/utils/Time.d.ts +9 -9
  44. package/dist/utils/Logger.js +59 -0
  45. package/dist/utils/Metadata.js +90 -0
  46. package/dist/utils/Probe.js +59 -0
  47. package/dist/utils/Time.js +54 -0
  48. 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
+ }