graphql-spotify
Advanced tools
Comparing version 0.4.141 to 0.43.0
382
dist/lib.js
@@ -85,3 +85,3 @@ module.exports = | ||
var _resolvers = __webpack_require__(14); | ||
var _resolvers = __webpack_require__(19); | ||
@@ -160,2 +160,22 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _Category = __webpack_require__(14); | ||
var _Category2 = _interopRequireDefault(_Category); | ||
var _RecommendationParameters = __webpack_require__(15); | ||
var _RecommendationParameters2 = _interopRequireDefault(_RecommendationParameters); | ||
var _RecommendationsResponse = __webpack_require__(16); | ||
var _RecommendationsResponse2 = _interopRequireDefault(_RecommendationsResponse); | ||
var _RecommendationsSeedObject = __webpack_require__(17); | ||
var _RecommendationsSeedObject2 = _interopRequireDefault(_RecommendationsSeedObject); | ||
var _RootQuery = __webpack_require__(18); | ||
var _RootQuery2 = _interopRequireDefault(_RootQuery); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -170,36 +190,4 @@ | ||
const RootQuery = ` | ||
type RootQuery { | ||
featuredPlaylists(limit: Int, offset: Int): Paging | ||
""" | ||
Returns the most recent 50 tracks played by a user | ||
""" | ||
recentlyPlayed: [PlayHistory] | ||
""" | ||
Get a playlist owned by a Spotify user | ||
""" | ||
playlist(userId: String!, playlistId: String!): Playlist | ||
""" | ||
Get an artist | ||
""" | ||
artist(artistId: String!): Artist | ||
""" | ||
Get audio features of a track | ||
""" | ||
audioFeatures(id: String!): AudioFeatures | ||
""" | ||
Get a track | ||
""" | ||
track(id: String!): Track | ||
} | ||
type Mutation { | ||
""" | ||
""" | ||
saveTrack(trackId: String!): Track | ||
} | ||
`; | ||
const typeDefs = [SchemaDefinition, _RootQuery2.default, _Playlist2.default, _Image2.default, _User2.default, _PlaylistTrack2.default, _Track2.default, _Album2.default, _Artist2.default, _Paging2.default, _AudioFeatures2.default, _PlayHistory2.default, _Category2.default, _RecommendationParameters2.default, _RecommendationsResponse2.default, _RecommendationsSeedObject2.default, _ExternalUrls2.default]; | ||
const typeDefs = [SchemaDefinition, RootQuery, _Playlist2.default, _Image2.default, _User2.default, _PlaylistTrack2.default, _Track2.default, _Album2.default, _Artist2.default, _Paging2.default, _AudioFeatures2.default, _PlayHistory2.default, _ExternalUrls2.default]; | ||
exports.default = typeDefs; | ||
@@ -397,2 +385,3 @@ | ||
saved in the user's library | ||
Required Scope: **user-library-read** | ||
""" | ||
@@ -476,3 +465,3 @@ saved: Boolean | ||
const Paging = ` | ||
union Item = Playlist | PlaylistTrack | ||
union Item = Playlist | PlaylistTrack | Category | ||
type Paging { | ||
@@ -575,5 +564,179 @@ href: String | ||
}); | ||
const Category = ` | ||
type Category { | ||
""" | ||
A link to the Web API endpoint returning full details of the category. | ||
""" | ||
href: String | ||
icons: [Image] | ||
id: String | ||
name: String | ||
""" | ||
Get a list of Spotify playlists tagged with a particular category | ||
limit: Default: 20. Minimum: 1. Maximum: 50 | ||
""" | ||
playlists(country: String, limit: Int, offset: Int): Paging | ||
} | ||
`; | ||
exports.default = Category; | ||
/***/ }), | ||
/* 15 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
// TODO after all parameters on targting attributes from https://beta.developer.spotify.com/documentation/web-api/reference/browse/get-recommendations/ | ||
const RecommendationParameters = ` | ||
input RecommendationParameters { | ||
limit: Int | ||
seed_artists: [String] | ||
seed_genres: [String] | ||
seed_tracks: [String] | ||
} | ||
`; | ||
exports.default = RecommendationParameters; | ||
/***/ }), | ||
/* 16 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
const RecommendationsResponse = ` | ||
type RecommendationsResponse { | ||
seeds: [RecommendationsSeedObject] | ||
""" | ||
An array of track object (simplified) ordered according to the parameters supplied | ||
""" | ||
tracks: [Track] | ||
} | ||
`; | ||
exports.default = RecommendationsResponse; | ||
/***/ }), | ||
/* 17 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
const RecommendationsSeedObject = ` | ||
type RecommendationsSeedObject { | ||
""" | ||
The number of tracks available after min_* and max_* filters have been applied | ||
""" | ||
afterFilteringSize: Int | ||
""" | ||
The number of tracks available after relinking for regional availability. | ||
""" | ||
afterRelinkingSize: Int | ||
""" | ||
A link to the full track or artist data for this seed. For tracks this will be a link to a Track Object. For artists a link to an Artist Object. For genre seeds, this value will be null. | ||
""" | ||
href: String | ||
""" | ||
The id used to select this seed. This will be the same as the string used in the seed_artists , seed_tracks or seed_genres parameter | ||
""" | ||
id: String | ||
""" | ||
The number of recommended tracks available for this seed. | ||
""" | ||
initialPoolSize: Int | ||
""" | ||
The entity type of this seed. One of artist , track or genre | ||
""" | ||
type: String | ||
} | ||
`; | ||
exports.default = RecommendationsSeedObject; | ||
/***/ }), | ||
/* 18 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
const RootQuery = ` | ||
type RootQuery { | ||
featuredPlaylists(limit: Int, offset: Int): Paging | ||
""" | ||
Returns the most recent 50 tracks played by a user | ||
Required Scope: **user-read-recently-played** | ||
""" | ||
recentlyPlayed: [PlayHistory] | ||
""" | ||
Get a playlist owned by a Spotify user | ||
""" | ||
playlist(userId: String!, playlistId: String!): Playlist | ||
""" | ||
Get an artist | ||
""" | ||
artist(artistId: String!): Artist | ||
""" | ||
Get audio features of a track | ||
""" | ||
audioFeatures(id: String!): AudioFeatures | ||
""" | ||
Get a track | ||
""" | ||
track(id: String!): Track | ||
""" | ||
https://beta.developer.spotify.com/documentation/web-api/reference/browse/get-list-categories/ | ||
Get a list of categories used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab). | ||
""" | ||
categories(limit: Int, offset: Int, country: String, locale: String): Paging | ||
""" | ||
Get a single category used to tag items in Spotify (on, for example, the Spotify player’s “Browse” tab) | ||
https://beta.developer.spotify.com/documentation/web-api/reference/browse/get-category/ | ||
""" | ||
category(id: String!): Category | ||
""" | ||
Save one or more tracks to the current user’s ‘Your Music’ library | ||
Required Scope: **user-library-modify** | ||
""" | ||
recommendations(parameters: RecommendationParameters): RecommendationsResponse | ||
} | ||
type Mutation { | ||
""" | ||
""" | ||
saveTrack(trackId: String!): Track | ||
} | ||
`; | ||
exports.default = RootQuery; | ||
/***/ }), | ||
/* 19 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.makeResolvers = makeResolvers; | ||
var _SpotifyWebApi = __webpack_require__(15); | ||
var _SpotifyWebApi = __webpack_require__(20); | ||
@@ -583,3 +746,4 @@ function makeResolvers(token) { | ||
PlaylistLoader, PlaylistTracksLoader, AlbumsLoader, UserLoader, ArtistsLoader, | ||
AudioFeaturesLoader, SavedContainsLoader, TracksLoader | ||
AudioFeaturesLoader, SavedContainsLoader, TracksLoader, CategoriesLoader, RecommendationsLoader, | ||
CategoryPlaylistLoader, CategoryLoader | ||
} = (0, _SpotifyWebApi.makeLoaders)(token); | ||
@@ -608,2 +772,13 @@ | ||
return await AudioFeaturesLoader.load(id); | ||
}, | ||
categories: async (obj, args) => { | ||
const res = await CategoriesLoader.load(args); | ||
return res.categories; | ||
}, | ||
category: async (obj, args) => { | ||
const res = await CategoryLoader.load(args.id); | ||
return res; | ||
}, | ||
recommendations: async (obj, args) => { | ||
return await RecommendationsLoader.load(args.parameters); | ||
} | ||
@@ -633,3 +808,5 @@ }, | ||
// otherwise always fetch all of the tracks | ||
let currentOffset = items.length; | ||
// when resolving a full playlist there are already items | ||
let allItems = items || []; | ||
let currentOffset = allItems.length; | ||
let fetches = []; | ||
@@ -641,3 +818,2 @@ while (currentOffset < total) { | ||
const fetchResults = await Promise.all(fetches); | ||
let allItems = items; | ||
fetchResults.forEach(result => { | ||
@@ -704,2 +880,8 @@ allItems = allItems.concat(result.items); | ||
}, | ||
Category: { | ||
playlists: async (category, args) => { | ||
const { playlists } = await CategoryPlaylistLoader.load({ id: category.id, queryParams: args }); | ||
return playlists; | ||
} | ||
}, | ||
Item: { | ||
@@ -717,2 +899,3 @@ __resolveType(object, context, info) { | ||
} | ||
return 'Category'; | ||
} | ||
@@ -725,3 +908,3 @@ } | ||
/***/ }), | ||
/* 15 */ | ||
/* 20 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -738,2 +921,6 @@ | ||
exports.getFeaturedPlaylists = getFeaturedPlaylists; | ||
exports.getCategoryPlaylists = getCategoryPlaylists; | ||
exports.getRecommendations = getRecommendations; | ||
exports.getCategories = getCategories; | ||
exports.getCategory = getCategory; | ||
exports.getRecentlyPlayed = getRecentlyPlayed; | ||
@@ -747,3 +934,2 @@ exports.getPlaylist = getPlaylist; | ||
exports.getAudioFeatures = getAudioFeatures; | ||
exports.makeLoaders = makeLoaders; | ||
exports.makeUserLoader = makeUserLoader; | ||
@@ -757,6 +943,11 @@ exports.makePlaylistLoader = makePlaylistLoader; | ||
exports.makeAudioFeaturesLoader = makeAudioFeaturesLoader; | ||
exports.makeCategoriesLoader = makeCategoriesLoader; | ||
exports.makeCategoryLoader = makeCategoryLoader; | ||
exports.makeCategoriesPlaylistsLoader = makeCategoriesPlaylistsLoader; | ||
exports.makeRecommendationsLoader = makeRecommendationsLoader; | ||
exports.makeLoaders = makeLoaders; | ||
__webpack_require__(16); | ||
__webpack_require__(21); | ||
var _dataloader = __webpack_require__(17); | ||
var _dataloader = __webpack_require__(22); | ||
@@ -775,2 +966,6 @@ var _dataloader2 = _interopRequireDefault(_dataloader); | ||
function cacheKeyFnForQueryKeys(key) { | ||
return JSON.stringify(key); | ||
} | ||
function serializeToURLParameters(obj) { | ||
@@ -807,2 +1002,38 @@ return Object.entries(obj).map(([key, val]) => `${key}=${val}`).join('&'); | ||
async function getCategoryPlaylists(token, id, queryParams = {}) { | ||
let res = await fetch(`https://api.spotify.com/v1/browse/categories/${id}/playlists?${serializeToURLParameters(queryParams)}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
async function getRecommendations(token, queryParams) { | ||
let res = await fetch(`https://api.spotify.com/v1/recommendations?${serializeToURLParameters(queryParams)}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
async function getCategories(token, queryParams = {}) { | ||
let res = await fetch(`https://api.spotify.com/v1/browse/categories?${serializeToURLParameters(queryParams)}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
async function getCategory(token, id) { | ||
let res = await fetch(`https://api.spotify.com/v1/browse/categories/${id}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
async function getRecentlyPlayed(token) { | ||
@@ -881,15 +1112,2 @@ const url = "https://api.spotify.com/v1/me/player/recently-played?limit=50"; | ||
function makeLoaders(token) { | ||
return { | ||
UserLoader: makeUserLoader(token), | ||
PlaylistLoader: makePlaylistLoader(token), | ||
PlaylistTracksLoader: makePlaylistTracksLoader(token), | ||
AlbumsLoader: makeAlbumsLoader(token), | ||
ArtistsLoader: makeArtistsLoader(token), | ||
TracksLoader: makeTracksLoader(token), | ||
SavedContainsLoader: makeSavedContainsLoader(token), | ||
AudioFeaturesLoader: makeAudioFeaturesLoader(token) | ||
}; | ||
} | ||
function makeUserLoader(token) { | ||
@@ -947,4 +1165,3 @@ const batchLoadFn = async ([key]) => { | ||
const batchLoadFn = async keys => { | ||
const saved = await getSavedContains(token, keys); | ||
return saved; | ||
return await getSavedContains(token, keys); | ||
}; | ||
@@ -962,4 +1179,49 @@ return new _dataloader2.default(batchLoadFn, { maxBatchSize: 50 }); | ||
function makeCategoriesLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getCategories(token, key)]; | ||
}; | ||
return new _dataloader2.default(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }); | ||
} | ||
function makeCategoryLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
const category = await getCategory(token, key); | ||
return [category]; | ||
}; | ||
return new _dataloader2.default(batchLoadFn, { batch: false }); | ||
} | ||
function makeCategoriesPlaylistsLoader(token) { | ||
const batchLoadFn = async ([{ id, queryParams }]) => { | ||
return [await getCategoryPlaylists(token, id, queryParams)]; | ||
}; | ||
return new _dataloader2.default(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }); | ||
} | ||
function makeRecommendationsLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getRecommendations(token, key)]; | ||
}; | ||
return new _dataloader2.default(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }); | ||
} | ||
function makeLoaders(token) { | ||
return { | ||
UserLoader: makeUserLoader(token), | ||
PlaylistLoader: makePlaylistLoader(token), | ||
PlaylistTracksLoader: makePlaylistTracksLoader(token), | ||
AlbumsLoader: makeAlbumsLoader(token), | ||
ArtistsLoader: makeArtistsLoader(token), | ||
TracksLoader: makeTracksLoader(token), | ||
SavedContainsLoader: makeSavedContainsLoader(token), | ||
AudioFeaturesLoader: makeAudioFeaturesLoader(token), | ||
CategoriesLoader: makeCategoriesLoader(token), | ||
RecommendationsLoader: makeRecommendationsLoader(token), | ||
CategoryPlaylistLoader: makeCategoriesPlaylistsLoader(token), | ||
CategoryLoader: makeCategoryLoader(token) | ||
}; | ||
} | ||
/***/ }), | ||
/* 16 */ | ||
/* 21 */ | ||
/***/ (function(module, exports) { | ||
@@ -970,3 +1232,3 @@ | ||
/***/ }), | ||
/* 17 */ | ||
/* 22 */ | ||
/***/ (function(module, exports) { | ||
@@ -973,0 +1235,0 @@ |
{ | ||
"name": "graphql-spotify", | ||
"version": "0.4.141", | ||
"version": "0.43.0", | ||
"description": "GraphQL Schema And Resolvers For Spotify Web API", | ||
@@ -12,3 +12,3 @@ "main": "dist/lib.js", | ||
}, | ||
"author": "Sitian Liu", | ||
"author": "Sitian Liu <goldensunliu@gmail.com> (https://www.sitianliu.com/)", | ||
"license": "MIT", | ||
@@ -15,0 +15,0 @@ "devDependencies": { |
import { | ||
getFeaturedPlaylists, getRecentlyPlayed, makeLoaders, saveTrackToLib | ||
getFeaturedPlaylists, getRecentlyPlayed, makeLoaders, saveTracksToLib, followPlaylist, unfollowPlaylist, | ||
removeTracksFromLib | ||
} from './SpotifyWebApi' | ||
const MAX_PLAYLIST_TRACKS_FETCH_LIMIT = 100 | ||
const MAX_TOP_TYPE_FETCH_LIMIT = 50 | ||
const MAX_PLAYLIST_FOLLOWERS_CONTAINS_LIMIT = 5; | ||
const makePlaylistResolver = ({ PlaylistLoader, PlaylistTracksLoader, PlaylistFollowersContainsLoader, MeLoader }) => { | ||
return { | ||
description: async (object) => { | ||
const {id: playlistId , owner: { id: userId }} = object | ||
const playlistFull = await PlaylistLoader.load({ playlistId, userId }) | ||
return playlistFull.description | ||
}, | ||
totalTracks: async ({ tracks: { total }}) => { | ||
return total | ||
}, | ||
followerCount: (obj) => { | ||
return obj.followers.total | ||
}, | ||
tracks: async (obj, args) => { | ||
const { id: playlistId , owner: { id: userId }, tracks: { total, offset, items }} = obj | ||
// fetch with limit and offset when specified | ||
if (args.limit && args.offset) { | ||
return await PlaylistTracksLoader.load({playlistId, userId, ...args }) | ||
} | ||
// otherwise always fetch all of the tracks | ||
// when resolving a full playlist, tracks are set the first 100 tracks | ||
let allItems = items || [] | ||
let currentOffset = allItems.length | ||
let fetches = [] | ||
while (currentOffset < total ) { | ||
const fetchSize = Math.min(MAX_PLAYLIST_TRACKS_FETCH_LIMIT, total - currentOffset); | ||
fetches.push(PlaylistTracksLoader.load({playlistId, userId, limit: fetchSize, offset: currentOffset })) | ||
currentOffset = currentOffset + fetchSize; | ||
} | ||
const fetchResults = await Promise.all(fetches); | ||
fetchResults.forEach((result) => { | ||
allItems = allItems.concat(result.items) | ||
}) | ||
return { total, items: allItems, limit: total, offset } | ||
}, | ||
followersContains: async (obj, { userIds }) => { | ||
const { id: playlistId , owner: { id: playlistUserId } } = obj | ||
let currentOffset = 0 | ||
let fetches = [] | ||
let total = userIds.length | ||
while (currentOffset < total ) { | ||
const fetchSize = Math.min(MAX_PLAYLIST_FOLLOWERS_CONTAINS_LIMIT, total - currentOffset); | ||
fetches.push(PlaylistFollowersContainsLoader.load({ playlistUserId, playlistId, | ||
userIds: userIds.slice(currentOffset, currentOffset + fetchSize) | ||
})) | ||
currentOffset = currentOffset + fetchSize; | ||
} | ||
const fetchResults = await Promise.all(fetches); | ||
return fetchResults.reduce((accu, value) => { return accu.concat(value) }, []) | ||
}, | ||
following: async (obj) => { | ||
if (obj.following || obj.following === false) return obj.following | ||
const { id: playlistId , owner: { id: playlistUserId } } = obj | ||
const me = await MeLoader.load() | ||
const userIds = [me.id] | ||
const [following] = await PlaylistFollowersContainsLoader.load({ playlistUserId, playlistId, userIds }) | ||
return following | ||
}, | ||
} | ||
} | ||
export function makeResolvers(token) { | ||
const { | ||
PlaylistLoader, PlaylistTracksLoader, AlbumsLoader, UserLoader, ArtistsLoader, | ||
AudioFeaturesLoader, SavedContainsLoader, TracksLoader | ||
AudioFeaturesLoader, SavedContainsLoader, TracksLoader, CategoriesLoader, RecommendationsLoader, | ||
CategoryPlaylistLoader, CategoryLoader, TopTypeLoader, PlaylistFollowersContainsLoader, MeLoader | ||
} = makeLoaders(token); | ||
@@ -32,43 +99,67 @@ | ||
return await AudioFeaturesLoader.load(id) | ||
} | ||
}, | ||
Mutation: { | ||
saveTrack: async (obj, {trackId}) => { | ||
await saveTrackToLib(token, [trackId]) | ||
return { id: trackId, saved: true } | ||
} | ||
}, | ||
Playlist: { | ||
description: async (object) => { | ||
const {id: playlistId , owner: { id: userId }} = object | ||
const playlistFull = await PlaylistLoader.load({ playlistId, userId }) | ||
return playlistFull.description | ||
}, | ||
totalTracks: async ({ tracks: { total }}) => { | ||
return total | ||
categories: async (obj, args) => { | ||
const res = await CategoriesLoader.load(args) | ||
return res.categories | ||
}, | ||
tracks: async (obj, args) => { | ||
const { id: playlistId , owner: { id: userId }, tracks: { total, offset, items, limit }} = obj | ||
// fetch with limit and offset when specified | ||
if (args.limit && args.offset) { | ||
return await PlaylistTracksLoader.load({playlistId, userId, ...args }) | ||
category: async (obj, args) => { | ||
const res = await CategoryLoader.load(args.id) | ||
return res | ||
}, | ||
recommendations: async (obj, args) => { | ||
return await RecommendationsLoader.load(args.parameters) | ||
}, | ||
top: async (obj, args) => { | ||
const { type, limit, offset, time_range } = args | ||
const result = await TopTypeLoader.load({ type, limit, offset, time_range }) | ||
if (result.items.length >= limit) { | ||
return result | ||
} | ||
// otherwise always fetch all of the tracks | ||
let currentOffset = items.length; | ||
let allItems = result.items | ||
const totalFetchSize = Math.min(limit, result.total) | ||
let currentOffset = allItems.length | ||
let fetches = [] | ||
while (currentOffset < total ) { | ||
fetches.push(PlaylistTracksLoader.load({playlistId, userId, limit, offset: currentOffset })) | ||
currentOffset = currentOffset + limit; | ||
while (currentOffset < totalFetchSize) { | ||
const fetchSize = Math.min(MAX_TOP_TYPE_FETCH_LIMIT, totalFetchSize - currentOffset); | ||
fetches.push(TopTypeLoader.load({type, time_range, limit: fetchSize, offset: currentOffset })) | ||
currentOffset = currentOffset + fetchSize; | ||
} | ||
const fetchResults = await Promise.all(fetches); | ||
let allItems = items | ||
fetchResults.forEach((result) => { | ||
allItems = allItems.concat(result.items) | ||
}) | ||
return { total, items: allItems, limit: total, offset } | ||
return { total: result.total, items: allItems, limit: allItems.length, offset } | ||
} | ||
}, | ||
Mutation: { | ||
saveTrack: async (obj, {trackId}) => { | ||
const res = await saveTracksToLib(token, [trackId]) | ||
if (res.status !== 200) return; | ||
return { id: trackId, saved: true } | ||
}, | ||
saveTracks: async (obj, {trackIds}) => { | ||
const res = await saveTracksToLib(token, trackIds) | ||
if (res.status !== 200) return; | ||
return trackIds.map(id => ({ id, saved: true })) | ||
}, | ||
removeTracks: async (obj, {trackIds}) => { | ||
const res = await removeTracksFromLib(token, trackIds) | ||
if (res.status !== 200) return; | ||
return trackIds.map(id => ({ id, saved: false })) | ||
}, | ||
followPlaylist: async (obj, { ownerId, playlistId, isPublic }) => { | ||
const res = await followPlaylist(token, { ownerId, playlistId, isPublic }) | ||
if (res.status !== 200) return; | ||
return { id: playlistId, following: true } | ||
}, | ||
unfollowPlaylist: async (obj, { ownerId, playlistId }) => { | ||
const res = await unfollowPlaylist(token, { ownerId, playlistId }) | ||
if (res.status !== 200) return; | ||
return { id: playlistId, following: false } | ||
} | ||
}, | ||
Playlist: makePlaylistResolver({ PlaylistLoader, PlaylistTracksLoader, PlaylistFollowersContainsLoader, MeLoader }), | ||
Track: { | ||
audio_features: async ({ id }) => { | ||
return await AudioFeatureLoader.load(id) | ||
return await AudioFeaturesLoader.load(id) | ||
}, | ||
@@ -126,2 +217,8 @@ saved: async ({id}) => { | ||
}, | ||
Category: { | ||
playlists: async(category, args) => { | ||
const { playlists } = await CategoryPlaylistLoader.load({ id: category.id, queryParams: args }) | ||
return playlists | ||
} | ||
}, | ||
Item: { | ||
@@ -133,2 +230,8 @@ __resolveType(object, context, info){ | ||
} | ||
if (type === 'artist') { | ||
return 'Artist' | ||
} | ||
if (type === 'track') { | ||
return 'Track' | ||
} | ||
if (type === 'playlist') { | ||
@@ -140,2 +243,3 @@ return 'Playlist' | ||
} | ||
return 'Category' | ||
} | ||
@@ -142,0 +246,0 @@ } |
@@ -12,2 +12,3 @@ const Artist = ` | ||
external_urls: ExternalUrls | ||
type: String | ||
} | ||
@@ -14,0 +15,0 @@ `; |
@@ -9,6 +9,14 @@ import Playlist from './Playlist' | ||
import Paging from './Paging' | ||
import TimeRange from './TimeRange' | ||
import TopType from './TopType' | ||
import AudioFeatures from './AudioFeatures' | ||
import PlayHistory from './PlayHistory' | ||
import ExternalUrls from './ExternalUrls' | ||
import Category from './Category' | ||
import RecommendationParameters from './RecommendationParameters' | ||
import RecommendationsResponse from './RecommendationsResponse' | ||
import RecommendationsSeedObject from './RecommendationsSeedObject' | ||
import RootQuery from './RootQuery' | ||
const SchemaDefinition = ` | ||
@@ -21,37 +29,6 @@ schema { | ||
const RootQuery = ` | ||
type RootQuery { | ||
featuredPlaylists(limit: Int, offset: Int): Paging | ||
""" | ||
Returns the most recent 50 tracks played by a user | ||
""" | ||
recentlyPlayed: [PlayHistory] | ||
""" | ||
Get a playlist owned by a Spotify user | ||
""" | ||
playlist(userId: String!, playlistId: String!): Playlist | ||
""" | ||
Get an artist | ||
""" | ||
artist(artistId: String!): Artist | ||
""" | ||
Get audio features of a track | ||
""" | ||
audioFeatures(id: String!): AudioFeatures | ||
""" | ||
Get a track | ||
""" | ||
track(id: String!): Track | ||
} | ||
type Mutation { | ||
""" | ||
""" | ||
saveTrack(trackId: String!): Track | ||
} | ||
`; | ||
const typeDefs = [SchemaDefinition, RootQuery, Playlist, Image, User, PlaylistTrack, Track, Album, Artist, Paging, | ||
AudioFeatures, PlayHistory, ExternalUrls]; | ||
AudioFeatures, PlayHistory, Category, RecommendationParameters, RecommendationsResponse, RecommendationsSeedObject, | ||
ExternalUrls, TimeRange, TopType]; | ||
export default typeDefs |
const Paging = ` | ||
union Item = Playlist | PlaylistTrack | ||
union Item = Playlist | PlaylistTrack | Category | Artist | Track | ||
type Paging { | ||
@@ -4,0 +4,0 @@ href: String |
@@ -48,2 +48,11 @@ const Playlist = ` | ||
uri: String | ||
""" | ||
Check to see if one or more Spotify users are following the playlist. | ||
""" | ||
followersContains(userIds: [String]!): [Boolean] | ||
""" | ||
Check to see the user is following the playlist. | ||
**Required Scope**: playlist-read-private | ||
""" | ||
following: Boolean | ||
} | ||
@@ -50,0 +59,0 @@ ` |
@@ -20,4 +20,6 @@ const Track = ` | ||
saved in the user's library | ||
Required Scope: **user-library-read** | ||
""" | ||
saved: Boolean | ||
type: String | ||
} | ||
@@ -24,0 +26,0 @@ ` |
@@ -12,8 +12,12 @@ import 'isomorphic-fetch' | ||
function cacheKeyFnForQueryKeys(key) { | ||
return serializeToURLParameters(key) | ||
} | ||
function serializeToURLParameters(obj) { | ||
return Object.entries(obj).map(([key, val]) => `${key}=${val}`).join('&') | ||
return Object.entries(obj).map(([key, val]) => val && `${key}=${val}`).join('&') | ||
} | ||
export async function saveTrackToLib(token, trackIds) { | ||
export async function saveTracksToLib(token, trackIds) { | ||
const url = `https://api.spotify.com/v1/me/tracks?ids=${trackIds.toString()}` | ||
@@ -26,2 +30,27 @@ return await fetch(url, { | ||
export async function removeTracksFromLib(token, trackIds) { | ||
const url = `https://api.spotify.com/v1/me/tracks?ids=${trackIds.toString()}` | ||
return await fetch(url, { | ||
method: 'DELETE', | ||
headers: makeHeaders(token) | ||
}) | ||
} | ||
export async function followPlaylist(token, { ownerId, playlistId, isPublic = true }) { | ||
const url = `https://api.spotify.com/v1/users/${ownerId}/playlists/${playlistId}/followers` | ||
return await fetch(url, { | ||
method: 'PUT', | ||
headers: makeHeaders(token), | ||
body: JSON.stringify({ public: isPublic }) | ||
}) | ||
} | ||
export async function unfollowPlaylist(token, { ownerId, playlistId }) { | ||
const url = `https://api.spotify.com/v1/users/${ownerId}/playlists/${playlistId}/followers` | ||
return await fetch(url, { | ||
method: 'DELETE', | ||
headers: makeHeaders(token) | ||
}) | ||
} | ||
export async function getSavedContains(token, trackIds) { | ||
@@ -47,2 +76,42 @@ const url = `https://api.spotify.com/v1/me/tracks/contains?ids=${trackIds.toString()}` | ||
export async function getCategoryPlaylists(token, id, queryParams = {}) | ||
{ | ||
let res = await fetch(`https://api.spotify.com/v1/browse/categories/${id}/playlists?${serializeToURLParameters(queryParams)}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getRecommendations(token, queryParams) | ||
{ | ||
let res = await fetch(`https://api.spotify.com/v1/recommendations?${serializeToURLParameters(queryParams)}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getCategories(token, queryParams = {}) | ||
{ | ||
let res = await fetch(`https://api.spotify.com/v1/browse/categories?${serializeToURLParameters(queryParams)}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getCategory(token, id) | ||
{ | ||
let res = await fetch(`https://api.spotify.com/v1/browse/categories/${id}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getRecentlyPlayed(token) { | ||
@@ -58,2 +127,12 @@ const url = "https://api.spotify.com/v1/me/player/recently-played?limit=50" | ||
export async function getTopType(token, params) { | ||
const { type, limit, offset, time_range } = params | ||
let res = await fetch(`https://api.spotify.com/v1/me/top/${type}?${serializeToURLParameters({ limit, offset, time_range })}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getPlaylist(token, userId, playlistId) | ||
@@ -79,2 +158,12 @@ { | ||
export async function getMe(token) | ||
{ | ||
let res = await fetch(`https://api.spotify.com/v1/me`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getPlaylistTracks(token, { userId, playlistId, limit = 100, offset = 0 }) | ||
@@ -90,2 +179,12 @@ { | ||
export async function getPlaylistFollowersContains(token, { playlistUserId, playlistId, userIds }) | ||
{ | ||
let res = await fetch(`https://api.spotify.com/v1/users/${playlistUserId}/playlists/${playlistId}/followers/contains?ids=${userIds.toString()}`, { | ||
method: 'GET', | ||
headers: makeHeaders(token) | ||
}); | ||
res = await res.json(); | ||
return res; | ||
} | ||
export async function getAlbums(token, ids) | ||
@@ -131,15 +230,2 @@ { | ||
export function makeLoaders(token) { | ||
return { | ||
UserLoader : makeUserLoader(token), | ||
PlaylistLoader : makePlaylistLoader(token), | ||
PlaylistTracksLoader: makePlaylistTracksLoader(token), | ||
AlbumsLoader: makeAlbumsLoader(token), | ||
ArtistsLoader: makeArtistsLoader(token), | ||
TracksLoader: makeTracksLoader(token), | ||
SavedContainsLoader: makeSavedContainsLoader(token), | ||
AudioFeaturesLoader: makeAudioFeaturesLoader(token) | ||
} | ||
} | ||
export function makeUserLoader(token) { | ||
@@ -197,4 +283,3 @@ const batchLoadFn = async ([key]) => { | ||
const batchLoadFn = async (keys) => { | ||
const saved = await getSavedContains(token, keys) | ||
return saved | ||
return await getSavedContains(token, keys) | ||
} | ||
@@ -210,2 +295,79 @@ return new Dataloader(batchLoadFn, { maxBatchSize: 50 }) | ||
return new Dataloader(batchLoadFn, { maxBatchSize: 50 }) | ||
} | ||
export function makeCategoriesLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getCategories(token, key)] | ||
} | ||
return new Dataloader(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }) | ||
} | ||
export function makeCategoryLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
const category = await getCategory(token, key) | ||
return [category] | ||
} | ||
return new Dataloader(batchLoadFn, { batch: false }) | ||
} | ||
export function makeCategoriesPlaylistsLoader(token) { | ||
const batchLoadFn = async ([{ id, queryParams }]) => { | ||
return [await getCategoryPlaylists(token, id, queryParams)] | ||
} | ||
return new Dataloader(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }) | ||
} | ||
export function makeRecommendationsLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getRecommendations(token, key)] | ||
} | ||
return new Dataloader(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }) | ||
} | ||
export function makeGetTopTypeLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getTopType(token, key)] | ||
} | ||
return new Dataloader(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }) | ||
} | ||
export function makePlaylistFollowersContainsLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getPlaylistFollowersContains(token, key)] | ||
} | ||
return new Dataloader(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }) | ||
} | ||
// Skipping the dataloader just here since the loader is meant to called with only .load() | ||
// Use it the same way as you would with a dataloader, i.e only on per request basis | ||
export function makeMeLoader(token) { | ||
let cacheMe = null; | ||
return { | ||
load : async () => { | ||
if (!cacheMe) { | ||
cacheMe = await getMe(token) | ||
} | ||
return cacheMe | ||
} | ||
} | ||
} | ||
export function makeLoaders(token) { | ||
return { | ||
UserLoader : makeUserLoader(token), | ||
PlaylistLoader : makePlaylistLoader(token), | ||
PlaylistTracksLoader: makePlaylistTracksLoader(token), | ||
AlbumsLoader: makeAlbumsLoader(token), | ||
ArtistsLoader: makeArtistsLoader(token), | ||
TracksLoader: makeTracksLoader(token), | ||
SavedContainsLoader: makeSavedContainsLoader(token), | ||
AudioFeaturesLoader: makeAudioFeaturesLoader(token), | ||
CategoriesLoader : makeCategoriesLoader(token), | ||
RecommendationsLoader: makeRecommendationsLoader(token), | ||
CategoryPlaylistLoader: makeCategoriesPlaylistsLoader(token), | ||
CategoryLoader: makeCategoryLoader(token), | ||
TopTypeLoader: makeGetTopTypeLoader(token), | ||
PlaylistFollowersContainsLoader: makePlaylistFollowersContainsLoader(token), | ||
MeLoader: makeMeLoader(token) | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
223672
31
2028
1
82
36