downlederigoೃ

This commit is contained in:
Reid 2025-04-26 18:40:13 -07:00
parent 4954f84a48
commit b8da59086b
Signed by: reidlab
GPG key ID: DAF5EAF6665839FD
10 changed files with 170 additions and 19 deletions

50
src/downloader/index.ts Normal file
View file

@ -0,0 +1,50 @@
import { config } from "../config.js";
import { spawn } from "node:child_process";
import path from "node:path";
import { addToCache, isCached } from "../cache.js";
// TODO: make this have a return type
export async function downloadSong(streamUrl: string, decryptionKey: string): Promise<void> {
const baseOutputName = streamUrl.split("/").at(-1)?.split("?").at(0)?.split(".").splice(0, 1).join(".")?.trim();
if (!baseOutputName) { throw "could not get base output name from stream url"; }
const encryptedName = baseOutputName + "_enc.mp4";
const encryptedPath = path.join(config.downloader.cache.directory, encryptedName);
const decryptedName = baseOutputName + ".mp4";
const decryptedPath = path.join(config.downloader.cache.directory, decryptedName);
if ( // TODO: remove check for encrypted file/cache for encrypted?
isCached(encryptedName) &&
isCached(decryptedName)
) { return; }
await new Promise<void>((res, rej) => {
const child = spawn(config.downloader.ytdlp_path, [
"--quiet",
"--no-warnings",
"--allow-unplayable-formats",
"--fixup", "never",
"--paths", config.downloader.cache.directory,
"--output", encryptedName,
streamUrl
]).on("error", (err) => { rej(err); });
child.stderr.on("data", (chunk) => { rej(chunk); });
child.on("exit", () => { res(); });
});
await new Promise<void>((res, rej) => {
const child = spawn(config.downloader.ffmpeg_path, [
"-loglevel", "error",
"-y",
"-decryption_key", decryptionKey,
"-i", encryptedPath,
"-c", "copy",
"-movflags", "+faststart",
decryptedPath
]).on("error", (err) => { rej(err); });
child.stderr.on("data", (chunk) => { rej(chunk); });
child.on("exit", () => { res(); } );
});
addToCache(encryptedName);
addToCache(decryptedName);
}

View file

@ -19,7 +19,7 @@ export async function getWidevineDecryptionKey(psshDataUri: string, trackId: str
// for some reason, if gotten from a webplayback manifest, the pssh is in a completely different format
// well, somewhat. it's just the raw data, we have to rebuild the pssh
const rebuiltPssh = psshTools.widevine.encodePssh({
contentId: "Hiiii", // lol?? i don't know what this is, random slop go!!!!
contentId: "meow", // this actually isn't even needed, but this library is stubborn
dataOnly: false,
keyIds: [Buffer.from(dataUriToBuffer(psshDataUri).buffer).toString("hex")]
});

View file

@ -7,6 +7,7 @@ import { getWidevineDecryptionKey } from "./keygen.js";
import { widevine, playready, fairplay } from "../constants/keyFormats.js";
import { select } from "@inquirer/prompts";
import type { WebplaybackResponse } from "api/appleMusicApi.js";
import { downloadSong } from "./index.js";
// ugliest type ever
// this library is so bad
@ -27,24 +28,29 @@ type M3u8 = ReturnType<typeof hls.default.parse>;
// OH. it doesn't seem to give the keys you want anyway LOLLLLLLL????
// i'm sure its used for *SOMETHING* so i'll keep it
export class StreamInfo {
export default class StreamInfo {
public readonly trackId: string;
public readonly streamUrl: string;
public readonly widevinePssh: string | undefined;
public readonly playreadyPssh: string | undefined;
public readonly fairplayKey: string | undefined;
private constructor(
trackId: string,
streamUrl: string,
widevinePssh: string | undefined,
playreadyPssh: string | undefined,
fairplayKey: string | undefined
) {
this.trackId = trackId;
this.streamUrl = streamUrl;
this.widevinePssh = widevinePssh;
this.playreadyPssh = playreadyPssh;
this.fairplayKey = fairplayKey;
}
// TODO: why can't we decrypt widevine ones with this?
// we get a valid key.. but it doesn't work :-(
public static async fromTrackMetadata(trackMetadata: SongAttributes<["extendedAssetUrls"]>): Promise<StreamInfo> {
log.warn("the track metadata method is experimental, and may not work or give correct values!");
log.warn("if there is a failure--use a codec that uses the webplayback method");
@ -70,6 +76,7 @@ export class StreamInfo {
return new StreamInfo(
trackId,
m3u8Url, // TODO: make this keep in mind the CODEC, yt-dlp will shit itself if not supplied i think
widevinePssh,
playreadyPssh,
fairplayKey
@ -95,6 +102,7 @@ export class StreamInfo {
// afaik this ONLY has widevine
return new StreamInfo(
trackId,
m3u8Url,
widevinePssh,
undefined,
undefined
@ -121,7 +129,6 @@ function getDrmInfos(m3u8Data: M3u8): DrmInfos {
throw "m3u8 missing audio session key info!";
}
type AssetInfos = { [key: string]: { "AUDIO-SESSION-KEY-IDS": string[]; }; }
function getAssetInfos(m3u8Data: M3u8): AssetInfos {
// LOL??? THIS LIBRARY IS SO BAD
@ -176,7 +183,9 @@ const getPlayreadyPssh = (drmInfos: DrmInfos, drmIds: string[]): string | undefi
const getFairplayKey = (drmInfos: DrmInfos, drmIds: string[]): string | undefined => getDrmData(drmInfos, drmIds, fairplay);
// TODO: remove later, this is just for testing
const streamInfo1 = await StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback("1615276490"), "32:ctrp64");
const streamInfo2 = await StreamInfo.fromTrackMetadata((await appleMusicApi.getSong("1615276490")).data[0].attributes);
if (streamInfo1.widevinePssh !== undefined) { log.debug(await getWidevineDecryptionKey(streamInfo1.widevinePssh, streamInfo1.trackId)); }
if (streamInfo2.widevinePssh !== undefined) { log.debug(await getWidevineDecryptionKey(streamInfo2.widevinePssh, streamInfo2.trackId)); }
const streamInfo1 = await StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback("1744965708"), "32:ctrp64");
// const streamInfo2 = await StreamInfo.fromTrackMetadata((await appleMusicApi.getSong("1615276490")).data[0].attributes);
// if (streamInfo1.widevinePssh !== undefined) { log.debug(await getWidevineDecryptionKey(streamInfo1.widevinePssh, streamInfo1.trackId)); }
// if (streamInfo2.widevinePssh !== undefined) { log.debug(await getWidevineDecryptionKey(streamInfo2.widevinePssh, streamInfo2.trackId)); }
if (streamInfo1.widevinePssh !== undefined) { await downloadSong(streamInfo1.streamUrl, await getWidevineDecryptionKey(streamInfo1.widevinePssh, streamInfo1.trackId)); }