delete db -- oops
This commit is contained in:
parent
387785df7a
commit
70279bdab0
9 changed files with 221 additions and 21 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -36,4 +36,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||
|
||||
tmp
|
||||
localstorage.json
|
||||
localStorage.sqlite
|
||||
downloaded
|
BIN
aiot-mass-dl.exe
Normal file
BIN
aiot-mass-dl.exe
Normal file
Binary file not shown.
11
bun.lock
11
bun.lock
|
@ -7,10 +7,11 @@
|
|||
"@tidal-music/auth": "^1.3.4",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"bun-storage": "^0.2.1",
|
||||
"dasha": "3",
|
||||
"dasha": "^3.1.6",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"flac-stream-tagger": "^1.0.10",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"webview-bun": "^2.4.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
@ -28,7 +29,7 @@
|
|||
|
||||
"@tidal-music/true-time": ["@tidal-music/true-time@0.3.0", "", { "dependencies": { "@tidal-music/true-time": "0.3.0" } }, "sha512-nO9DfLKLu5sCI/v6Yp8DSseu8qis0RI1r6l7oIFCusOAedkIbQT/K+ELaaz7cxXR4cboopUZGW55v4RVwxHE/g=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.12", "", { "dependencies": { "bun-types": "1.2.12" } }, "sha512-lY/GQTXDGsolT/TiH72p1tuyUORuRrdV7VwOTOjDOt8uTBJQOJc5zz3ufwwDl0VBaoxotSk4LdP0hhjLJ6ypIQ=="],
|
||||
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||
|
||||
"@types/fluent-ffmpeg": ["@types/fluent-ffmpeg@2.1.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A=="],
|
||||
|
||||
|
@ -44,13 +45,13 @@
|
|||
|
||||
"bun-storage": ["bun-storage@0.2.1", "", {}, "sha512-yEgiKZ38eI8v4KO7mQcsRR7suCv+ZVQmM1uETyWc0CRQgJ8vZqyY6AbQCqlLfQ41EBOHiDvHhTxy3EquZomyZg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-tvWMx5vPqbRXgE8WUZI94iS1xAYs8bkqESR9cxBB1Wi+urvfTrF1uzuDgBHFAdO0+d2lmsbG3HmeKMvUyj6pWA=="],
|
||||
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
|
||||
|
||||
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
||||
|
||||
"concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="],
|
||||
|
||||
"dasha": ["dasha@3.1.5", "", { "dependencies": { "m3u8-parser": "^7.2.0" } }, "sha512-Zz/oCfZBPa2g5PlZA7uwfH9B76D0QrX6DYsTFFqBfAuMtB9MBPGOkEkHAbXL9JX7wyU2OiMXYZbMlyUT7uQ9hQ=="],
|
||||
"dasha": ["dasha@3.1.6", "", { "dependencies": { "m3u8-parser": "^7.2.0" } }, "sha512-3wAxSibBWzEMRjHCBQoHEd7YyeVbmiaqhDHUbBR6pZ/axOz5fq2jAo9c+QCTrbEnazCAW3bjnbit13v35WiupA=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
|
@ -100,6 +101,8 @@
|
|||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"webview-bun": ["webview-bun@2.4.0", "", {}, "sha512-0+ugnQlcUHmuW+iLeb+Lzb8rGUJh7WEdXvNsuvaVEXT3EagK380XdD7heVJu0Ek/mNxMY3G2JM142YRQ1hDUGQ=="],
|
||||
|
||||
"which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
|
||||
"http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="],
|
||||
|
|
BIN
index.exe
Normal file
BIN
index.exe
Normal file
Binary file not shown.
192
index.ts
192
index.ts
|
@ -13,13 +13,26 @@ Bun.serve({
|
|||
"/api/track/:id": async req => {
|
||||
const trackId = parseInt(req.params.id)
|
||||
|
||||
const file = Bun.file(`downloaded/${trackId}.flac`)
|
||||
const flac = Bun.file(`downloaded/${trackId}.flac`)
|
||||
|
||||
if (await file.exists()) {
|
||||
return new Response(await file.arrayBuffer(), {
|
||||
if (await flac.exists()) {
|
||||
return new Response(flac, {
|
||||
headers: {
|
||||
"Content-Type": "audio/flac",
|
||||
"Content-Disposition": `attachment; filename="${trackId}.flac"`,
|
||||
//"Content-Disposition": `attachment; filename="${trackId}.flac"`,
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"ETag": trackId.toString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const m4a = Bun.file(`downloaded/${trackId}.m4a`)
|
||||
|
||||
if (await m4a.exists()) {
|
||||
return new Response(m4a, {
|
||||
headers: {
|
||||
"Content-Type": "audio/m4a",
|
||||
//"Content-Disposition": `attachment; filename="${trackId}.m4a"`,
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"ETag": trackId.toString(),
|
||||
}
|
||||
|
@ -31,13 +44,14 @@ Bun.serve({
|
|||
const audio = await utils.downloadFlac(manifestMimeType, manifest);
|
||||
if (audio.mimeType === "audio/flac") {
|
||||
audio.buffer = Buffer.from(await utils.tagFlac(trackId, audio.buffer))
|
||||
await Bun.write(`downloaded/${trackId}.flac`, audio.buffer)
|
||||
} else if (audio.mimeType === "audio/m4a") {
|
||||
await Bun.write(`downloaded/${trackId}.m4a`, audio.buffer)
|
||||
}
|
||||
//await Bun.write(`downloaded/${trackId}.flac`, audio)
|
||||
|
||||
return new Response(audio.buffer, {
|
||||
headers: {
|
||||
"Content-Type": audio.mimeType,
|
||||
//"Content-Disposition": `attachment; filename="${trackId}.flac"`,
|
||||
"Cache-Control": "public, max-age=31536000",
|
||||
"ETag": trackId.toString(),
|
||||
}
|
||||
|
@ -55,10 +69,169 @@ Bun.serve({
|
|||
})
|
||||
}
|
||||
},
|
||||
development: true
|
||||
development: true,
|
||||
idleTimeout: 255
|
||||
})
|
||||
|
||||
const tracks = await utils.fetchTracks() as {
|
||||
limit: number,
|
||||
offset: number,
|
||||
totalNumberOfItems: number,
|
||||
items: {
|
||||
created: string,
|
||||
item: {
|
||||
id: string
|
||||
title: string
|
||||
duration: number, // in seconds
|
||||
replayGain: number,
|
||||
peak: number,
|
||||
allowStreaming: boolean,
|
||||
streamReady: boolean,
|
||||
payToStream: boolean,
|
||||
adSupportedStreamReady: boolean,
|
||||
djReady: boolean,
|
||||
stemReady: boolean,
|
||||
streamStartDate: string,
|
||||
premiumStreamingOnly: boolean,
|
||||
trackNumber: number,
|
||||
volumeNumber: number, // discNumber
|
||||
version: null, // ?
|
||||
popularity: number,
|
||||
copyright: string,
|
||||
bpm: null, // think this is only provided to DJ users
|
||||
url: string,
|
||||
isrc: string,
|
||||
editable: boolean,
|
||||
explicit: boolean,
|
||||
audioQuality: string,
|
||||
audioModes: "STEREO"[], // todo find other modes
|
||||
mediaMetadata: {
|
||||
tags: "LOSSLESS"[], // todo find other tags
|
||||
},
|
||||
upload: boolean, // was uploaded by a user
|
||||
artist: {
|
||||
id: number,
|
||||
name: string,
|
||||
handle: null, // ?
|
||||
type: "MAIN", // todo find other types
|
||||
picture: string,
|
||||
},
|
||||
artists: [
|
||||
{
|
||||
id: number,
|
||||
name: string,
|
||||
handle: null,
|
||||
type: "MAIN",
|
||||
picture: string,
|
||||
}
|
||||
],
|
||||
album: {
|
||||
id: number,
|
||||
title: string,
|
||||
cover: string,
|
||||
vibrantColor: string,
|
||||
videoCover: null,
|
||||
},
|
||||
mixes: {
|
||||
TRACK_MIX: string,
|
||||
},
|
||||
}
|
||||
}[]
|
||||
};
|
||||
|
||||
/* Remove all tracks that are unavailable
|
||||
for (const track of tracks.items) {
|
||||
if (track.item.streamReady) {
|
||||
continue
|
||||
}
|
||||
|
||||
await utils.fetch("/users/199235629/favorites/tracks/" + track.item.id, {
|
||||
method: "DELETE",
|
||||
})
|
||||
console.log("Removed from favorites", track.item.id)
|
||||
}
|
||||
*/
|
||||
/*
|
||||
const tracks = await utils.fetchTracks();
|
||||
function findDuplicateTracksByName(tracks: typeof tracks.items) {
|
||||
const seen = new Map<string, { count: number, originals: typeof tracks }>();
|
||||
|
||||
for (const trackObj of tracks) {
|
||||
const title = trackObj.item.title;
|
||||
const normalized = title.trim().toLowerCase();
|
||||
|
||||
if (!seen.has(normalized)) {
|
||||
seen.set(normalized, { count: 1, originals: [trackObj] });
|
||||
} else {
|
||||
const entry = seen.get(normalized)!;
|
||||
entry.count++;
|
||||
entry.originals.push(trackObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to only include titles with duplicates
|
||||
return Array.from(seen.entries())
|
||||
.filter(([_, v]) => v.count > 1)
|
||||
.map(([normalizedTitle, data]) => ({
|
||||
normalizedTitle,
|
||||
count: data.count,
|
||||
duplicates: data.originals,
|
||||
}));
|
||||
}
|
||||
|
||||
function findDuplicateTracksByISRC(tracks: typeof tracks.items) {
|
||||
const seen = new Map<string, { count: number, originals: typeof tracks }>();
|
||||
|
||||
for (const trackObj of tracks) {
|
||||
const isrc = trackObj.item.isrc;
|
||||
|
||||
// Skip if ISRC is missing or empty
|
||||
if (!isrc) continue;
|
||||
|
||||
if (!seen.has(isrc)) {
|
||||
seen.set(isrc, { count: 1, originals: [trackObj] });
|
||||
} else {
|
||||
const entry = seen.get(isrc)!;
|
||||
entry.count++;
|
||||
entry.originals.push(trackObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Only return those with duplicates
|
||||
return Array.from(seen.entries())
|
||||
.filter(([_, v]) => v.count > 1)
|
||||
.map(([isrc, data]) => ({
|
||||
name: data.originals[0].item.title,
|
||||
isrc,
|
||||
count: data.count,
|
||||
duplicates: data.originals,
|
||||
}));
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
// Usage:
|
||||
const duplicatesByISRC = findDuplicateTracksByISRC(tracks.items);
|
||||
duplicatesByISRC.forEach(({ name, isrc, count, duplicates }) => {
|
||||
console.log(`Name: ${name} ISRC: "${isrc}" - Count: ${count}`);
|
||||
duplicates.forEach((trackObj) => {
|
||||
//console.log(` - Track ID: ${trackObj.item.id}, Created: ${trackObj.created}`);
|
||||
});
|
||||
});
|
||||
*/
|
||||
/*
|
||||
// Usage:
|
||||
const duplicates = findDuplicateTracksByName(tracks.items);
|
||||
|
||||
console.log("Duplicate tracks:");
|
||||
duplicates.forEach(({ normalizedTitle, count, duplicates }) => {
|
||||
console.log(`Title: "${normalizedTitle}" - Count: ${count}`);
|
||||
duplicates.forEach((trackObj) => {
|
||||
//console.log(` - Track ID: ${trackObj.item.id}, Created: ${trackObj.created}`);
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
|
||||
let i = 1;
|
||||
for await (const track of tracks.items) {
|
||||
|
@ -99,11 +272,8 @@ for await (const track of tracks.items) {
|
|||
console.error(`Failed to download ${trackId}.flac`, e)
|
||||
}
|
||||
|
||||
// Downloaded track 1 of 10, etc
|
||||
|
||||
console.log(`Downloaded track ${i} of ${tracks.items.length}`)
|
||||
i++;
|
||||
}
|
||||
|
||||
console.log("Done")
|
||||
*/
|
Binary file not shown.
|
@ -11,10 +11,11 @@
|
|||
"@tidal-music/auth": "^1.3.4",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"bun-storage": "^0.2.1",
|
||||
"dasha": "3",
|
||||
"dasha": "^3.1.6",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"flac-stream-tagger": "^1.0.10",
|
||||
"fluent-ffmpeg": "^2.1.3"
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"webview-bun": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
|
|
|
@ -2,6 +2,8 @@ import "./localStorage";
|
|||
|
||||
import { init, initializeLogin, finalizeLogin, credentialsProvider } from "@tidal-music/auth";
|
||||
|
||||
import { Webview } from "webview-bun";
|
||||
|
||||
const clientId = "mhPVJJEBNRzVjr2p";
|
||||
|
||||
export default async () => {
|
||||
|
@ -19,9 +21,29 @@ export default async () => {
|
|||
redirectUri: "https://desktop.tidal.com/login/auth"
|
||||
});
|
||||
|
||||
console.log(`Please open ${response} to login.`)
|
||||
const webview = new Webview(true);
|
||||
|
||||
await finalizeLogin(new URL(prompt("Enter the URL you were redirected to: ") || "").search)
|
||||
webview.navigate(response);
|
||||
|
||||
let didLogin = false;
|
||||
webview.bind("getURL", async (url: string) => {
|
||||
if (url.startsWith("https://desktop.tidal.com")) {
|
||||
const code = new URL(url)
|
||||
|
||||
await finalizeLogin(code.search);
|
||||
didLogin = true;
|
||||
}
|
||||
});
|
||||
|
||||
webview.init("getURL(location.href)");
|
||||
|
||||
webview.run()
|
||||
|
||||
while (!didLogin) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
webview.destroy()
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class {
|
|||
"en_US").replaceAll("-", "_").split(".")[0];
|
||||
}
|
||||
|
||||
async fetch(url: string) {
|
||||
async fetch(url: string, opts: { [key: string]: any } = {}) {
|
||||
const parsedUrl = new URL(`https://desktop.tidal.com/v1${url}`);
|
||||
|
||||
parsedUrl.searchParams.set("countryCode", this._userCountry);
|
||||
|
@ -37,6 +37,7 @@ export default class {
|
|||
"x-tidal-token": "mhPVJJEBNRzVjr2p",
|
||||
},
|
||||
referrer: "https://desktop.tidal.com/",
|
||||
...opts
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
@ -150,6 +151,7 @@ export default class {
|
|||
status?: string,
|
||||
};
|
||||
|
||||
|
||||
const albumReq = await fetch(`https://desktop.tidal.com/v1/albums/${track.album.id}/?countryCode=US`, {
|
||||
headers: this._authHeaders
|
||||
})
|
||||
|
@ -170,6 +172,7 @@ export default class {
|
|||
const albumArtReq = await fetch(`https://resources.tidal.com/images/${album?.cover.replaceAll("-", "/") || ""}/1280x1280.jpg`)
|
||||
const albumArt = albumArtReq.ok ? await albumArtReq.arrayBuffer() : new ArrayBuffer(0);
|
||||
|
||||
|
||||
return FlacStreamTagger.fromBuffer(Buffer.from(Buffer.isBuffer(audioBuffer) ? audioBuffer : Buffer.from(audioBuffer)), {
|
||||
tagMap: {
|
||||
title: track?.title || "",
|
||||
|
|
Loading…
Add table
Reference in a new issue