Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using local API to retrieve current track #104

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ yarn start



## How it works
## Known platform limitations

We retrieve the current song of spotify client using the spotify built-in web server that allow us to ask for the current status of the player.
The built-in web server could run in a range of ports starting at 4370. Lyricfier will launch multiple connections hoping find the actual port.
You can read a more detailed explanation here: [Deconstructing Spotify's built-in HTTP server](http://cgbystrom.com/articles/deconstructing-spotifys-builtin-http-server/)
### Mac OS
- fetching album cover does is not supported

## Scraping plugins

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@
"dependencies": {
"async": "^2.5.0",
"cheerio": "^0.20.0",
"dbus-native": "^0.2.5",
"electron-json-storage": "^2.0.0",
"he": "^1.1.0",
"jquery": "^3.1.0",
"lodash": "^4.17.11",
"os": "^0.1.1",
"ps": "^1.0.0",
"request": "^2.74.0",
"spotify-node-applescript": "^1.1.1",
"toastr": "^2.1.2",
"vue": "^1.0.26",
"vue-class-component": "^3.2.0"
Expand Down
3 changes: 2 additions & 1 deletion render/SongRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Component from 'vue-class-component';
import {Searcher} from "./Searcher";
import {template} from './template';
import {SpotifyService} from './SpotifyService';
import {Song} from './api/Song';

@Component({
props: {
Expand Down Expand Up @@ -55,7 +56,7 @@ export class SongRender {

refresh() {
this.resizeOnLyricsHide();
this.getSpotify().getCurrentSong((err, song) => {
this.getSpotify().getCurrentSong((err: any, song: Song) => {
if (err) {
this.showError('Current song error: ' + err);
this.scheduleNextCall();
Expand Down
23 changes: 23 additions & 0 deletions render/SpotifyClientFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {SpotifyClient}from './api/SpotifyClient';
import {SpotifyDarwin}from "./spotify/SpotifyDarwin";
import {SpotifyLinux}from "./spotify/SpotifyLinux";
import {SpotifyDefault}from "./spotify/SpotifyDefault";

const os = require('os');

export class SpotifyClientFactory {

static getSpotifyClient(): SpotifyClient {
switch (os.platform()) {
case 'darwin':
return new SpotifyDarwin();
case 'linux':
return new SpotifyLinux();
case 'win32':
// return new SpotifyWindows();
default:
return new SpotifyDefault()
}
}

}
244 changes: 22 additions & 222 deletions render/SpotifyService.ts
Original file line number Diff line number Diff line change
@@ -1,237 +1,37 @@
import {encodeData} from "./Utils";
import { Song } from "./api/Song";
import { SongMetadata } from "./api/SongMetadata";
import { SpotifyClient } from "./api/SpotifyClient";
import { SpotifyClientFactory } from "./SpotifyClientFactory";
import { join, reduce, split, toLower } from "lodash";

const request = require('request').defaults({timeout: 5000});
const async = require('async');
const initialPortTest = 4370;

export class SpotifyService {
protected https = false;
protected foundPort = false;
protected port: number;
protected portTries = 15;
protected albumImagesCache = {};
private albumImageCache = {};

protected oAuthToken = {
t: null,
expires: null
};
protected csrfToken = null;



protected static headers() {
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', // Placeholder user-agent
'Origin': 'https://open.spotify.com'
};
}

protected url(u: string) {
const protocol = this.https ? 'https' : 'http';
return `${protocol}://127.0.0.1:${this.port}${u}`;
}

public getOAuthToken(cb) {
if (this.oAuthToken.t) {
return cb(null, this.oAuthToken.t);
}
request.get({
url: 'https://open.spotify.com/token',
rejectUnauthorized: false,
headers: SpotifyService.headers()
}, (err, status, body) => {
if (err) {
return cb(err);
}
try {
const json = JSON.parse(body);
this.oAuthToken.t = json.t;
return cb(null, json.t);
} catch (e) {
return cb(e);
}
});
}

public detectPort(cb: (err, port: number) => void) {
if (!this.foundPort) {
this.port = initialPortTest;
}
async.retry(this.portTries * 2, (finish) => {
this.getCsrfToken((err) => {
if (err) {
console.log('FAILED WITH PORT: ', this.port, ' and https is ', this.https);
if (this.https) {
this.port++;
this.https = false;
} else {
this.https = true;
}
return finish(err);
}
this.foundPort = true;
console.log('VALID PORT', this.port);
finish(err, this.port)
});
}, cb);
}


public getCsrfToken(cb) {
if (this.csrfToken) {
return cb(null, this.csrfToken);
}
const url = this.url('/simplecsrf/token.json');
request(url, {
headers: SpotifyService.headers(),
'rejectUnauthorized': false
}, (err, status, body) => {
if (err) {
console.error('Error getting csrf token URL: ', status);
console.error(err);
return cb(err);
}
const json = JSON.parse(body);
this.csrfToken = json.token;
cb(null, this.csrfToken);
});
}

public needsTokens(fn) {
this.detectPort((err) => {
if (err) {
const failDetectPort = 'No port found! Is spotify running?';
console.error(failDetectPort, err);
return fn(failDetectPort);
}
const parallelJob = {
csrf: this.getCsrfToken.bind(this),
oauth: this.getOAuthToken.bind(this),
};
async.parallel(parallelJob, fn);
});
private spotifyClient: SpotifyClient;

constructor() {
this.spotifyClient = SpotifyClientFactory.getSpotifyClient();
}

public getStatus(cb) {
this.needsTokens((err, tokens) => {
if (err) return cb(err);
const params = {
'oauth': tokens.oauth,
'csrf': tokens.csrf,
};
const url = this.url('/remote/status.json') + '?' + encodeData(params);
public getCurrentSong(cb: (err: any, song?: Song) => any) {
this.spotifyClient.getTrack().then((status: SongMetadata) => {
console.log('Retrieved song metadata: ' + JSON.stringify(status));

request(url, {
headers: SpotifyService.headers(),
'rejectUnauthorized': false,
}, (err, status, body) => {

if (err) {
console.error('Error asking for status', err, ' url used: ', url);
return cb(err);
}
try {
const json = JSON.parse(body);
cb(null, json);
} catch (e) {
const msgParseFailed = 'Status response from spotify failed';
console.error(msgParseFailed, ' JSON body: ', body);
cb(msgParseFailed, null);
}

});
});
}

protected getAlbumImages(albumUri: string, cb) {
if (this.albumImagesCache[albumUri]) {
return cb(null, this.albumImagesCache[albumUri])
}
async.retry(2, (finish) => {
const id = albumUri.split('spotify:album:')[1];
const url = `https://api.spotify.com/v1/albums/${id}?oauth=${this.oAuthToken.t}`;
request(url, (err, status, body) => {
if (err) {
console.error('Error getting album images', err, ' URL: ', url);
return finish(err, null)
}
try {
const parsed = JSON.parse(body);
finish(null, parsed.images);
this.albumImagesCache[albumUri] = parsed.images;
} catch (e) {
const msgParseFail = 'Failed to parse response from spotify api';
console.error(msgParseFail, 'URL USED: ', url);
finish(msgParseFail, null);
}

});
}, cb);


}

public pause(pause: boolean, cb) {
this.needsTokens((err, tokens) => {
if (err) return cb(err);
const params = {
'oauth': tokens.oauth,
'csrf': tokens.csrf,
'pause': pause ? 'true' : 'false',
};
const url = this.url('/remote/pause.json') + '?' + encodeData(params);
request(url, {
headers: SpotifyService.headers(),
'rejectUnauthorized': false,
}, (err, status, body) => {
if (err) {
return cb(err);
}
const json = JSON.parse(body);
cb(null, json);
});
});

}

public getCurrentSong(cb) {
this.getStatus((err, status) => {
if (err) {
this.foundPort = false;
this.csrfToken = null;
this.oAuthToken.t = null;
return cb(err);
}

if (status.track && status.track.track_resource) {
const result = {
playing: status.playing,
artist: status.track.artist_resource ? status.track.artist_resource.name : 'Unknown',
title: status.track.track_resource.name,
if (status.id || status.trackid) {
const result: Song = {
playing: true,
artist: status.artist || 'Unknown',
title: status.name,
album: {
name: 'Unknown',
images: null
name: status.album || 'Unknown',
imageUrl: status.artUrl || null
},
duration: status.track.length
duration: status.length
};

if (status.track.album_resource) {
result.album.name = status.track.album_resource.name;
return this.getAlbumImages(status.track.album_resource.uri, (err, images) => {
if (!err) {
result.album.images = images;
}
return cb(null, result);
});
} else {
return cb(null, result);
}

return cb(null, result);
}
return cb('No song', null)
});
}, (err) => cb(err));
}


}
15 changes: 15 additions & 0 deletions render/api/Song.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@


export interface Song {
lyric?: string;
title: string;
artist: string;
sourceUrl?: string;
sourceName?: string;
playing: boolean;
album: {
name: string;
imageUrl: string;
};
duration: number;
}
17 changes: 17 additions & 0 deletions render/api/SongMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


export interface SongMetadata {
trackid: string;
id: string; // no track id on darwin
length: number;
artUrl: string;
album: string;
albumArtist: string;
artist: string;
autoRating: number;
discNumber: number;
title: string;
trackNumber: number;
url: string;
name: string;
}
11 changes: 11 additions & 0 deletions render/api/SpotifyClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@


export interface SpotifyClient {
supportedActions();
isRunning();
getState();
getTrack();
togglePlayPause();
previousTrack();
nextTrack();
}
Loading