diff --git a/README.md b/README.md index 8b985fa..930d804 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/public/favicon.png b/public/favicon.png index 70ad317..bb376ca 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/src/appleMusicApi/index.ts b/src/appleMusicApi/index.ts index 04da88a..4347ea5 100644 --- a/src/appleMusicApi/index.ts +++ b/src/appleMusicApi/index.ts @@ -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 = ["tracks"] + > ( + id: string, + extend: T = [] as never as T, + relationships: U = ["tracks"] as U + ): Promise> { + return (await this.http.get>(`/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 = ["albums"] > ( diff --git a/src/appleMusicApi/types/attributes.ts b/src/appleMusicApi/types/attributes.ts index b9ef22b..248b0ce 100644 --- a/src/appleMusicApi/types/attributes.ts +++ b/src/appleMusicApi/types/attributes.ts @@ -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 +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 + export type SongAttributes< T extends SongAttributesExtensionTypes, > = { diff --git a/src/appleMusicApi/types/extensions.ts b/src/appleMusicApi/types/extensions.ts index 6e2cb75..f1f553c 100644 --- a/src/appleMusicApi/types/extensions.ts +++ b/src/appleMusicApi/types/extensions.ts @@ -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 = { diff --git a/src/appleMusicApi/types/extras.ts b/src/appleMusicApi/types/extras.ts index 7a6a37e..89e5818 100644 --- a/src/appleMusicApi/types/extras.ts +++ b/src/appleMusicApi/types/extras.ts @@ -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 diff --git a/src/appleMusicApi/types/relationships.ts b/src/appleMusicApi/types/relationships.ts index 84d23a6..55229ca 100644 --- a/src/appleMusicApi/types/relationships.ts +++ b/src/appleMusicApi/types/relationships.ts @@ -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 = { @@ -11,7 +11,7 @@ export type Relationship = { 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 = keyof Rela export type RelationshipTypes = RelationshipType[]; export type RelationshipTypeMap = { albums: AlbumAttributes>, - tracks: SongAttributes> // 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>, + // TODO: tracks can also be music videos, uh oh. + tracks: SongAttributes> } diff --git a/src/appleMusicApi/types/responses.ts b/src/appleMusicApi/types/responses.ts index 7b96ce1..1023d75 100644 --- a/src/appleMusicApi/types/responses.ts +++ b/src/appleMusicApi/types/responses.ts @@ -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 +> { + // 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 + // https://developer.apple.com/documentation/applemusicapi/playlists/relationships-data.dictionary + relationships: { + [K in U[number]]: Relationship< + K extends RelationshipType ? RelationshipTypeMap[K] : never + > + } + }[] +} + // https://developer.apple.com/documentation/applemusicapi/get-a-catalog-song export interface GetSongResponse< T extends SongAttributesExtensionTypes, diff --git a/src/constants/storefrontMappings.ts b/src/constants/storefrontMappings.ts deleted file mode 100644 index 9563d4e..0000000 --- a/src/constants/storefrontMappings.ts +++ /dev/null @@ -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 D’Ivoire", - "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 - } -]; diff --git a/src/web/endpoints/back/getPlaylistMetadata.ts b/src/web/endpoints/back/getPlaylistMetadata.ts new file mode 100644 index 0000000..f109291 --- /dev/null +++ b/src/web/endpoints/back/getPlaylistMetadata.ts @@ -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; diff --git a/src/web/endpoints/index.ts b/src/web/endpoints/index.ts index 5e73ac7..cd23b9a 100644 --- a/src/web/endpoints/index.ts +++ b/src/web/endpoints/index.ts @@ -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 ];