kill the codec enums
This commit is contained in:
parent
7a3d74dc87
commit
8113c36a47
6 changed files with 71 additions and 49 deletions
|
@ -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+/,
|
export const songCodecRegex: { [key in RegularCodecType]: RegExp } = {
|
||||||
[RegularCodec.AacHe]: /audio-HE-stereo-\d+/,
|
"aac": /audio-stereo-\d+/,
|
||||||
[RegularCodec.AacBinaural]: /audio-stereo-\d+-binaural/,
|
"aac_he": /audio-HE-stereo-\d+/,
|
||||||
[RegularCodec.AacDownmix]: /audio-stereo-\d+-downmix/,
|
"aac_binaural": /audio-stereo-\d+-binaural/,
|
||||||
[RegularCodec.AacHeBinaural]: /audio-HE-stereo-\d+-binaural/,
|
"aac_downmix": /audio-stereo-\d+-downmix/,
|
||||||
[RegularCodec.AacHeDownmix]: /audio-HE-stereo-\d+-downmix/,
|
"aac_he_binaural": /audio-HE-stereo-\d+-binaural/,
|
||||||
[RegularCodec.Atmos]: /audio-atmos-.*/,
|
"aac_he_downmix": /audio-HE-stereo-\d+-downmix/,
|
||||||
[RegularCodec.Ac3]: /audio-ac3-.*/,
|
"atmos": /audio-atmos-.*/,
|
||||||
[RegularCodec.Alac]: /audio-alac-.*/
|
"ac3": /audio-ac3-.*/,
|
||||||
|
"alac": /audio-alac-.*/
|
||||||
};
|
};
|
||||||
|
|
40
src/downloader/codecType.ts
Normal file
40
src/downloader/codecType.ts
Normal file
|
@ -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<typeof regularCodecTypeSchema>;
|
||||||
|
export type WebplaybackCodecType = z.infer<typeof webplaybackCodecTypeSchema>;
|
||||||
|
|
||||||
|
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}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,9 @@ import { config } from "../config.js";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { addToCache, isCached } from "../cache.js";
|
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<string> {
|
export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodecType | WebplaybackCodecType): Promise<string> {
|
||||||
let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1];
|
let baseOutputName = streamUrl.match(/(?:.*\/)\s*(\S*?)[.?]/)?.[1];
|
||||||
if (!baseOutputName) { throw new Error("could not get base output name from stream url!"); }
|
if (!baseOutputName) { throw new Error("could not get base output name from stream url!"); }
|
||||||
baseOutputName += `_${songCodec}`;
|
baseOutputName += `_${songCodec}`;
|
||||||
|
@ -53,21 +54,3 @@ export async function downloadSong(streamUrl: string, decryptionKey: string, son
|
||||||
|
|
||||||
return decryptedPath;
|
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"
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import axios from "axios";
|
||||||
import { widevine, playready, fairplay } from "../constants/keyFormats.js";
|
import { widevine, playready, fairplay } from "../constants/keyFormats.js";
|
||||||
import { songCodecRegex } from "../constants/codecs.js";
|
import { songCodecRegex } from "../constants/codecs.js";
|
||||||
import type { WebplaybackResponse } from "appleMusicApi/index.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
|
// why is this private
|
||||||
// i wish pain on the person who wrote this /j :smile:
|
// 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?
|
// TODO: why can't we decrypt widevine ones with this?
|
||||||
// we get a valid key.. but it doesn't work :-(
|
// we get a valid key.. but it doesn't work :-(
|
||||||
public static async fromTrackMetadata(trackMetadata: SongAttributes<["extendedAssetUrls"]>, codec: RegularCodec): Promise<StreamInfo> {
|
// upd: it seems thats just how the cookie crumbles. oh well
|
||||||
|
public static async fromTrackMetadata(trackMetadata: SongAttributes<["extendedAssetUrls"]>, codec: RegularCodecType): Promise<StreamInfo> {
|
||||||
log.warn("the track metadata method is experimental, and may not work or give correct values!");
|
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");
|
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
|
// webplayback is the more "legacy" way
|
||||||
// only supports widevine, from what i can tell
|
// only supports widevine, from what i can tell
|
||||||
public static async fromWebplayback(webplayback: WebplaybackResponse, codec: WebplaybackCodec): Promise<StreamInfo> {
|
public static async fromWebplayback(webplayback: WebplaybackResponse, codec: WebplaybackCodecType): Promise<StreamInfo> {
|
||||||
const song = webplayback.songList[0];
|
const song = webplayback.songList[0];
|
||||||
|
|
||||||
let flavor: string;
|
let flavor: string;
|
||||||
if (codec === WebplaybackCodec.AacHeLegacy) { flavor = "32:ctrp64"; }
|
if (codec === "aac_he_legacy") { flavor = "32:ctrp64"; }
|
||||||
else if (codec === WebplaybackCodec.AacLegacy) { flavor = "28:ctrp256"; }
|
else if (codec === "aac_legacy") { flavor = "28:ctrp256"; }
|
||||||
|
|
||||||
const asset = song.assets.find((asset) => { return asset.flavor === flavor; });
|
const asset = song.assets.find((asset) => { return asset.flavor === flavor; });
|
||||||
if (asset === undefined) { throw new Error("webplayback info for requested flavor doesn't exist!"); }
|
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!");
|
throw new Error("m3u8 missing audio asset metadata!");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPlaylist(m3u8Data: M3u8, codec: RegularCodec): Promise<Item> {
|
async function getPlaylist(m3u8Data: M3u8, codec: RegularCodecType): Promise<Item> {
|
||||||
const masterPlaylists = m3u8Data.streamRenditions;
|
const masterPlaylists = m3u8Data.streamRenditions;
|
||||||
const masterPlaylist = masterPlaylists.find((playlist) => {
|
const masterPlaylist = masterPlaylists.find((playlist) => {
|
||||||
const line = playlist.properties[0].attributes?.audio;
|
const line = playlist.properties[0].attributes?.audio;
|
||||||
|
|
|
@ -46,7 +46,6 @@ function stackPrefix(): string {
|
||||||
|
|
||||||
if (file === null || line === null || column === null) { return chalk.gray("unknown caller!"); }
|
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 relative = path.relative(process.cwd(), fileURLToPath(file));
|
||||||
const parts = relative.split(path.sep);
|
const parts = relative.split(path.sep);
|
||||||
const srcIndex = parts.indexOf("src");
|
const srcIndex = parts.indexOf("src");
|
||||||
|
|
|
@ -1,34 +1,31 @@
|
||||||
import { getWidevineDecryptionKey } from "../../../downloader/keygen.js";
|
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 express from "express";
|
||||||
import StreamInfo from "../../../downloader/streamInfo.js";
|
import StreamInfo from "../../../downloader/streamInfo.js";
|
||||||
import { appleMusicApi } from "../../../appleMusicApi/index.js";
|
import { appleMusicApi } from "../../../appleMusicApi/index.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { validate } from "../../validate.js";
|
import { validate } from "../../validate.js";
|
||||||
|
import { CodecType, regularCodecTypeSchema, webplaybackCodecTypeSchema, type RegularCodecType, type WebplaybackCodecType } from "../../../downloader/codecType.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
query: z.object({
|
query: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
codec: z.nativeEnum(RegularCodec).or(z.nativeEnum(WebplaybackCodec))
|
codec: regularCodecTypeSchema.or(webplaybackCodecTypeSchema)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: support more encryption schemes
|
// TODO: support more encryption schemes
|
||||||
// TODO: some type of agnostic-ness for the encryption schemes on regular codec
|
// 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) => {
|
router.get("/download", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { id, codec } = (await validate(req, schema)).query;
|
const { id, codec } = (await validate(req, schema)).query;
|
||||||
|
|
||||||
// TODO: write helper function for this
|
const codecType = new CodecType(codec);
|
||||||
// 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; }
|
|
||||||
|
|
||||||
if (regularCodec !== undefined) {
|
if (codecType.regularOrWebplayback === "regular") {
|
||||||
|
const regularCodec = codecType.codecType as RegularCodecType; // safe cast, zod
|
||||||
const trackMetadata = await appleMusicApi.getSong(id);
|
const trackMetadata = await appleMusicApi.getSong(id);
|
||||||
const trackAttributes = trackMetadata.data[0].attributes;
|
const trackAttributes = trackMetadata.data[0].attributes;
|
||||||
const streamInfo = await StreamInfo.fromTrackMetadata(trackAttributes, regularCodec);
|
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);
|
const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, regularCodec);
|
||||||
res.download(filePath);
|
res.download(filePath);
|
||||||
} else {
|
} 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 webplaybackResponse = await appleMusicApi.getWebplayback(id);
|
||||||
const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
|
const streamInfo = await StreamInfo.fromWebplayback(webplaybackResponse, webplaybackCodec);
|
||||||
if (streamInfo.widevinePssh !== undefined) {
|
if (streamInfo.widevinePssh !== undefined) {
|
||||||
|
@ -47,7 +45,7 @@ router.get("/download", async (req, res, next) => {
|
||||||
const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, webplaybackCodec);
|
const filePath = await downloadSong(streamInfo.streamUrl, decryptionKey, webplaybackCodec);
|
||||||
res.download(filePath);
|
res.download(filePath);
|
||||||
} else {
|
} 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) {
|
} catch (err) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue