From fdd1151cbe413028c306e444e86ec8c0279e5634 Mon Sep 17 00:00:00 2001 From: uh wot Date: Mon, 20 Jun 2022 20:08:01 +0200 Subject: [PATCH] added album and playlist support, cleaned up code --- package.json | 2 +- src/index.js | 109 +++++++++++++++++++++++++++++++++++++++------------ yarn.lock | 8 ++-- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index c45678b..152cd3a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tdlworker", "version": "0.0.0", "devDependencies": { - "wrangler": "2.0.12" + "wrangler": "^2.0.14" }, "private": true, "scripts": { diff --git a/src/index.js b/src/index.js index f9c3240..690cc07 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,11 @@ const CLIENT_ID = 'dN2N95wCyEBTllu4' // mobile atmos const AUDIO_QUALITIES = ['LOW', 'HIGH', 'LOSSLESS', 'HI_RES'] const VIDEO_QUALITIES = ['AUDIO_ONLY', 'LOW', 'MEDIUM', 'HIGH'] +const TYPE_QUALITY_INFO = { + tracks: { default: 'LOSSLESS', list: AUDIO_QUALITIES, param: 'audioquality' }, + videos: { default: 'HIGH', list: VIDEO_QUALITIES, param: 'videoquality' }, +} + function to_form_data(obj) { return Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('&') } @@ -62,25 +67,28 @@ async function api_call(method, path, params={}, body=null) { return { status, json } } -router.get('/track/:id', async ({ params, query }) => { - let id = parseInt(params.id) +async function playbackinfopostpaywall(type, id, q) { + id = parseInt(id) if (isNaN(id)) { - return new Response('Invalid track id', { status: 400 }) + return new Response('Invalid id', { status: 400 }) } - let quality = query.q || 'LOSSLESS' + const quality_info = TYPE_QUALITY_INFO[type] + let quality = q || quality_info.default quality = quality.toUpperCase() - if (!AUDIO_QUALITIES.includes(quality)) { + if (!quality_info.list.includes(quality)) { return new Response('Invalid quality', { status: 400 }) } - const res = await api_call('GET', `tracks/${id}/playbackinfopostpaywall`, { - audioquality: quality, + let params = { assetpresentation: 'FULL', playbackmode: 'OFFLINE' - }) + } + params[quality_info.param] = quality - console.log(res.status, res.json) + const res = await api_call('GET', `${type}/${id}/playbackinfopostpaywall`, params) + + console.log('playbackinfopostpaywall', res.status, res.json) if (res.status !== 200) { return new Response('Couldn\'t get manifest', { status: 403 }) @@ -92,38 +100,89 @@ router.get('/track/:id', async ({ params, query }) => { const manifest = JSON.parse(atob(res.json.manifest)) return Response.redirect(manifest.urls[0], 302) +} + +router.get('/track/:id', async ({ params, query }) => { + return await playbackinfopostpaywall('tracks', params.id, query.q) }) router.get('/video/:id', async ({ params, query }) => { - let id = parseInt(params.id) - if (isNaN(id)) { - return new Response('Invalid video id', { status: 400 }) + return await playbackinfopostpaywall('videos', params.id, query.q) +}) + +function m3u8(items, audio_quality, video_quality, host) { + let list = '#EXTM3U\n' + + for (const item_info of items) { + const item = item_info.item + const type = item_info.type + + let quality + if (type === 'track') { + quality = audio_quality + } else { + quality = video_quality + } + + const url = `https://${host}/${type}/${item.id}?q=${quality}` + list += `#EXTINF:${item.duration},${item.title}\n${url}\n` } - let quality = query.q || 'HIGH' - quality = quality.toUpperCase() - if (!VIDEO_QUALITIES.includes(quality)) { - return new Response('Invalid quality', { status: 400 }) + return list +} + +async function get_items(type, id, aq, vq, host) { + let audio_quality = aq || 'LOSSLESS' + audio_quality = audio_quality.toUpperCase() + if (!AUDIO_QUALITIES.includes(audio_quality)) { + return new Response('Invalid audio quality', { status: 400 }) } - const res = await api_call('GET', `videos/${id}/playbackinfopostpaywall`, { - videoquality: quality, - assetpresentation: 'FULL', - playbackmode: 'OFFLINE' + let video_quality = vq || 'HIGH' + video_quality = video_quality.toUpperCase() + if (!VIDEO_QUALITIES.includes(video_quality)) { + return new Response('Invalid video quality', { status: 400 }) + } + + const res = await api_call('GET', `${type}/${id}/items`, { + offset: 0, + limit: 4294967295, + countryCode: 'GB', }) console.log(res.status, res.json) if (res.status !== 200) { - return new Response('Couldn\'t get manifest', { status: 403 }) + return new Response('Couldn\'t get items', { status: 403 }) } - if (res.json.manifestMimeType !== 'application/vnd.tidal.bts') { - return new Response('Invalid manifest mime type', { status: 500 }) + const m3u = m3u8(res.json.items, audio_quality, video_quality, host) + + return new Response(m3u, { + headers: { + 'Content-Type': 'application/x-mpegurl' + } + }) +} + +router.get('/album/:id', async ({ params, query, headers }) => { + let id = parseInt(params.id) + if (isNaN(id)) { + return new Response('Invalid album id', { status: 400 }) } - const manifest = JSON.parse(atob(res.json.manifest)) - return Response.redirect(manifest.urls[0], 302) + return await get_items('albums', id, query.aq, query.vq, headers.get('host')) +}) + +router.get('/playlist/:id', async ({ params, query, headers }) => { + let id = params.id + + const uuid_regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; + if (!uuid_regex.test(id)) { + return new Response('Invalid playlist id', { status: 400 }) + } + + return await get_items('playlists', id, query.aq, query.vq, headers.get('host')) }) addEventListener('fetch', event => diff --git a/yarn.lock b/yarn.lock index 4d73d9b..f3a7562 100644 --- a/yarn.lock +++ b/yarn.lock @@ -503,10 +503,10 @@ urlpattern-polyfill@^4.0.3: resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-4.0.3.tgz#c1fa7a73eb4e6c6a1ffb41b24cf31974f7392d3b" integrity sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ== -wrangler@2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-2.0.12.tgz#506af20c67aa2b8dbed480713f1103814a943d08" - integrity sha512-Dnzqxl3vW5J16Lt3gEbl/teNnTuAqMwsuMexR7BFSHWTwfWjHT9Sc7B6bPIKw0O01zDU/hUscw8nkyhMMM3xDw== +wrangler@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-2.0.14.tgz#3f89bde117ba5e459f29a9be3a4f51a2f2706813" + integrity sha512-uPfz2yJ13nbhEdSqdXBoXBANB60cwZ77KkHcP8AIUWHj2GJovQFvvfbBZwO1Ok8JWPhfpkICnIiGU6IBrT7p/w== dependencies: "@cloudflare/kv-asset-handler" "^0.2.0" "@esbuild-plugins/node-globals-polyfill" "^0.1.1"