streamify-audio 2.0.5 → 2.1.0

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/index.js CHANGED
@@ -249,4 +249,14 @@ Streamify.Manager = Manager;
249
249
  Streamify.Player = Manager ? require('./src/discord/Player') : null;
250
250
  Streamify.Queue = Manager ? require('./src/discord/Queue') : null;
251
251
 
252
+ const persistence = require('./src/persistence');
253
+ Streamify.PersistenceAdapter = persistence.PersistenceAdapter;
254
+ Streamify.MemoryAdapter = persistence.MemoryAdapter;
255
+ Streamify.MongoAdapter = persistence.MongoAdapter;
256
+
257
+ const presets = require('./src/filters/presets');
258
+ Streamify.EffectPresets = presets;
259
+ Streamify.getEffectPresetNames = presets.getPresetNames;
260
+ Streamify.getEffectPresetsInfo = presets.getAllPresetsInfo;
261
+
252
262
  module.exports = Streamify;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamify-audio",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "Dual-mode audio library: HTTP streaming proxy + Discord player (Lavalink alternative). Supports YouTube, Spotify, SoundCloud with audio filters.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -5,6 +5,7 @@ const spotify = require('../providers/spotify');
5
5
  const soundcloud = require('../providers/soundcloud');
6
6
  const log = require('../utils/logger');
7
7
  const { loadConfig } = require('../config');
8
+ const { MemoryAdapter, MongoAdapter, serializeTrack, deserializeTrack } = require('../persistence');
8
9
 
9
10
  function checkDependencies() {
10
11
  const missing = [];
@@ -73,11 +74,164 @@ class Manager extends EventEmitter {
73
74
  maxTracks: options.autoplay?.maxTracks ?? 5
74
75
  };
75
76
 
77
+ this.fallback = {
78
+ enabled: options.fallback?.enabled ?? false,
79
+ order: options.fallback?.order || ['youtube', 'soundcloud']
80
+ };
81
+
82
+ this.persistence = null;
83
+ this._persistenceInterval = null;
84
+ this._persistenceIntervalMs = options.persistence?.interval || 30000;
85
+
76
86
  this._setupVoiceStateListener();
77
87
 
78
88
  log.info('MANAGER', 'Streamify Manager initialized');
79
89
  }
80
90
 
91
+ async enablePersistence(options = {}) {
92
+ if (options.adapter) {
93
+ this.persistence = options.adapter;
94
+ } else if (options.type === 'mongodb') {
95
+ this.persistence = new MongoAdapter({
96
+ client: options.client,
97
+ uri: options.uri,
98
+ db: options.db,
99
+ collection: options.collection
100
+ });
101
+ } else {
102
+ this.persistence = new MemoryAdapter();
103
+ }
104
+
105
+ if (options.autoSave !== false) {
106
+ this._persistenceInterval = setInterval(() => {
107
+ this._saveAllSessions();
108
+ }, this._persistenceIntervalMs);
109
+ }
110
+
111
+ log.info('MANAGER', `Persistence enabled (type: ${options.type || 'memory'})`);
112
+ return this;
113
+ }
114
+
115
+ async _saveAllSessions() {
116
+ if (!this.persistence) return;
117
+
118
+ for (const [guildId, player] of this.players) {
119
+ if (player.connected) {
120
+ await this.saveSession(guildId);
121
+ }
122
+ }
123
+ }
124
+
125
+ async saveSession(guildId) {
126
+ if (!this.persistence) return false;
127
+
128
+ const player = this.players.get(guildId);
129
+ if (!player || !player.connected) return false;
130
+
131
+ const current = player.queue.current;
132
+ if (!current && player.queue.tracks.length === 0) {
133
+ await this.persistence.delete(guildId);
134
+ return true;
135
+ }
136
+
137
+ const sessionData = {
138
+ guildId,
139
+ voiceChannelId: player.voiceChannelId,
140
+ textChannelId: player.textChannelId,
141
+ currentTrack: serializeTrack(current),
142
+ positionMs: player.position || 0,
143
+ queue: player.queue.tracks.map(serializeTrack),
144
+ volume: player.volume,
145
+ loop: player.queue.repeatMode || 'off',
146
+ filters: player.filters || {}
147
+ };
148
+
149
+ await this.persistence.save(guildId, sessionData);
150
+ log.debug('MANAGER', `Session saved for guild ${guildId}`);
151
+ return true;
152
+ }
153
+
154
+ async restoreSession(guildId, options = {}) {
155
+ if (!this.persistence) return null;
156
+
157
+ const session = await this.persistence.load(guildId);
158
+ if (!session) return null;
159
+
160
+ const guild = this.client.guilds.cache.get(guildId);
161
+ if (!guild) {
162
+ log.debug('MANAGER', `Guild ${guildId} not found, skipping restore`);
163
+ return null;
164
+ }
165
+
166
+ const voiceChannel = this.client.channels.cache.get(session.voiceChannelId);
167
+ if (!voiceChannel) {
168
+ log.debug('MANAGER', `Voice channel ${session.voiceChannelId} not found, skipping restore`);
169
+ await this.persistence.delete(guildId);
170
+ return null;
171
+ }
172
+
173
+ log.info('MANAGER', `Restoring session for guild ${guildId}`);
174
+
175
+ const player = await this.create(guildId, session.voiceChannelId, session.textChannelId);
176
+
177
+ if (session.volume !== undefined) player.setVolume(session.volume);
178
+ if (session.loop && session.loop !== 'off') player.setLoop(session.loop);
179
+
180
+ if (session.currentTrack) {
181
+ const track = deserializeTrack(session.currentTrack);
182
+ await player.play(track);
183
+
184
+ if (session.positionMs > 0 && !track.isLive && options.seekToPosition !== false) {
185
+ try {
186
+ await player.seek(session.positionMs);
187
+ } catch (e) {
188
+ log.debug('MANAGER', `Could not seek to position: ${e.message}`);
189
+ }
190
+ }
191
+ }
192
+
193
+ if (session.queue && session.queue.length > 0) {
194
+ const tracks = session.queue.map(deserializeTrack);
195
+ player.queue.addMany(tracks);
196
+ }
197
+
198
+ const members = voiceChannel.members?.filter(m => !m.user.bot);
199
+ if (members && members.size === 0 && player.autoPause.enabled) {
200
+ player.pause();
201
+ player._autoPaused = true;
202
+ log.info('MANAGER', `Session restored but auto-paused (empty channel)`);
203
+ }
204
+
205
+ this.emit('sessionRestored', { guildId, player, session });
206
+ return player;
207
+ }
208
+
209
+ async restoreAllSessions(options = {}) {
210
+ if (!this.persistence) return [];
211
+
212
+ const sessions = await this.persistence.loadAll();
213
+ const restored = [];
214
+
215
+ for (const session of sessions) {
216
+ try {
217
+ const player = await this.restoreSession(session.guildId, options);
218
+ if (player) {
219
+ restored.push({ guildId: session.guildId, player });
220
+ }
221
+ } catch (error) {
222
+ log.error('MANAGER', `Failed to restore session ${session.guildId}: ${error.message}`);
223
+ }
224
+ }
225
+
226
+ log.info('MANAGER', `Restored ${restored.length}/${sessions.length} sessions`);
227
+ return restored;
228
+ }
229
+
230
+ async deleteSession(guildId) {
231
+ if (!this.persistence) return false;
232
+ return this.persistence.delete(guildId);
233
+ }
234
+
81
235
  _setupVoiceStateListener() {
82
236
  this.client.on('voiceStateUpdate', (oldState, newState) => {
83
237
  const player = this.players.get(oldState.guild.id) || this.players.get(newState.guild.id);
@@ -474,7 +628,84 @@ class Manager extends EventEmitter {
474
628
  }
475
629
  }
476
630
 
631
+ async searchWithFallback(query, options = {}) {
632
+ if (!this.fallback.enabled) {
633
+ return this.search(query, options);
634
+ }
635
+
636
+ const sources = options.sources || this.fallback.order;
637
+ let lastError = null;
638
+
639
+ for (const source of sources) {
640
+ try {
641
+ const result = await this.search(query, { ...options, source });
642
+ if (result.loadType !== 'error' && result.tracks.length > 0) {
643
+ return result;
644
+ }
645
+ } catch (error) {
646
+ lastError = error;
647
+ log.debug('MANAGER', `Fallback: ${source} failed, trying next...`);
648
+ }
649
+ }
650
+
651
+ return {
652
+ loadType: 'error',
653
+ tracks: [],
654
+ error: lastError?.message || 'All sources failed'
655
+ };
656
+ }
657
+
658
+ async resolveWithFallback(query) {
659
+ if (!this.fallback.enabled) {
660
+ return this.resolve(query);
661
+ }
662
+
663
+ const result = await this.resolve(query);
664
+ if (result.loadType !== 'error' && result.tracks.length > 0) {
665
+ return result;
666
+ }
667
+
668
+ const detected = this._detectSource(query);
669
+ if (detected === 'spotify') {
670
+ const match = this._extractId(query, detected);
671
+ if (match) {
672
+ try {
673
+ const track = await this.getInfo(match.id, 'spotify');
674
+ const searchQuery = `${track.author} - ${track.title}`;
675
+
676
+ for (const source of this.fallback.order) {
677
+ if (source === 'spotify') continue;
678
+ try {
679
+ const fallbackResult = await this.search(searchQuery, { source, limit: 1 });
680
+ if (fallbackResult.tracks.length > 0) {
681
+ const fallbackTrack = fallbackResult.tracks[0];
682
+ fallbackTrack._originalSpotifyTrack = track;
683
+ log.info('MANAGER', `Spotify fallback: resolved via ${source}`);
684
+ return {
685
+ loadType: 'track',
686
+ tracks: [fallbackTrack],
687
+ fallbackUsed: source
688
+ };
689
+ }
690
+ } catch (e) {
691
+ continue;
692
+ }
693
+ }
694
+ } catch (e) {
695
+ log.debug('MANAGER', `Spotify info fetch failed: ${e.message}`);
696
+ }
697
+ }
698
+ }
699
+
700
+ return result;
701
+ }
702
+
477
703
  destroyAll() {
704
+ if (this._persistenceInterval) {
705
+ clearInterval(this._persistenceInterval);
706
+ this._persistenceInterval = null;
707
+ }
708
+
478
709
  for (const [guildId, player] of this.players) {
479
710
  player.destroy();
480
711
  }
@@ -2,6 +2,7 @@ const { EventEmitter } = require('events');
2
2
  const Queue = require('./Queue');
3
3
  const { createStream } = require('./Stream');
4
4
  const log = require('../utils/logger');
5
+ const { buildPresetFilters, getPresetNames, getAllPresetsInfo } = require('../filters/presets');
5
6
 
6
7
  let voiceModule;
7
8
  try {
@@ -587,6 +588,70 @@ class Player extends EventEmitter {
587
588
  return this.setFilter('preset', presetName);
588
589
  }
589
590
 
591
+ async setEffectPreset(presetName, intensity = 0.5) {
592
+ const filters = buildPresetFilters(presetName, intensity);
593
+
594
+ for (const [key, value] of Object.entries(filters)) {
595
+ if (key === '_intensityFactor') continue;
596
+ this._filters[key] = value;
597
+ }
598
+
599
+ this._activePresets = this._activePresets || new Map();
600
+ this._activePresets.set(presetName, intensity);
601
+
602
+ if (this._playing && this.queue.current) {
603
+ return this.setFilter('_trigger', null);
604
+ }
605
+ return true;
606
+ }
607
+
608
+ async setEffectPresets(presets) {
609
+ this._activePresets = new Map();
610
+
611
+ for (const { name, intensity = 0.5 } of presets) {
612
+ const filters = buildPresetFilters(name, intensity);
613
+ for (const [key, value] of Object.entries(filters)) {
614
+ if (key === '_intensityFactor') continue;
615
+ this._filters[key] = value;
616
+ }
617
+ this._activePresets.set(name, intensity);
618
+ }
619
+
620
+ if (this._playing && this.queue.current) {
621
+ return this.setFilter('_trigger', null);
622
+ }
623
+ return true;
624
+ }
625
+
626
+ async clearEffectPresets() {
627
+ const presetsToRemove = ['bass', 'treble', 'speed', 'pitch', 'rotation', 'karaoke',
628
+ 'lowpass', 'highpass', 'bandpass', 'vibrato', 'tremolo', 'chorus',
629
+ 'compressor', 'normalizer', 'nightcore', 'vaporwave', 'bassboost', '8d'];
630
+
631
+ for (const key of presetsToRemove) {
632
+ delete this._filters[key];
633
+ }
634
+
635
+ this._activePresets = new Map();
636
+
637
+ if (this._playing && this.queue.current) {
638
+ return this.setFilter('_trigger', null);
639
+ }
640
+ return true;
641
+ }
642
+
643
+ getActiveEffectPresets() {
644
+ return this._activePresets ? Array.from(this._activePresets.entries()).map(([name, intensity]) => ({ name, intensity })) : [];
645
+ }
646
+
647
+ getAvailableEffectPresets() {
648
+ return getAllPresetsInfo();
649
+ }
650
+
651
+ static getEffectPresetNames() {
652
+ return getPresetNames();
653
+ }
654
+
590
655
  async clearEQ() {
591
656
  delete this._filters.equalizer;
592
657
  delete this._filters.preset;
@@ -0,0 +1,227 @@
1
+ const EFFECT_PRESETS = {
2
+ bassboost: {
3
+ description: 'Boost bass frequencies',
4
+ build: (intensity = 0.5) => {
5
+ const gain = 5 + (intensity * 15);
6
+ return { bass: gain };
7
+ }
8
+ },
9
+
10
+ nightcore: {
11
+ description: 'Speed up with higher pitch (anime style)',
12
+ build: (intensity = 0.5) => {
13
+ const speedBoost = 0.15 + (intensity * 0.2);
14
+ return {
15
+ speed: 1 + speedBoost,
16
+ pitch: 1 + speedBoost
17
+ };
18
+ }
19
+ },
20
+
21
+ vaporwave: {
22
+ description: 'Slow down with lower pitch (aesthetic)',
23
+ build: (intensity = 0.5) => {
24
+ const slowdown = 0.1 + (intensity * 0.2);
25
+ return {
26
+ speed: 1 - slowdown,
27
+ pitch: 1 - slowdown
28
+ };
29
+ }
30
+ },
31
+
32
+ '8d': {
33
+ description: 'Rotating 8D audio effect',
34
+ build: (intensity = 0.5) => {
35
+ const speed = 0.05 + (intensity * 0.2);
36
+ return { rotation: { speed } };
37
+ }
38
+ },
39
+
40
+ karaoke: {
41
+ description: 'Reduce vocals (center channel removal)',
42
+ build: () => ({ karaoke: true })
43
+ },
44
+
45
+ trebleboost: {
46
+ description: 'Boost high frequencies',
47
+ build: (intensity = 0.5) => {
48
+ const gain = 5 + (intensity * 15);
49
+ return { treble: gain };
50
+ }
51
+ },
52
+
53
+ deep: {
54
+ description: 'Deep bass with reduced highs',
55
+ build: (intensity = 0.5) => {
56
+ const bassGain = 8 + (intensity * 12);
57
+ const trebleCut = -3 - (intensity * 7);
58
+ return { bass: bassGain, treble: trebleCut };
59
+ }
60
+ },
61
+
62
+ pop: {
63
+ description: 'Enhanced clarity for pop music',
64
+ build: (intensity = 0.5) => {
65
+ const factor = 0.5 + (intensity * 0.5);
66
+ return {
67
+ preset: 'pop',
68
+ _intensityFactor: factor
69
+ };
70
+ }
71
+ },
72
+
73
+ rock: {
74
+ description: 'Punchy mids and lows for rock',
75
+ build: (intensity = 0.5) => {
76
+ const factor = 0.5 + (intensity * 0.5);
77
+ return {
78
+ preset: 'rock',
79
+ _intensityFactor: factor
80
+ };
81
+ }
82
+ },
83
+
84
+ electronic: {
85
+ description: 'Enhanced bass and highs for EDM',
86
+ build: (intensity = 0.5) => {
87
+ const factor = 0.5 + (intensity * 0.5);
88
+ return {
89
+ preset: 'electronic',
90
+ _intensityFactor: factor
91
+ };
92
+ }
93
+ },
94
+
95
+ lofi: {
96
+ description: 'Lo-fi aesthetic with warmth',
97
+ build: (intensity = 0.5) => ({
98
+ lowpass: 8000 - (intensity * 4000),
99
+ bass: 3 + (intensity * 5)
100
+ })
101
+ },
102
+
103
+ radio: {
104
+ description: 'AM radio effect',
105
+ build: (intensity = 0.5) => ({
106
+ lowpass: 5000 - (intensity * 2000),
107
+ highpass: 300 + (intensity * 200)
108
+ })
109
+ },
110
+
111
+ telephone: {
112
+ description: 'Telephone/walkie-talkie effect',
113
+ build: () => ({
114
+ bandpass: { frequency: 1500, width: 1000 }
115
+ })
116
+ },
117
+
118
+ soft: {
119
+ description: 'Soft, gentle sound',
120
+ build: (intensity = 0.5) => ({
121
+ lowpass: 12000 - (intensity * 4000),
122
+ compressor: true
123
+ })
124
+ },
125
+
126
+ loud: {
127
+ description: 'Louder, more compressed',
128
+ build: (intensity = 0.5) => ({
129
+ preset: 'loudness',
130
+ compressor: true,
131
+ normalizer: true
132
+ })
133
+ },
134
+
135
+ chipmunk: {
136
+ description: 'High-pitched chipmunk voice',
137
+ build: (intensity = 0.5) => ({
138
+ pitch: 1.3 + (intensity * 0.5),
139
+ speed: 1.2 + (intensity * 0.3)
140
+ })
141
+ },
142
+
143
+ darth: {
144
+ description: 'Deep Darth Vader-like voice',
145
+ build: (intensity = 0.5) => ({
146
+ pitch: 0.7 - (intensity * 0.2),
147
+ speed: 0.9 - (intensity * 0.1)
148
+ })
149
+ },
150
+
151
+ echo: {
152
+ description: 'Echo/reverb effect',
153
+ build: () => ({
154
+ chorus: true
155
+ })
156
+ },
157
+
158
+ vibrato: {
159
+ description: 'Vibrating pitch effect',
160
+ build: (intensity = 0.5) => ({
161
+ vibrato: {
162
+ frequency: 4 + (intensity * 6),
163
+ depth: 0.3 + (intensity * 0.4)
164
+ }
165
+ })
166
+ },
167
+
168
+ tremolo: {
169
+ description: 'Trembling volume effect',
170
+ build: (intensity = 0.5) => ({
171
+ tremolo: {
172
+ frequency: 4 + (intensity * 8),
173
+ depth: 0.4 + (intensity * 0.4)
174
+ }
175
+ })
176
+ }
177
+ };
178
+
179
+ function getPresetNames() {
180
+ return Object.keys(EFFECT_PRESETS);
181
+ }
182
+
183
+ function getPresetInfo(name) {
184
+ const preset = EFFECT_PRESETS[name];
185
+ if (!preset) return null;
186
+ return {
187
+ name,
188
+ description: preset.description
189
+ };
190
+ }
191
+
192
+ function getAllPresetsInfo() {
193
+ return Object.entries(EFFECT_PRESETS).map(([name, preset]) => ({
194
+ name,
195
+ description: preset.description
196
+ }));
197
+ }
198
+
199
+ function buildPresetFilters(name, intensity = 0.5) {
200
+ const preset = EFFECT_PRESETS[name];
201
+ if (!preset) {
202
+ throw new Error(`Unknown preset: ${name}. Available: ${getPresetNames().join(', ')}`);
203
+ }
204
+
205
+ const clampedIntensity = Math.max(0, Math.min(1, intensity));
206
+ return preset.build(clampedIntensity);
207
+ }
208
+
209
+ function combinePresets(presetNames, intensity = 0.5) {
210
+ const combined = {};
211
+
212
+ for (const name of presetNames) {
213
+ const filters = buildPresetFilters(name, intensity);
214
+ Object.assign(combined, filters);
215
+ }
216
+
217
+ return combined;
218
+ }
219
+
220
+ module.exports = {
221
+ EFFECT_PRESETS,
222
+ getPresetNames,
223
+ getPresetInfo,
224
+ getAllPresetsInfo,
225
+ buildPresetFilters,
226
+ combinePresets
227
+ };
@@ -0,0 +1,156 @@
1
+ const log = require('../utils/logger');
2
+
3
+ class PersistenceAdapter {
4
+ async save(guildId, sessionData) {
5
+ throw new Error('save() must be implemented by adapter');
6
+ }
7
+
8
+ async load(guildId) {
9
+ throw new Error('load() must be implemented by adapter');
10
+ }
11
+
12
+ async delete(guildId) {
13
+ throw new Error('delete() must be implemented by adapter');
14
+ }
15
+
16
+ async loadAll() {
17
+ throw new Error('loadAll() must be implemented by adapter');
18
+ }
19
+ }
20
+
21
+ class MemoryAdapter extends PersistenceAdapter {
22
+ constructor() {
23
+ super();
24
+ this.sessions = new Map();
25
+ }
26
+
27
+ async save(guildId, sessionData) {
28
+ this.sessions.set(guildId, {
29
+ ...sessionData,
30
+ updatedAt: Date.now()
31
+ });
32
+ return true;
33
+ }
34
+
35
+ async load(guildId) {
36
+ return this.sessions.get(guildId) || null;
37
+ }
38
+
39
+ async delete(guildId) {
40
+ return this.sessions.delete(guildId);
41
+ }
42
+
43
+ async loadAll() {
44
+ return Array.from(this.sessions.entries()).map(([guildId, data]) => ({
45
+ guildId,
46
+ ...data
47
+ }));
48
+ }
49
+ }
50
+
51
+ class MongoAdapter extends PersistenceAdapter {
52
+ constructor(options = {}) {
53
+ super();
54
+ this.collection = null;
55
+ this.collectionName = options.collection || 'streamify_sessions';
56
+ this.mongoClient = options.client || null;
57
+ this.mongoUri = options.uri || null;
58
+ this.db = options.db || null;
59
+ }
60
+
61
+ async connect() {
62
+ if (this.collection) return;
63
+
64
+ if (this.mongoClient) {
65
+ const dbName = this.db || 'streamify';
66
+ this.collection = this.mongoClient.db(dbName).collection(this.collectionName);
67
+ } else if (this.mongoUri) {
68
+ const { MongoClient } = require('mongodb');
69
+ const client = new MongoClient(this.mongoUri);
70
+ await client.connect();
71
+ const dbName = this.db || 'streamify';
72
+ this.collection = client.db(dbName).collection(this.collectionName);
73
+ } else {
74
+ throw new Error('MongoAdapter requires either client or uri option');
75
+ }
76
+
77
+ log.info('PERSISTENCE', `Connected to MongoDB collection: ${this.collectionName}`);
78
+ }
79
+
80
+ async save(guildId, sessionData) {
81
+ await this.connect();
82
+ await this.collection.updateOne(
83
+ { _id: guildId },
84
+ { $set: { ...sessionData, updatedAt: new Date() } },
85
+ { upsert: true }
86
+ );
87
+ return true;
88
+ }
89
+
90
+ async load(guildId) {
91
+ await this.connect();
92
+ const doc = await this.collection.findOne({ _id: guildId });
93
+ return doc || null;
94
+ }
95
+
96
+ async delete(guildId) {
97
+ await this.connect();
98
+ const result = await this.collection.deleteOne({ _id: guildId });
99
+ return result.deletedCount > 0;
100
+ }
101
+
102
+ async loadAll() {
103
+ await this.connect();
104
+ const docs = await this.collection.find({}).toArray();
105
+ return docs.map(doc => ({
106
+ guildId: doc._id,
107
+ ...doc
108
+ }));
109
+ }
110
+ }
111
+
112
+ function serializeTrack(track) {
113
+ if (!track) return null;
114
+ return {
115
+ id: track.id,
116
+ title: track.title,
117
+ author: track.author,
118
+ duration: track.duration,
119
+ thumbnail: track.thumbnail,
120
+ uri: track.uri,
121
+ source: track.source,
122
+ isLive: track.isLive || false,
123
+ _resolvedId: track._resolvedId,
124
+ requestedBy: track.requestedBy ? {
125
+ id: track.requestedBy.id || null,
126
+ username: track.requestedBy.username || track.requestedBy,
127
+ avatarUrl: track.requestedBy.avatarUrl || null
128
+ } : null,
129
+ artistUrl: track.artistUrl || null
130
+ };
131
+ }
132
+
133
+ function deserializeTrack(data) {
134
+ if (!data) return null;
135
+ return {
136
+ id: data.id,
137
+ title: data.title,
138
+ author: data.author,
139
+ duration: data.duration,
140
+ thumbnail: data.thumbnail,
141
+ uri: data.uri,
142
+ source: data.source,
143
+ isLive: data.isLive || false,
144
+ _resolvedId: data._resolvedId,
145
+ requestedBy: data.requestedBy,
146
+ artistUrl: data.artistUrl
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ PersistenceAdapter,
152
+ MemoryAdapter,
153
+ MongoAdapter,
154
+ serializeTrack,
155
+ deserializeTrack
156
+ };