Fix Google Play Music and Spotify

This commit is contained in:
Jonathan Cremin 2017-07-20 14:31:07 +01:00
parent 688ec0f2f9
commit 6c29d50f1e
21 changed files with 566 additions and 559 deletions

View file

@ -1,19 +0,0 @@
image: node:6-slim
services:
- mongo
before_script:
- npm install
test:
stage: test
script:
- npm test
tags:
- docker
cache:
key: "$CI_BUILD_REF_NAME"
untracked: true
paths:
- node_modules

30
docker-compose.yml Normal file
View file

@ -0,0 +1,30 @@
version: "2"
services:
app:
build: ./
environment:
DEBUG: "match.audio*"
VUE_ENV: server
DATABASE_URL:
GOOGLE_EMAIL:
GOOGLE_PASSWORD:
XBOX_CLIENT_ID:
XBOX_CLIENT_SECRET:
YOUTUBE_KEY:
SPOTIFY_CLIENT_ID:
SPOTIFY_CLIENT_SECRET:
volumes:
- ./:/app:cached
ports:
- "3000:3000"
command: yarn run watch-server
database:
image: "postgres:9.6"
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "password"
POSTGRES_USER: "matchaudio"
POSTGRES_DB: "matchaudio"

View file

@ -13,7 +13,8 @@ fs.readdirSync(path.join(__dirname, 'services')).forEach(function(file) {
export default function* (url) {
let matchedService;
for (let service of services) {
matchedService = yield service.match(url);
console.log(service)
matchedService = service.match(url);
if (matchedService) {
const result = yield service.parseUrl(url);
return yield service.lookupId(result.id, result.type);

View file

@ -1,86 +1,110 @@
import { parse } from 'url';
import request from 'superagent';
import 'superagent-bluebird-promise';
import { match as urlMatch } from './url';
export let id = 'deezer';
import urlMatch from './url';
const apiRoot = 'https://api.deezer.com';
export const match = urlMatch;
export function parseUrl(url) {
let matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/);
const matches = parse(url).path.match(/\/(album|track)[/]+([^/]+)/);
if (matches && matches[2]) {
return module.exports.lookupId(matches[2], matches[1]);
} else {
throw new Error();
}
};
throw new Error();
}
function exactMatch(needle, haystack, type, various) {
// try to find exact match
return haystack.find((entry) => {
if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) {
return false;
}
const title = entry[type].title;
if (title) {
return entry;
}
return false;
});
}
function looseMatch(needle, haystack, type, various) {
// try to find exact match
return haystack.find((entry) => {
if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) {
return false;
}
const name = entry[type].title || entry[type].name;
if (name.indexOf(needle) >= 0) {
return entry[type];
}
return false;
});
}
export function* lookupId(id, type) {
let path = '/' + type + '/' + id;
const path = `/${type}/${id}`;
let {body} = yield request.get(apiRoot + path).promise();
const { body } = yield request.get(apiRoot + path).promise();
if (!body || body.error) {
let error = new Error('Not Found');
const error = new Error('Not Found');
error.status = 404;
return Promise.reject(error);
}
let item = body;
let coverUrl = item.cover || item.album.cover;
const item = body;
const coverUrl = item.cover || item.album.cover;
let cover = 'test';
// nasty hacks for superagent-bluebird-promise
try {
cover = yield request.get(coverUrl).redirects(0);
} catch(err) {
} catch (err) {
cover = err.originalError.response;
}
let artwork = {
const artwork = {
small: cover.headers.location.replace('120x120', '200x200'),
large: cover.headers.location.replace('120x120', '800x800')
large: cover.headers.location.replace('120x120', '800x800'),
};
if (type === 'album') {
return Promise.resolve({
service: 'deezer',
type: type,
type,
id: item.id,
name: item.title,
streamUrl: item.link,
purchaseUrl: null,
artwork: artwork,
artwork,
artist: {
name: item.artist.name
}
name: item.artist.name,
},
});
} else if (type === 'track') {
return Promise.resolve({
service: 'deezer',
type: type,
type,
id: item.id,
name: item.title,
streamUrl: item.album.link,
purchaseUrl: null,
artwork: artwork,
artwork,
artist: {
name: item.artist.name
name: item.artist.name,
},
album: {
name: item.album.title
}
name: item.album.title,
},
});
} else {
return Promise.reject(new Error());
}
};
return Promise.reject(new Error());
}
export function* search(data, original={}) {
let cleanParam = function(str) {
return str.replace(/[\:\?\&]+/, '');
};
let query, album;
let {type} = data;
export function* search(data, original = {}) {
function cleanParam(str) {
return str.replace(/[:?&]+/, '');
}
let query;
let album;
const { type } = data;
const various = data.artist.name === 'Various Artists' || data.artist.name === 'Various';
@ -89,65 +113,40 @@ export function* search(data, original={}) {
if (various) {
query = cleanParam(data.name);
} else {
query = cleanParam(data.artist.name) + ' ' + cleanParam(data.name);
query = `${cleanParam(data.artist.name)} ${cleanParam(data.name)}`;
}
album = data.name;
} else if (type === 'track') {
query = cleanParam(data.artist.name) + ' ' + cleanParam(data.albumName) + ' ' + cleanParam(data.name);
query = `${cleanParam(data.artist.name)} ${cleanParam(data.albumName)} ${cleanParam(data.name)}`;
album = data.albumName;
}
var path = '/search/' + type + '?q=' + encodeURIComponent(query);
const path = `/search/${type}?q=${encodeURIComponent(query)}`;
let response = yield request.get(apiRoot + path);
const response = yield request.get(apiRoot + path);
const name = original.name || data.name;
if (response.body.data.length > 0) {
let match;
if (!(match = exactMatch(name, response.body.data, data.type, various))) {
let match = exactMatch(name, response.body.data, data.type, various);
if (!match) {
match = looseMatch(name, response.body.data, data.type, various);
}
return yield module.exports.lookupId(response.body.data[0].id, type);
} else {
var matches = album.match(/^[^\(\[]+/);
if (matches && matches[0] && matches[0] !== album) {
var cleanedData = JSON.parse(JSON.stringify(data));
if (type === 'album') {
cleanedData.name = matches[0].trim();
} else if (type === 'track') {
cleanedData.albumName = matches[0].trim();
}
return yield module.exports.search(cleanedData, data);
} else {
return Promise.resolve({service: 'deezer'});
}
}
};
function exactMatch(needle, haystack, type, various) {
// try to find exact match
return haystack.find(function(entry) {
if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) {
return false;
const matches = album.match(/^[^([]+/);
if (matches && matches[0] && matches[0] !== album) {
const cleanedData = JSON.parse(JSON.stringify(data));
if (type === 'album') {
cleanedData.name = matches[0].trim();
} else if (type === 'track') {
cleanedData.albumName = matches[0].trim();
}
entry = entry[type];
if (entry.title === needle) {
return entry;
}
});
return yield module.exports.search(cleanedData, data);
}
return Promise.resolve({ service: 'deezer' });
}
function looseMatch(needle, haystack, type, various) {
// try to find exact match
return haystack.find(function(entry) {
if (!entry[type] || (various && (entry.artist.name !== 'Various' || entry.artist.name !== 'Various Artists'))) {
return false;
}
const name = entry[type].title || entry[type].name;
if (name.indexOf(needle) >= 0) {
return entry[type];
}
});
}
export const id = 'deezer';
export const match = urlMatch;

View file

@ -1,10 +1,10 @@
import { parse } from 'url';
export function* match(url) {
export default function match(url) {
const parsed = parse(url);
if (!parsed.host.match(/deezer\.com$/)) {
return false;
}
const matches = parsed.path.match(/\/(album|track)[\/]+([^\/]+)/);
const matches = parsed.path.match(/\/(album|track)[/]+([^/]+)/);
return matches.length > 1;
};
}

View file

@ -1,21 +1,143 @@
import { parse } from 'url';
import bluebird from 'bluebird';
import PlayMusic from 'playmusic';
import { match as urlMatch } from './url';
import debuglog from 'debug';
import urlMatch from './url';
const debug = debuglog('match.audio');
const pm = bluebird.promisifyAll(new PlayMusic());
export let id = 'google';
if (!process.env.GOOGLE_EMAIL || !process.env.GOOGLE_PASSWORD) {
console.warn('GOOGLE_EMAIL or GOOGLE_PASSWORD environment variables not found, deactivating Google Play Music.');
debug('GOOGLE_EMAIL or GOOGLE_PASSWORD environment variables not found, deactivating Google Play Music.');
}
let ready = pm.initAsync({email: process.env.GOOGLE_EMAIL, password: process.env.GOOGLE_PASSWORD}).catch(function(err) {
console.log(err);
});
const ready = pm.initAsync({
email: process.env.GOOGLE_EMAIL,
password: process.env.GOOGLE_PASSWORD })
.catch((err) => {
debug(err);
});
export const match = urlMatch;
export function* lookupId(id, type) {
yield ready;
if (type === 'album') {
const album = yield pm.getAlbumAsync(id, false);
return {
service: 'google',
type: 'album',
id: album.albumId,
name: album.name,
streamUrl: `https://play.google.com/music/m/${album.albumId}?signup_if_needed=1`,
purchaseUrl: `https://play.google.com/store/music/album?id=${album.albumId}`,
artwork: {
small: album.albumArtRef.replace('http:', 'https:'),
large: album.albumArtRef.replace('http:', 'https:'),
},
artist: {
name: album.artist,
},
};
} else if (type === 'track') {
const track = yield pm.getAllAccessTrackAsync(id);
return {
service: 'google',
type: 'track',
id: track.nid,
name: track.title,
streamUrl: `https://play.google.com/music/m/${track.nid}?signup_if_needed=1`,
purchaseUrl: `https://play.google.com/store/music/album?id=${track.albumId}`,
artwork: {
small: track.albumArtRef[0].url.replace('http:', 'https:'),
large: track.albumArtRef[0].url.replace('http:', 'https:'),
},
album: {
name: track.album,
},
artist: {
name: track.artist,
},
};
}
return { service: 'google' };
}
function exactMatch(needle, haystack, type) {
// try to find exact match
return haystack.find((entry) => {
if (!entry[type]) {
return false;
}
const title = entry[type].title;
if (title === needle) {
return entry;
}
return false;
});
}
function looseMatch(needle, haystack, type) {
// try to find exact match
return haystack.find((entry) => {
if (!entry[type]) {
return false;
}
const name = entry[type].title || entry[type].name;
if (name.indexOf(needle) >= 0) {
return entry[type];
}
return false;
});
}
export function* search(data, original = {}) {
yield ready;
let query;
let album;
const type = data.type;
if (type === 'album') {
query = `${data.artist.name} ${data.name}`;
album = data.name;
} else if (type === 'track') {
query = `${data.artist.name} ${data.albumName} ${data.name}`;
album = data.albumName;
}
const result = yield pm.searchAsync(query, 5);
if (!result.entries) {
const matches = album.match(/^[^([]+/);
if (matches && matches[0]) {
const cleanedData = JSON.parse(JSON.stringify(data));
if (type === 'album') {
cleanedData.name = data.name.match(/^[^([]+/)[0].trim();
} else if (type === 'track') {
cleanedData.albumName = data.albumName.match(/^[^([]+/)[0].trim();
cleanedData.name = data.name.match(/^[^([]+/)[0].trim();
}
return yield search(cleanedData, data);
}
return { service: 'google' };
}
const name = original.name || data.name;
let match = exactMatch(name, result.entries, data.type);
if (!match) {
match = looseMatch(name, result.entries, data.type);
}
if (!match) {
return { service: 'google' };
}
if (type === 'album') {
return yield lookupId(match.album.albumId, type);
} else if (type === 'track') {
return yield lookupId(match.track.storeId, type);
}
return { service: 'google' };
}
export function* parseUrl(url) {
yield ready;
@ -34,131 +156,16 @@ export function* parseUrl(url) {
}
if (id.length > 0) {
return {id: id, type: type};
} else {
return yield search({type: type, name: album, artist: {name: artist}});
return { id, type };
}
} else if(path) {
return yield search({ type, name: album, artist: { name: artist } });
} else if (path) {
const matches = path.match(/\/music\/m\/([\w]+)/);
const type = matches[1][0] === 'T' ? 'track' : 'album';
return yield lookupId(matches[1], type);
}
return false;
};
export function* lookupId(id, type) {
yield ready;
if (type === 'album') {
const album = yield pm.getAlbumAsync(id, false);
return {
service: 'google',
type: 'album',
id: album.albumId,
name: album.name,
streamUrl: 'https://play.google.com/music/m/' + album.albumId + '?signup_if_needed=1',
purchaseUrl: 'https://play.google.com/store/music/album?id=' + album.albumId,
artwork: {
small: album.albumArtRef.replace('http:', 'https:'),
large: album.albumArtRef.replace('http:', 'https:')
},
artist: {
name: album.artist
}
};
} else if (type === 'track') {
const track = yield pm.getAllAccessTrackAsync(id);
return {
service: 'google',
type: 'track',
id: track.nid,
name: track.title,
streamUrl: 'https://play.google.com/music/m/' + track.nid + '?signup_if_needed=1',
purchaseUrl: 'https://play.google.com/store/music/album?id=' + track.albumId,
artwork: {
small: track.albumArtRef[0].url.replace('http:', 'https:'),
large: track.albumArtRef[0].url.replace('http:', 'https:')
},
album: {
name: track.album
},
artist: {
name: track.artist
}
};
}
};
export function* search(data, original={}) {
yield ready;
let query, album;
const type = data.type;
if (type === 'album') {
query = data.artist.name + ' ' + data.name;
album = data.name;
} else if (type === 'track') {
query = data.artist.name + ' ' + data.albumName + ' ' + data.name;
album = data.albumName;
}
let result = yield pm.searchAsync(query, 5)
if (!result.entries) {
const matches = album.match(/^[^\(\[]+/);
if (matches && matches[0]) {
const cleanedData = JSON.parse(JSON.stringify(data));
if (type === 'album') {
cleanedData.name = data.name.match(/^[^\(\[]+/)[0].trim();
} else if (type === 'track') {
cleanedData.albumName = data.albumName.match(/^[^\(\[]+/)[0].trim();
cleanedData.name = data.name.match(/^[^\(\[]+/)[0].trim();
}
return yield search(cleanedData, data);
} else {
return {service: 'google'};
}
}
const name = original.name || data.name;
let match;
if (!(match = exactMatch(name, result.entries, data.type))) {
match = looseMatch(name, result.entries, data.type);
}
if (!match) {
return {service: 'google'};
} else {
if (type === 'album') {
return yield lookupId(match.album.albumId, type);
} else if (type === 'track') {
return yield lookupId(match.track.storeId, type);
}
}
};
function exactMatch(needle, haystack, type) {
// try to find exact match
return haystack.find(function(entry) {
if (!entry[type]) {
return false;
}
entry = entry[type];
if (entry.title === needle) {
return entry;
}
});
}
function looseMatch(needle, haystack, type) {
// try to find exact match
return haystack.find(function(entry) {
if (!entry[type]) {
return false;
}
const name = entry[type].title || entry[type].name;
if (name.indexOf(needle) >= 0) {
return entry[type];
}
});
}
export const match = urlMatch;
export const id = 'google';

View file

@ -1,7 +1,7 @@
import { parse } from 'url';
export function* match(url) {
var parsed = parse(url.replace(/\+/g, "%20"));
export default function match(url) {
const parsed = parse(url.replace(/\+/g, '%20'));
if (!parsed.host.match(/play\.google\.com$/)) {
return false;
}
@ -10,7 +10,7 @@ export function* match(url) {
const hash = parsed.hash;
if (hash) {
const parts = hash.split("/");
const parts = hash.split('/');
const id = parts[2];
const artist = parts[3];
@ -19,11 +19,11 @@ export function* match(url) {
} else if (artist.length > 0) {
return true;
}
} else if(path) {
} else if (path) {
const matches = path.match(/\/music\/m\/([\w]+)/);
if (matches[1]) {
return true
return true;
}
}
return false
};
return false;
}

View file

@ -2,17 +2,13 @@ import { parse } from 'url';
import querystring from 'querystring';
import request from 'superagent';
import 'superagent-bluebird-promise';
import { match as urlMatch } from './url';
export let id = 'itunes';
import urlMatch from './url';
const apiRoot = 'https://itunes.apple.com';
export const match = urlMatch;
export function* parseUrl(url) {
const parsed = parse(url);
const matches = parsed.path.match(/[\/]?([\/]?[a-z]{2}?)?[\/]+album[\/]+([^\/]+)[\/]+([^\?]+)/);
const matches = parsed.path.match(/[/]?([/]?[a-z]{2}?)?[/]+album[/]+([^/]+)[/]+([^?]+)/);
const query = querystring.parse(parsed.query);
if (matches) {
@ -23,20 +19,21 @@ export function* parseUrl(url) {
id = query.i;
}
return yield module.exports.lookupId(id, type, matches[1] || 'us');
} else {
throw new Error();
}
};
throw new Error();
}
export function* lookupId(id, type, cc) {
if (String(id).match(/^[a-z]{2}/)) {
cc = id.substr(0, 2);
id = id.substr(2);
export function* lookupId(possibleId, type, countrycode) {
let cc = countrycode;
let id = possibleId;
if (String(possibleId).match(/^[a-z]{2}/)) {
cc = possibleId.substr(0, 2);
id = possibleId.substr(2);
}
let path = '/lookup?id=' + id;
let path = `/lookup?id=${id}`;
if (cc) {
path = '/' + cc + path;
path = `/${cc}${path}`;
}
const response = yield request.get(apiRoot + path);
@ -49,54 +46,56 @@ export function* lookupId(id, type, cc) {
} else {
result = result.results[0];
let item = {
const item = {
service: 'itunes',
type: type,
type,
id: cc + id,
name: result.trackName ? result.trackName : result.collectionName,
streamUrl: null,
purchaseUrl: result.collectionViewUrl,
artwork: {
small: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '200x200').replace('http://', ''),
large: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '600x600').replace('http://', '')
small: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '200x200').replace('http://', '')}`,
large: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '600x600').replace('http://', '')}`,
},
artist: {
name: result.artistName
}
name: result.artistName,
},
};
if (type === 'track') {
item.album = {
name: result.collectionName
name: result.collectionName,
};
}
return item;
}
};
}
export function* search(data) {
const markets = ['us', 'gb', 'jp', 'br', 'de', 'es'];
let query, album, entity;
let query;
let album;
let entity;
const type = data.type;
if (type === 'album') {
query = data.artist.name + ' ' + data.name;
query = `${data.artist.name} ${data.name}`;
album = data.name;
entity = 'album';
} else if (type === 'track') {
query = data.artist.name + ' ' + data.albumName + ' ' + data.name;
query = `${data.artist.name} ${data.albumName} ${data.name}`;
album = data.albumName;
entity = 'musicTrack';
}
for (let market of markets) {
const path = '/' + market + '/search?term=' + encodeURIComponent(query) + '&media=music&entity=' + entity;
for (const market of markets) { // eslint-disable-line
const path = `/${market}/search?term=${encodeURIComponent(query)}&media=music&entity=${entity}`;
const response = yield request.get(apiRoot + path);
let result = JSON.parse(response.text);
if (!result.results[0]) {
const matches = album.match(/^[^\(\[]+/);
const matches = album.match(/^[^([]+/);
if (matches && matches[0] && matches[0] !== album) {
const cleanedData = JSON.parse(JSON.stringify(data));
if (type === 'album') {
@ -111,27 +110,30 @@ export function* search(data) {
const item = {
service: 'itunes',
type: type,
id: 'us' + result.collectionId,
type,
id: `us${result.collectionId}`,
name: result.trackName ? result.trackName : result.collectionName,
streamUrl: null,
purchaseUrl: result.collectionViewUrl,
artwork: {
small: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '200x200').replace('http://', ''),
large: 'https://match.audio/itunes/' + result.artworkUrl100.replace('100x100', '600x600').replace('http://', '')
small: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '200x200').replace('http://', '')}`,
large: `https://match.audio/itunes/${result.artworkUrl100.replace('100x100', '600x600').replace('http://', '')}`,
},
artist: {
name: result.artistName
}
name: result.artistName,
},
};
if (type === 'track') {
item.album = {
name: result.collectionName
name: result.collectionName,
};
}
return item;
}
}
return {service: 'itunes'};
};
return { service: 'itunes' };
}
export const id = 'itunes';
export const match = urlMatch;

View file

@ -1,15 +1,13 @@
import { parse } from 'url';
import querystring from 'querystring';
export function* match(url, type) {
export default function match(url) {
const parsed = parse(url);
if (!parsed.host.match(/itunes.apple\.com$/)) {
return false;
}
const matches = parsed.path.match(/[\/]?([\/]?[a-z]{2}?)?[\/]+album[\/]+([^\/]+)[\/]+([^\?]+)/);
const query = querystring.parse(parsed.query);
const matches = parsed.path.match(/[/]?([/]?[a-z]{2}?)?[/]+album[/]+([^/]+)[/]+([^?]+)/);
return !!matches[3];
};
}

View file

@ -1,101 +1,122 @@
import { parse } from 'url';
import bluebird from 'bluebird';
import spotifyCB from 'spotify';
import request from 'superagent';
import 'superagent-bluebird-promise';
const spotify = bluebird.promisifyAll(spotifyCB);
import { match as urlMatch } from './url';
import SpotifyWebApi from 'spotify-web-api-node';
import urlMatch from './url';
export let id = "spotify";
const spotify = new SpotifyWebApi({
clientId: process.env.SPOTIFY_CLIENT_ID,
clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
redirectUri: 'https://match.audio',
});
export const match = urlMatch;
export function* parseUrl(url) {
var matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/);
function exactMatch(needle, haystack, type) {
// try to find exact match
return haystack.find((entry) => {
if (entry.type !== type) {
return false;
}
if (matches && matches[2]) {
return yield lookupId(matches[2], matches[1]);
}
if (entry.name === needle) {
return entry;
}
return false;
});
}
function looseMatch(needle, haystack, type) {
// try to find exact match
return haystack.find((entry) => {
if (entry.type !== type) {
return false;
}
if (entry.name.indexOf(needle) >= 0) {
return entry;
}
return false;
});
}
export function* lookupId(id, type) {
const data = yield spotify.lookupAsync({id: id, type: type});
if (data.error) {
var error = new Error("Not Found");
error.status = 404;
throw error;
}
const token = yield spotify.clientCredentialsGrant();
spotify.setAccessToken(token.body['access_token']);
let data = yield spotify[`get${type.charAt(0).toUpperCase()}${type.slice(1)}s`]([id]);
var artist = data.artists[0];
data = data.body[`${type}s`][0];
if (type == "album") {
const artist = data.artists[0];
if (type === 'album') {
return {
service: "spotify",
type: type,
service: 'spotify',
type,
id: data.id,
name: data.name,
streamUrl: "https://play.spotify.com/" + type + "/" + data.id,
streamUrl: `https://play.spotify.com/${type}/${data.id}`,
purchaseUrl: null,
artwork: {
small: data.images[1].url.replace("http:", "https:"),
large: data.images[0].url.replace("http:", "https:"),
small: data.images[1].url.replace('http:', 'https:'),
large: data.images[0].url.replace('http:', 'https:'),
},
artist: {
name: artist.name
}
name: artist.name,
},
};
} else if (type == "track") {
} else if (type === 'track') {
return {
service: "spotify",
type: type,
service: 'spotify',
type,
id: data.id,
name: data.name,
streamUrl: "https://play.spotify.com/" + type + "/" + data.id,
streamUrl: `https://play.spotify.com/${type}/${data.id}`,
purchaseUrl: null,
artwork: {
small: data.album.images[1].url.replace("http:", "https:"),
large: data.album.images[0].url.replace("http:", "https:"),
small: data.album.images[1].url.replace('http:', 'https:'),
large: data.album.images[0].url.replace('http:', 'https:'),
},
artist: {
name: artist.name
name: artist.name,
},
album: {
name: data.album.name
}
name: data.album.name,
},
};
}
return { service: 'spotify' };
}
export function* search(data, original={}) {
export function* search(data, original = {}) {
const token = yield spotify.clientCredentialsGrant();
spotify.setAccessToken(token.body['access_token']);
const markets = ['US', 'GB', 'JP', 'BR', 'DE', 'ES'];
const cleanParam = function(str) {
var chopChars = ['&', '[', '('];
chopChars.forEach(function(chr) {
function cleanParam(str) {
const chopChars = ['&', '[', '('];
chopChars.forEach((chr) => {
if (data.artist.name.indexOf('&') > 0) {
str = str.substring(0, data.artist.name.indexOf(chr));
str = str.substring(0, data.artist.name.indexOf(chr)); // eslint-disable-line no-param-reassign,max-len
}
})
return str.replace(/[\:\?]+/, "");
});
return str.replace(/[:?]+/, '');
}
let query, album;
let query;
const type = data.type;
if (type == "album") {
query = "artist:" + cleanParam(data.artist.name) + " album:" + cleanParam(data.name);
album = data.name;
} else if (type == "track") {
query = "artist:" + cleanParam(data.artist.name) + " track:" + cleanParam(data.name) + ( cleanParam(data.albumName).length > 0 ? " album:" + cleanParam(data.albumName): "");
album = data.albumName;
if (type === 'album') {
query = `artist:${cleanParam(data.artist.name)} album:${cleanParam(data.name)}`;
} else if (type === 'track') {
query = `artist:${cleanParam(data.artist.name)} track:${cleanParam(data.name)}${cleanParam(data.albumName).length > 0 ? ` album:${cleanParam(data.albumName)}` : ''}`;
}
for (let market of markets) {
const response = yield request.get('https://api.spotify.com/v1/search?type=' + type + '&q=' + encodeURI(query) + '&market=' + market);
const items = response.body[type + 's'].items;
for (const market of markets) { // eslint-disable-line
const response = yield spotify[`search${type.charAt(0).toUpperCase()}${type.slice(1)}s`](query, { market });
const items = response.body[`${type}s`].items;
const name = original.name || data.name;
let match;
if (!(match = exactMatch(name, items, type))) {
let match = exactMatch(name, items, type);
if (!match) {
match = looseMatch(name, items, type);
}
@ -107,31 +128,17 @@ export function* search(data, original={}) {
}
}
}
return {service: "spotify"};
return { service: 'spotify' };
}
function exactMatch(needle, haystack, type) {
// try to find exact match
return haystack.find(function(entry) {
if (entry.type !== type) {
return false;
}
export function* parseUrl(url) {
const matches = parse(url).path.match(/\/(album|track)[/]+([^/]+)/);
if (entry.name === needle) {
return entry;
}
});
if (matches && matches[2]) {
return yield lookupId(matches[2], matches[1]);
}
throw new Error();
}
function looseMatch(needle, haystack, type) {
// try to find exact match
return haystack.find(function(entry) {
if (entry.type !== type) {
return false;
}
if (entry.name.indexOf(needle) >= 0) {
return entry
}
});
}
export const id = 'spotify';
export const match = urlMatch;

View file

@ -1,11 +1,11 @@
import { parse } from 'url';
export function* match(url, type) {
export default function match(url) {
const parsed = parse(url);
if (!parsed.host.match(/spotify\.com$/)) {
return false;
}
const matches = parse(url).path.match(/\/(album|track)[\/]+([^\/]+)/);
const matches = parse(url).path.match(/\/(album|track)[/]+([^/]+)/);
return matches && !!matches[2];
};
}

View file

@ -1,21 +1,18 @@
import { parse } from 'url';
import querystring from 'querystring';
import request from 'superagent';
import 'superagent-bluebird-promise';
import { match as urlMatch } from './url';
import debuglog from 'debug';
import urlMatch from './url';
const debug = debuglog('match.audio:xbox');
export let id = 'xbox';
if (!process.env.XBOX_CLIENT_ID || !process.env.XBOX_CLIENT_SECRET) {
console.warn('XBOX_CLIENT_ID and XBOX_CLIENT_SECRET environment variables not found, deactivating Xbox Music.');
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
clientSecret: process.env.XBOX_CLIENT_SECRET,
};
const apiRoot = 'https://music.xboxlive.com/1/content';
@ -28,10 +25,14 @@ function* getAccessToken() {
const data = {
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
scope: scope,
grant_type: grantType
scope,
grant_type: grantType,
};
const result = yield request.post(authUrl).timeout(10000).send(data).set('Content-type', 'application/x-www-form-urlencoded').promise();
const result = yield request.post(authUrl)
.timeout(10000)
.send(data)
.set('Content-type', 'application/x-www-form-urlencoded')
.promise();
return result.body.access_token;
}
@ -44,41 +45,27 @@ function formatResponse(match) {
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'
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
}
name: match.Artists[0].Artist.Name,
},
};
if (match.Album) {
item.album = {name: match.Album.Name}
item.album = { name: match.Album.Name };
}
return item;
}
function* apiCall(path) {
const access_token = yield getAccessToken();
return request.get(apiRoot + path).timeout(10000).set('Authorization', 'Bearer ' + access_token).promise();
}
export const match = urlMatch;
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);
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';
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]);
@ -86,31 +73,61 @@ export function* lookupId(id, type) {
if (e.status !== 404) {
debug(e.body);
}
return {service: 'xbox'};
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) {
var cleanParam = function(str) {
return str.replace(/[\:\?\&\(\)\[\]]+/g, '');
function cleanParam(str) {
return str.replace(/[:?&()[\]]+/g, '');
}
let query, album;
let query;
const type = data.type;
if (type == 'album') {
query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name);
album = data.name;
} else if (type == 'track') {
query = cleanParam(data.artist.name.substring(0, data.artist.name.indexOf('&'))) + ' ' + cleanParam(data.name);
album = data.albumName
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';
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';
const apiType = `${type.charAt(0).toUpperCase() + type.substr(1)}s`;
let match = exactMatch(name, data.artist.name, result.body[apiType].Items, type);
if (!match) {
@ -121,24 +138,10 @@ export function* search(data) {
return formatResponse(match);
}
} catch (err) {
return {service: 'xbox'};
return { service: 'xbox' };
}
};
function exactMatch(item, artist, haystack, type) {
// try to find exact match
return haystack.find(function(entry) {
if (entry.Name === item && entry.Artists[0].Artist.Name === artist) {
return entry;
}
});
return { service: 'xbox' };
}
function looseMatch(item, artist, haystack, type) {
// try to find exact match
return haystack.find(function(entry) {
if (entry.Name.indexOf(item) >= 0 && entry.Artists[0].Artist.Name.indexOf(artist) >= 0) {
return entry;
}
});
}
export const id = 'xbox';
export const match = urlMatch;

View file

@ -1,11 +1,11 @@
import { parse } from 'url';
export function* match(url, type) {
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];
};
return (parts[1] === 'album' || parts[1] === 'track') && parts[4];
}

View file

@ -1,15 +0,0 @@
import { parse } from 'url';
import querystring from 'querystring';
import request from 'superagent';
import 'superagent-bluebird-promise';
const credentials = {
key: process.env.YOUTUBE_KEY,
};
const apiRoot = 'https://www.googleapis.com/freebase/v1/topic';
export function* get(topic) {
const result = yield request.get(apiRoot + topic + '?key=' + credentials.key).promise();
return result.body;
}

View file

@ -1,18 +1,15 @@
import { parse } from 'url';
import querystring from 'querystring';
import moment from 'moment';
import request from 'superagent';
import Nodebrainz from 'nodebrainz';
import 'superagent-bluebird-promise';
import { match as urlMatch } from './url';
import freebase from './freebase';
import debuglog from 'debug';
import urlMatch from './url';
const debug = debuglog('match.audio:youtube');
module.exports.id = 'youtube';
if (!process.env.YOUTUBE_KEY) {
console.warn('YOUTUBE_KEY environment variable not found, deactivating Youtube.');
debug('YOUTUBE_KEY environment variable not found, deactivating Youtube.');
}
const credentials = {
@ -21,7 +18,77 @@ const credentials = {
const apiRoot = 'https://www.googleapis.com/youtube/v3';
export const match = urlMatch;
const nodebrainz = new Nodebrainz({
userAgent: 'match-audio ( https://match.audio )',
defaultLimit: 10,
retryOn: true,
retryDelay: 3000,
retryCount: 10,
});
export function* lookupId(id) {
const path = `/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=${id}&key=${credentials.key}`;
try {
const result = yield request.get(apiRoot + path).promise();
const item = result.body.items[0];
nodebrainz.luceneSearch('release', { query: item.snippet.title }, (err, response) => {
response.releases.forEach((release) => {
//console.log(release);
});
});
const match = {
id,
service: 'youtube',
name: item.snippet.title,
type: 'track',
album: { name: '' },
streamUrl: `https://youtu.be/${id}`,
purchaseUrl: null,
artwork: {
small: item.snippet.thumbnails.medium.url,
large: item.snippet.thumbnails.high.url,
},
};
return match;
} catch (err) {
debug(err);
return { service: 'youtube' };
}
}
export function* search(data) {
let query;
const type = data.type;
if (type === 'album') {
query = `${data.artist.name} ${data.name}`;
} else if (type === 'track') {
query = `${data.artist.name} ${data.name}`;
}
const path = `/search?part=snippet&q=${encodeURIComponent(query)}&type=video&videoCaption=any&videoCategoryId=10&key=${credentials.key}`;
const result = yield request.get(apiRoot + path).promise();
const item = result.body.items[0];
if (!item) {
return { service: 'youtube', type: 'video' };
}
return {
service: 'youtube',
type: 'video',
id: item.id.videoId,
name: item.snippet.title,
streamUrl: `https://www.youtube.com/watch?v=${item.id.videoId}`,
purchaseUrl: null,
artwork: {
small: item.snippet.thumbnails.medium.url,
large: item.snippet.thumbnails.high.url,
},
};
}
export function parseUrl(url) {
const parsed = parse(url);
@ -37,93 +104,5 @@ export function parseUrl(url) {
return lookupId(id, 'track');
}
export function* lookupId(id, type) {
const path = '/videos?part=snippet%2CtopicDetails%2CcontentDetails&id=' + id + '&key=' + credentials.key;
try {
const result = yield request.get(apiRoot + path).promise();
const item = result.body.items[0];
if (!item.topicDetails.topicIds) {
return {service: 'youtube'};
}
const match = {
id: id,
service: 'youtube',
name: item.snippet.title,
type: 'track',
album: {name: ''},
streamUrl: 'https://youtu.be/' + id,
purchaseUrl: null,
artwork: {
small: item.snippet.thumbnails.medium.url,
large: item.snippet.thumbnails.high.url,
}
};
for (let topic of yield freebase.get(topicId)) {
const musicalArtist = topic.property['/type/object/type'].values.some((value) => {
return value.text == 'Musical Artist';
});
const musicalRecording = topic.property['/type/object/type'].values.some(function(value) {
return value.text == 'Musical Recording';
});
const musicalAlbum = topic.property['/type/object/type'].values.some(function(value) {
return value.text == 'Musical Album';
})
if (musicalArtist) {
match.artist = {name: topic.property['/type/object/name'].values[0].text};
} else if (musicalRecording) {
match.name = topic.property['/type/object/name'].values[0].text;
if (topic.property['/music/recording/releases']) {
match.type = 'album';
match.albumName = topic.property['/music/recording/releases'].values[0].text;
}
} else if (musicalAlbum) {
match.name = topic.property['/type/object/name'].values[0].text;
match.type = 'album';
}
}
return match;
} catch (e) {
debug(e.body);
return {'service': 'youtube'};
}
};
export function* search(data) {
let query, album;
const type = data.type;
if (type == 'album') {
query = data.artist.name + ' ' + data.name;
album = data.name;
} else if (type == 'track') {
query = data.artist.name + ' ' + data.name;
album = data.albumName
}
const path = '/search?part=snippet&q=' + encodeURIComponent(query) + '&type=video&videoCaption=any&videoCategoryId=10&key=' + credentials.key;
const result = yield request.get(apiRoot + path).promise();
const item = result.body.items[0];
if (!item) {
return {service:'youtube', type: 'video'};
} else {
return {
service: 'youtube',
type: 'video',
id: item.id.videoId,
name: item.snippet.title,
streamUrl: 'https://www.youtube.com/watch?v=' + item.id.videoId,
purchaseUrl: null,
artwork: {
small: item.snippet.thumbnails.medium.url,
large: item.snippet.thumbnails.high.url,
}
};
}
};
export const id = 'youtube';
export const match = urlMatch;

View file

@ -1,7 +1,7 @@
import { parse } from 'url';
import querystring from 'querystring';
export function* match(url, type) {
export default function match(url) {
const parsed = parse(url);
if (parsed.host.match(/youtu\.be$/)) {
return true;
@ -10,4 +10,4 @@ export function* match(url, type) {
return !!query.v;
}
return false;
};
}

View file

@ -70,11 +70,12 @@
"koa-websocket": "^2.1.0",
"moment": "^2.14.1",
"node-uuid": "~1.4.2",
"nodebrainz": "^2.1.1",
"pg": "^6.1.0",
"playmusic": "~2.2.1",
"playmusic": "https://github.com/jamon/playmusic.git#37e98f39c33fc5359a8a30b8c8e422161a4be9a8",
"raven": "^2.0.2",
"sequelize": "^3.24.3",
"spotify": "~0.3.0",
"spotify-web-api-node": "^2.4.0",
"style-loader": "^0.17.0",
"superagent": "^2.1.0",
"superagent-bluebird-promise": "^3.0.2",

View file

@ -21,8 +21,8 @@ describe('Deezer', function(){
});
it('should find album with various artists by search', function* (){
const result = yield deezer.search({type: 'album', artist: {name: 'Various Artists'}, name: 'The Trevor Nelson Collection'});
result.name.should.equal('The Trevor Nelson Collection');
const result = yield deezer.search({type: 'album', artist: {name: 'Various Artists'}, name: 'The Trevor Nelson Collection 2'});
result.name.should.equal('The Trevor Nelson Collection 2');
});
it('should find track by search', function* (){

View file

@ -29,7 +29,7 @@ describe('Xbox Music', function(){
describe('lookupUrl', function(){
it('should parse regular url into album ID', function* (){
const result = yield xbox.parseUrl('https://music.xbox.com/album/kyuss/muchas-gracias-the-best-of-kyuss/8b558d00-0100-11db-89ca-0019b92a3933');
const result = yield xbox.parseUrl('https://music.microsoft.com/album/kyuss/muchas-gracias-the-best-of-kyuss/bz.8b558d00-0100-11db-89ca-0019b92a3933');
result.id.should.equal('music.8D6KGX5BZ8WB');
});
});

View file

@ -2,6 +2,13 @@ import 'should';
import * as youtube from '../../lib/services/youtube';
describe('Youtube', function(){
describe('lookup', function(){
it('should find album by lookup', function* (){
const result = yield youtube.lookupId('6JnGBs88sL0');
result.name.should.equal('Nelly Furtado - Say It Right');
});
});
describe('search', function(){
it('should find album by search', function* (){
const result = yield youtube.search({type: 'track', artist: {name: 'Aesop Rock'}, album: {name: 'Skeconsthon'}, name: 'Zero Dark Thirty'});

View file

@ -3317,6 +3317,10 @@ node-uuid@~1.4.1, node-uuid@~1.4.2:
version "1.4.8"
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907"
nodebrainz@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nodebrainz/-/nodebrainz-2.1.1.tgz#debf0cbf69ffeaec7439a36409ed9c10404b112f"
nodemon@^1.10.2:
version "1.11.0"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c"
@ -3669,12 +3673,13 @@ pkg-up@^1.0.0:
dependencies:
find-up "^1.0.0"
playmusic@~2.2.1:
"playmusic@https://github.com/jamon/playmusic.git#37e98f39c33fc5359a8a30b8c8e422161a4be9a8":
version "2.2.1"
resolved "https://registry.yarnpkg.com/playmusic/-/playmusic-2.2.1.tgz#32c5a4f3dee6e350e61ca879d4ce779f8e801a07"
resolved "https://github.com/jamon/playmusic.git#37e98f39c33fc5359a8a30b8c8e422161a4be9a8"
dependencies:
crypto-js ">= 3.1"
node-uuid "~1.4.1"
rsa-pem-from-mod-exp "^0.8.4"
pluralize@^1.2.1:
version "1.2.1"
@ -4333,6 +4338,10 @@ ripemd160@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"
rsa-pem-from-mod-exp@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz#362a42c6d304056d493b3f12bceabb2c6576a6d4"
run-async@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@ -4587,9 +4596,11 @@ split@^1.0.0:
dependencies:
through "2"
spotify@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/spotify/-/spotify-0.3.0.tgz#42b85105cfc30f174c050f2227c21a9d7edb1be4"
spotify-web-api-node@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/spotify-web-api-node/-/spotify-web-api-node-2.4.0.tgz#948f5bcfe098e5027367361dd2b003c7e3ce4cd5"
dependencies:
superagent "^2.0.0"
sprintf-js@~1.0.2:
version "1.0.3"
@ -4714,7 +4725,7 @@ superagent-bluebird-promise@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/superagent-bluebird-promise/-/superagent-bluebird-promise-3.0.2.tgz#3562fc7f26fe07306119ca8ab9943e1571b1deec"
superagent@^2.1.0:
superagent@^2.0.0, superagent@^2.1.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-2.3.0.tgz#703529a0714e57e123959ddefbce193b2e50d115"
dependencies:
@ -4984,7 +4995,7 @@ util@0.10.3, util@^0.10.3:
dependencies:
inherits "2.0.1"
uuid@3.0.0:
uuid@3.0.0, uuid@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728"
@ -4992,10 +5003,6 @@ uuid@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
uuid@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
v8flags@^2.0.10:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"