-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
325 lines (267 loc) · 9.97 KB
/
index.js
File metadata and controls
325 lines (267 loc) · 9.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
const TARGET_URL = 'https://shadolith.bandcamp.com/album/do-no-harm';
const COOKIE_CONSENT_BUTTON = 'button.g-button.dark-mode[type="submit"]';
const PLAY_BUTTON_CLASS = 'playbutton';
async function captureAudio() {
console.log('Launching browser...');
// Launch browser with necessary permissions
const browser = await chromium.launch({
headless: false, // Set to true if you want headless mode
args: [
'--use-fake-ui-for-media-stream', // Auto-accept media permissions
'--use-fake-device-for-media-stream',
'--allow-file-access',
'--autoplay-policy=no-user-gesture-required',
'--disable-web-security', // Allow CORS for audio capture
'--disable-features=IsolateOrigins,site-per-process'
]
});
const context = await browser.newContext({
permissions: ['microphone', 'camera'],
bypassCSP: true,
extraHTTPHeaders: {
'Access-Control-Allow-Origin': '*'
}
});
const page = await context.newPage();
// Listen to console messages from the page
page.on('console', msg => {
const type = msg.type();
const text = msg.text();
console.log(`[Browser ${type}]:`, text);
});
// Listen to page errors
page.on('pageerror', error => {
console.error('[Page Error]:', error.message);
});
console.log('Navigating to URL...');
await page.goto(TARGET_URL, { waitUntil: 'networkidle' });
// Wait a bit for the page to fully load
await page.waitForTimeout(3000);
console.log('Looking for cookie consent button...');
// Try to click the cookie consent button
try {
const cookieButton = await page.locator(COOKIE_CONSENT_BUTTON).first();
await cookieButton.waitFor({ timeout: 5000 });
console.log('Found cookie consent button, clicking...');
await cookieButton.click();
console.log('Cookie consent accepted!');
await page.waitForTimeout(1000);
} catch (error) {
console.log('No cookie consent button found or already accepted');
}
console.log('Looking for play button...');
// Try to find and click the play button
try {
const playButton = await page.locator(`.${PLAY_BUTTON_CLASS}`).first();
await playButton.waitFor({ timeout: 10000 });
console.log('Found play button, clicking...');
await playButton.click();
console.log('Play button clicked!');
} catch (error) {
console.log('Could not find play button with exact class, trying alternative selectors...');
// Try alternative selectors for Bandcamp
const alternatives = [
'.playbutton',
'div.playbutton',
'button[aria-label*="Play"]',
'[role="button"][aria-label*="play" i]'
];
for (const selector of alternatives) {
try {
await page.click(selector, { timeout: 2000 });
console.log(`Clicked using selector: ${selector}`);
break;
} catch (e) {
console.log(`Selector ${selector} not found`);
}
}
}
// Wait for audio to start playing
await page.waitForTimeout(2000);
console.log('Setting up audio capture...');
// First, let's try to get the audio URL and download it directly
const audioUrl = await page.evaluate(() => {
const audioElement = document.querySelector('audio');
return audioElement ? audioElement.src : null;
});
if (audioUrl) {
console.log('Audio URL found:', audioUrl);
// Download the audio file directly
try {
console.log('Attempting to download audio directly...');
const response = await page.context().request.get(audioUrl);
if (response.ok()) {
const audioBuffer = await response.body();
const outputPath = path.join(process.cwd(), 'captured_audio.mp3');
fs.writeFileSync(outputPath, audioBuffer);
console.log(`Audio downloaded successfully!`);
console.log(`Audio saved to: ${outputPath}`);
console.log(`File size: ${(audioBuffer.length / 1024).toFixed(2)} KB`);
await page.waitForTimeout(2000);
await browser.close();
console.log('Done!');
return;
}
} catch (error) {
console.log('Direct download failed, falling back to recording method:', error.message);
}
}
// Fallback: use recording method with CORS bypass
console.log('Using recording method...');
// Expose a function to send audio data back to Node.js
await page.exposeFunction('sendAudioChunk', (chunk) => {
// This will be called from the browser context
return chunk;
});
// Start audio capture and recording
const recordingPromise = page.evaluate(async (recordingDuration) => {
return new Promise((resolve, reject) => {
const chunks = [];
// Find audio element
const audioElement = document.querySelector('audio');
if (!audioElement) {
reject(new Error('No audio element found on the page'));
return;
}
console.log('Audio element found:', audioElement.src);
console.log('Audio element paused:', audioElement.paused);
console.log('Audio element duration:', audioElement.duration);
// Make sure audio is playing
if (audioElement.paused) {
audioElement.play().catch(e => console.error('Failed to play:', e));
}
// Create audio context
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
console.log('Audio context state:', audioContext.state);
console.log('Audio context sample rate:', audioContext.sampleRate);
// Resume audio context if suspended
if (audioContext.state === 'suspended') {
audioContext.resume();
}
// Create source from audio element
let source;
try {
source = audioContext.createMediaElementSource(audioElement);
} catch (e) {
console.error('Error creating media element source:', e);
reject(e);
return;
}
// Create destination for recording
const destination = audioContext.createMediaStreamDestination();
// Connect source to both destination (for recording) and context.destination (for playback)
source.connect(destination);
source.connect(audioContext.destination);
console.log('Audio nodes connected');
console.log('Destination stream tracks:', destination.stream.getTracks().length);
// Get the stream from destination
const stream = destination.stream;
const audioTracks = stream.getAudioTracks();
if (audioTracks.length === 0) {
reject(new Error('No audio tracks in destination stream'));
return;
}
console.log('Audio tracks:', audioTracks.length);
console.log('Track settings:', audioTracks[0].getSettings());
// Create MediaRecorder with better settings
let mediaRecorder;
const mimeTypes = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/mp4'
];
let selectedMimeType;
for (const mimeType of mimeTypes) {
if (MediaRecorder.isTypeSupported(mimeType)) {
selectedMimeType = mimeType;
break;
}
}
console.log('Using MIME type:', selectedMimeType);
try {
mediaRecorder = new MediaRecorder(stream, {
mimeType: selectedMimeType,
audioBitsPerSecond: 128000
});
} catch (e) {
console.error('Error creating MediaRecorder:', e);
reject(e);
return;
}
mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
chunks.push(event.data);
console.log(`Recorded chunk: ${event.data.size} bytes, total chunks: ${chunks.length}`);
}
};
mediaRecorder.onstop = async () => {
console.log('Recording stopped, total chunks:', chunks.length);
if (chunks.length === 0) {
reject(new Error('No audio data recorded'));
return;
}
const blob = new Blob(chunks, { type: selectedMimeType });
console.log('Final blob size:', blob.size, 'bytes');
// Convert blob to base64
const reader = new FileReader();
reader.onloadend = () => {
const base64data = reader.result.split(',')[1];
resolve(base64data);
};
reader.onerror = (error) => {
reject(error);
};
reader.readAsDataURL(blob);
// Cleanup
stream.getTracks().forEach(track => track.stop());
audioContext.close();
};
mediaRecorder.onerror = (event) => {
console.error('MediaRecorder error:', event.error);
reject(event.error);
};
mediaRecorder.onstart = () => {
console.log('MediaRecorder started, state:', mediaRecorder.state);
};
// Start recording
try {
mediaRecorder.start(100); // Collect data every 100ms
console.log('Recording started...');
} catch (e) {
console.error('Error starting recording:', e);
reject(e);
return;
}
// Stop recording after duration
setTimeout(() => {
console.log('Stopping recording...');
if (mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
}, recordingDuration);
});
}, 10000); // Record for 10 seconds
console.log('Waiting for recording to complete...');
const audioData = await recordingPromise;
console.log('Audio captured! Saving to file...');
// Save the audio data
const audioBuffer = Buffer.from(audioData, 'base64');
const outputPath = path.join(process.cwd(), 'captured_audio.webm');
fs.writeFileSync(outputPath, audioBuffer);
console.log(`Audio saved to: ${outputPath}`);
console.log(`File size: ${(audioBuffer.length / 1024).toFixed(2)} KB`);
// Keep browser open for a bit to see the result
await page.waitForTimeout(2000);
await browser.close();
console.log('Done!');
}
// Run the function
captureAudio().catch(error => {
console.error('Error:', error);
process.exit(1);
});