codecત્યpl
This commit is contained in:
parent
b8da59086b
commit
f233d9e64f
10 changed files with 343 additions and 1121 deletions
|
@ -70,6 +70,6 @@ function loadSchemaSomething<T extends ZodSchema>(schema: T, something: string |
|
|||
}
|
||||
|
||||
export const config = loadSchemaSomething(configSchema, "config.toml");
|
||||
log.debug("config loaded");
|
||||
log.info("config loaded");
|
||||
export const env = loadSchemaSomething(envSchema, process.env);
|
||||
log.debug("env loaded");
|
||||
log.info("env loaded");
|
||||
|
|
13
src/constants/codecs.ts
Normal file
13
src/constants/codecs.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { RegularCodec } from "../downloader/index.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-.*/
|
||||
};
|
|
@ -3,10 +3,13 @@ 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();
|
||||
// TODO: make this have a return type (file path)
|
||||
// TODO: refresh cache timer on download
|
||||
// TODO: remux to m4a?
|
||||
export async function downloadSong(streamUrl: string, decryptionKey: string, songCodec: RegularCodec | WebplaybackCodec): Promise<void> {
|
||||
let 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"; }
|
||||
baseOutputName += `_${songCodec}`;
|
||||
const encryptedName = baseOutputName + "_enc.mp4";
|
||||
const encryptedPath = path.join(config.downloader.cache.directory, encryptedName);
|
||||
const decryptedName = baseOutputName + ".mp4";
|
||||
|
@ -48,3 +51,21 @@ export async function downloadSong(streamUrl: string, decryptionKey: string): Pr
|
|||
addToCache(encryptedName);
|
||||
addToCache(decryptedName);
|
||||
}
|
||||
|
||||
// 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,12 +5,11 @@ import hls, { Item } from "parse-hls";
|
|||
import axios from "axios";
|
||||
import { getWidevineDecryptionKey } from "./keygen.js";
|
||||
import { widevine, playready, fairplay } from "../constants/keyFormats.js";
|
||||
import { select } from "@inquirer/prompts";
|
||||
import { songCodecRegex } from "../constants/codecs.js";
|
||||
import type { WebplaybackResponse } from "api/appleMusicApi.js";
|
||||
import { downloadSong } from "./index.js";
|
||||
import { downloadSong, RegularCodec, WebplaybackCodec } from "./index.js";
|
||||
|
||||
// ugliest type ever
|
||||
// this library is so bad
|
||||
// why is this private
|
||||
// i wish pain on the person who wrote this /j :smile:
|
||||
type M3u8 = ReturnType<typeof hls.default.parse>;
|
||||
|
||||
|
@ -51,7 +50,7 @@ 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"]>): Promise<StreamInfo> {
|
||||
public static async fromTrackMetadata(trackMetadata: SongAttributes<["extendedAssetUrls"]>, codec: RegularCodec): 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");
|
||||
|
||||
|
@ -61,7 +60,7 @@ export default class StreamInfo {
|
|||
|
||||
const drmInfos = getDrmInfos(m3u8Parsed);
|
||||
const assetInfos = getAssetInfos(m3u8Parsed);
|
||||
const playlist = await getPlaylist(m3u8Parsed);
|
||||
const playlist = await getPlaylist(m3u8Parsed, codec);
|
||||
const variantId = playlist.properties[0].attributes.stableVariantId;
|
||||
if (variantId === undefined) { throw "variant id does not exist!"; }
|
||||
if (typeof variantId !== "string") { throw "variant id is not a string!"; }
|
||||
|
@ -85,10 +84,16 @@ export default class StreamInfo {
|
|||
|
||||
// webplayback is the more "legacy" way
|
||||
// only supports widevine, from what i can tell
|
||||
public static async fromWebplayback(webplayback: WebplaybackResponse, flavor: string): Promise<StreamInfo> {
|
||||
public static async fromWebplayback(webplayback: WebplaybackResponse, codec: WebplaybackCodec): Promise<StreamInfo> {
|
||||
const song = webplayback.songList[0];
|
||||
|
||||
let flavor: string;
|
||||
if (codec === WebplaybackCodec.AacHeLegacy) { flavor = "32:ctrp64"; }
|
||||
else if (codec === WebplaybackCodec.AacLegacy) { flavor = "28:ctrp256"; }
|
||||
|
||||
const asset = song.assets.find((asset) => { return asset.flavor === flavor; });
|
||||
if (asset === undefined) { throw "webplayback info for requested flavor doesn't exist!"; }
|
||||
|
||||
const trackId = song.songId;
|
||||
|
||||
const m3u8Url = asset.URL;
|
||||
|
@ -153,16 +158,18 @@ function getAssetInfos(m3u8Data: M3u8): AssetInfos {
|
|||
// SUPER TODO: remove inquery for the codec, including its library, this is for testing
|
||||
// add a config option for preferred codec ?
|
||||
// or maybe in the streaminfo function
|
||||
async function getPlaylist(m3u8Data: M3u8): Promise<Item> {
|
||||
async function getPlaylist(m3u8Data: M3u8, codec: RegularCodec): Promise<Item> {
|
||||
const masterPlaylists = m3u8Data.streamRenditions;
|
||||
const masterPlaylist = await select({
|
||||
message: "codec ?",
|
||||
choices: masterPlaylists.map((playlist) => ({
|
||||
name: playlist.properties[0].attributes.audio as string,
|
||||
value: playlist
|
||||
}))
|
||||
const masterPlaylist = masterPlaylists.find((playlist) => {
|
||||
const line = playlist.properties[0].attributes?.audio;
|
||||
if (line === undefined) { return false; }
|
||||
if (typeof line !== "string") { return false; }
|
||||
const match = line.match(songCodecRegex[codec]);
|
||||
return match !== null;
|
||||
});
|
||||
|
||||
if (masterPlaylist === undefined) { throw "no master playlist for codec found!"; }
|
||||
|
||||
return masterPlaylist;
|
||||
}
|
||||
|
||||
|
@ -183,9 +190,29 @@ 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("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)); }
|
||||
// const streamInfo2 = await StreamInfo.fromTrackMetadata((await appleMusicApi.getSong("1615276490")).data[0].attributes, RegularCodec.Aac);
|
||||
|
||||
if (streamInfo1.widevinePssh !== undefined) { await downloadSong(streamInfo1.streamUrl, await getWidevineDecryptionKey(streamInfo1.widevinePssh, streamInfo1.trackId)); }
|
||||
const streamCodec1 = WebplaybackCodec.AacLegacy;
|
||||
const streamInfo1 = await StreamInfo.fromWebplayback(await appleMusicApi.getWebplayback("1705366148"), streamCodec1);
|
||||
if (streamInfo1.widevinePssh !== undefined) {
|
||||
await downloadSong(
|
||||
streamInfo1.streamUrl,
|
||||
await getWidevineDecryptionKey(streamInfo1.widevinePssh, streamInfo1.trackId),
|
||||
streamCodec1
|
||||
);
|
||||
}
|
||||
|
||||
// try {
|
||||
// const streamCodec2 = RegularCodec.AacHe;
|
||||
// const streamInfo2 = await StreamInfo.fromTrackMetadata((await appleMusicApi.getSong("1705366148")).data[0].attributes, streamCodec2);
|
||||
// if (streamInfo2.widevinePssh !== undefined) {
|
||||
// await downloadSong(
|
||||
// streamInfo2.streamUrl,
|
||||
// await getWidevineDecryptionKey(streamInfo2.widevinePssh, streamInfo2.trackId),
|
||||
// streamCodec2
|
||||
// );
|
||||
// }
|
||||
// } catch (err) {
|
||||
// log.error("failed to download song");
|
||||
// log.error(err);
|
||||
// }
|
||||
|
|
16
src/index.ts
16
src/index.ts
|
@ -4,7 +4,7 @@ import process from "node:process";
|
|||
import * as log from "./log.js";
|
||||
import { appleMusicApi } from "./api/index.js";
|
||||
|
||||
class HttpException extends Error {
|
||||
export class HttpException extends Error {
|
||||
public readonly status?: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
|
@ -16,6 +16,14 @@ class HttpException extends Error {
|
|||
|
||||
const app = express();
|
||||
|
||||
app.disable("x-powered-by");
|
||||
|
||||
app.set("trust proxy", ["loopback", "uniquelocal"]);
|
||||
|
||||
app.use("/", express.static("public"));
|
||||
|
||||
app.use("/data", express.static(config.downloader.cache.directory, { extensions: ["mp4"] }));
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
next(new HttpException(404, `${req.path} not found`));
|
||||
});
|
||||
|
@ -31,15 +39,13 @@ app.use((err: HttpException, _req: Request, res: Response, _next: NextFunction)
|
|||
res.status(status).send(message);
|
||||
});
|
||||
|
||||
// TODO: reorganize this
|
||||
// a bit gugly..
|
||||
|
||||
await appleMusicApi.login().catch((err) => {
|
||||
log.error("failed to login to apple music api");
|
||||
log.error(err);
|
||||
process.exit(1);
|
||||
}).finally(() => {
|
||||
log.info("logged in to apple music api");
|
||||
});
|
||||
log.debug("logged in to apple music api");
|
||||
|
||||
try {
|
||||
const listener = app.listen(config.server.port, () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue