streamify-audio 2.2.7 → 2.2.9

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 CHANGED
@@ -229,12 +229,21 @@ const manager = new Streamify.Manager(client, {
229
229
 
230
230
  ### Search with Filters
231
231
 
232
- Filter for live streams or sort results by popularity/date.
232
+ Filter for live streams, sort results, or filter by artist.
233
233
 
234
234
  ```javascript
235
235
  const results = await manager.search('lofi hip hop', {
236
- type: 'live',
237
- sort: 'popularity'
236
+ source: 'youtube', // youtube, spotify, soundcloud
237
+ type: 'live', // video, live, playlist
238
+ sort: 'popularity', // relevance, popular, latest
239
+ limit: 10
240
+ });
241
+
242
+ // Artist search - filters results to only include tracks by that artist
243
+ const artistTracks = await manager.search('Drake', {
244
+ source: 'spotify',
245
+ artist: 'Drake', // filters results to tracks BY this artist
246
+ limit: 5
238
247
  });
239
248
  ```
240
249
 
@@ -76,6 +76,7 @@ Searches for tracks.
76
76
  - `limit` (number) - Number of results (default: 10)
77
77
  - `type` (string) - `video`, `live`, or `all` (YouTube only)
78
78
  - `sort` (string) - `relevance`, `popularity`, `date`, or `rating` (YouTube only)
79
+ - `artist` (string) - Filter results to only include tracks by this artist
79
80
 
80
81
  **Example:**
81
82
 
@@ -85,6 +86,13 @@ const result = await manager.search('lofi hip hop', {
85
86
  type: 'live',
86
87
  sort: 'popularity'
87
88
  });
89
+
90
+ // Artist search - filters results to tracks BY the artist
91
+ const artistTracks = await manager.search('Drake', {
92
+ source: 'spotify',
93
+ artist: 'Drake',
94
+ limit: 5
95
+ });
88
96
  ```
89
97
 
90
98
  // Result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamify-audio",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
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",
@@ -261,7 +261,7 @@ class Manager extends EventEmitter {
261
261
  if (!this.config.spotify?.clientId) {
262
262
  throw new Error('Spotify credentials not configured');
263
263
  }
264
- result = await spotify.search(query, limit, this.config);
264
+ result = await spotify.search(query, limit, this.config, options);
265
265
  break;
266
266
 
267
267
  case 'soundcloud':
@@ -269,7 +269,7 @@ class Manager extends EventEmitter {
269
269
  if (!this._isProviderEnabled('soundcloud')) {
270
270
  throw new Error('SoundCloud provider is disabled');
271
271
  }
272
- result = await soundcloud.search(query, limit, this.config);
272
+ result = await soundcloud.search(query, limit, this.config, options);
273
273
  break;
274
274
 
275
275
  case 'twitch':
@@ -33,6 +33,7 @@ async function getInfo(url, config) {
33
33
  title: data.title,
34
34
  duration: data.duration || 0,
35
35
  author: data.uploader || data.artist,
36
+ authorUrl: data.uploader_url || null,
36
37
  thumbnail: data.thumbnail,
37
38
  uri: data.webpage_url,
38
39
  streamUrl: `/bandcamp/stream/${Buffer.from(url).toString('base64')}`,
@@ -33,6 +33,7 @@ async function getInfo(url, config) {
33
33
  title: data.title,
34
34
  duration: data.duration || 0,
35
35
  author: data.uploader || data.user?.username,
36
+ authorUrl: data.uploader_url || null,
36
37
  thumbnail: data.thumbnail,
37
38
  uri: data.webpage_url,
38
39
  streamUrl: `/mixcloud/stream/${Buffer.from(url).toString('base64')}`,
@@ -3,9 +3,11 @@ const { buildFfmpegArgs } = require('../filters/ffmpeg');
3
3
  const { registerStream, unregisterStream } = require('../utils/stream');
4
4
  const log = require('../utils/logger');
5
5
 
6
- async function search(query, limit, config) {
6
+ async function search(query, limit, config, options = {}) {
7
7
  const startTime = Date.now();
8
- log.info('SOUNDCLOUD', `Searching: "${query}" (limit: ${limit})`);
8
+ const artistFilter = options.artist ? options.artist.toLowerCase() : null;
9
+ const searchLimit = artistFilter ? Math.min(limit * 3, 25) : limit;
10
+ log.info('SOUNDCLOUD', `Searching: "${query}" (limit: ${limit}${artistFilter ? `, artist: ${options.artist}` : ''})`);
9
11
 
10
12
  return new Promise((resolve, reject) => {
11
13
  const args = [
@@ -13,7 +15,7 @@ async function search(query, limit, config) {
13
15
  '--flat-playlist',
14
16
  '--skip-download',
15
17
  '-J',
16
- `scsearch${limit}:${query}`
18
+ `scsearch${searchLimit}:${query}`
17
19
  ];
18
20
 
19
21
  const proc = spawn(config.ytdlpPath, args, {
@@ -32,16 +34,27 @@ async function search(query, limit, config) {
32
34
  }
33
35
  try {
34
36
  const data = JSON.parse(stdout);
35
- const tracks = (data.entries || []).map(entry => ({
37
+ let tracks = (data.entries || []).map(entry => ({
36
38
  id: entry.id,
37
39
  title: entry.title,
38
40
  duration: entry.duration,
39
41
  author: entry.uploader,
42
+ authorUrl: entry.uploader_url || null,
40
43
  thumbnail: entry.thumbnails?.[0]?.url,
41
44
  uri: entry.url || entry.webpage_url,
42
45
  streamUrl: `/soundcloud/stream/${entry.id}`,
43
46
  source: 'soundcloud'
44
47
  }));
48
+
49
+ if (artistFilter) {
50
+ tracks = tracks.filter(t => {
51
+ const author = (t.author || '').toLowerCase();
52
+ const title = (t.title || '').toLowerCase();
53
+ return author.includes(artistFilter) || title.includes(artistFilter);
54
+ }).slice(0, limit);
55
+ log.info('SOUNDCLOUD', `Filtered to ${tracks.length} tracks by artist "${options.artist}"`);
56
+ }
57
+
45
58
  const elapsed = Date.now() - startTime;
46
59
  log.info('SOUNDCLOUD', `Found ${tracks.length} results (${elapsed}ms)`);
47
60
  resolve({ tracks, source: 'soundcloud', searchTime: elapsed });
@@ -82,6 +95,7 @@ async function getInfo(trackId, config) {
82
95
  title: data.title,
83
96
  duration: data.duration,
84
97
  author: data.uploader,
98
+ authorUrl: data.uploader_url || null,
85
99
  thumbnail: data.thumbnail,
86
100
  uri: data.webpage_url,
87
101
  streamUrl: `/soundcloud/stream/${data.id}`,
@@ -52,16 +52,19 @@ async function spotifyApi(endpoint, config) {
52
52
  return response.json();
53
53
  }
54
54
 
55
- async function search(query, limit, config) {
55
+ async function search(query, limit, config, options = {}) {
56
56
  const startTime = Date.now();
57
- log.info('SPOTIFY', `Searching: "${query}" (limit: ${limit})`);
57
+ const artistFilter = options.artist ? options.artist.toLowerCase() : null;
58
+ const searchLimit = artistFilter ? Math.min(limit * 2, 50) : limit;
59
+ log.info('SPOTIFY', `Searching: "${query}" (limit: ${limit}${artistFilter ? `, artist: ${options.artist}` : ''})`);
58
60
 
59
- const data = await spotifyApi(`/search?q=${encodeURIComponent(query)}&type=track&limit=${limit}`, config);
61
+ const data = await spotifyApi(`/search?q=${encodeURIComponent(query)}&type=track&limit=${searchLimit}`, config);
60
62
 
61
- const tracks = (data.tracks?.items || []).map(track => ({
63
+ let tracks = (data.tracks?.items || []).map(track => ({
62
64
  id: track.id,
63
65
  title: track.name,
64
66
  author: track.artists.map(a => a.name).join(', '),
67
+ authorUrl: track.artists[0]?.external_urls?.spotify || null,
65
68
  album: track.album.name,
66
69
  duration: Math.floor(track.duration_ms / 1000),
67
70
  thumbnail: track.album.images?.[0]?.url,
@@ -70,6 +73,14 @@ async function search(query, limit, config) {
70
73
  source: 'spotify'
71
74
  }));
72
75
 
76
+ if (artistFilter) {
77
+ tracks = tracks.filter(t => {
78
+ const author = (t.author || '').toLowerCase();
79
+ return author.includes(artistFilter);
80
+ }).slice(0, limit);
81
+ log.info('SPOTIFY', `Filtered to ${tracks.length} tracks by artist "${options.artist}"`);
82
+ }
83
+
73
84
  const elapsed = Date.now() - startTime;
74
85
  log.info('SPOTIFY', `Found ${tracks.length} results (${elapsed}ms)`);
75
86
  return { tracks, source: 'spotify', searchTime: elapsed };
@@ -83,6 +94,7 @@ async function getInfo(trackId, config) {
83
94
  id: track.id,
84
95
  title: track.name,
85
96
  author: track.artists.map(a => a.name).join(', '),
97
+ authorUrl: track.artists[0]?.external_urls?.spotify || null,
86
98
  album: track.album.name,
87
99
  duration: Math.floor(track.duration_ms / 1000),
88
100
  thumbnail: track.album.images?.[0]?.url,
@@ -142,6 +154,7 @@ async function getPlaylist(playlistId, config) {
142
154
  id: item.track.id,
143
155
  title: item.track.name,
144
156
  author: item.track.artists.map(a => a.name).join(', '),
157
+ authorUrl: item.track.artists[0]?.external_urls?.spotify || null,
145
158
  album: item.track.album.name,
146
159
  duration: Math.floor(item.track.duration_ms / 1000),
147
160
  thumbnail: item.track.album.images?.[0]?.url,
@@ -171,6 +184,7 @@ async function getAlbum(albumId, config) {
171
184
  id: track.id,
172
185
  title: track.name,
173
186
  author: track.artists.map(a => a.name).join(', '),
187
+ authorUrl: track.artists[0]?.external_urls?.spotify || null,
174
188
  album: data.name,
175
189
  duration: Math.floor(track.duration_ms / 1000),
176
190
  thumbnail: data.images?.[0]?.url,
@@ -200,6 +214,7 @@ async function getRecommendations(trackId, limit, config) {
200
214
  id: track.id,
201
215
  title: track.name,
202
216
  author: track.artists.map(a => a.name).join(', '),
217
+ authorUrl: track.artists[0]?.external_urls?.spotify || null,
203
218
  album: track.album.name,
204
219
  duration: Math.floor(track.duration_ms / 1000),
205
220
  thumbnail: track.album.images?.[0]?.url,
@@ -28,11 +28,14 @@ async function getInfo(url, config) {
28
28
  }
29
29
  try {
30
30
  const data = JSON.parse(stdout);
31
+ const authorUrl = data.uploader_url || data.channel_url ||
32
+ (data.channel ? `https://www.twitch.tv/${data.channel}` : null);
31
33
  resolve({
32
34
  id: data.id,
33
35
  title: data.title,
34
36
  duration: data.duration || 0,
35
37
  author: data.uploader || data.channel,
38
+ authorUrl,
36
39
  thumbnail: data.thumbnail,
37
40
  uri: data.webpage_url,
38
41
  streamUrl: `/twitch/stream/${Buffer.from(url).toString('base64')}`,
@@ -5,7 +5,9 @@ const log = require('../utils/logger');
5
5
 
6
6
  async function search(query, limit, config, options = {}) {
7
7
  const startTime = Date.now();
8
- log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit}, type: ${options.type || 'all'}, sort: ${options.sort || 'relevance'})`);
8
+ const artistFilter = options.artist ? options.artist.toLowerCase() : null;
9
+ const searchLimit = artistFilter ? Math.min(limit * 3, 25) : limit;
10
+ log.info('YOUTUBE', `Searching: "${query}" (limit: ${limit}, type: ${options.type || 'all'}, sort: ${options.sort || 'relevance'}${artistFilter ? `, artist: ${options.artist}` : ''})`);
9
11
 
10
12
  return new Promise((resolve, reject) => {
11
13
  const args = [
@@ -20,15 +22,13 @@ async function search(query, limit, config, options = {}) {
20
22
  }
21
23
 
22
24
  let searchQuery = query;
23
-
24
- // Handle sorting by modifying search query or using filters
25
+
25
26
  if (options.sort === 'views' || options.sort === 'popular') {
26
27
  searchQuery += ' most viewed';
27
28
  } else if (options.sort === 'date' || options.sort === 'latest') {
28
29
  searchQuery += ' new';
29
30
  }
30
31
 
31
- // Handle type filtering
32
32
  if (options.type === 'live') {
33
33
  args.push('--match-filter', 'live_status = is_live');
34
34
  } else if (options.type === 'playlist') {
@@ -37,7 +37,7 @@ async function search(query, limit, config, options = {}) {
37
37
  args.push('--match-filter', 'live_status != is_live');
38
38
  }
39
39
 
40
- args.push(`ytsearch${limit}:${searchQuery}`);
40
+ args.push(`ytsearch${searchLimit}:${searchQuery}`);
41
41
 
42
42
  const proc = spawn(config.ytdlpPath, args, {
43
43
  env: { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH }
@@ -59,7 +59,7 @@ async function search(query, limit, config, options = {}) {
59
59
  }
60
60
  try {
61
61
  const data = JSON.parse(stdout);
62
- const tracks = (data.entries || []).map(entry => {
62
+ let tracks = (data.entries || []).map(entry => {
63
63
  const bestAudio = entry.formats
64
64
  ?.filter(f => f.vcodec === 'none' && f.acodec !== 'none')
65
65
  .sort((a, b) => (b.abr || 0) - (a.abr || 0))[0];
@@ -82,6 +82,16 @@ async function search(query, limit, config, options = {}) {
82
82
  _headers: entry.http_headers || data.http_headers || null
83
83
  };
84
84
  });
85
+
86
+ if (artistFilter) {
87
+ tracks = tracks.filter(t => {
88
+ const author = (t.author || '').toLowerCase();
89
+ const title = (t.title || '').toLowerCase();
90
+ return author.includes(artistFilter) || title.includes(artistFilter);
91
+ }).slice(0, limit);
92
+ log.info('YOUTUBE', `Filtered to ${tracks.length} tracks by artist "${options.artist}"`);
93
+ }
94
+
85
95
  const elapsed = Date.now() - startTime;
86
96
  log.info('YOUTUBE', `Found ${tracks.length} results (${elapsed}ms)`);
87
97
  resolve({ tracks, source: 'youtube', searchTime: elapsed });
package/src/server.js CHANGED
@@ -98,14 +98,14 @@ class Server {
98
98
  if (!this._isProviderEnabled('youtube')) {
99
99
  return res.status(400).json({ error: 'YouTube provider is disabled' });
100
100
  }
101
- const { q, limit = 10, type, sort } = req.query;
101
+ const { q, limit = 10, type, sort, artist } = req.query;
102
102
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
103
103
 
104
- const cacheKey = `yt:search:${q}:${limit}:${type}:${sort}`;
104
+ const cacheKey = `yt:search:${q}:${limit}:${type}:${sort}:${artist || ''}`;
105
105
  const cached = cache.get(cacheKey);
106
106
  if (cached) return res.json(cached);
107
107
 
108
- const results = await youtube.search(q, parseInt(limit), this.config, { type, sort });
108
+ const results = await youtube.search(q, parseInt(limit), this.config, { type, sort, artist });
109
109
  cache.set(cacheKey, results, this.config.cache.searchTTL);
110
110
  res.json(results);
111
111
  } catch (error) {
@@ -149,14 +149,14 @@ class Server {
149
149
  if (!this.config.spotify?.clientId) {
150
150
  return res.status(400).json({ error: 'Spotify not configured' });
151
151
  }
152
- const { q, limit = 10 } = req.query;
152
+ const { q, limit = 10, artist } = req.query;
153
153
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
154
154
 
155
- const cacheKey = `sp:search:${q}:${limit}`;
155
+ const cacheKey = `sp:search:${q}:${limit}:${artist || ''}`;
156
156
  const cached = cache.get(cacheKey);
157
157
  if (cached) return res.json(cached);
158
158
 
159
- const results = await spotify.search(q, parseInt(limit), this.config);
159
+ const results = await spotify.search(q, parseInt(limit), this.config, { artist });
160
160
  cache.set(cacheKey, results, this.config.cache.searchTTL);
161
161
  res.json(results);
162
162
  } catch (error) {
@@ -208,14 +208,14 @@ class Server {
208
208
  if (!this._isProviderEnabled('soundcloud')) {
209
209
  return res.status(400).json({ error: 'SoundCloud provider is disabled' });
210
210
  }
211
- const { q, limit = 10 } = req.query;
211
+ const { q, limit = 10, artist } = req.query;
212
212
  if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
213
213
 
214
- const cacheKey = `sc:search:${q}:${limit}`;
214
+ const cacheKey = `sc:search:${q}:${limit}:${artist || ''}`;
215
215
  const cached = cache.get(cacheKey);
216
216
  if (cached) return res.json(cached);
217
217
 
218
- const results = await soundcloud.search(q, parseInt(limit), this.config);
218
+ const results = await soundcloud.search(q, parseInt(limit), this.config, { artist });
219
219
  cache.set(cacheKey, results, this.config.cache.searchTTL);
220
220
  res.json(results);
221
221
  } catch (error) {