Skip to content

Commit e1f2cf3

Browse files
feat: unify autoplay, add retries, fix clearFilters, improve logs (#36)
* Update build/structures/Node.js Co-authored-by: Emmanuel Lobo <76094069+UnschooledGamer@users.noreply.github.com> * Update package.json Co-authored-by: Emmanuel Lobo <76094069+UnschooledGamer@users.noreply.github.com> * Update test/v4.js * Fix import path for autoPlay function in tests * Update build/structures/Queue.js * Refactor client.login to use environment variable Updated client.login to use default token retrieval. --------- Co-authored-by: Emmanuel Lobo <76094069+UnschooledGamer@users.noreply.github.com>
1 parent 921c278 commit e1f2cf3

14 files changed

Lines changed: 307 additions & 250 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
pnpm-lock.yaml
3+
package-lock.json
34
.env
5+
testLab

build/functions/autoPlay.js

Lines changed: 103 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
const undici = require('undici');
22
const { JSDOM } = require('jsdom');
3-
const crypto = require('crypto');
43

5-
async function scAutoPlay(url) {
6-
const res = await undici.fetch(`${url}/recommended`);
4+
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
5+
6+
async function autoPlay(url, source) {
7+
if (source === "sound-cloud") {
8+
return await soundcloud(url);
9+
} else if (source === "spotify") {
10+
return await spotify(url);
11+
} else if (source === "apple-music") {
12+
return await appleMusic(url);
13+
} else {
14+
throw new Error("Unsupported source for autoPlay");
15+
}
16+
}
17+
18+
async function soundcloud(url) {
19+
const res = await undici.fetch(`${url}/recommended`, {
20+
headers: {
21+
'User-Agent': USER_AGENT
22+
}
23+
});
724

825
if (res.status !== 200) {
926
throw new Error(`Failed to fetch URL. Status code: ${res.status}`);
@@ -14,32 +31,94 @@ async function scAutoPlay(url) {
1431
const dom = new JSDOM(html);
1532
const document = dom.window.document;
1633

17-
const secondNoscript = document.querySelectorAll('noscript')[1];
18-
const sectionElement = secondNoscript.querySelector('section');
19-
const articleElements = sectionElement.querySelectorAll('article');
34+
const noscripts = document.querySelectorAll('noscript');
35+
const tracks = [];
2036

21-
articleElements.forEach(articleElement => {
22-
const h2Element = articleElement.querySelector('h2[itemprop="name"]');
37+
if (noscripts.length > 1) {
38+
const secondNoscript = noscripts[1];
39+
const section = secondNoscript.querySelector('section');
2340

24-
const aElement = h2Element.querySelector('a[itemprop="url"]');
25-
const href = `https://soundcloud.com${aElement.getAttribute('href')}`
41+
if (section) {
42+
const articles = section.querySelectorAll('article');
43+
articles.forEach(article => {
44+
const h2 = article.querySelector('h2[itemprop="name"]');
45+
if (h2) {
46+
const a = h2.querySelector('a[itemprop="url"]');
47+
if (a) {
48+
const href = a.getAttribute('href');
49+
if (href) {
50+
tracks.push(`https://soundcloud.com${href}`);
51+
}
52+
}
53+
}
54+
});
55+
}
56+
}
2657

27-
return href;
28-
});
58+
return tracks.length > 0 ? tracks[Math.floor(Math.random() * tracks.length)] : "";
59+
}
60+
61+
async function spotify(url) {
62+
const res = await undici.fetch(url);
63+
if (res.status !== 200) {
64+
throw new Error(`Failed to fetch URL. Status code: ${res.status}`);
65+
}
66+
67+
const html = await res.text();
68+
69+
const dom = new JSDOM(html);
70+
const document = dom.window.document;
71+
72+
const recommender = document.querySelector('div[data-testid="track-internal-link-recommender"]');
73+
const tracks = [];
74+
75+
if (recommender) {
76+
const anchors = recommender.querySelectorAll('a');
77+
anchors.forEach(a => {
78+
const href = a.getAttribute('href');
79+
if (href && href.startsWith('/track/')) {
80+
tracks.push(`https://open.spotify.com${href}`);
81+
}
82+
});
83+
}
84+
85+
return tracks.length > 0 ? tracks[Math.floor(Math.random() * tracks.length)] : "";
2986
}
3087

31-
async function spAutoPlay(track_id) {
32-
// Since Spotify's recommendations API is deprecated and unreliable,
33-
// This approach is more reliable and it uses official YT recommendations API.
34-
35-
try {
36-
// For now, return null to indicate we need track info from the player
37-
// The actual implementation will be handled in the Player.autoplay method
38-
return null;
39-
} catch (error) {
40-
console.error('Spotify autoplay error:', error);
41-
return null;
88+
async function appleMusic(url) {
89+
const res = await undici.fetch(url);
90+
if (res.status !== 200) {
91+
throw new Error(`Failed to fetch URL. Status code: ${res.status}`);
4292
}
93+
94+
const html = await res.text();
95+
96+
const dom = new JSDOM(html);
97+
const document = dom.window.document;
98+
99+
const sections = document.querySelectorAll('div[data-testid="section-container"]');
100+
const tracks = [];
101+
102+
sections.forEach(section => {
103+
const ariaLabel = section.getAttribute('aria-label');
104+
if (ariaLabel && ariaLabel.startsWith('More By')) {
105+
const shelfContent = section.querySelector('ul[slot="shelf-content"]');
106+
if (shelfContent) {
107+
const lockups = shelfContent.querySelectorAll('div[data-testid="lockup-control"]');
108+
lockups.forEach(lockup => {
109+
const a = lockup.querySelector('a');
110+
if (a) {
111+
const href = a.getAttribute('href');
112+
if (href) {
113+
tracks.push(href.startsWith('http') ? href : `https://music.apple.com${href}`);
114+
}
115+
}
116+
});
117+
}
118+
}
119+
});
120+
121+
return tracks.length > 0 ? tracks[Math.floor(Math.random() * tracks.length)] : "";
43122
}
44123

45-
module.exports = { scAutoPlay, spAutoPlay };
124+
module.exports = { autoPlay };

build/index.d.ts

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { EventEmitter } from "events";
22

33
type Nullable<T> = T | null;
4-
type Prettify<T> =
4+
type Prettify<T> =
55
[T] extends [object]
6-
? { [K in keyof T]: Prettify<T[K]>}
7-
: T;
6+
? { [K in keyof T]: Prettify<T[K]> }
7+
: T;
88

9-
type PrettifyWithNull<T> =
10-
[T] extends [object]
9+
type PrettifyWithNull<T> =
10+
[T] extends [object]
1111
? { [K in keyof T]: PrettifyWithNullOrUndefined<T[K]> } & {}
1212
: (null extends T ? (Exclude<T, null> | null) : never);
1313

1414
type PrettifyWithNullOrUndefined<T> =
15-
[T] extends [object]
15+
[T] extends [object]
1616
? { [K in keyof T]: PrettifyWithNullOrUndefined<T[K]> } & {}
1717
: (null extends T ? (Exclude<T, null> | null) : (undefined extends T ? (Exclude<T, undefined> | undefined) : T));
1818

@@ -144,7 +144,7 @@ export declare class Player extends EventEmitter {
144144
* @since 1.0.9
145145
*/
146146
public readonly connectionTimeout: number;
147-
147+
148148
/**
149149
* @warn Lazily (defined; Only when autoplay is called/used.) Initialized.
150150
*/
@@ -195,7 +195,7 @@ export declare class Player extends EventEmitter {
195195

196196
/**
197197
* @description clears All custom Data set on the Player
198-
*/
198+
*/
199199
public clearData(): this;
200200
private send(data: any): void;
201201

@@ -540,7 +540,7 @@ export declare class Riffy extends EventEmitter {
540540
*/
541541
public readonly migrationStrategyFn?: Function;
542542
public defaultSearchPlatform: string;
543-
543+
544544
public restVersion: RiffyOptions["restVersion"];
545545

546546
/**
@@ -765,8 +765,8 @@ type NodeInfo = {
765765
} | {
766766
node: string;
767767
voice: {
768-
name: string;
769-
version: string;
768+
name: string;
769+
version: string;
770770
};
771771
isNodelink: boolean;
772772
}
@@ -934,34 +934,34 @@ export declare class Node {
934934
*/
935935
getCurrentTrack: <TPlugin extends LyricPluginWithoutLavaLyrics | (string & {}) >(guildId: string, skipTrackSource: boolean, plugin?: TPlugin) => Promise<TPlugin extends LyricPluginWithoutLavaLyrics ? LyricPluginWithoutLavaLyricsResult : NodeLyricsResult | null>;
936936
}
937-
937+
938938
/**
939939
* Nodelink Mixer API (Works Only when Node is hosted with [Nodelink Server](https://nodelink.js.org))
940940
* @description The Audio Mixer allows overlaying auxiliary audio tracks (like TTS, sound effects, or background music) on top of the main active track.
941941
*/
942942
mixer: {
943-
/**
944-
* Check if Node is hosted with Nodelink Server
945-
*/
946-
check: () => boolean;
947-
/**
948-
* Adds a new audio track to be mixed over the current playback.
949-
* @param {string} guildId
950-
* @param {AddMixLayerOptions} mixLayerOptions
951-
*/
952-
addMixLayer: (guildId: string, mixLayerOptions: AddMixLayerOptions) => Promise<NodelinkMixLayer>;
953-
/**
954-
* Retrieves a list of currently active mix layers.
955-
*/
956-
getActiveMixLayers: (guildId: string) => Promise<NodelinkMixLayer[]>;
957-
/**
958-
* Update Mix Layer Volume
959-
*/
960-
updateMixLayerVolume: (guildId: string, mixId: string, volume: number) => Promise<void>;
961-
/**
962-
* Remove Mix Layer
963-
*/
964-
removeMixLayer: (guildId: string, mixId: string) => Promise<void>;
943+
/**
944+
* Check if Node is hosted with Nodelink Server
945+
*/
946+
check: () => boolean;
947+
/**
948+
* Adds a new audio track to be mixed over the current playback.
949+
* @param {string} guildId
950+
* @param {AddMixLayerOptions} mixLayerOptions
951+
*/
952+
addMixLayer: (guildId: string, mixLayerOptions: AddMixLayerOptions) => Promise<NodelinkMixLayer>;
953+
/**
954+
* Retrieves a list of currently active mix layers.
955+
*/
956+
getActiveMixLayers: (guildId: string) => Promise<NodelinkMixLayer[]>;
957+
/**
958+
* Update Mix Layer Volume
959+
*/
960+
updateMixLayerVolume: (guildId: string, mixId: string, volume: number) => Promise<void>;
961+
/**
962+
* Remove Mix Layer
963+
*/
964+
removeMixLayer: (guildId: string, mixId: string) => Promise<void>;
965965
}
966966

967967
public connect(): void;
@@ -978,39 +978,39 @@ export declare class Node {
978978
* Options for adding a mix layer.
979979
*/
980980
export type AddMixLayerOptions = {
981-
track: {
982-
/**
983-
* Base64 encoded track string (optional if identifier provided)
984-
*/
985-
encoded?: string;
981+
track: {
982+
/**
983+
* Base64 encoded track string (optional if identifier provided)
984+
*/
985+
encoded?: string;
986986

987-
/**
988-
* Track identifier (optional if encoded provided)
989-
*/
990-
identifier?: string;
987+
/**
988+
* Track identifier (optional if encoded provided)
989+
*/
990+
identifier?: string;
991+
992+
/**
993+
* (Optional) Track User Data
994+
*/
995+
userData?: string;
996+
};
991997

992998
/**
993-
* (Optional) Track User Data
999+
* Float 0.0 to 1.0 (Default: 0.8)
9941000
*/
995-
userData?: string;
996-
};
997-
998-
/**
999-
* Float 0.0 to 1.0 (Default: 0.8)
1000-
*/
1001-
volume?: number;
1001+
volume?: number;
10021002
};
10031003

10041004
export type NodelinkMixLayer = {
1005-
id: string;
1006-
track: {
1007-
encoded: string;
1008-
identifier: string;
1009-
userData: string;
1010-
};
1011-
volume: number;
1012-
position?: number;
1013-
startTime?: number;
1005+
id: string;
1006+
track: {
1007+
encoded: string;
1008+
identifier: string;
1009+
userData: string;
1010+
};
1011+
volume: number;
1012+
position?: number;
1013+
startTime?: number;
10141014
};
10151015

10161016
export type FilterOptions = {
@@ -1236,11 +1236,11 @@ export declare class Connection {
12361236
* @private
12371237
* @since 1.0.9
12381238
*/
1239-
private deferred: {
1240-
promise: Promise<void>;
1241-
resolve: (value?: void | PromiseLike<void>) => void
1239+
private deferred: {
1240+
promise: Promise<void>;
1241+
resolve: (value?: void | PromiseLike<void>) => void
12421242
} | null;
1243-
1243+
12441244
/**
12451245
* Tracks the promise for the active REST update to the Node
12461246
* @private
@@ -1252,18 +1252,18 @@ export declare class Connection {
12521252
* @since 1.0.9
12531253
*/
12541254
public establishing: boolean;
1255-
1255+
12561256
/**
12571257
* Checks if we have all necessary voice credentials.
12581258
*/
12591259
get isReady(): boolean;
1260-
1260+
12611261
/**
12621262
* Waits for the connection to be ready and for any active voice updates to the Node to complete.
12631263
* Optimization: Returns immediately if ready and idle to save resources.
12641264
*/
12651265
public resolve(): Promise<any>;
1266-
1266+
12671267
/**
12681268
* Checks if ready, performs the update, and manages the resolution flow.
12691269
*/
@@ -1279,4 +1279,4 @@ export declare class Connection {
12791279
}): void;
12801280

12811281
private updatePlayerVoiceData(): void;
1282-
}
1282+
}

0 commit comments

Comments
 (0)