mirror of
https://git.stupid.fish/teidesu/scripts.git
synced 2025-07-28 02:32:11 +10:00
chore: update public repo
This commit is contained in:
parent
c2410ec787
commit
bb5311f828
1 changed files with 118 additions and 37 deletions
|
@ -16,7 +16,7 @@ const ffetchApi = ffetchBase.extend({
|
||||||
baseUrl: 'https://api-v2.soundcloud.com',
|
baseUrl: 'https://api-v2.soundcloud.com',
|
||||||
query: {
|
query: {
|
||||||
client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4',
|
client_id: '4BowhSywvkJtklODQDzjNMq9sK9wyDJ4',
|
||||||
app_version: '1736857534',
|
app_version: '1738322252',
|
||||||
app_locale: 'en',
|
app_locale: 'en',
|
||||||
},
|
},
|
||||||
addons: [
|
addons: [
|
||||||
|
@ -83,6 +83,18 @@ const ScPlaylist = z.object({
|
||||||
})
|
})
|
||||||
type ScPlaylist = z.infer<typeof ScPlaylist>
|
type ScPlaylist = z.infer<typeof ScPlaylist>
|
||||||
|
|
||||||
|
const ScUser = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
kind: z.literal('user'),
|
||||||
|
permalink_url: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
|
||||||
|
likes_count: z.number(),
|
||||||
|
track_count: z.number(),
|
||||||
|
playlist_likes_count: z.number(),
|
||||||
|
})
|
||||||
|
type ScUser = z.infer<typeof ScUser>
|
||||||
|
|
||||||
const ScLike = z.object({
|
const ScLike = z.object({
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
kind: z.literal('like'),
|
kind: z.literal('like'),
|
||||||
|
@ -96,15 +108,6 @@ function extractHydrationData(html: string) {
|
||||||
return JSON.parse(script.html()!.replace('window.__sc_hydration = ', '').slice(0, -1))
|
return JSON.parse(script.html()!.replace('window.__sc_hydration = ', '').slice(0, -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchTrackByUrl(url: string) {
|
|
||||||
const html = await ffetchHtml(url).text()
|
|
||||||
const hydrationData = extractHydrationData(html)
|
|
||||||
const track = hydrationData.find(it => it.hydratable === 'sound')
|
|
||||||
if (!track) throw new Error('no track found')
|
|
||||||
|
|
||||||
return ScTrack.parse(track.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPlaylistByUrl(url: string) {
|
async function fetchPlaylistByUrl(url: string) {
|
||||||
const html = await ffetchHtml(url).text()
|
const html = await ffetchHtml(url).text()
|
||||||
const hydrationData = extractHydrationData(html)
|
const hydrationData = extractHydrationData(html)
|
||||||
|
@ -318,11 +321,7 @@ async function downloadLikes(username: string) {
|
||||||
const hydrationData = extractHydrationData(userPage)
|
const hydrationData = extractHydrationData(userPage)
|
||||||
const user = hydrationData.find(it => it.hydratable === 'user')
|
const user = hydrationData.find(it => it.hydratable === 'user')
|
||||||
if (!user) throw new Error('no user found')
|
if (!user) throw new Error('no user found')
|
||||||
const userData = z.object({
|
const userData = ScUser.parse(user.data)
|
||||||
likes_count: z.number(),
|
|
||||||
playlist_likes_count: z.number(),
|
|
||||||
id: z.number(),
|
|
||||||
}).parse(user.data)
|
|
||||||
|
|
||||||
const tracks: ScTrack[] = []
|
const tracks: ScTrack[] = []
|
||||||
const playlists: ScPlaylist[] = []
|
const playlists: ScPlaylist[] = []
|
||||||
|
@ -366,7 +365,7 @@ async function downloadLikes(username: string) {
|
||||||
spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` })
|
spinnies.succeed('collect', { text: `collected ${tracks.length} tracks and ${playlists.length} playlists` })
|
||||||
|
|
||||||
spinnies.add('tracks', { text: 'downloading tracks...' })
|
spinnies.add('tracks', { text: 'downloading tracks...' })
|
||||||
const downloaded = 0
|
let downloaded = 0
|
||||||
const updateTracksSpinner = () => {
|
const updateTracksSpinner = () => {
|
||||||
spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` })
|
spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` })
|
||||||
}
|
}
|
||||||
|
@ -375,21 +374,22 @@ async function downloadLikes(username: string) {
|
||||||
const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`)
|
const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(username)}-likes`)
|
||||||
await mkdir(baseDir, { recursive: true })
|
await mkdir(baseDir, { recursive: true })
|
||||||
|
|
||||||
// await asyncPool(tracks, async (track) => {
|
await asyncPool(tracks, async (track) => {
|
||||||
// const filename = `${track.user.username} - ${track.title}`
|
const filename = `${track.user.username} - ${track.title}`
|
||||||
// spinnies.add(`${track.id}`, { text: filename })
|
spinnies.add(`${track.id}`, { text: filename })
|
||||||
// await downloadTrack(track, {
|
await downloadTrack(track, {
|
||||||
// destination: join(baseDir, sanitizeFilename(filename)),
|
destination: join(baseDir, sanitizeFilename(filename)),
|
||||||
// onRateLimit: (wait) => {
|
onRateLimit: (wait) => {
|
||||||
// spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` })
|
spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` })
|
||||||
// },
|
},
|
||||||
// onCdnRateLimit: () => {
|
onCdnRateLimit: () => {
|
||||||
// spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` })
|
spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` })
|
||||||
// },
|
},
|
||||||
// })
|
})
|
||||||
// spinnies.remove(`${track.id}`)
|
spinnies.remove(`${track.id}`)
|
||||||
// updateTracksSpinner()
|
downloaded += 1
|
||||||
// })
|
updateTracksSpinner()
|
||||||
|
})
|
||||||
|
|
||||||
spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` })
|
spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` })
|
||||||
spinnies.stopAll()
|
spinnies.stopAll()
|
||||||
|
@ -404,6 +404,75 @@ async function downloadLikes(username: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadUser(user: ScUser) {
|
||||||
|
const tracks: ScTrack[] = []
|
||||||
|
const spinnies = new Spinnies()
|
||||||
|
|
||||||
|
spinnies.add('collect')
|
||||||
|
const updateSpinner = () => {
|
||||||
|
const percent = Math.floor(tracks.length / user.track_count * 100)
|
||||||
|
spinnies.update('collect', {
|
||||||
|
text: `[${percent}%] collecting user tracks: ${tracks.length}/${user.track_count}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
updateSpinner()
|
||||||
|
|
||||||
|
let offset = '0'
|
||||||
|
while (true) {
|
||||||
|
const res = await ffetchApi(`/users/${user.id}/tracks`, {
|
||||||
|
query: {
|
||||||
|
limit: 100,
|
||||||
|
offset,
|
||||||
|
linked_partitioning: '1',
|
||||||
|
},
|
||||||
|
}).parsedJson(z.object({
|
||||||
|
collection: z.array(ScTrack),
|
||||||
|
next_href: z.string().nullable(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
for (const track of res.collection) {
|
||||||
|
tracks.push(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSpinner()
|
||||||
|
|
||||||
|
if (!res.next_href) break
|
||||||
|
offset = new URL(res.next_href).searchParams.get('offset')!
|
||||||
|
}
|
||||||
|
|
||||||
|
spinnies.succeed('collect', { text: `collected ${tracks.length} tracks` })
|
||||||
|
|
||||||
|
spinnies.add('tracks', { text: 'downloading tracks...' })
|
||||||
|
let downloaded = 0
|
||||||
|
const updateTracksSpinner = () => {
|
||||||
|
spinnies.update('tracks', { text: `[${downloaded}/${tracks.length}] downloading tracks...` })
|
||||||
|
}
|
||||||
|
updateTracksSpinner()
|
||||||
|
|
||||||
|
const baseDir = join('assets/soundcloud-dl', `${sanitizeFilename(user.username)}-tracks`)
|
||||||
|
await mkdir(baseDir, { recursive: true })
|
||||||
|
|
||||||
|
await asyncPool(tracks, async (track) => {
|
||||||
|
const filename = track.title
|
||||||
|
spinnies.add(`${track.id}`, { text: filename })
|
||||||
|
await downloadTrack(track, {
|
||||||
|
destination: join(baseDir, sanitizeFilename(filename)),
|
||||||
|
onRateLimit: (wait) => {
|
||||||
|
spinnies.update(`${track.id}`, { text: `[rate limit ${Math.floor(wait / 1000)}s] ${filename}` })
|
||||||
|
},
|
||||||
|
onCdnRateLimit: () => {
|
||||||
|
spinnies.update(`${track.id}`, { text: `[cdn rate limit] ${filename}` })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
downloaded += 1
|
||||||
|
spinnies.remove(`${track.id}`)
|
||||||
|
updateTracksSpinner()
|
||||||
|
})
|
||||||
|
|
||||||
|
spinnies.succeed('tracks', { text: `downloaded ${downloaded} tracks` })
|
||||||
|
spinnies.stopAll()
|
||||||
|
}
|
||||||
|
|
||||||
const url = process.argv[2] ?? await question('url > ')
|
const url = process.argv[2] ?? await question('url > ')
|
||||||
if (!url.startsWith('https://soundcloud.com/')) {
|
if (!url.startsWith('https://soundcloud.com/')) {
|
||||||
console.error('url must start with https://soundcloud.com/')
|
console.error('url must start with https://soundcloud.com/')
|
||||||
|
@ -415,10 +484,22 @@ if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/sets\//i)) {
|
||||||
} else if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/likes/i)) {
|
} else if (url.match(/^https:\/\/soundcloud.com\/[a-z0-9-]+\/likes/i)) {
|
||||||
await downloadLikes(url.match(/^https:\/\/soundcloud.com\/([a-z0-9-]+)\/likes/i)![1])
|
await downloadLikes(url.match(/^https:\/\/soundcloud.com\/([a-z0-9-]+)\/likes/i)![1])
|
||||||
} else {
|
} else {
|
||||||
const track = await fetchTrackByUrl(url)
|
const html = await ffetchHtml(url).text()
|
||||||
|
|
||||||
|
const hydrationData = extractHydrationData(html)
|
||||||
|
const trackData = hydrationData.find(it => it.hydratable === 'sound')
|
||||||
|
if (trackData) {
|
||||||
|
const track = ScTrack.parse(trackData.data)
|
||||||
const filename = `${track.user.username} - ${track.title}`
|
const filename = `${track.user.username} - ${track.title}`
|
||||||
console.log('downloading track:', filename)
|
console.log('downloading track:', filename)
|
||||||
await downloadTrack(track, {
|
await downloadTrack(track, {
|
||||||
destination: join('assets/soundcloud-dl', sanitizeFilename(filename)),
|
destination: join('assets/soundcloud-dl', sanitizeFilename(filename)),
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
const userData = hydrationData.find(it => it.hydratable === 'user')
|
||||||
|
if (userData) {
|
||||||
|
const user = ScUser.parse(userData.data)
|
||||||
|
await downloadUser(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue