initial commit

This commit is contained in:
uh wot 2022-06-15 02:29:57 +02:00
commit 19199efe16
Signed by: uhwot
GPG key ID: CB2454984587B781
5 changed files with 864 additions and 0 deletions

131
src/index.js Normal file
View 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))
)