Add initial Amazon Music support
This commit is contained in:
parent
bd85678303
commit
b0fcf55071
25 changed files with 248 additions and 222 deletions
128
lib/services/amazon/index.js
Normal file
128
lib/services/amazon/index.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
import { parse } from 'url';
|
||||
import { inspect } from 'util';
|
||||
import amazon from 'amazon-product-api';
|
||||
import urlMatch from './url';
|
||||
|
||||
const client = amazon.createClient({
|
||||
awsId: process.env.AWS_ACCESS_KEY_ID,
|
||||
awsSecret: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
awsTag: process.env.AWS_TAG,
|
||||
});
|
||||
|
||||
export function* lookupId(id, type) {
|
||||
|
||||
const results = yield client.itemLookup({
|
||||
itemId: id,
|
||||
responseGroup: 'ItemAttributes,Images,ItemIds',
|
||||
});
|
||||
|
||||
const result = results[0];
|
||||
|
||||
if (!result) {
|
||||
return { service: 'amazon' };
|
||||
}
|
||||
|
||||
if (type === 'album') {
|
||||
return {
|
||||
service: 'amazon',
|
||||
type: 'album',
|
||||
id: result.ASIN[0],
|
||||
name: result.ItemAttributes[0].Title[0],
|
||||
streamUrl: result.DetailPageURL[0],
|
||||
purchaseUrl: result.DetailPageURL[0],
|
||||
artwork: {
|
||||
small: result.SmallImage[0].URL[0],
|
||||
large: result.LargeImage[0].URL[0],
|
||||
},
|
||||
artist: {
|
||||
name: result.ItemAttributes[0].Creator[0]._,
|
||||
},
|
||||
};
|
||||
} else if (type === 'track') {
|
||||
return {
|
||||
service: 'amazon',
|
||||
type: 'track',
|
||||
id: result.ASIN[0],
|
||||
name: result.ItemAttributes[0].Title[0],
|
||||
streamUrl: result.DetailPageURL[0],
|
||||
purchaseUrl: result.DetailPageURL[0],
|
||||
artwork: {
|
||||
small: result.SmallImage[0].URL[0],
|
||||
large: result.LargeImage[0].URL[0],
|
||||
},
|
||||
album: {
|
||||
name: result.ItemAttributes[0],
|
||||
},
|
||||
artist: {
|
||||
name: result.ItemAttributes[0].Creator[0]._,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { service: 'amazon' };
|
||||
}
|
||||
|
||||
export function* search(data, original = {}) {
|
||||
|
||||
const type = data.type;
|
||||
const results = yield client.itemSearch({
|
||||
author: data.artist.name,
|
||||
title: data.name,
|
||||
searchIndex: 'MP3Downloads',
|
||||
responseGroup: 'ItemAttributes,Tracks,Images,ItemIds',
|
||||
});
|
||||
|
||||
const result = results[0];
|
||||
|
||||
if (result) {
|
||||
if (type === 'album') {
|
||||
return {
|
||||
service: 'amazon',
|
||||
type,
|
||||
id: result.ASIN[0],
|
||||
name: result.ItemAttributes[0].Title[0],
|
||||
streamUrl: result.DetailPageURL[0],
|
||||
purchaseUrl: result.DetailPageURL[0],
|
||||
artwork: {
|
||||
small: result.SmallImage[0].URL[0],
|
||||
large: result.LargeImage[0].URL[0],
|
||||
},
|
||||
artist: {
|
||||
name: result.ItemAttributes[0].Creator[0]._,
|
||||
},
|
||||
};
|
||||
} else if (type === 'track') {
|
||||
return {
|
||||
service: 'amazon',
|
||||
type,
|
||||
id: result.ASIN[0],
|
||||
name: result.ItemAttributes[0].Title[0],
|
||||
streamUrl: result.DetailPageURL[0],
|
||||
purchaseUrl: result.DetailPageURL[0],
|
||||
artwork: {
|
||||
small: result.SmallImage[0].URL[0],
|
||||
large: result.LargeImage[0].URL[0],
|
||||
},
|
||||
artist: {
|
||||
name: result.ItemAttributes[0].Creator[0]._,
|
||||
},
|
||||
album: {
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { service: 'amazon' };
|
||||
}
|
||||
|
||||
export function* parseUrl(url) {
|
||||
const matches = parse(url).path.match(/\/(albums|tracks)[/]+([^?]+)/);
|
||||
|
||||
if (matches && matches[2]) {
|
||||
return { type: matches[1].substring(0, 5), id: matches[2] };
|
||||
}
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
export const id = 'amazon';
|
||||
export const match = urlMatch;
|
11
lib/services/amazon/url.js
Normal file
11
lib/services/amazon/url.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { parse } from 'url';
|
||||
|
||||
export default function match(url) {
|
||||
const parsed = parse(url);
|
||||
if (!parsed.host.match(/\.amazon\.com$/)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = parse(url).path.match(/\/(albums)[/]+([^/]+)/);
|
||||
return (matches && !!matches[2]);
|
||||
}
|
|
@ -18,7 +18,7 @@ export function parseUrl(url) {
|
|||
}
|
||||
|
||||
function exactMatch(needle, haystack, type, various) {
|
||||
// try to find exact match
|
||||
// try to find exact match
|
||||
return haystack.find((entry) => {
|
||||
if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) {
|
||||
return false;
|
||||
|
@ -32,7 +32,7 @@ function exactMatch(needle, haystack, type, various) {
|
|||
}
|
||||
|
||||
function looseMatch(needle, haystack, type, various) {
|
||||
// try to find exact match
|
||||
// try to find exact match
|
||||
return haystack.find((entry) => {
|
||||
if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) {
|
||||
return false;
|
||||
|
|
|
@ -58,8 +58,8 @@ export function* lookupId(possibleId, type, countrycode) {
|
|||
streamUrl: null,
|
||||
purchaseUrl: result.collectionViewUrl,
|
||||
artwork: {
|
||||
small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '-ssl.mzstatic.com').replace('http://', 'https://')}`,
|
||||
large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '-ssl.mzstatic.com').replace('http://', 'https://')}`,
|
||||
small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`,
|
||||
large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`,
|
||||
},
|
||||
artist: {
|
||||
name: result.artistName,
|
||||
|
@ -120,8 +120,8 @@ export function* search(data) {
|
|||
streamUrl: result.collectionViewUrl,
|
||||
purchaseUrl: result.collectionViewUrl,
|
||||
artwork: {
|
||||
small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '-ssl.mzstatic.com').replace('http://', 'https://')}`,
|
||||
large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '-ssl.mzstatic.com').replace('http://', 'https://')}`,
|
||||
small: `${result.artworkUrl100.replace('100x100', '200x200').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`,
|
||||
large: `${result.artworkUrl100.replace('100x100', '600x600').replace('.mzstatic.com', '.mzstatic.com').replace('http://', 'https://')}`,
|
||||
},
|
||||
artist: {
|
||||
name: result.artistName,
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
import { parse } from 'url';
|
||||
import request from 'superagent';
|
||||
import 'superagent-bluebird-promise';
|
||||
import debuglog from 'debug';
|
||||
import urlMatch from './url';
|
||||
|
||||
const debug = debuglog('combine.fm:xbox');
|
||||
|
||||
if (!process.env.XBOX_CLIENT_ID || !process.env.XBOX_CLIENT_SECRET) {
|
||||
debug('XBOX_CLIENT_ID and XBOX_CLIENT_SECRET environment variables not found, deactivating Xbox Music.');
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
clientId: process.env.XBOX_CLIENT_ID,
|
||||
clientSecret: process.env.XBOX_CLIENT_SECRET,
|
||||
};
|
||||
|
||||
const apiRoot = 'https://music.xboxlive.com/1/content';
|
||||
|
||||
function* getAccessToken() {
|
||||
const authUrl = 'https://login.live.com/accesstoken.srf';
|
||||
const scope = 'app.music.xboxlive.com';
|
||||
const grantType = 'client_credentials';
|
||||
|
||||
const data = {
|
||||
client_id: credentials.clientId,
|
||||
client_secret: credentials.clientSecret,
|
||||
scope,
|
||||
grant_type: grantType,
|
||||
};
|
||||
const result = yield request.post(authUrl)
|
||||
.timeout(10000)
|
||||
.send(data)
|
||||
.set('Content-type', 'application/x-www-form-urlencoded')
|
||||
.promise();
|
||||
return result.body.access_token;
|
||||
}
|
||||
|
||||
function formatResponse(match) {
|
||||
const item = {
|
||||
service: 'xbox',
|
||||
type: match.Album ? 'track' : 'album',
|
||||
id: match.Id,
|
||||
name: match.Name,
|
||||
streamUrl: match.Link,
|
||||
purchaseUrl: null,
|
||||
artwork: {
|
||||
small: `${match.ImageUrl.replace('http://', 'https://')}&w=250&h=250`,
|
||||
large: `${match.ImageUrl.replace('http://', 'https://')}&w=500&h=500`,
|
||||
},
|
||||
artist: {
|
||||
name: match.Artists[0].Artist.Name,
|
||||
},
|
||||
};
|
||||
if (match.Album) {
|
||||
item.album = { name: match.Album.Name };
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function* apiCall(path) {
|
||||
const accessToken = yield getAccessToken();
|
||||
return request.get(apiRoot + path).timeout(10000).set('Authorization', `Bearer ${accessToken}`).promise();
|
||||
}
|
||||
|
||||
export function* lookupId(id, type) {
|
||||
const path = `/${id}/lookup`;
|
||||
const apiType = `${type.charAt(0).toUpperCase() + type.substr(1)}s`;
|
||||
try {
|
||||
const result = yield apiCall(path);
|
||||
return formatResponse(result.body[apiType].Items[0]);
|
||||
} catch (e) {
|
||||
if (e.status !== 404) {
|
||||
debug(e.body);
|
||||
}
|
||||
return { service: 'xbox' };
|
||||
}
|
||||
}
|
||||
|
||||
export function* parseUrl(url) {
|
||||
const parsed = parse(url);
|
||||
const parts = parsed.path.split('/');
|
||||
const type = parts[1];
|
||||
const idMatches = parts[4].match(/bz.[\w-]+/);
|
||||
const id = idMatches[0].replace('bz.', 'music.');
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
return yield lookupId(id, type);
|
||||
}
|
||||
|
||||
function exactMatch(item, artist, haystack) {
|
||||
// try to find exact match
|
||||
return haystack.find((entry) => {
|
||||
if (entry.Name === item && entry.Artists[0].Artist.Name === artist) {
|
||||
return entry;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function looseMatch(item, artist, haystack) {
|
||||
// try to find exact match
|
||||
return haystack.find((entry) => {
|
||||
if (entry.Name.indexOf(item) >= 0 && entry.Artists[0].Artist.Name.indexOf(artist) >= 0) {
|
||||
return entry;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export function* search(data) {
|
||||
function cleanParam(str) {
|
||||
return str.replace(/[:?&()[\]]+/g, '');
|
||||
}
|
||||
let query;
|
||||
const type = data.type;
|
||||
|
||||
if (type === 'album') {
|
||||
query = `${cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&')))} ${cleanParam(data.name)}`;
|
||||
} else if (type === 'track') {
|
||||
query = `${cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&')))} ${cleanParam(data.name)}`;
|
||||
}
|
||||
|
||||
const name = data.name;
|
||||
const path = `/music/search?q=${encodeURIComponent(query.trim())}&filters=${type}s`;
|
||||
try {
|
||||
const result = yield apiCall(path);
|
||||
|
||||
const apiType = `${type.charAt(0).toUpperCase() + type.substr(1)}s`;
|
||||
|
||||
let match = exactMatch(name, data.artist.name, result.body[apiType].Items, type);
|
||||
if (!match) {
|
||||
match = looseMatch(name, data.artist.name, result.body[apiType].Items, type);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
return formatResponse(match);
|
||||
}
|
||||
} catch (err) {
|
||||
return { service: 'xbox' };
|
||||
}
|
||||
return { service: 'xbox' };
|
||||
}
|
||||
|
||||
export const id = 'xbox';
|
||||
export const match = urlMatch;
|
|
@ -1,11 +0,0 @@
|
|||
import { parse } from 'url';
|
||||
|
||||
export default function match(url) {
|
||||
const parsed = parse(url);
|
||||
if (!parsed.host.match(/music.microsoft.com$/)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parts = parsed.path.split('/');
|
||||
return (parts[1] === 'album' || parts[1] === 'track') && parts[4];
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue