playlist endpoint

This commit is contained in:
Reid 2025-05-19 23:49:13 -07:00
parent 4893de0d28
commit 7a3d74dc87
Signed by: reidlab
GPG key ID: DAF5EAF6665839FD
11 changed files with 111 additions and 663 deletions

View file

@ -32,11 +32,11 @@ after configuring, it's just as easy as running `npm run build` and running the
a system module is provided for your convenience, and the main output is `nixosModules.default`
after importing this module, the option `services.amdl` will show up, which is documented in [`flake.nix`](./flake.nix) somewhat well. everything under the `config` tree follows the `config.toml` well, along with everything under the `env` tree. defaults are provided for everything that isn't the private keys and client ids inside of the env section. make sure to set those!!
after importing this module, the option `services.amdl` will show up, which is documented in [`flake.nix`](./flake.nix) somewhat well. everything under the `config` tree follows the `config.toml` well, along with everything under the `env` tree. defaults are provided for everything that isn't the ITUA inside of the env section. make sure to set those!!
## limitations / the formats
currently you can only get basic widevine ones, everything related to playready and fairplay encryption methods are not supported, sorry!! someday i will get this working, at least for playready. it's just that no one has written a library yet but has for python (yuck!!)
currently you can only get basic widevine ones, everything related to playready and fairplay encryption methods are not supported, sorry!! someday i will get this working, at least for playready. it's just that no one has written a library yet but has for python (yuck!!) lossless audio is unfortunately out of the question currently. it will be a while till someone breaks fairplay drm
guaranteed formats to work include:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 769 B

Before After
Before After

View file

@ -1,6 +1,6 @@
import axios, { type AxiosInstance } from "axios";
import { ampApiUrl, appleMusicHomepageUrl, licenseApiUrl, webplaybackApiUrl } from "../constants/urls.js";
import type { GetSongResponse, SearchResponse } from "./types/responses.js";
import type { GetPlaylistResponse, GetSongResponse, SearchResponse } from "./types/responses.js";
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, SongAttributesExtensionTypes } from "./types/extensions.js";
import { getToken } from "./token.js";
import { config, env } from "../config.js";
@ -50,9 +50,29 @@ export default class AppleMusicApi {
})).data;
}
// TODO: make it so you can get more than the first 100 tracks
// you can't since it's entirely undocumented
// and the "trivial" way simply throws a 400, which is awesome
async getPlaylist<
T extends SongAttributesExtensionTypes = [],
U extends RelationshipTypes<T> = ["tracks"]
> (
id: string,
extend: T = [] as never as T,
relationships: U = ["tracks"] as U
): Promise<GetPlaylistResponse<T, U>> {
return (await this.http.get<GetPlaylistResponse<T, U>>(`/v1/catalog/${this.storefront}/playlists/${id}`, {
params: {
extend: extend.join(","),
include: relationships.join(",")
}
})).data;
}
async getSong<
// TODO: possibly make this any, and use the addScopingParameters function?
// would be a bit cleaner, almost everywhere, use above in `getAlbum` perchancibly
// and `getPlaylst`.... maybe just rewrite the whole thing at this point,, scoping parameters are my OPP
T extends SongAttributesExtensionTypes = ["extendedAssetUrls"],
U extends RelationshipTypes<T> = ["albums"]
> (

View file

@ -1,6 +1,7 @@
import type { Artwork, EditorialNotes, PlayParameters, Preview } from "./extras.js";
import type { Artwork, DescriptionAttribute, EditorialNotes, PlayParameters, Preview } from "./extras.js";
import type {
AlbumAttributesExtensionMap, AlbumAttributesExtensionTypes,
PlaylistAttributesExtensionMap, PlaylistAttributesExtensionTypes,
SongAttributesExtensionMap, SongAttributesExtensionTypes
} from "./extensions.js";
@ -27,6 +28,21 @@ export type AlbumAttributes<
}
& Pick<AlbumAttributesExtensionMap, T[number]>
export type PlaylistAttributes<
T extends PlaylistAttributesExtensionTypes,
> = {
artwork?: Artwork
curatorName: string
description?: DescriptionAttribute,
isChart: boolean,
lastModifiedDate?: string
name: string
playlistType: string
playParams?: PlayParameters
url: string
}
& Pick<PlaylistAttributesExtensionMap, T[number]>
export type SongAttributes<
T extends SongAttributesExtensionTypes,
> = {

View file

@ -1,4 +1,4 @@
export type AnyAttributesExtensionType = AlbumAttributesExtensionType | SongAttributesExtensionType;
export type AnyAttributesExtensionType = AlbumAttributesExtensionType | PlaylistAttributesExtensionType | SongAttributesExtensionType;
export type AnyAttributesExtensionTypes = AnyAttributesExtensionType[];
export type AlbumAttributesExtensionType = keyof AlbumAttributesExtensionMap;
@ -8,6 +8,12 @@ export type AlbumAttributesExtensionMap = {
audioVariants?: string[]
}
export type PlaylistAttributesExtensionType = keyof PlaylistAttributesExtensionMap;
export type PlaylistAttributesExtensionTypes = PlaylistAttributesExtensionType[];
export type PlaylistAttributesExtensionMap = {
trackTypes: string[]
}
export type SongAttributesExtensionType = keyof SongAttributesExtensionMap;
export type SongAttributesExtensionTypes = SongAttributesExtensionType[];
export type SongAttributesExtensionMap = {

View file

@ -1,3 +1,9 @@
// https://developer.apple.com/documentation/applemusicapi/descriptionattribute
export interface DescriptionAttribute {
short?: string
standard: string
}
// https://developer.apple.com/documentation/applemusicapi/artwork
export interface Artwork {
bgColor?: string

View file

@ -1,8 +1,8 @@
// you will shit yourself if you don't read this:
// required reading: https://developer.apple.com/documentation/applemusicapi/handling-resource-representation-and-relationships
import type { AlbumAttributes, SongAttributes } from "./attributes.js";
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, SongAttributesExtensionTypes } from "./extensions.js";
import type { AlbumAttributes, PlaylistAttributes, SongAttributes } from "./attributes.js";
import type { AlbumAttributesExtensionTypes, AnyAttributesExtensionTypes, PlaylistAttributesExtensionTypes, SongAttributesExtensionTypes } from "./extensions.js";
// TODO: have something like this for every resource
export type Relationship<T> = {
@ -11,7 +11,7 @@ export type Relationship<T> = {
data: {
// TODO: there is extra types here (id, type, etc) i just can't cba to add them lol
// probably not important ! ahahahah
// seems to be the same basic "resource" pattern i'm starting to notice (id(?), href, type, meta, etc)
// seems to be the same basic "resource" pattern i'm starting to notice (id(?), href, type, meta (not included), etc)
attributes: T
}[]
}
@ -20,5 +20,8 @@ export type RelationshipType<T extends AnyAttributesExtensionTypes> = keyof Rela
export type RelationshipTypes<T extends AnyAttributesExtensionTypes> = RelationshipType<T>[];
export type RelationshipTypeMap<T extends AnyAttributesExtensionTypes> = {
albums: AlbumAttributes<Extract<T, AlbumAttributesExtensionTypes>>,
tracks: SongAttributes<Extract<T, SongAttributesExtensionTypes>> // TODO: tracks can also be music videos, uh oh.
// TODO: from what i can tell, playlists can NOT be used as a relationship type? kept in case
playlists: PlaylistAttributes<Extract<T, PlaylistAttributesExtensionTypes>>,
// TODO: tracks can also be music videos, uh oh.
tracks: SongAttributes<Extract<T, SongAttributesExtensionTypes>>
}

View file

@ -28,6 +28,27 @@ export interface GetAlbumResponse<
}[]
}
// https://developer.apple.com/documentation/applemusicapi/get-a-catalog-playlist
export interface GetPlaylistResponse<
T extends SongAttributesExtensionTypes,
U extends RelationshipTypes<T>
> {
// https://developer.apple.com/documentation/applemusicapi/playlists
data: {
id: string
type: "playlists"
href: string
// https://developer.apple.com/documentation/applemusicapi/playlists/attributes-data.dictionary
attributes: SongAttributes<T>
// https://developer.apple.com/documentation/applemusicapi/playlists/relationships-data.dictionary
relationships: {
[K in U[number]]: Relationship<
K extends RelationshipType<T> ? RelationshipTypeMap<T>[K] : never
>
}
}[]
}
// https://developer.apple.com/documentation/applemusicapi/get-a-catalog-song
export interface GetSongResponse<
T extends SongAttributesExtensionTypes,

View file

@ -1,654 +0,0 @@
// thank u goat
// https://gist.github.com/BrychanOdlum/2208578ba151d1d7c4edeeda15b4e9b1
export default [
{
"name": "Algeria",
"code": "DZ",
"storefrontId": 143563
},
{
"name": "Angola",
"code": "AO",
"storefrontId": 143564
},
{
"name": "Anguilla",
"code": "AI",
"storefrontId": 143538
},
{
"name": "Antigua & Barbuda",
"code": "AG",
"storefrontId": 143540
},
{
"name": "Argentina",
"code": "AR",
"storefrontId": 143505
},
{
"name": "Armenia",
"code": "AM",
"storefrontId": 143524
},
{
"name": "Australia",
"code": "AU",
"storefrontId": 143460
},
{
"name": "Austria",
"code": "AT",
"storefrontId": 143445
},
{
"name": "Azerbaijan",
"code": "AZ",
"storefrontId": 143568
},
{
"name": "Bahrain",
"code": "BH",
"storefrontId": 143559
},
{
"name": "Bangladesh",
"code": "BD",
"storefrontId": 143490
},
{
"name": "Barbados",
"code": "BB",
"storefrontId": 143541
},
{
"name": "Belarus",
"code": "BY",
"storefrontId": 143565
},
{
"name": "Belgium",
"code": "BE",
"storefrontId": 143446
},
{
"name": "Belize",
"code": "BZ",
"storefrontId": 143555
},
{
"name": "Bermuda",
"code": "BM",
"storefrontId": 143542
},
{
"name": "Bolivia",
"code": "BO",
"storefrontId": 143556
},
{
"name": "Botswana",
"code": "BW",
"storefrontId": 143525
},
{
"name": "Brazil",
"code": "BR",
"storefrontId": 143503
},
{
"name": "British Virgin Islands",
"code": "VG",
"storefrontId": 143543
},
{
"name": "Brunei",
"code": "BN",
"storefrontId": 143560
},
{
"name": "Bulgaria",
"code": "BG",
"storefrontId": 143526
},
{
"name": "Canada",
"code": "CA",
"storefrontId": 143455
},
{
"name": "Cayman Islands",
"code": "KY",
"storefrontId": 143544
},
{
"name": "Chile",
"code": "CL",
"storefrontId": 143483
},
{
"name": "China",
"code": "CN",
"storefrontId": 143465
},
{
"name": "Colombia",
"code": "CO",
"storefrontId": 143501
},
{
"name": "Costa Rica",
"code": "CR",
"storefrontId": 143495
},
{
"name": "Cote DIvoire",
"code": "CI",
"storefrontId": 143527
},
{
"name": "Croatia",
"code": "HR",
"storefrontId": 143494
},
{
"name": "Cyprus",
"code": "CY",
"storefrontId": 143557
},
{
"name": "Czech Republic",
"code": "CZ",
"storefrontId": 143489
},
{
"name": "Denmark",
"code": "DK",
"storefrontId": 143458
},
{
"name": "Dominica",
"code": "DM",
"storefrontId": 143545
},
{
"name": "Dominican Rep.",
"code": "DO",
"storefrontId": 143508
},
{
"name": "Ecuador",
"code": "EC",
"storefrontId": 143509
},
{
"name": "Egypt",
"code": "EG",
"storefrontId": 143516
},
{
"name": "El Salvador",
"code": "SV",
"storefrontId": 143506
},
{
"name": "Estonia",
"code": "EE",
"storefrontId": 143518
},
{
"name": "Finland",
"code": "FI",
"storefrontId": 143447
},
{
"name": "France",
"code": "FR",
"storefrontId": 143442
},
{
"name": "Germany",
"code": "DE",
"storefrontId": 143443
},
{
"name": "Ghana",
"code": "GH",
"storefrontId": 143573
},
{
"name": "Greece",
"code": "GR",
"storefrontId": 143448
},
{
"name": "Grenada",
"code": "GD",
"storefrontId": 143546
},
{
"name": "Guatemala",
"code": "GT",
"storefrontId": 143504
},
{
"name": "Guyana",
"code": "GY",
"storefrontId": 143553
},
{
"name": "Honduras",
"code": "HN",
"storefrontId": 143510
},
{
"name": "Hong Kong",
"code": "HK",
"storefrontId": 143463
},
{
"name": "Hungary",
"code": "HU",
"storefrontId": 143482
},
{
"name": "Iceland",
"code": "IS",
"storefrontId": 143558
},
{
"name": "India",
"code": "IN",
"storefrontId": 143467
},
{
"name": "Indonesia",
"code": "ID",
"storefrontId": 143476
},
{
"name": "Ireland",
"code": "IE",
"storefrontId": 143449
},
{
"name": "Israel",
"code": "IL",
"storefrontId": 143491
},
{
"name": "Italy",
"code": "IT",
"storefrontId": 143450
},
{
"name": "Jamaica",
"code": "JM",
"storefrontId": 143511
},
{
"name": "Japan",
"code": "JP",
"storefrontId": 143462
},
{
"name": "Jordan",
"code": "JO",
"storefrontId": 143528
},
{
"name": "Kazakstan",
"code": "KZ",
"storefrontId": 143517
},
{
"name": "Kenya",
"code": "KE",
"storefrontId": 143529
},
{
"name": "Korea, Republic Of",
"code": "KR",
"storefrontId": 143466
},
{
"name": "Kuwait",
"code": "KW",
"storefrontId": 143493
},
{
"name": "Latvia",
"code": "LV",
"storefrontId": 143519
},
{
"name": "Lebanon",
"code": "LB",
"storefrontId": 143497
},
{
"name": "Liechtenstein",
"code": "LI",
"storefrontId": 143522
},
{
"name": "Lithuania",
"code": "LT",
"storefrontId": 143520
},
{
"name": "Luxembourg",
"code": "LU",
"storefrontId": 143451
},
{
"name": "Macau",
"code": "MO",
"storefrontId": 143515
},
{
"name": "Macedonia",
"code": "MK",
"storefrontId": 143530
},
{
"name": "Madagascar",
"code": "MG",
"storefrontId": 143531
},
{
"name": "Malaysia",
"code": "MY",
"storefrontId": 143473
},
{
"name": "Maldives",
"code": "MV",
"storefrontId": 143488
},
{
"name": "Mali",
"code": "ML",
"storefrontId": 143532
},
{
"name": "Malta",
"code": "MT",
"storefrontId": 143521
},
{
"name": "Mauritius",
"code": "MU",
"storefrontId": 143533
},
{
"name": "Mexico",
"code": "MX",
"storefrontId": 143468
},
{
"name": "Moldova, Republic Of",
"code": "MD",
"storefrontId": 143523
},
{
"name": "Montserrat",
"code": "MS",
"storefrontId": 143547
},
{
"name": "Nepal",
"code": "NP",
"storefrontId": 143484
},
{
"name": "Netherlands",
"code": "NL",
"storefrontId": 143452
},
{
"name": "New Zealand",
"code": "NZ",
"storefrontId": 143461
},
{
"name": "Nicaragua",
"code": "NI",
"storefrontId": 143512
},
{
"name": "Niger",
"code": "NE",
"storefrontId": 143534
},
{
"name": "Nigeria",
"code": "NG",
"storefrontId": 143561
},
{
"name": "Norway",
"code": "NO",
"storefrontId": 143457
},
{
"name": "Oman",
"code": "OM",
"storefrontId": 143562
},
{
"name": "Pakistan",
"code": "PK",
"storefrontId": 143477
},
{
"name": "Panama",
"code": "PA",
"storefrontId": 143485
},
{
"name": "Paraguay",
"code": "PY",
"storefrontId": 143513
},
{
"name": "Peru",
"code": "PE",
"storefrontId": 143507
},
{
"name": "Philippines",
"code": "PH",
"storefrontId": 143474
},
{
"name": "Poland",
"code": "PL",
"storefrontId": 143478
},
{
"name": "Portugal",
"code": "PT",
"storefrontId": 143453
},
{
"name": "Qatar",
"code": "QA",
"storefrontId": 143498
},
{
"name": "Romania",
"code": "RO",
"storefrontId": 143487
},
{
"name": "Russia",
"code": "RU",
"storefrontId": 143469
},
{
"name": "Saudi Arabia",
"code": "SA",
"storefrontId": 143479
},
{
"name": "Senegal",
"code": "SN",
"storefrontId": 143535
},
{
"name": "Serbia",
"code": "RS",
"storefrontId": 143500
},
{
"name": "Singapore",
"code": "SG",
"storefrontId": 143464
},
{
"name": "Slovakia",
"code": "SK",
"storefrontId": 143496
},
{
"name": "Slovenia",
"code": "SI",
"storefrontId": 143499
},
{
"name": "South Africa",
"code": "ZA",
"storefrontId": 143472
},
{
"name": "Spain",
"code": "ES",
"storefrontId": 143454
},
{
"name": "Sri Lanka",
"code": "LK",
"storefrontId": 143486
},
{
"name": "St. Kitts & Nevis",
"code": "KN",
"storefrontId": 143548
},
{
"name": "St. Lucia",
"code": "LC",
"storefrontId": 143549
},
{
"name": "St. Vincent & The Grenadines",
"code": "VC",
"storefrontId": 143550
},
{
"name": "Suriname",
"code": "SR",
"storefrontId": 143554
},
{
"name": "Sweden",
"code": "SE",
"storefrontId": 143456
},
{
"name": "Switzerland",
"code": "CH",
"storefrontId": 143459
},
{
"name": "Taiwan",
"code": "TW",
"storefrontId": 143470
},
{
"name": "Tanzania",
"code": "TZ",
"storefrontId": 143572
},
{
"name": "Thailand",
"code": "TH",
"storefrontId": 143475
},
{
"name": "The Bahamas",
"code": "BS",
"storefrontId": 143539
},
{
"name": "Trinidad & Tobago",
"code": "TT",
"storefrontId": 143551
},
{
"name": "Tunisia",
"code": "TN",
"storefrontId": 143536
},
{
"name": "Turkey",
"code": "TR",
"storefrontId": 143480
},
{
"name": "Turks & Caicos",
"code": "TC",
"storefrontId": 143552
},
{
"name": "Uganda",
"code": "UG",
"storefrontId": 143537
},
{
"name": "UK",
"code": "GB",
"storefrontId": 143444
},
{
"name": "Ukraine",
"code": "UA",
"storefrontId": 143492
},
{
"name": "United Arab Emirates",
"code": "AE",
"storefrontId": 143481
},
{
"name": "Uruguay",
"code": "UY",
"storefrontId": 143514
},
{
"name": "USA",
"code": "US",
"storefrontId": 143441
},
{
"name": "Uzbekistan",
"code": "UZ",
"storefrontId": 143566
},
{
"name": "Venezuela",
"code": "VE",
"storefrontId": 143502
},
{
"name": "Vietnam",
"code": "VN",
"storefrontId": 143471
},
{
"name": "Yemen",
"code": "YE",
"storefrontId": 143571
}
];

View file

@ -0,0 +1,28 @@
import { appleMusicApi } from "../../../appleMusicApi/index.js";
import express from "express";
import { validate } from "../../validate.js";
import { z } from "zod";
const router = express.Router();
const schema = z.object({
query: z.object({
id: z.string()
})
});
// see comments in `getTrackMetadata.ts`
// awawawawawa
router.get("/getPlaylistMetadata", async (req, res, next) => {
try {
const { id } = (await validate(req, schema)).query;
const trackMetadata = await appleMusicApi.getPlaylist(id);
res.json(trackMetadata);
} catch (err) {
next(err);
}
});
export default router;

View file

@ -7,9 +7,11 @@ export const front = [
import backDownload from "./back/download.js";
import getAlbumMetadata from "./back/getAlbumMetadata.js";
import getPlaylistMetadata from "./back/getPlaylistMetadata.js";
import getTrackMetadata from "./back/getTrackMetadata.js";
export const back = [
backDownload,
getAlbumMetadata,
getPlaylistMetadata,
getTrackMetadata
];