graphql-spotify
Advanced tools
Comparing version 0.43.0 to 0.44.0
401
dist/lib.js
@@ -85,3 +85,3 @@ module.exports = | ||
var _resolvers = __webpack_require__(19); | ||
var _resolvers = __webpack_require__(21); | ||
@@ -148,31 +148,39 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _AudioFeatures = __webpack_require__(11); | ||
var _TimeRange = __webpack_require__(11); | ||
var _TimeRange2 = _interopRequireDefault(_TimeRange); | ||
var _TopType = __webpack_require__(12); | ||
var _TopType2 = _interopRequireDefault(_TopType); | ||
var _AudioFeatures = __webpack_require__(13); | ||
var _AudioFeatures2 = _interopRequireDefault(_AudioFeatures); | ||
var _PlayHistory = __webpack_require__(12); | ||
var _PlayHistory = __webpack_require__(14); | ||
var _PlayHistory2 = _interopRequireDefault(_PlayHistory); | ||
var _ExternalUrls = __webpack_require__(13); | ||
var _ExternalUrls = __webpack_require__(15); | ||
var _ExternalUrls2 = _interopRequireDefault(_ExternalUrls); | ||
var _Category = __webpack_require__(14); | ||
var _Category = __webpack_require__(16); | ||
var _Category2 = _interopRequireDefault(_Category); | ||
var _RecommendationParameters = __webpack_require__(15); | ||
var _RecommendationParameters = __webpack_require__(17); | ||
var _RecommendationParameters2 = _interopRequireDefault(_RecommendationParameters); | ||
var _RecommendationsResponse = __webpack_require__(16); | ||
var _RecommendationsResponse = __webpack_require__(18); | ||
var _RecommendationsResponse2 = _interopRequireDefault(_RecommendationsResponse); | ||
var _RecommendationsSeedObject = __webpack_require__(17); | ||
var _RecommendationsSeedObject = __webpack_require__(19); | ||
var _RecommendationsSeedObject2 = _interopRequireDefault(_RecommendationsSeedObject); | ||
var _RootQuery = __webpack_require__(18); | ||
var _RootQuery = __webpack_require__(20); | ||
@@ -190,3 +198,3 @@ var _RootQuery2 = _interopRequireDefault(_RootQuery); | ||
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, _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, _TimeRange2.default, _TopType2.default]; | ||
@@ -252,2 +260,11 @@ exports.default = typeDefs; | ||
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 | ||
} | ||
@@ -389,2 +406,3 @@ `; | ||
saved: Boolean | ||
type: String | ||
} | ||
@@ -450,2 +468,3 @@ `; | ||
external_urls: ExternalUrls | ||
type: String | ||
} | ||
@@ -467,3 +486,3 @@ `; | ||
const Paging = ` | ||
union Item = Playlist | PlaylistTrack | Category | ||
union Item = Playlist | PlaylistTrack | Category | Artist | Track | ||
type Paging { | ||
@@ -488,2 +507,54 @@ href: String | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
const TimeRange = ` | ||
""" | ||
Over what time frame the affinities are computed | ||
""" | ||
enum TimeRange { | ||
""" | ||
calculated from several years of data and including all new data as it becomes available | ||
""" | ||
long_term | ||
""" | ||
approximately last 6 months | ||
""" | ||
medium_term | ||
""" | ||
approximately last 4 weeks | ||
""" | ||
short_term | ||
} | ||
`; | ||
exports.default = TimeRange; | ||
/***/ }), | ||
/* 12 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
const TopType = ` | ||
""" | ||
The type of entity to return | ||
""" | ||
enum TopType { | ||
artists | ||
tracks | ||
} | ||
`; | ||
exports.default = TopType; | ||
/***/ }), | ||
/* 13 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
@@ -514,3 +585,3 @@ }); | ||
/***/ }), | ||
/* 12 */ | ||
/* 14 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -541,3 +612,3 @@ | ||
/***/ }), | ||
/* 13 */ | ||
/* 15 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -560,3 +631,3 @@ | ||
/***/ }), | ||
/* 14 */ | ||
/* 16 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -590,3 +661,3 @@ | ||
/***/ }), | ||
/* 15 */ | ||
/* 17 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -613,3 +684,3 @@ | ||
/***/ }), | ||
/* 16 */ | ||
/* 18 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -636,3 +707,3 @@ | ||
/***/ }), | ||
/* 17 */ | ||
/* 19 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -678,3 +749,3 @@ | ||
/***/ }), | ||
/* 18 */ | ||
/* 20 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -727,8 +798,37 @@ | ||
recommendations(parameters: RecommendationParameters): RecommendationsResponse | ||
""" | ||
Get the current user’s top artists or tracks based on calculated affinity. | ||
(https://beta.developer.spotify.com/documentation/web-api/reference/personalization/get-users-top-artists-and-tracks/) | ||
Required Scope: **user-top-read** | ||
""" | ||
top(type: TopType!, limit: Int, offset: Int, time_range: TimeRange): Paging | ||
} | ||
type Mutation { | ||
""" | ||
save a track | ||
***returned Track only contains { id, saved }, won't resolve other fields*** | ||
""" | ||
saveTrack(trackId: String!): Track | ||
""" | ||
save several tracks. Max 50 | ||
***returned Track only contains { id, saved }, won't resolve other fields*** | ||
""" | ||
saveTracks(trackIds: [String]!): [Track] | ||
""" | ||
remove several tracks. Max 50 | ||
***returned Track only contains { id, saved }, won't resolve other fields*** | ||
""" | ||
removeTracks(trackIds: [String]!): [Track] | ||
""" | ||
follow a playlist | ||
***returned Playlist only contains { id, following }, won't resolve other fields*** | ||
assuming id is unique ATM so client with defaultDataIdFromObject will update its cache automatically | ||
""" | ||
followPlaylist(ownerId: String!, playlistId: String!, isPublic: Boolean = true): Playlist | ||
""" | ||
unfollow a playlist | ||
***returned Playlist only contains { id, following }, won't resolve other fields*** | ||
assuming id is unique ATM so client with defaultDataIdFromObject will update its cache automatically | ||
""" | ||
unfollowPlaylist(ownerId: String!, playlistId: String!): Playlist | ||
} | ||
@@ -740,3 +840,3 @@ `; | ||
/***/ }), | ||
/* 19 */ | ||
/* 21 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -752,4 +852,71 @@ | ||
var _SpotifyWebApi = __webpack_require__(20); | ||
var _SpotifyWebApi = __webpack_require__(22); | ||
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(Object.assign({ 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; | ||
} | ||
}; | ||
}; | ||
function makeResolvers(token) { | ||
@@ -759,3 +926,3 @@ const { | ||
AudioFeaturesLoader, SavedContainsLoader, TracksLoader, CategoriesLoader, RecommendationsLoader, | ||
CategoryPlaylistLoader, CategoryLoader | ||
CategoryPlaylistLoader, CategoryLoader, TopTypeLoader, PlaylistFollowersContainsLoader, MeLoader | ||
} = (0, _SpotifyWebApi.makeLoaders)(token); | ||
@@ -795,33 +962,17 @@ | ||
return await RecommendationsLoader.load(args.parameters); | ||
} | ||
}, | ||
Mutation: { | ||
saveTrack: async (obj, { trackId }) => { | ||
await (0, _SpotifyWebApi.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; | ||
}, | ||
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(Object.assign({ playlistId, userId }, args)); | ||
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 | ||
// when resolving a full playlist there are already items | ||
let allItems = items || []; | ||
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; | ||
} | ||
@@ -832,8 +983,36 @@ const fetchResults = await Promise.all(fetches); | ||
}); | ||
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 (0, _SpotifyWebApi.saveTracksToLib)(token, [trackId]); | ||
if (res.status !== 200) return; | ||
return { id: trackId, saved: true }; | ||
}, | ||
saveTracks: async (obj, { trackIds }) => { | ||
const res = await (0, _SpotifyWebApi.saveTracksToLib)(token, trackIds); | ||
if (res.status !== 200) return; | ||
return trackIds.map(id => ({ id, saved: true })); | ||
}, | ||
removeTracks: async (obj, { trackIds }) => { | ||
const res = await (0, _SpotifyWebApi.removeTracksFromLib)(token, trackIds); | ||
if (res.status !== 200) return; | ||
return trackIds.map(id => ({ id, saved: false })); | ||
}, | ||
followPlaylist: async (obj, { ownerId, playlistId, isPublic }) => { | ||
const res = await (0, _SpotifyWebApi.followPlaylist)(token, { ownerId, playlistId, isPublic }); | ||
if (res.status !== 200) return; | ||
return { id: playlistId, following: true }; | ||
}, | ||
unfollowPlaylist: async (obj, { ownerId, playlistId }) => { | ||
const res = await (0, _SpotifyWebApi.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); | ||
}, | ||
@@ -903,2 +1082,8 @@ saved: async ({ id }) => { | ||
} | ||
if (type === 'artist') { | ||
return 'Artist'; | ||
} | ||
if (type === 'track') { | ||
return 'Track'; | ||
} | ||
if (type === 'playlist') { | ||
@@ -918,3 +1103,3 @@ return 'Playlist'; | ||
/***/ }), | ||
/* 20 */ | ||
/* 22 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -928,3 +1113,6 @@ | ||
}); | ||
exports.saveTrackToLib = saveTrackToLib; | ||
exports.saveTracksToLib = saveTracksToLib; | ||
exports.removeTracksFromLib = removeTracksFromLib; | ||
exports.followPlaylist = followPlaylist; | ||
exports.unfollowPlaylist = unfollowPlaylist; | ||
exports.getSavedContains = getSavedContains; | ||
@@ -937,5 +1125,8 @@ exports.getFeaturedPlaylists = getFeaturedPlaylists; | ||
exports.getRecentlyPlayed = getRecentlyPlayed; | ||
exports.getTopType = getTopType; | ||
exports.getPlaylist = getPlaylist; | ||
exports.getUser = getUser; | ||
exports.getMe = getMe; | ||
exports.getPlaylistTracks = getPlaylistTracks; | ||
exports.getPlaylistFollowersContains = getPlaylistFollowersContains; | ||
exports.getAlbums = getAlbums; | ||
@@ -957,7 +1148,10 @@ exports.getTracks = getTracks; | ||
exports.makeRecommendationsLoader = makeRecommendationsLoader; | ||
exports.makeGetTopTypeLoader = makeGetTopTypeLoader; | ||
exports.makePlaylistFollowersContainsLoader = makePlaylistFollowersContainsLoader; | ||
exports.makeMeLoader = makeMeLoader; | ||
exports.makeLoaders = makeLoaders; | ||
__webpack_require__(21); | ||
__webpack_require__(23); | ||
var _dataloader = __webpack_require__(22); | ||
var _dataloader = __webpack_require__(24); | ||
@@ -977,10 +1171,10 @@ var _dataloader2 = _interopRequireDefault(_dataloader); | ||
function cacheKeyFnForQueryKeys(key) { | ||
return JSON.stringify(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('&'); | ||
} | ||
async function saveTrackToLib(token, trackIds) { | ||
async function saveTracksToLib(token, trackIds) { | ||
const url = `https://api.spotify.com/v1/me/tracks?ids=${trackIds.toString()}`; | ||
@@ -993,2 +1187,27 @@ return await fetch(url, { | ||
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) | ||
}); | ||
} | ||
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 }) | ||
}); | ||
} | ||
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) | ||
}); | ||
} | ||
async function getSavedContains(token, trackIds) { | ||
@@ -1059,2 +1278,12 @@ const url = `https://api.spotify.com/v1/me/tracks/contains?ids=${trackIds.toString()}`; | ||
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; | ||
} | ||
async function getPlaylist(token, userId, playlistId) { | ||
@@ -1078,2 +1307,11 @@ let res = await fetch(`https://api.spotify.com/v1/users/${userId}/playlists/${playlistId}`, { | ||
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; | ||
} | ||
async function getPlaylistTracks(token, { userId, playlistId, limit = 100, offset = 0 }) { | ||
@@ -1088,2 +1326,11 @@ let res = await fetch(`https://api.spotify.com/v1/users/${userId}/playlists/${playlistId}/tracks?${serializeToURLParameters({ limit, offset })}`, { | ||
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; | ||
} | ||
async function getAlbums(token, ids) { | ||
@@ -1211,2 +1458,3 @@ let res = await fetch(`https://api.spotify.com/v1/albums?ids=${ids.toString()}`, { | ||
} | ||
function makeRecommendationsLoader(token) { | ||
@@ -1219,2 +1467,30 @@ const batchLoadFn = async ([key]) => { | ||
function makeGetTopTypeLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getTopType(token, key)]; | ||
}; | ||
return new _dataloader2.default(batchLoadFn, { batch: false, cacheKeyFn: cacheKeyFnForQueryKeys }); | ||
} | ||
function makePlaylistFollowersContainsLoader(token) { | ||
const batchLoadFn = async ([key]) => { | ||
return [await getPlaylistFollowersContains(token, key)]; | ||
}; | ||
return new _dataloader2.default(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 | ||
function makeMeLoader(token) { | ||
let cacheMe = null; | ||
return { | ||
load: async () => { | ||
if (!cacheMe) { | ||
cacheMe = await getMe(token); | ||
} | ||
return cacheMe; | ||
} | ||
}; | ||
} | ||
function makeLoaders(token) { | ||
@@ -1233,3 +1509,6 @@ return { | ||
CategoryPlaylistLoader: makeCategoriesPlaylistsLoader(token), | ||
CategoryLoader: makeCategoryLoader(token) | ||
CategoryLoader: makeCategoryLoader(token), | ||
TopTypeLoader: makeGetTopTypeLoader(token), | ||
PlaylistFollowersContainsLoader: makePlaylistFollowersContainsLoader(token), | ||
MeLoader: makeMeLoader(token) | ||
}; | ||
@@ -1239,3 +1518,3 @@ } | ||
/***/ }), | ||
/* 21 */ | ||
/* 23 */ | ||
/***/ (function(module, exports) { | ||
@@ -1246,3 +1525,3 @@ | ||
/***/ }), | ||
/* 22 */ | ||
/* 24 */ | ||
/***/ (function(module, exports) { | ||
@@ -1249,0 +1528,0 @@ |
{ | ||
"name": "graphql-spotify", | ||
"version": "0.43.0", | ||
"version": "0.44.0", | ||
"description": "GraphQL Schema And Resolvers For Spotify Web API", | ||
@@ -5,0 +5,0 @@ "main": "dist/lib.js", |
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
251144
2284
42