137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
import { createWriteStream } from "node:fs";
|
|
import type { GetSongResponse } from "appleMusicApi/types/responses.js";
|
|
import path from "node:path";
|
|
import { config } from "../config.js";
|
|
import { pipeline } from "node:stream/promises";
|
|
import { addToCache, isCached } from "../cache.js";
|
|
|
|
// TODO: simply add more fields. ha!
|
|
// TODO: add lyrics (what format??)
|
|
export class FileMetadata {
|
|
public readonly artist: string;
|
|
public readonly title: string;
|
|
public readonly album: string;
|
|
public readonly albumArtist: string;
|
|
public readonly isPartOfCompilation: boolean;
|
|
public readonly artwork: string;
|
|
public readonly track?: number;
|
|
public readonly disc?: number;
|
|
public readonly date?: string;
|
|
public readonly copyright?: string;
|
|
public readonly isrc?: string;
|
|
public readonly composer?: string;
|
|
|
|
constructor(
|
|
artist: string,
|
|
title: string,
|
|
album: string,
|
|
albumArtist: string,
|
|
isPartOfCompilation: boolean,
|
|
artwork: string,
|
|
track?: number,
|
|
disc?: number,
|
|
date?: string,
|
|
copyright?: string,
|
|
isrc?: string,
|
|
composer?: string
|
|
) {
|
|
this.artist = artist;
|
|
this.title = title;
|
|
this.album = album.replace(/- (EP|Single)$/, "").trim();
|
|
this.albumArtist = albumArtist;
|
|
this.isPartOfCompilation = isPartOfCompilation;
|
|
this.artwork = artwork;
|
|
this.track = track;
|
|
this.disc = disc;
|
|
this.date = date;
|
|
this.copyright = copyright;
|
|
this.isrc = isrc;
|
|
this.composer = composer;
|
|
}
|
|
|
|
public static fromSongResponse(trackMetadata: GetSongResponse<[], ["albums"]>): FileMetadata {
|
|
const trackAttributes = trackMetadata.data[0].attributes;
|
|
const albumAttributes = trackMetadata.data[0].relationships.albums.data[0].attributes;
|
|
|
|
const artworkUrl = trackAttributes.artwork.url
|
|
.replace("{w}", trackAttributes.artwork.width.toString())
|
|
.replace("{h}", trackAttributes.artwork.height.toString());
|
|
|
|
return new FileMetadata(
|
|
trackAttributes.artistName,
|
|
trackAttributes.name,
|
|
albumAttributes.name,
|
|
albumAttributes.artistName,
|
|
albumAttributes.isCompilation,
|
|
artworkUrl,
|
|
trackAttributes.trackNumber,
|
|
trackAttributes.discNumber,
|
|
trackAttributes.releaseDate,
|
|
albumAttributes.copyright,
|
|
trackAttributes.isrc,
|
|
trackAttributes.composerName
|
|
);
|
|
}
|
|
|
|
public async setupFfmpegInputs(encryptedPath: string): Promise<string[]> {
|
|
// url is in a weird format
|
|
// only things we care about is the uuid and file extension i think?
|
|
// i dont wanna use the original file name because what if. what if theres a collision
|
|
const extension = this.artwork.slice(this.artwork.lastIndexOf(".") + 1);
|
|
const uuid = this.artwork.split("/").at(-3);
|
|
|
|
if (uuid === undefined) { throw new Error("could not get uuid from artwork url!"); }
|
|
|
|
const imageFileName = `${uuid}.${extension}`;
|
|
const imagePath = path.join(config.downloader.cache.directory, imageFileName);
|
|
|
|
if (!isCached(imageFileName)) {
|
|
const response = await fetch(this.artwork);
|
|
|
|
if (!response.ok) { throw new Error(`failed to fetch artwork: ${response.status}`); }
|
|
if (!response.body) { throw new Error("no response body for artwork!"); }
|
|
|
|
await pipeline(response.body as ReadableStream, createWriteStream(imagePath));
|
|
|
|
addToCache(imageFileName);
|
|
}
|
|
|
|
return [
|
|
"-i", encryptedPath,
|
|
"-i", imagePath,
|
|
"-map", "0",
|
|
"-map", "1",
|
|
"-disposition:v", "attached_pic",
|
|
"-c:a", "copy",
|
|
"-c:v", "copy"
|
|
];
|
|
}
|
|
|
|
public async toFfmpegArgs(): Promise<string[]> {
|
|
return [
|
|
// standard album cover metadata
|
|
"-metadata:s:v","comment='Cover (front)'",
|
|
// bog standard metadata
|
|
"-metadata", "artist=" + this.artist,
|
|
"-metadata", "title=" + this.title,
|
|
"-metadata", "album=" + this.album,
|
|
"-metadata", "album_artist=" + this.albumArtist,
|
|
// oh how i'd love to do <track>/<totaltracks> but...
|
|
// it feels weird only doing it on tracks, since MZ doesn't have total disks
|
|
// so i'm just doing non-full numbers because it feels weird only doing it for one
|
|
...(this.track !== undefined ? ["-metadata", "track=" + this.track] : []),
|
|
...(this.disc !== undefined ? ["-metadata", "disc=" + this.disc] : []),
|
|
...(this.date !== undefined ? ["-metadata", "date=" + this.date] : []),
|
|
...(this.copyright !== undefined ? ["-metadata", "copyright=" + this.copyright] : []),
|
|
...(this.isrc !== undefined ? ["-metadata", "isrc=" + this.isrc] : []),
|
|
...(this.composer !== undefined ? ["-metadata", "composer=" + this.composer] : []),
|
|
// from https://id3.org/Developer%20Information:
|
|
// > TCMP: iTunes Compilation flag
|
|
// > TSO2: iTunes uses this for Album Artist sort order
|
|
// > TSOC: iTunes uses this for Composer sort order
|
|
"-metadata", "TCMP=" + (this.isPartOfCompilation ? "1" : "0"),
|
|
"-metadata", "TSO2=" + this.albumArtist,
|
|
"-metadata", "TSOC=" + this.composer
|
|
];
|
|
}
|
|
}
|