您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download SoundCloud without external service.
- // ==UserScript==
- // @name Local SoundCloud Downloader
- // @namespace https://blog.maple3142.net/
- // @version 0.1.4
- // @description Download SoundCloud without external service.
- // @author maple3142
- // @match https://soundcloud.com/*
- // @require https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js
- // @require https://cdn.jsdelivr.net/npm/streamsaver@2.0.3/StreamSaver.min.js
- // @grant none
- // @icon https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico
- // ==/UserScript==
- streamSaver.mitm = 'https://maple3142.github.io/StreamSaver.js/mitm.html'
- function hook(obj, name, callback, type) {
- const fn = obj[name]
- obj[name] = function (...args) {
- if (type === 'before') callback.apply(this, args)
- fn.apply(this, args)
- if (type === 'after') callback.apply(this, args)
- }
- return () => {
- // restore
- obj[name] = fn
- }
- }
- function triggerDownload(url, name) {
- const a = document.createElement('a')
- document.body.appendChild(a)
- a.href = url
- a.download = name
- a.click()
- a.remove()
- }
- const btn = {
- init() {
- this.el = document.createElement('button')
- this.el.textContent = 'Download'
- this.el.classList.add('sc-button')
- this.el.classList.add('sc-button-medium')
- this.el.classList.add('sc-button-responsive')
- this.el.classList.add('sc-button-download')
- },
- cb() {
- const par = document.querySelector('.sc-button-toolbar .sc-button-group')
- if (par && this.el.parentElement !== par) par.insertAdjacentElement('beforeend', this.el)
- },
- attach() {
- this.detach()
- this.observer = new MutationObserver(this.cb.bind(this))
- this.observer.observe(document.body, { childList: true, subtree: true })
- this.cb()
- },
- detach() {
- if (this.observer) this.observer.disconnect()
- }
- }
- btn.init()
- async function getClientId() {
- return new Promise(resolve => {
- const restore = hook(
- XMLHttpRequest.prototype,
- 'open',
- async (method, url) => {
- const u = new URL(url, document.baseURI)
- const clientId = u.searchParams.get('client_id')
- if (!clientId) return
- console.log('got clientId', clientId)
- restore()
- resolve(clientId)
- },
- 'after'
- )
- })
- }
- const clientIdPromise = getClientId()
- let controller = null
- async function load(by) {
- btn.detach()
- console.log('load by', by, location.href)
- if (/^(\/(you|stations|discover|stream|upload|search|settings))/.test(location.pathname)) return
- const clientId = await clientIdPromise
- if (controller) {
- controller.abort()
- controller = null
- }
- controller = new AbortController()
- const result = await fetch(
- `https://api-v2.soundcloud.com/resolve?url=${encodeURIComponent(location.href)}&client_id=${clientId}`,
- { signal: controller.signal }
- ).then(r => r.json())
- console.log('result', result)
- if (result.kind !== 'track') return
- btn.el.onclick = async () => {
- const progressive = result.media.transcodings.find(t => t.format.protocol === 'progressive')
- if (progressive) {
- const { url } = await fetch(progressive.url + `?client_id=${clientId}`).then(r => r.json())
- const resp = await fetch(url)
- const ws = streamSaver.createWriteStream(result.title + '.mp3', {
- size: resp.headers.get('Content-Length')
- })
- const rs = resp.body
- if (rs.pipeTo) {
- console.log(rs, ws)
- return rs.pipeTo(ws)
- }
- const reader = rs.getReader()
- const writer = ws.getWriter()
- const pump = () =>
- reader.read().then(res => (res.done ? writer.close() : writer.write(res.value).then(pump)))
- return pump()
- }
- alert('Sorry, downloading this music is currently unsupported.')
- }
- btn.attach()
- console.log('attached')
- }
- load('init')
- hook(history, 'pushState', () => load('pushState'), 'after')
- window.addEventListener('popstate', () => load('popstate'))