diff --git a/src/constants/codecs.ts b/src/constants/codecs.ts index 92a830a..991f66e 100644 --- a/src/constants/codecs.ts +++ b/src/constants/codecs.ts @@ -1,13 +1,14 @@ -import { RegularCodec } from "../downloader/index.js"; +import type { RegularCodecType } from "downloader/codecType.js"; -export const songCodecRegex: { [key in RegularCodec]: RegExp } = { - [RegularCodec.Aac]: /audio-stereo-\d+/, - [RegularCodec.AacHe]: /audio-HE-stereo-\d+/, - [RegularCodec.AacBinaural]: /audio-stereo-\d+-binaural/, - [RegularCodec.AacDownmix]: /audio-stereo-\d+-downmix/, - [RegularCodec.AacHeBinaural]: /audio-HE-stereo-\d+-binaural/, - [RegularCodec.AacHeDownmix]: /audio-HE-stereo-\d+-downmix/, - [RegularCodec.Atmos]: /audio-atmos-.*/, - [RegularCodec.Ac3]: /audio-ac3-.*/, - [RegularCodec.Alac]: /audio-alac-.*/ + +export const songCodecRegex: { [key in RegularCodecType]: RegExp } = { + "aac": /audio-stereo-\d+/, + "aac_he": /audio-HE-stereo-\d+/, + "aac_binaural": /audio-stereo-\d+-binaural/, + "aac_downmix": /audio-stereo-\d+-downmix/, + "aac_he_binaural": /audio-HE-stereo-\d+-binaural/, + "aac_he_downmix": /audio-HE-stereo-\d+-downmix/, + "atmos": /audio-atmos-.*/, + "ac3": /audio-ac3-.*/, + "alac": /audio-alac-.*/ }; diff --git a/src/downloader/codecType.ts b/src/downloader/codecType.ts new file mode 100644 index 0000000..c12ce7a --- /dev/null +++ b/src/downloader/codecType.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +export const regularCodecTypeSchema = z.enum([ + "aac", + "aac_he", + "aac_binaural", + "aac_downmix", + "aac_he_binaural", + "aac_he_downmix", + "atmos", + "ac3", + "alac" +]); +export const webplaybackCodecTypeSchema = z.enum([ + "aac_legacy", + "aac_he_legacy" +]); + +export type RegularCodecType = z.infer; +export type WebplaybackCodecType = z.infer; + +export class CodecType { + public readonly codecType: RegularCodecType | WebplaybackCodecType; + public readonly regularOrWebplayback: "regular" | "webplayback"; + + constructor(codecType: string) { + const regularCheck = regularCodecTypeSchema.safeParse(codecType); + const webplaybackCheck = webplaybackCodecTypeSchema.safeParse(codecType); + + if (regularCheck.success) { + this.regularOrWebplayback = "regular"; + this.codecType = regularCheck.data; + } else if (webplaybackCheck.success) { + this.regularOrWebplayback = "webplayback"; + this.codecType = webplaybackCheck.data; + } else { + throw new Error(`invalid codec type: ${codecType}!`); + } + } +} diff --git a/src/downloader/index.ts b/src/downloader/index.ts index 4f67888..a189804 100644 --- a/src/downloader/index.ts +++ b/src/downloader/index.ts @@ -2,8 +2,9 @@ import { config } from "../config.js"; import { spawn } from "node:child_process"; import path from "node:path"; import { addToCache, isCached } from "../cache.js"; +import type { RegularCodecType, WebplaybackCodecType } from "./codecType.js"; -export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodec | WebplaybackCodec): Promise { +export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType): Promise { let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1]; if (!baseOutputName) { throw new Error("could not get base output name from stream url!"); } baseOutputName += `_${songCodec}`; @@ -53,21 +54,3 @@ export async function downloadSong(streamUrl: string, decryptionKey: string, son return decryptedPath; } - -// TODO: find a better spot for this -export enum RegularCodec { - Aac = "aac", - AacHe = "aac_he", - AacBinaural = "aac_binaural", - AacDownmix = "aac_downmix", - AacHeBinaural = "aac_he_binaural", - AacHeDownmix = "aac_he_downmix", - Atmos = "atmos", - Ac3 = "ac3", - Alac = "alac" -} - -export enum WebplaybackCodec { - AacLegacy = "aac_legacy", - AacHeLegacy = "aac_he_legacy" -} diff --git a/src/downloader/streamInfo.ts b/src/downloader/streamInfo.ts index a6fea0e..2f656e1 100644 --- a/src/downloader/streamInfo.ts +++ b/src/downloader/streamInfo.ts @@ -5,7 +5,7 @@ import axios from "axios"; import { widevine, playready, fairplay } from "../constants/keyFormats.js"; import { songCodecRegex } from "../constants/codecs.js"; import type { WebplaybackResponse } from "appleMusicApi/index.js"; -import { RegularCodec, WebplaybackCodec } from "./index.js"; +import type { RegularCodecType, WebplaybackCodecType } from "./codecType.js"; // why is this private // i wish pain on the person who wrote this /j :smile: @@ -34,7 +34,8 @@ export default class StreamInfo { // 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"]>, codec: RegularCodec): Promise { + // upd: it seems thats just how the cookie crumbles. oh well + public static async fromTrackMetadata(trackMetadata: SongAttributes<["extendedAssetUrls"]>, codec: RegularCodecType): Promise { 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,12 +71,12 @@ export default class StreamInfo { // webplayback is the more "legacy" way // only supports widevine, from what i can tell - public static async fromWebplayback(webplayback: WebplaybackResponse, codec: WebplaybackCodec): Promise { + public static async fromWebplayback(webplayback: WebplaybackResponse, codec: WebplaybackCodecType): Promise { const song = webplayback.songList[0]; let flavor: string; - if (codec === WebplaybackCodec.AacHeLegacy) { flavor = "32:ctrp64"; } - else if (codec === WebplaybackCodec.AacLegacy) { flavor = "28:ctrp256"; } + if (codec === "aac_he_legacy") { flavor = "32:ctrp64"; } + else if (codec === "aac_legacy") { flavor = "28:ctrp256"; } const asset = song.assets.find((asset) => { return asset.flavor === flavor; }); if (asset === undefined) { throw new Error("webplayback info for requested flavor doesn't exist!"); } @@ -141,7 +142,7 @@ function getAssetInfos(m3u8Data: M3u8): AssetInfos { throw new Error("m3u8 missing audio asset metadata!"); } -async function getPlaylist(m3u8Data: M3u8, codec: RegularCodec): Promise { +async function getPlaylist(m3u8Data: M3u8, codec: RegularCodecType): Promise { const masterPlaylists = m3u8Data.streamRenditions; const masterPlaylist = masterPlaylists.find((playlist) => { const line = playlist.properties[0].attributes?.audio; diff --git a/src/log.ts b/src/log.ts index 3f557db..1bf3082 100644 --- a/src/log.ts +++ b/src/log.ts @@ -46,7 +46,6 @@ function stackPrefix(): string { if (file === null || line === null || column === null) { return chalk.gray("unknown caller!"); } - // TODO: optimize this -- probably not very great currently const relative = path.relative(process.cwd(), fileURLToPath(file)); const parts = relative.split(path.sep); const srcIndex = parts.indexOf("src"); diff --git a/src/web/endpoints/back/download.ts b/src/web/endpoints/back/download.ts index 674009c..983adf6 100644 --- a/src/web/endpoints/back/download.ts +++ b/src/web/endpoints/back/download.ts @@ -1,34 +1,31 @@ import { getWidevineDecryptionKey } from "../../../downloader/keygen.js"; -import { downloadSong, RegularCodec, WebplaybackCodec } from "../../../downloader/index.js"; +import { downloadSong } from "../../../downloader/index.js"; import express from "express"; import StreamInfo from "../../../downloader/streamInfo.js"; import { appleMusicApi } from "../../../appleMusicApi/index.js"; import { z } from "zod"; import { validate } from "../../validate.js"; +import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js"; const router = express.Router(); const schema = z.object({ query: z.object({ id: z.string(), - codec: z.nativeEnum(RegularCodec).or(z.nativeEnum(WebplaybackCodec)) + codec: regularCodecTypeSchema.or(webplaybackCodecTypeSchema) }) }); // TODO: support more encryption schemes // TODO: some type of agnostic-ness for the encryption schemes on regular codec -// TODO: make it less ugly,, hahahiwehio router.get("/download", async (req, res, next) => { try { const { id, codec } = (await validate(req, schema)).query; - // TODO: write helper function for this - // or make it a class so we can use `instanceof` - const regularCodec = Object.values(RegularCodec).find((c) => { return c === codec; }); - const webplaybackCodec = Object.values(WebplaybackCodec).find((c) => { return c === codec; }); - if (regularCodec === undefined && webplaybackCodec === undefined) { res.status(400).send("codec is invalid!"); return; } + const codecType = new CodecType(codec); - if (regularCodec !== undefined) { + if (codecType.regularOrWebplayback === "regular") { + const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod const trackMetadata = await appleMusicApi.getSong(id); const trackAttributes = trackMetadata.data[0].attributes; const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec); @@ -37,9 +34,10 @@ router.get("/download", async (req, res, next) => { const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, regularCodec); res.download(filePath); } else { - res.status(400).send("no decryption key found!"); + throw new Error("no decryption key found for regular codec! this is typical. don't fret!"); } - } else if (webplaybackCodec !== undefined) { + } else if (codecType.regularOrWebplayback === "webplayback") { + const webplaybackCodec = codecType.codecType as WebplaybackCodecType; // safe cast, zod const webplaybackResponse = await appleMusicApi.getWebplayback(id); const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec); if (streamInfo.widevinePssh !== undefined) { @@ -47,7 +45,7 @@ router.get("/download", async (req, res, next) => { const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, webplaybackCodec); res.download(filePath); } else { - res.status(400).send("no decryption key found!"); + throw new Error("no decryption key found for web playback! this should not happen.."); } } } catch (err) {