initial commit
This commit is contained in:
commit
19199efe16
5 changed files with 864 additions and 0 deletions
131
src/index.js
Normal file
131
src/index.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
import { Router } from 'itty-router'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const CLIENT_ID = 'WAU9gXp3tHhK4Nns' // mobile default
|
||||
|
||||
const AUDIO_QUALITIES = ['LOW', 'HIGH', 'LOSSLESS', 'HI_RES']
|
||||
const VIDEO_QUALITIES = ['AUDIO_ONLY', 'LOW', 'MEDIUM', 'HIGH']
|
||||
|
||||
function to_form_data(obj) {
|
||||
return Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('&')
|
||||
}
|
||||
|
||||
async function renew_token() {
|
||||
const res = await fetch('https://auth.tidal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: to_form_data({
|
||||
client_id: CLIENT_ID,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: REFRESH_TOKEN,
|
||||
scope: 'r_usr'
|
||||
})
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
|
||||
const access_token = json.access_token
|
||||
console.log('access_token', access_token)
|
||||
await KV.put('access_token', access_token, {expirationTtl: json.expires_in})
|
||||
return access_token
|
||||
}
|
||||
|
||||
async function api_call(method, path, params={}, body=null) {
|
||||
let url = `https://api.tidal.com/v1/${path}`
|
||||
if (params) {
|
||||
url += '?' + to_form_data(params)
|
||||
}
|
||||
|
||||
let access_token = await KV.get('access_token')
|
||||
if (access_token === null) {
|
||||
access_token = await renew_token()
|
||||
}
|
||||
|
||||
if (body) {
|
||||
body = to_form_data(body)
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${access_token}`
|
||||
},
|
||||
body
|
||||
})
|
||||
|
||||
const status = res.status
|
||||
const json = await res.json()
|
||||
return { status, json }
|
||||
}
|
||||
|
||||
router.get('/track/:id', async ({ params, query }) => {
|
||||
let id = parseInt(params.id)
|
||||
if (isNaN(id)) {
|
||||
return new Response('Invalid track id', { status: 400 })
|
||||
}
|
||||
|
||||
let quality = query.q || 'LOSSLESS'
|
||||
quality = quality.toUpperCase()
|
||||
if (!AUDIO_QUALITIES.includes(quality)) {
|
||||
return new Response('Invalid quality', { status: 400 })
|
||||
}
|
||||
|
||||
const res = await api_call('GET', `tracks/${id}/playbackinfopostpaywall`, {
|
||||
audioquality: quality,
|
||||
assetpresentation: 'FULL',
|
||||
playbackmode: 'OFFLINE'
|
||||
})
|
||||
|
||||
console.log(res.status, res.json)
|
||||
|
||||
if (res.status !== 200) {
|
||||
return new Response('Couldn\'t get manifest', { status: 403 })
|
||||
}
|
||||
|
||||
if (res.json.manifestMimeType !== 'application/vnd.tidal.bts') {
|
||||
return new Response('Invalid manifest mime type', { status: 500 })
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(atob(res.json.manifest))
|
||||
return Response.redirect(manifest.urls[0], 302)
|
||||
})
|
||||
|
||||
router.get('/video/:id', async ({ params, query }) => {
|
||||
let id = parseInt(params.id)
|
||||
if (isNaN(id)) {
|
||||
return new Response('Invalid video id', { status: 400 })
|
||||
}
|
||||
|
||||
let quality = query.q || 'HIGH'
|
||||
quality = quality.toUpperCase()
|
||||
if (!VIDEO_QUALITIES.includes(quality)) {
|
||||
return new Response('Invalid quality', { status: 400 })
|
||||
}
|
||||
|
||||
const res = await api_call('GET', `videos/${id}/playbackinfopostpaywall`, {
|
||||
videoquality: quality,
|
||||
assetpresentation: 'FULL',
|
||||
playbackmode: 'OFFLINE'
|
||||
})
|
||||
|
||||
console.log(res.status, res.json)
|
||||
|
||||
if (res.status !== 200) {
|
||||
return new Response('Couldn\'t get manifest', { status: 403 })
|
||||
}
|
||||
|
||||
if (res.json.manifestMimeType !== 'application/vnd.tidal.bts') {
|
||||
return new Response('Invalid manifest mime type', { status: 500 })
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(atob(res.json.manifest))
|
||||
return Response.redirect(manifest.urls[0], 302)
|
||||
})
|
||||
|
||||
addEventListener('fetch', event =>
|
||||
event.respondWith(router.handle(event.request))
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue