harmonyy 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ <div align='center'>
2
+ <h1>harmony</h1>
3
+ <h3>a keyless search tool for music streaming services</h3>
4
+ </div>
5
+
6
+ <br>
7
+
8
+ ```
9
+ npm i harmonyy
10
+ pnpm i harmonyy
11
+ bun add harmonyy
12
+ ```
13
+
14
+ each service lives in its own subpath and is the default export:
15
+
16
+ ```ts
17
+ import Spotify from 'harmonyy/spotify';
18
+ import AppleMusic from 'harmonyy/apple';
19
+ import YoutubeMusic from 'harmonyy/youtube';
20
+ ```
21
+
22
+ every service returns its own response structure, so results are not interchangeable between services. the response types are exported from the same subpath as the class; see `src/<service>/types.ts` for the full shapes.
23
+
24
+ ## /spotify
25
+
26
+ ```ts
27
+ import Spotify from 'harmonyy/spotify';
28
+
29
+ const spotify = new Spotify();
30
+
31
+ const results = await spotify.search('daft punk', { limit: 5 });
32
+ const popular = await spotify.getPopular();
33
+ const album = await spotify.getAlbum('spotify:album:2noRn2Aes5aoNVsU6iWThc');
34
+ const artist = await spotify.getArtist('spotify:artist:4tZwfgrHOc3mvqYlEYSvVi');
35
+ ```
36
+
37
+ - `search(query, opts?)`
38
+ - `getPopular(timezone?)`
39
+ - `getAlbum(uri)`
40
+ - `getArtist(uri)`
41
+
42
+ `search` options: `offset`, `limit`, `numberOfTopResults`, `includeAudiobooks`, `includeArtistHasConcertsField`, `includePreReleases`, `includeLocalConcertsField`, `includeAuthors`. any other keys are passed through to spotify.
43
+
44
+ `uri` is a full spotify uri (`spotify:album:...`, `spotify:artist:...`).
45
+
46
+ ## /apple
47
+
48
+ ```ts
49
+ import AppleMusic from 'harmonyy/apple';
50
+
51
+ const apple = new AppleMusic('us'); // optional: storefront, defaults to 'us'
52
+
53
+ const results = await apple.search('daft punk');
54
+ const artist = await apple.getArtist('5468295');
55
+ const album = await apple.getAlbum('1440833098');
56
+ ```
57
+
58
+ - `search(query)`
59
+ - `getArtist(id)`
60
+ - `getAlbum(id)`
61
+
62
+ ids are the numeric ids from apple music urls.
63
+
64
+ ## /youtube
65
+
66
+ ```ts
67
+ import YoutubeMusic from 'harmonyy/youtube';
68
+
69
+ const youtube = new YoutubeMusic();
70
+
71
+ const results = await youtube.search('daft punk');
72
+ const album = await youtube.getAlbum('MPREb_...');
73
+ const artist = await youtube.getArtist('UC...');
74
+ ```
75
+
76
+ - `search(query)`
77
+ - `getAlbum(browseId)`
78
+ - `getArtist(browseId)`
79
+
80
+ `browseId` is the youtube music browse id (`MPREb_...` for albums, `UC...` for artists).
81
+
82
+ ## App
83
+
84
+ every service class implements at minimum the `App` interface (`src/App.ts`):
85
+
86
+ ```ts
87
+ export interface App {
88
+ $getHeaders(): Promise<Record<string, string>>;
89
+
90
+ search(query: string): Promise<any>;
91
+ getPopular?(): Promise<any>;
92
+ getAlbum(uri: string): Promise<any>;
93
+ getArtist(uri: string): Promise<any>;
94
+ getTrack?(uri: string): Promise<any>;
95
+ }
96
+ ```
97
+
98
+ - `$getHeaders` builds the request headers (fetching tokens or cookies first if needed).
99
+ - `search`, `getAlbum` and `getArtist` are implemented by every service.
100
+ - `getPopular` and `getTrack` are optional and depend on whether the service supports it.
101
+
102
+ <br><br>
103
+ <h5 align='center'>made with ❤️</h5>
package/dist/App.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface App {
2
+ $getHeaders(): Promise<Record<string, string>>;
3
+ search(query: string): Promise<any>;
4
+ getPopular?(): Promise<any>;
5
+ getAlbum(uri: string): Promise<any>;
6
+ getArtist(uri: string): Promise<any>;
7
+ getTrack?(uri: string): Promise<any>;
8
+ }
package/dist/App.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { AlbumResponse, ArtistResponse, SearchResponse } from './types.js';
2
+ import type { App } from '../App.js';
3
+ export * from './types.js';
4
+ export default class AppleMusic implements App {
5
+ authorization: string;
6
+ storefront: string;
7
+ constructor(storefront?: string);
8
+ getAuthorization(): Promise<void>;
9
+ $getHeaders(): Promise<Record<string, string>>;
10
+ search(query: string): Promise<SearchResponse>;
11
+ getArtist(uri: string): Promise<ArtistResponse>;
12
+ getAlbum(uri: string): Promise<AlbumResponse>;
13
+ }
@@ -0,0 +1,97 @@
1
+ export * from './types.js';
2
+ export default class AppleMusic {
3
+ authorization;
4
+ storefront = 'us';
5
+ constructor(storefront = 'us') {
6
+ this.storefront = storefront;
7
+ }
8
+ async getAuthorization() {
9
+ const mainPageReq = await fetch(`https://music.apple.com/${this.storefront}/new`);
10
+ const mainPage = await mainPageReq.text();
11
+ const indexFileName = mainPage.match(/\<script type="module" crossorigin src="(.*?)"\>/)[1];
12
+ const indexFileReq = await fetch(`https://music.apple.com${indexFileName}`);
13
+ const indexFile = await indexFileReq.text();
14
+ this.authorization = indexFile.match(/="(eyJ.*?)"/)[1];
15
+ }
16
+ async $getHeaders() {
17
+ if (!this.authorization)
18
+ await this.getAuthorization();
19
+ return {
20
+ 'Accept': '*/*',
21
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
22
+ 'Accept-Language': 'en-US,en;q=0.9',
23
+ 'Authorization': `Bearer ${this.authorization}`,
24
+ 'Cache-Control': 'no-cache',
25
+ 'Cookie': 'geo=US',
26
+ 'Origin': 'https://music.apple.com',
27
+ 'Referer': 'https://music.apple.com/',
28
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
29
+ };
30
+ }
31
+ async search(query) {
32
+ const url = new URL(`https://amp-api-edge.music.apple.com/v1/catalog/${this.storefront}/search`);
33
+ url.searchParams.append('art[music-videos:url]', 'c');
34
+ url.searchParams.append('art[url]', 'f');
35
+ url.searchParams.append('extend', 'artistUrl');
36
+ url.searchParams.append('fields[albums]', 'artistName,artistUrl,artwork,contentRating,editorialArtwork,editorialNotes,name,playParams,releaseDate,url,trackCount');
37
+ url.searchParams.append('fields[artists]', 'url,name,artwork');
38
+ url.searchParams.append('format[resources]', 'map');
39
+ url.searchParams.append('include[albums]', 'artists');
40
+ url.searchParams.append('include[music-videos]', 'artists');
41
+ url.searchParams.append('include[songs]', 'artists');
42
+ url.searchParams.append('include[stations]', 'radio-show');
43
+ url.searchParams.append('l', 'en-US');
44
+ url.searchParams.append('limit', '21');
45
+ url.searchParams.append('omit[resource]', 'autos');
46
+ url.searchParams.append('platform', 'web');
47
+ url.searchParams.append('relate[albums]', 'artists');
48
+ url.searchParams.append('relate[songs]', 'albums');
49
+ url.searchParams.append('term', query);
50
+ url.searchParams.append('types', 'activities,albums,apple-curators,artists,curators,editorial-items,music-movies,music-videos,playlists,record-labels,songs,stations,tv-episodes,uploaded-videos');
51
+ url.searchParams.append('with', 'lyricHighlights,lyrics,naturalLanguage,serverBubbles,subtitles');
52
+ const searchReq = await fetch(url.toString(), { headers: await this.$getHeaders() });
53
+ const searchRes = await searchReq.json();
54
+ return searchRes;
55
+ }
56
+ async getArtist(uri) {
57
+ const url = new URL(`https://amp-api.music.apple.com/v1/catalog/${this.storefront}/artists/${uri}`);
58
+ url.searchParams.set('art[url]', 'c,f');
59
+ url.searchParams.set('extend', 'artistBio,bornOrFormed,editorialArtwork,editorialVideo,extendedAssetUrls,hero,isGroup,origin,plainEditorialNotes,seoDescription,seoTitle');
60
+ url.searchParams.set('extend[playlists]', 'trackCount');
61
+ url.searchParams.set('format[resources]', 'map');
62
+ url.searchParams.set('include', 'record-labels,artists,persons,bands');
63
+ url.searchParams.set('include[concerts]', 'venues');
64
+ url.searchParams.set('include[music-videos]', 'artists');
65
+ url.searchParams.set('include[songs]', 'artists,albums');
66
+ url.searchParams.set('l', 'en-US');
67
+ url.searchParams.set('limit[all-upcoming-concerts]', '8');
68
+ url.searchParams.set('limit[artists:top-songs]', '24');
69
+ url.searchParams.set('meta[albums:tracks]', 'popularity');
70
+ url.searchParams.set('omit[resource]', 'autos');
71
+ url.searchParams.set('platform', 'web');
72
+ url.searchParams.set('views', 'all-upcoming-concerts,appears-on-albums,compilation-albums,featured-albums,featured-on-albums,featured-release,full-albums,latest-release,live-albums,more-to-hear,more-to-see,music-videos,playlists,radio-shows,similar-artists,singles,top-songs');
73
+ const artistReq = await fetch(url.toString(), { headers: await this.$getHeaders() });
74
+ const artistRes = await artistReq.json();
75
+ return artistRes;
76
+ }
77
+ async getAlbum(uri) {
78
+ const url = new URL(`https://amp-api.music.apple.com/v1/catalog/${this.storefront}/albums/${uri}`);
79
+ url.searchParams.set('art[url]', 'f');
80
+ url.searchParams.set('extend', 'editorialArtwork,editorialVideo,extendedAssetUrls,offers,seoDescription,seoTitle');
81
+ url.searchParams.set('fields[artists]', 'name,url');
82
+ url.searchParams.set('fields[curators]', 'name');
83
+ url.searchParams.set('fields[record-labels]', 'name,url');
84
+ url.searchParams.set('format[resources]', 'map');
85
+ url.searchParams.set('include', 'record-labels,artists');
86
+ url.searchParams.set('include[music-videos]', 'artists');
87
+ url.searchParams.set('include[playlists]', 'curator');
88
+ url.searchParams.set('include[songs]', 'artists,composers,albums');
89
+ url.searchParams.set('l', 'en-US');
90
+ url.searchParams.set('meta[albums:tracks]', 'popularity');
91
+ url.searchParams.set('platform', 'web');
92
+ url.searchParams.set('views', 'appears-on,audio-extras,more-by-artist,other-versions,related-videos,video-extras,you-might-also-like');
93
+ const req = await fetch(url.toString(), { headers: await this.$getHeaders() });
94
+ const data = await req.json();
95
+ return data;
96
+ }
97
+ }