diff --git a/packages/core/src/plugins/meta/index.ts b/packages/core/src/plugins/meta/index.ts index 264e56daae..0ec6d8df3f 100644 --- a/packages/core/src/plugins/meta/index.ts +++ b/packages/core/src/plugins/meta/index.ts @@ -3,3 +3,4 @@ export { default as DiscogsMetaProvider } from './discogs'; export { default as MusicbrainzMetaProvider } from './musicbrainz'; export { default as BandcampMetaProvider } from './bandcamp'; export { default as iTunesPodcastMetaProvider } from './itunespodcast'; +export { default as iTunesMusicMetaProvider } from './itunesmusic'; diff --git a/packages/core/src/plugins/meta/itunesmusic.test.ts b/packages/core/src/plugins/meta/itunesmusic.test.ts new file mode 100644 index 0000000000..0d2baf468e --- /dev/null +++ b/packages/core/src/plugins/meta/itunesmusic.test.ts @@ -0,0 +1,99 @@ +import iTunesMusicMetaProvider from './itunesmusic'; +import * as iTunesMocks from './metaMocks/iTunesMusicMocks'; +import Track from '../../structs/Track'; + +describe('iTunes music metaprovider tests', () => { + it('search for artists', async () => { + iTunesMocks.mockArtistResult(); + const itunesMeta = new iTunesMusicMetaProvider(); + const response = await itunesMeta.searchForArtists('Queen'); + expect(fetch).toHaveBeenCalledTimes(1); + expect(response).toEqual([ + { + 'coverImage': '', + 'id': 3296287, + 'name': 'Queen', + 'resourceUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'source': 'iTunesMusic', + 'thumb': '' + } + ]); + }); + + it('search for releases', async () => { + iTunesMocks.mockAlbumResult(); + const itunesMeta = new iTunesMusicMetaProvider(); + const response = await itunesMeta.searchForReleases('The Platinum Collection (Greatest Hits I, II & III)'); + expect(fetch).toHaveBeenCalledTimes(1); + expect(response).toEqual([ + { + 'id': 1440650428, + 'coverImage': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/250x250bb.jpg', + 'thumb': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/1600x1600bb.jpg', + 'title': 'The Platinum Collection (Greatest Hits I, II & III)', + 'artist': 'Queen', + 'resourceUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'source': 'iTunesMusic' + } + ]); + }); + + it('search for tracks', async () => { + iTunesMocks.mockTrackResult(); + const itunesMeta = new iTunesMusicMetaProvider(); + const response = await itunesMeta.searchForTracks('The Platinum Collection (Greatest Hits I, II & III)'); + expect(fetch).toHaveBeenCalledTimes(1); + expect(response).toEqual([ + { + 'id': 1440651216, + 'title': 'We Will Rock You', + 'artist': 'Queen', + 'source': 'iTunesMusic' + } + ]); + }); + + it('fetch artist albums', async () => { + iTunesMocks.mockArtistAlbumsResult(); + const itunesMeta = new iTunesMusicMetaProvider(); + const response = await itunesMeta.fetchArtistAlbums('3296287'); + expect(fetch).toHaveBeenCalledTimes(1); + expect(response).toEqual([ + { + 'id': 1440650428, + 'coverImage': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/250x250bb.jpg', + 'thumb': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/1600x1600bb.jpg', + 'title': 'The Platinum Collection (Greatest Hits I, II & III)', + 'artist': 'Queen', + 'resourceUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'source': 'iTunesMusic' + } + ]); + }); + + it('fetch albums details', async () => { + iTunesMocks.mockAlbumSongsSearch(); + const itunesMeta = new iTunesMusicMetaProvider(); + const response = await itunesMeta.fetchAlbumDetails('1440650428', 'master'); + expect(fetch).toHaveBeenCalledTimes(1); + const track = new Track ({ + 'artist': 'The Platinum Collection (Greatest Hits I, II & III)', + 'duration': 356, + 'position': 1, + 'thumbnail': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', + 'title': 'Bohemian Rhapsody' + }); + track.uuid = ''; + response.tracklist[0].uuid = ''; + expect(response).toEqual({ + 'artist': 'Queen', + 'coverImage': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/250x250bb.jpg', + 'id': 1440650428, + 'thumb': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/1600x1600bb.jpg', + 'title': 'The Platinum Collection (Greatest Hits I, II & III)', + 'tracklist': [track], + 'type': 'master', + 'year': '2014-01-01T08:00:00Z' + }); + }); +}); diff --git a/packages/core/src/plugins/meta/itunesmusic.ts b/packages/core/src/plugins/meta/itunesmusic.ts new file mode 100644 index 0000000000..011184f4df --- /dev/null +++ b/packages/core/src/plugins/meta/itunesmusic.ts @@ -0,0 +1,152 @@ +import _ from 'lodash'; + +import MetaProvider from '../metaProvider'; +import * as iTunes from '../../rest/iTunes'; +import { + SearchResultsArtist, + SearchResultsAlbum, + SearchResultsTrack, + ArtistDetails, + AlbumDetails, + SearchResultsSource, + AlbumType +} from '../plugins.types'; +import LastFmApi from '../../rest/Lastfm'; +import Track from '../../structs/Track'; +import { LastFmArtistInfo, LastfmTopTracks } from '../../rest/Lastfm.types'; + +class iTunesMusicMetaProvider extends MetaProvider { + lastfm: LastFmApi; + constructor() { + super(); + this.name = 'iTunesMusic Meta Provider'; + this.sourceName = 'iTunesMusic Meta Provider'; + this.description = 'Metadata provider that uses iTunes as a source.'; + this.searchName = 'iTunesMusic'; + this.image = null; + this.lastfm = new LastFmApi(process.env.LAST_FM_API_KEY, process.env.LASTFM_API_SECRET); + } + + async searchForArtists(query: string): Promise { + const artistInfo = (await ((await iTunes.artistSearch(query, '50')).json())); + return artistInfo.results + .filter(artist => artist.artistType === 'Artist') + .map(artist => ({ + id: artist.artistId, + coverImage: '', + thumb: '', + name: artist.artistName, + resourceUrl: artist.artistLinkUrl, + source: SearchResultsSource.iTunesMusic + })); + } + + async searchForReleases(query: string): Promise { + const albumInfo = (await ((await iTunes.albumSearch(query, '50')).json())); + return albumInfo.results.map(album => ({ + id: album.collectionId, + coverImage: album.artworkUrl100.replace('100x100bb.jpg', '250x250bb.jpg'), + thumb: album.artworkUrl100.replace('100x100bb.jpg', '1600x1600bb.jpg'), + title: album.collectionName, + artist: album.artistName, + resourceUrl: album.artistViewUrl, + source: SearchResultsSource.iTunesMusic + })); + } + + async searchForTracks(query: string): Promise { + const musicInfo = (await ((await iTunes.musicSearch(query, '50')).json())); + return musicInfo.results.map(music => ({ + id: music.trackId, + title: music.trackName, + artist: music.artistName, + source: SearchResultsSource.iTunesMusic + })); + } + + async searchAll(query): Promise<{ + artists: Array; + releases: Array; + tracks: Array; + }> { + const artists = await this.searchForArtists(query); + const releases = await this.searchForReleases(query); + const tracks = await this.searchForTracks(query); + return Promise.resolve({ artists, releases, tracks }); + } + + async fetchArtistDetails(artistId: string): Promise { + const artistDetails = (await ((await iTunes.artistDetailsSearch(artistId, '1')).json())); + const lastFmInfo: LastFmArtistInfo = + (await (await this.lastfm.getArtistInfo(artistDetails.results[0].artistName)).json()).artist; + const lastFmTopTracks: LastfmTopTracks = + (await (await this.lastfm.getArtistTopTracks(artistDetails.results[0].artistName)).json()).toptracks; + + return ({ + id: artistId, + name: artistDetails.results[0].artistName, + coverImage: artistDetails.results[1].artworkUrl100.replace('100x100bb.jpg', '1600x1600bb.jpg'), + similar: _.map(lastFmInfo.similar.artist, artist => ({ + name: artist.name, + thumbnail: _.get(_.find(artist.image, { size: 'large' }), '#text') + })), + topTracks: _.map(lastFmTopTracks.track, (track) => ({ + name: track.name, + title: track.name, + thumb: artistDetails.results[1].artworkUrl100.replace('100x100bb.jpg', '250x250bb.jpg'), + playcount: track.playcount, + listeners: track.listeners, + artist: track.artist + })), + source: SearchResultsSource.iTunesMusic + }); + } + + async fetchArtistDetailsByName(artistName: string): Promise { + const artists = await this.searchForArtists(artistName); + return this.fetchArtistDetails(artists[0]?.id); + } + + async fetchArtistAlbums(artistId: string): Promise { + const artistAlbums = (await ((await iTunes.artistAlbumsSearch(artistId)).json())); + return artistAlbums.results.slice(1).map(album => ({ + id: album.collectionId, + coverImage: album.artworkUrl100.replace('100x100bb.jpg', '250x250bb.jpg'), + thumb: album.artworkUrl100.replace('100x100bb.jpg', '1600x1600bb.jpg'), + title: album.collectionName, + artist: album.artistName, + resourceUrl: album.artistViewUrl, + source: SearchResultsSource.iTunesMusic + })); + } + + async fetchAlbumDetails( + albumId: string, + albumType: 'master' | 'release' + ): Promise { + const albumInfo = (await ((await iTunes.albumSongsSearch(albumId, '50')).json())).results; + return Promise.resolve({ + id: albumInfo[0].collectionId, + artist: albumInfo[0].artistName, + title: albumInfo[0].collectionName, + thumb: albumInfo[0].artworkUrl100.replace('100x100bb.jpg', '1600x1600bb.jpg'), + coverImage: albumInfo[0].artworkUrl100.replace('100x100bb.jpg', '250x250bb.jpg'), + year: albumInfo[0].releaseDate, + type: albumType as AlbumType, + tracklist: _.map(albumInfo.slice(1), (episode, index) => new Track ({ + artist: episode.collectionName, + title: episode.trackName, + duration: Math.ceil(episode.trackTimeMillis/1000), + thumbnail: episode.artworkUrl60, + position: index + 1 + })) + }); + } + + async fetchAlbumDetailsByName(albumName: string): Promise { + const albums = await this.searchForReleases(albumName); + return this.fetchAlbumDetails(albums[0]?.id, 'master'); + } + +} +export default iTunesMusicMetaProvider; diff --git a/packages/core/src/plugins/meta/itunespodcast.test.ts b/packages/core/src/plugins/meta/itunespodcast.test.ts new file mode 100644 index 0000000000..760339edff --- /dev/null +++ b/packages/core/src/plugins/meta/itunespodcast.test.ts @@ -0,0 +1,49 @@ +import iTunesPodcastMetaProvider from './itunespodcast'; +import * as iTunesMocks from './metaMocks/iTunesPodcastMocks'; +import { Track } from '../..'; + +describe('iTunes podcast metaprovider tests', () => { + it('search for podcasts', async () => { + iTunesMocks.mockPodcastResult(); + const itunesMeta = new iTunesPodcastMetaProvider(); + const response = await itunesMeta.searchForPodcast('Programming Throwdown'); + expect(fetch).toHaveBeenCalledTimes(1); + expect(response).toEqual([ + { + 'id': 427166321, + 'coverImage': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'thumb': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'title': 'Programming Throwdown', + 'author': 'Patrick Wheeler and Jason Gauci', + 'type': 'podcast', + 'source': 'iTunesPodcast' + } + ]); + }); + + it('search for podcasts details', async () => { + iTunesMocks.mockPodcastEpisodesResult(); + const itunesMeta = new iTunesPodcastMetaProvider(); + const response = await itunesMeta.fetchAlbumDetails('Programming Throwdown'); + expect(fetch).toHaveBeenCalledTimes(1); + const track = new Track ({ + 'artist': 'Programming Throwdown', + 'duration': 4554, + 'position': 1, + 'thumbnail': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/60x60bb.jpg', + 'title': 'Route Planning with Parker Woodward' + }); + track.uuid = ''; + response.tracklist[0].uuid = ''; + expect(response).toEqual({ + 'artist': 'Programming Throwdown', + 'coverImage': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'id': 427166321, + 'thumb': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'title': 'Programming Throwdown', + 'tracklist': [track], + 'type': 'master', + 'year': '2021-07-07T17:08:00Z' + }); + }); +}); diff --git a/packages/core/src/plugins/meta/itunespodcast.ts b/packages/core/src/plugins/meta/itunespodcast.ts index afaa1ebcd7..6315abf443 100644 --- a/packages/core/src/plugins/meta/itunespodcast.ts +++ b/packages/core/src/plugins/meta/itunespodcast.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import MetaProvider from '../metaProvider'; -import * as iTunes from '../../rest/iTunesPodcast'; +import * as iTunes from '../../rest/iTunes'; import { SearchResultsArtist, SearchResultsAlbum, diff --git a/packages/core/src/plugins/meta/metaMocks/iTunesMusicMocks.ts b/packages/core/src/plugins/meta/metaMocks/iTunesMusicMocks.ts new file mode 100644 index 0000000000..41f77e0631 --- /dev/null +++ b/packages/core/src/plugins/meta/metaMocks/iTunesMusicMocks.ts @@ -0,0 +1,211 @@ +export function mockArtistResult() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 1, + 'results': [ + { + 'wrapperType': 'artist', + 'artistType': 'Artist', + 'artistName': 'Queen', + 'artistLinkUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'artistId': 3296287, + 'amgArtistId': 5205, + 'primaryGenreName': 'Rock', + 'primaryGenreId': 21 + } + ] + })) + }) + ) as any; +} + +export function mockAlbumResult() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 1, + 'results': [ + { + 'wrapperType': 'collection', + 'collectionType': 'Album', + 'artistId': 3296287, + 'collectionId': 1440650428, + 'amgArtistId': 5205, + 'artistName': 'Queen', + 'collectionName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'collectionCensoredName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'artistViewUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'collectionViewUrl': 'https://music.apple.com/us/album/the-platinum-collection-greatest-hits-i-ii-iii/1440650428?uo=4', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/100x100bb.jpg', + 'collectionPrice': 24.99, + 'collectionExplicitness': 'notExplicit', + 'trackCount': 51, + 'copyright': '℗ 2014 Hollywood Records, Inc.', + 'country': 'USA', + 'currency': 'USD', + 'releaseDate': '2014-01-01T08:00:00Z', + 'primaryGenreName': 'Rock' + } + ] + })) + }) + ) as any; +} + +export function mockTrackResult() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 1, + 'results': [ + { + 'wrapperType': 'track', + 'kind': 'song', + 'artistId': 3296287, + 'collectionId': 1440650428, + 'trackId': 1440651216, + 'artistName': 'Queen', + 'collectionName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'trackName': 'We Will Rock You', + 'collectionCensoredName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'trackCensoredName': 'We Will Rock You', + 'artistViewUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'collectionViewUrl': 'https://music.apple.com/us/album/we-will-rock-you/1440650428?i=1440651216&uo=4', + 'trackViewUrl': 'https://music.apple.com/us/album/we-will-rock-you/1440650428?i=1440651216&uo=4', + 'previewUrl': 'https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/14/f0/9a/14f09aae-20d5-641b-2fc4-6d8151f623d0/mzaf_5184580475352686527.plus.aac.p.m4a', + 'artworkUrl30': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/30x30bb.jpg', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/100x100bb.jpg', + 'collectionPrice': 24.99, + 'trackPrice': 0.69, + 'releaseDate': '1977-10-07T12:00:00Z', + 'collectionExplicitness': 'notExplicit', + 'trackExplicitness': 'notExplicit', + 'discCount': 3, + 'discNumber': 1, + 'trackCount': 17, + 'trackNumber': 16, + 'trackTimeMillis': 122123, + 'country': 'USA', + 'currency': 'USD', + 'primaryGenreName': 'Rock', + 'isStreamable': true + } + ] + })) + }) + ) as any; +} + +export function mockArtistAlbumsResult() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 2, + 'results': [ + { + 'wrapperType': 'artist', + 'artistType': 'Artist', + 'artistName': 'Queen', + 'artistLinkUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'artistId': 3296287, + 'amgArtistId': 5205, + 'primaryGenreName': 'Rock', + 'primaryGenreId': 21 + }, + { + 'wrapperType': 'collection', + 'collectionType': 'Album', + 'artistId': 3296287, + 'collectionId': 1440650428, + 'amgArtistId': 5205, + 'artistName': 'Queen', + 'collectionName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'collectionCensoredName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'artistViewUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'collectionViewUrl': 'https://music.apple.com/us/album/the-platinum-collection-greatest-hits-i-ii-iii/1440650428?uo=4', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/100x100bb.jpg', + 'collectionPrice': 24.99, + 'collectionExplicitness': 'notExplicit', + 'trackCount': 51, + 'copyright': '℗ 2014 Hollywood Records, Inc.', + 'country': 'USA', + 'currency': 'USD', + 'releaseDate': '2014-01-01T08:00:00Z', + 'primaryGenreName': 'Rock' + } + ] + })) + }) + ) as any; +} + +export function mockAlbumSongsSearch() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 2, + 'results': [ + { + 'wrapperType': 'collection', + 'collectionType': 'Album', + 'artistId': 3296287, + 'collectionId': 1440650428, + 'amgArtistId': 5205, + 'artistName': 'Queen', + 'collectionName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'collectionCensoredName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'artistViewUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'collectionViewUrl': 'https://music.apple.com/us/album/the-platinum-collection-greatest-hits-i-ii-iii/1440650428?uo=4', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/100x100bb.jpg', + 'collectionPrice': 24.99, + 'collectionExplicitness': 'notExplicit', + 'trackCount': 51, + 'copyright': '℗ 2014 Hollywood Records, Inc.', + 'country': 'USA', + 'currency': 'USD', + 'releaseDate': '2014-01-01T08:00:00Z', + 'primaryGenreName': 'Rock' + }, + { + 'wrapperType': 'track', + 'kind': 'song', + 'artistId': 3296287, + 'collectionId': 1440650428, + 'trackId': 1440650711, + 'artistName': 'Queen', + 'collectionName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'trackName': 'Bohemian Rhapsody', + 'collectionCensoredName': 'The Platinum Collection (Greatest Hits I, II & III)', + 'trackCensoredName': 'Bohemian Rhapsody', + 'artistViewUrl': 'https://music.apple.com/us/artist/queen/3296287?uo=4', + 'collectionViewUrl': 'https://music.apple.com/us/album/bohemian-rhapsody/1440650428?i=1440650711&uo=4', + 'trackViewUrl': 'https://music.apple.com/us/album/bohemian-rhapsody/1440650428?i=1440650711&uo=4', + 'previewUrl': 'https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/41/3d/83/413d8349-283e-d476-10ba-01bd0979ecda/mzaf_6563023006229503211.plus.aac.p.m4a', + 'artworkUrl30': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/30x30bb.jpg', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Music115/v4/83/23/e4/8323e48b-3467-448b-1ce0-8981d8a97437/source/100x100bb.jpg', + 'collectionPrice': 24.99, + 'trackPrice': 0.69, + 'releaseDate': '1975-10-31T12:00:00Z', + 'collectionExplicitness': 'notExplicit', + 'trackExplicitness': 'notExplicit', + 'discCount': 3, + 'discNumber': 1, + 'trackCount': 17, + 'trackNumber': 1, + 'trackTimeMillis': 355145, + 'country': 'USA', + 'currency': 'USD', + 'primaryGenreName': 'Rock', + 'isStreamable': true + } + ] + })) + }) + ) as any; +} diff --git a/packages/core/src/plugins/meta/metaMocks/iTunesPodcastMocks.ts b/packages/core/src/plugins/meta/metaMocks/iTunesPodcastMocks.ts new file mode 100644 index 0000000000..3a3f6f44b5 --- /dev/null +++ b/packages/core/src/plugins/meta/metaMocks/iTunesPodcastMocks.ts @@ -0,0 +1,148 @@ +export function mockPodcastResult() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 1, + 'results': [ + { + 'wrapperType': 'track', + 'kind': 'podcast', + 'collectionId': 427166321, + 'trackId': 427166321, + 'artistName': 'Patrick Wheeler and Jason Gauci', + 'collectionName': 'Programming Throwdown', + 'trackName': 'Programming Throwdown', + 'collectionCensoredName': 'Programming Throwdown', + 'trackCensoredName': 'Programming Throwdown', + 'collectionViewUrl': 'https://podcasts.apple.com/us/podcast/programming-throwdown/id427166321?uo=4', + 'feedUrl': 'https://feeds.transistor.fm/programming-throwdown', + 'trackViewUrl': 'https://podcasts.apple.com/us/podcast/programming-throwdown/id427166321?uo=4', + 'artworkUrl30': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/30x30bb.jpg', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/100x100bb.jpg', + 'collectionPrice': 0.00, + 'trackPrice': 0.00, + 'trackRentalPrice': 0, + 'collectionHdPrice': 0, + 'trackHdPrice': 0, + 'trackHdRentalPrice': 0, + 'releaseDate': '2021-07-07T17:08:00Z', + 'collectionExplicitness': 'cleaned', + 'trackExplicitness': 'cleaned', + 'trackCount': 115, + 'country': 'USA', + 'currency': 'USD', + 'primaryGenreName': 'How To', + 'contentAdvisoryRating': 'Clean', + 'artworkUrl600': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'genreIds': [ + '1499', + '26', + '1304', + '1489', + '1528' + ], + 'genres': [ + 'How To', + 'Podcasts', + 'Education', + 'News', + 'Tech News' + ] + } + ] + })) + }) + ) as any; +} + +export function mockPodcastEpisodesResult() { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + 'resultCount': 2, + 'results': [ + { + 'wrapperType': 'track', + 'kind': 'podcast', + 'collectionId': 427166321, + 'trackId': 427166321, + 'artistName': 'Patrick Wheeler and Jason Gauci', + 'collectionName': 'Programming Throwdown', + 'trackName': 'Programming Throwdown', + 'collectionCensoredName': 'Programming Throwdown', + 'trackCensoredName': 'Programming Throwdown', + 'collectionViewUrl': 'https://podcasts.apple.com/us/podcast/programming-throwdown/id427166321?uo=4', + 'feedUrl': 'https://feeds.transistor.fm/programming-throwdown', + 'trackViewUrl': 'https://podcasts.apple.com/us/podcast/programming-throwdown/id427166321?uo=4', + 'artworkUrl30': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/30x30bb.jpg', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/60x60bb.jpg', + 'artworkUrl100': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/100x100bb.jpg', + 'collectionPrice': 0.00, + 'trackPrice': 0.00, + 'trackRentalPrice': 0, + 'collectionHdPrice': 0, + 'trackHdPrice': 0, + 'trackHdRentalPrice': 0, + 'releaseDate': '2021-07-07T17:08:00Z', + 'collectionExplicitness': 'cleaned', + 'trackExplicitness': 'cleaned', + 'trackCount': 115, + 'country': 'USA', + 'currency': 'USD', + 'primaryGenreName': 'How To', + 'contentAdvisoryRating': 'Clean', + 'artworkUrl600': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'genreIds': [ + '1499', + '26', + '1304', + '1489', + '1528' + ], + 'genres': [ + 'How To', + 'Podcasts', + 'Education', + 'News', + 'Tech News' + ] + }, + { + 'collectionViewUrl': 'https://itunes.apple.com/us/podcast/programming-throwdown/id427166321?mt=2&uo=4', + 'trackTimeMillis': 4554000, + 'episodeUrl': 'https://pdst.fm/e/media.transistor.fm/283dafdd/f6b075ca.mp3', + 'artworkUrl600': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/600x600bb.jpg', + 'previewUrl': 'https://pdst.fm/e/media.transistor.fm/283dafdd/f6b075ca.mp3', + 'artworkUrl160': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/160x160bb.jpg', + 'episodeContentType': 'audio', + 'artworkUrl60': 'https://is3-ssl.mzstatic.com/image/thumb/Podcasts125/v4/83/e8/a9/83e8a9d5-df87-b19d-7050-55e4ce4df89d/mza_13511678666604160959.jpg/60x60bb.jpg', + 'contentAdvisoryRating': 'Clean', + 'trackViewUrl': 'https://podcasts.apple.com/us/podcast/route-planning-with-parker-woodward/id427166321?i=1000528145455&uo=4', + 'episodeFileExtension': 'mp3', + 'artistIds': [], + 'closedCaptioning': 'none', + 'trackId': 1000528145455, + 'trackName': 'Route Planning with Parker Woodward', + 'shortDescription': 'Ever wondered how route planning apps, well, plan routes? In this episode, we navigate through this fascinating topic, a field as data-driven and systemic as it is magical and compelling. \n\nJoining us is Parker Woodward, Route Expert and Marketing Direc', + 'feedUrl': 'https://feeds.transistor.fm/programming-throwdown', + 'collectionId': 427166321, + 'collectionName': 'Programming Throwdown', + 'country': 'USA', + 'description': 'Ever wondered how route planning apps, well, plan routes? In this episode, we navigate through this fascinating topic, a field as data-driven and systemic as it is magical and compelling. \n\nJoining us is Parker Woodward, Route Expert and Marketing Director for Route4Me. We discuss how route planning works, the intricacies behind it, and how services like Route4Me perform complex balancing acts between machine learning and user-generated feedback.', + 'genres': [ + { + 'name': 'How To', + 'id': '1499' + } + ], + 'episodeGuid': '0b5e5727-6569-4185-b08f-5414b76005ff', + 'releaseDate': '2021-07-07T17:08:21Z', + 'kind': 'podcast-episode', + 'wrapperType': 'podcastEpisode' + } + ] + })) + }) + ) as any; +} diff --git a/packages/core/src/plugins/plugins.types.ts b/packages/core/src/plugins/plugins.types.ts index 1789f98577..5ce14a73be 100644 --- a/packages/core/src/plugins/plugins.types.ts +++ b/packages/core/src/plugins/plugins.types.ts @@ -5,7 +5,8 @@ export enum SearchResultsSource { Discogs = 'Discogs', Musicbrainz = 'Musicbrainz', Bandcamp = 'Bandcamp', - iTunesPodcast = 'iTunesPodcast' + iTunesPodcast = 'iTunesPodcast', + iTunesMusic = 'iTunesMusic' } export enum AlbumType { diff --git a/packages/core/src/plugins/stream/iTunesPodcastPlugin.ts b/packages/core/src/plugins/stream/iTunesPodcastPlugin.ts index fff2ca3866..cdf007d51a 100644 --- a/packages/core/src/plugins/stream/iTunesPodcastPlugin.ts +++ b/packages/core/src/plugins/stream/iTunesPodcastPlugin.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import { StreamData, StreamQuery } from '../plugins.types'; import StreamProviderPlugin from '../streamProvider'; -import * as iTunes from '../../rest/iTunesPodcast'; +import * as iTunes from '../../rest/iTunes'; class iTunesPodcastPlugin extends StreamProviderPlugin { constructor() { diff --git a/packages/core/src/rest/iTunes.ts b/packages/core/src/rest/iTunes.ts new file mode 100644 index 0000000000..ff94ce7962 --- /dev/null +++ b/packages/core/src/rest/iTunes.ts @@ -0,0 +1,43 @@ +const apiUrl = 'https://itunes.apple.com'; + +/* PODCAST */ +export function podcastSearch(terms: string, limit: string): Promise { + return fetch(`${apiUrl}/search?limit=${limit}&media=podcast&term=${terms}`); +} + +export function episodesSearch(terms: string, limit: string) { + return fetch(`${apiUrl}/search?entity=podcastEpisode&limit=${limit}&media=podcast&term=${terms}`); +} + +export function getPodcast(id: string): Promise { + return fetch(`${apiUrl}/lookup?id=${id}`); +} + +export function getPodcastEpisodes(id: string, limit: string): Promise { + return fetch(`${apiUrl}/lookup?id=${id}&media=podcast&entity=podcastEpisode&limit=${limit}`); +} + +/* MUSIC */ +export function artistSearch(terms: string, limit: string): Promise { + return fetch(`${apiUrl}/search?limit=${limit}&media=music&entity=musicArtist&term=${terms}`); +} + +export function albumSearch(terms: string, limit: string): Promise { + return fetch(`${apiUrl}/search?limit=${limit}&media=music&entity=album&term=${terms}`); +} + +export function musicSearch(terms: string, limit: string): Promise { + return fetch(`${apiUrl}/search?limit=${limit}&media=music&entity=musicTrack&term=${terms}`); +} + +export function artistDetailsSearch(id: string, limit: string): Promise { + return fetch(`${apiUrl}/lookup?limit=${limit}&id=${id}&entity=song`); +} + +export function artistAlbumsSearch(id: string): Promise { + return fetch(`${apiUrl}/lookup?id=${id}&entity=album`); +} + +export function albumSongsSearch(id: string, limit: string): Promise { + return fetch(`${apiUrl}/lookup?limit=${limit}&id=${id}&entity=song`); +} diff --git a/packages/core/src/rest/iTunesPodcast.ts b/packages/core/src/rest/iTunesPodcast.ts deleted file mode 100644 index 505e00fd1a..0000000000 --- a/packages/core/src/rest/iTunesPodcast.ts +++ /dev/null @@ -1,17 +0,0 @@ -const apiUrl = 'https://itunes.apple.com'; - -export function podcastSearch(terms: string, limit: string): Promise { - return fetch(`${apiUrl}/search?limit=${limit}&media=podcast&term=${terms}`); -} - -export function episodesSearch(terms: string, limit: string) { - return fetch(`${apiUrl}/search?entity=podcastEpisode&limit=${limit}&media=podcast&term=${terms}`); -} - -export function getPodcast(id: string): Promise { - return fetch(`${apiUrl}/lookup?id=${id}`); -} - -export function getPodcastEpisodes(id: string, limit: string): Promise { - return fetch(`${apiUrl}/lookup?id=${id}&media=podcast&entity=podcastEpisode&limit=${limit}`); -} diff --git a/packages/core/src/rest/index.ts b/packages/core/src/rest/index.ts index 85471e8b70..6f5ad85d88 100644 --- a/packages/core/src/rest/index.ts +++ b/packages/core/src/rest/index.ts @@ -21,5 +21,5 @@ export { Deezer }; import * as Mastodon from './Mastodon'; export { Mastodon }; -import * as iTunesPodcast from './iTunesPodcast'; -export { iTunesPodcast }; +import * as iTunes from './iTunes'; +export { iTunes }; diff --git a/packages/core/src/rest/itunes.test.ts b/packages/core/src/rest/itunes.test.ts new file mode 100644 index 0000000000..d0133a47a8 --- /dev/null +++ b/packages/core/src/rest/itunes.test.ts @@ -0,0 +1,104 @@ +import _ from 'lodash'; +import { rest } from '..'; + +const mockFetch = (data) => { + global.fetch = jest.fn(() => + Promise.resolve({ + json: jest.fn(() => ({ + data + })) + }) + ) as any; +}; + +describe('iTunes podcast tests', () => { + beforeEach(() => { + _.invoke(fetch, 'resetMocks'); + }); + + it('search for podcasts', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.podcastSearch('Programming Throwdown', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search for episodes', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.episodesSearch('Programming Throwdown', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search for a podcast by podcastid', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.getPodcast('427166321')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search for a podcast episodes by podcastid', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.getPodcastEpisodes('427166321', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); +}); + +describe('iTunes music tests', () => { + beforeEach(() => { + _.invoke(fetch, 'resetMocks'); + }); + + it('search for artists', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.artistSearch('Queen', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search for albums', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.albumSearch('Live at Wembley Stadium', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search for music', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.musicSearch('We Will Rock You', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search an artist details by artistid', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.artistDetailsSearch('3296287', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search an artist albums by artistid', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.artistAlbumsSearch('3296287')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); + + it('search an album songs by albumid', async () => { + mockFetch('test'); + mockFetch([{ test: 'test data' }]); + const json = await (await rest.iTunes.albumSongsSearch('1440806041', '1')).json(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(json.data).toEqual([{ test: 'test data'}]); + }); +});