-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
207 lines (174 loc) · 6.18 KB
/
index.ts
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
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import axios, { AxiosResponse } from "axios";
import * as cheerio from "cheerio";
import type { CheerioAPI } from "cheerio";
interface DownloadResult {
success: boolean;
path?: string;
error?: string;
}
interface ScrapeResult {
coverPath?: string;
audioPath?: string;
error?: string;
}
// Function to sanitize filename
const sanitizeFilename = (name: string): string => {
return name
.replace(/[<>:"/\\|?*]/g, '') // Remove invalid characters
.replace(/\s+/g, '_') // Replace spaces with underscores
.toLowerCase();
};
// Function to get file extension from URL
const getExtension = (url: string): string => {
const ext = path.extname(new URL(url).pathname);
return ext || '.unknown';
};
// Function to download a file
const downloadFile = async (url: string, outputPath: string): Promise<DownloadResult> => {
try {
console.log(`Starting download from: ${url}`);
console.log(`Saving to: ${outputPath}`);
const response: AxiosResponse = await axios.get(url, {
responseType: "stream",
timeout: 30000, // 30 seconds timeout
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
});
const writer = fs.createWriteStream(outputPath);
return new Promise((resolve) => {
response.data.pipe(writer);
writer.on("finish", () => {
console.log(`Successfully downloaded to: ${outputPath}`);
resolve({ success: true, path: outputPath });
});
writer.on("error", (error: Error) => {
console.error(`Write stream error: ${error.message}`);
fs.unlink(outputPath, () => {
console.log(`Cleaned up failed file: ${outputPath}`);
});
resolve({
success: false,
error: `Failed to write file: ${error.message}`
});
});
response.data.on("error", (error: Error) => {
console.error(`Download stream error: ${error.message}`);
fs.unlink(outputPath, () => {
console.log(`Cleaned up failed file: ${outputPath}`);
});
resolve({
success: false,
error: `Failed to download: ${error.message}`
});
});
});
} catch (error) {
console.error(`Axios error: ${error instanceof Error ? error.message : String(error)}`);
return {
success: false,
error: `Error downloading ${url}: ${error instanceof Error ? error.message : String(error)}`
};
}
};
// Function to scrape Suno webpage
const scrapeSuno = async (sunoUrl: string): Promise<ScrapeResult> => {
try {
console.log(`\nFetching page: ${sunoUrl}`);
const { data: html } = await axios.get(sunoUrl, { timeout: 30000 });
const $: CheerioAPI = cheerio.load(html);
// Extract song title from og:title meta tag
const title = $("meta[property='og:title']").attr("content") || "unknown";
const songName = title.split('|')[0].trim().replace(" by @", "_by_");
const sanitizedName = sanitizeFilename(songName);
console.log(`Song name: ${songName}`);
// Extract cover image URL from og:image meta tag
const coverUrl = $("meta[property='og:image']").attr("content");
if (!coverUrl) throw new Error("Cover image URL not found.");
const coverExt = getExtension(coverUrl);
const coverPath = path.join(process.cwd(), `${sanitizedName}_cover${coverExt}`);
console.log(`Downloading cover image: ${coverUrl}`);
const coverResult = await downloadFile(coverUrl, coverPath);
if (!coverResult.success) {
throw new Error(coverResult.error);
}
// Extract audio URL from og:audio meta tag
const audioUrl = $("meta[property='og:audio']").attr("content");
if (!audioUrl) throw new Error("Audio file URL not found.");
const audioExt = getExtension(audioUrl);
const audioPath = path.join(process.cwd(), `${sanitizedName}${audioExt}`);
console.log(`Downloading audio file: ${audioUrl}`);
const audioResult = await downloadFile(audioUrl, audioPath);
if (!audioResult.success) {
throw new Error(audioResult.error);
}
console.log("Download complete!");
console.log(`Files saved as:\n- ${path.basename(coverPath)}\n- ${path.basename(audioPath)}`);
return {
coverPath: coverPath,
audioPath: audioPath
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Error scraping Suno: ${errorMessage}`);
return { error: errorMessage };
}
};
// Validate URL format
const isValidUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
// Process multiple URLs
const processUrls = async (urls: string[]): Promise<void> => {
console.log(`Found ${urls.length} URLs to process`);
for (const url of urls) {
const trimmedUrl = url.trim();
if (!trimmedUrl) continue;
if (!isValidUrl(trimmedUrl)) {
console.error(`Invalid URL: ${trimmedUrl}`);
continue;
}
try {
const result = await scrapeSuno(trimmedUrl);
if (result.error) {
console.error(`Failed to process ${trimmedUrl}: ${result.error}`);
}
} catch (error) {
console.error(`Error processing ${trimmedUrl}: ${error instanceof Error ? error.message : String(error)}`);
}
}
console.log("\nAll downloads completed!");
};
// CLI entry point
const main = async (): Promise<void> => {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: suno <suno_url(s)>");
console.error("Multiple URLs should be separated by newlines");
process.exit(1);
}
// Join all arguments and split by newlines to handle multiple URLs
const urls = args.join(" ").split("\n").filter(url => url.trim());
if (urls.length === 0) {
console.error("No valid URLs provided");
process.exit(1);
}
await processUrls(urls);
};
// Handle unhandled rejections
process.on("unhandledRejection", (error: Error) => {
console.error("Unhandled rejection:", error);
process.exit(1);
});
main().catch((error: Error) => {
console.error("Fatal error:", error);
process.exit(1);
});