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 +103 -0
- package/dist/App.d.ts +8 -0
- package/dist/App.js +1 -0
- package/dist/apple/index.d.ts +13 -0
- package/dist/apple/index.js +97 -0
- package/dist/apple/types.d.ts +533 -0
- package/dist/apple/types.js +1 -0
- package/dist/spotify/index.d.ts +53 -0
- package/dist/spotify/index.js +212 -0
- package/dist/spotify/types.d.ts +406 -0
- package/dist/spotify/types.js +2 -0
- package/dist/youtube/index.d.ts +16 -0
- package/dist/youtube/index.js +93 -0
- package/dist/youtube/types.d.ts +418 -0
- package/dist/youtube/types.js +1 -0
- package/package.json +61 -0
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
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
|
+
}
|