560 lines
18 KiB
TypeScript
560 lines
18 KiB
TypeScript
import { Injectable } from '@angular/core';
|
|
import { HttpClient, HttpHeaders } from '@angular/common/http'
|
|
|
|
import { Observable } from 'rxjs';
|
|
import { map, catchError } from 'rxjs/operators';
|
|
|
|
import { Song } from './model/song';
|
|
import { Album } from './model/album';
|
|
import { Artist } from './model/artist';
|
|
import { Bucket } from './model/bucket';
|
|
import { Suggested } from './model/suggested';
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
|
@Injectable()
|
|
export class ElsService {
|
|
public static readonly DEFAULT_SIZE: number = 50;
|
|
public static readonly SONG_INDEX_NAME = '/itunes-songs';
|
|
public static readonly ARTIST_INDEX_NAME = '/itunes-artists';
|
|
public static readonly ALBUM_INDEX_NAME = '/itunes-albums';
|
|
public static readonly SUGGEST_INDEX_NAME = '/itunes-suggest';
|
|
|
|
protected static readonly ACTION_SEARCH = '/_search';
|
|
protected static readonly ACTION_COUNT = '/_count';
|
|
|
|
protected elsUrl = 'http://localhost:9200';
|
|
protected headers = new HttpHeaders({'Content-Type': 'application/json'});
|
|
protected defaultLocation = "/F:/Musique" // TODO Use conf
|
|
|
|
constructor(
|
|
protected http: HttpClient,
|
|
protected route: ActivatedRoute) { }
|
|
|
|
fetchToSortParam() {
|
|
const toSort = this.route.snapshot.queryParams['backend'] || 'defaultUrl';
|
|
console.log('fetchToSortParam return ' + toSort)
|
|
return toSort;
|
|
// ? Retrieve to sort on Service or on each component and add a parameter for each methods?
|
|
}
|
|
|
|
getTime(): Promise<number> {
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
aggs: {
|
|
sum_time: {
|
|
sum: { field: 'Total Time'}
|
|
}
|
|
},
|
|
'size': 0
|
|
}), {headers: this.headers})
|
|
.toPromise()
|
|
.then(res => res.aggregations.sum_time.value as number)
|
|
.catch(error => this.handleError(error, 'getTime()'));
|
|
}
|
|
|
|
getSize(): Promise<number> {
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
aggs: {
|
|
sum_time: {
|
|
sum: { field: 'Size' }
|
|
}
|
|
},
|
|
'size': 0
|
|
}), {headers: this.headers})
|
|
.toPromise()
|
|
.then(res => res.aggregations.sum_time.value as number)
|
|
.catch(error => this.handleError(error, 'getSize()'));
|
|
}
|
|
|
|
getCountSong(index: string): Promise<number> {
|
|
return this.http
|
|
.get<any>(this.elsUrl + index + ElsService.ACTION_COUNT)
|
|
.toPromise()
|
|
.then(res => res.count as number)
|
|
.catch(error => this.handleError(error, 'getCountSong(' + index + ')'));
|
|
}
|
|
|
|
getCountNeverListenSong(): Promise<number> {
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_COUNT,
|
|
JSON.stringify({
|
|
'query': {
|
|
'bool': {
|
|
'must_not': {
|
|
'exists': { 'field': 'Play Count'}
|
|
}
|
|
}
|
|
}
|
|
}), {headers: this.headers})
|
|
.toPromise()
|
|
.then(res => res.count as number)
|
|
.catch(error => this.handleError(error, 'getCountNeverListenSong()'));
|
|
}
|
|
|
|
getMostPlayedTrack(): Observable<Song[]> {
|
|
// Thank to http://chariotsolutions.com/blog/post/angular2-observables-http-separating-services-components/
|
|
// for the map part
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'sort': [ {
|
|
'Play Count': {
|
|
'order': 'desc'
|
|
}
|
|
} ],
|
|
'size': 5
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToSongs(res)),
|
|
catchError(error => this.handleError(error, 'getMostPlayedTrack()'))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A basic get of albums ordered by 'Play Count' field.
|
|
*/
|
|
getMostPlayedAlbumNaive(): Promise<Album[]> {
|
|
return this.http
|
|
.get(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH + '?sort=Play Count:desc&size=20')
|
|
.toPromise()
|
|
.then(res => this.responseToAlbums(res))
|
|
.catch(error => this.handleError(error, 'getMostPlayedAlbumNaive'));
|
|
// TODO Excluse 'Divers' + compilation
|
|
}
|
|
|
|
/**
|
|
* More complicated query to calculate Avg. Play and get top ordrered by this results.
|
|
* Result is in '.hits.hits[].sort' field - not casted by conversion method
|
|
*/
|
|
getMostPlayedAlbum(): Observable<Album[]> {
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'sort': [
|
|
{
|
|
'_script': {
|
|
'type': 'number',
|
|
'script': {
|
|
'inline': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
|
|
},
|
|
'order': 'desc'
|
|
}
|
|
}
|
|
]
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToAlbums(res)),
|
|
catchError(error => this.handleError(error, 'getMostPlayedAlbum()'))
|
|
);
|
|
}
|
|
|
|
getMostPlayedArtistNaive(): Promise<Artist[]> {
|
|
return this.http
|
|
.get(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH + '?sort=Play Count:desc&size=20')
|
|
.toPromise()
|
|
.then(res => this.responseToAlbums(res))
|
|
.catch(error => this.handleError(error, 'getMostPlayedArtistNaive'));
|
|
// TODO Excluse 'Divers' + compilation
|
|
}
|
|
|
|
getMostPlayedArtist(): Observable<Artist[]> {
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'sort': [
|
|
{
|
|
'_script': {
|
|
'type': 'number',
|
|
'script': {
|
|
'inline': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
|
|
},
|
|
'order': 'desc'
|
|
}
|
|
},
|
|
],
|
|
'size': 100
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToArtists(res)),
|
|
catchError(error => this.handleError(error, 'getMostPlayedArtist()'))
|
|
);
|
|
}
|
|
|
|
getAlbumSongs(albumName: string, from: number = 0, toSortFilter = false): Observable<Song[]> {
|
|
// TODO Move in els-album service
|
|
console.info(
|
|
"getAlbumSongs- Album name: " + albumName + " - from: " + from
|
|
);
|
|
|
|
let query = {
|
|
query: {
|
|
bool: {
|
|
must: [
|
|
{
|
|
match_phrase: {
|
|
"Album.raw": albumName,
|
|
},
|
|
},
|
|
],
|
|
must_not: [],
|
|
},
|
|
},
|
|
size: ElsService.DEFAULT_SIZE,
|
|
from: from,
|
|
};
|
|
|
|
if (toSortFilter) {
|
|
console.log("getAlbumSongs- TO SORT filter enabled");
|
|
query.query.bool.must_not.push({
|
|
term: {
|
|
"Location.tree": this.defaultLocation,
|
|
},
|
|
});
|
|
}
|
|
|
|
return this.http
|
|
.post(
|
|
this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify(query),
|
|
{ headers: this.headers }
|
|
)
|
|
.pipe(
|
|
map((res) => this.responseToSongs(res)),
|
|
catchError((error) =>
|
|
this.handleError(
|
|
error,
|
|
"getAlbumSongs(" + albumName + "," + from + ")"
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
getGenreSongs(genreName: string, from: number = 0): Observable<Song[]> {
|
|
console.info('getGenreSongs- Genre name: ' + genreName + ' - from: ' + from);
|
|
// TODO Code repetition => to refactor
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'query': {
|
|
'match_phrase': { 'Genre': genreName }
|
|
},
|
|
'size': ElsService.DEFAULT_SIZE,
|
|
'from': from
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToSongs(res)),
|
|
catchError(error => this.handleError(error, 'getAlbumSongs(' + genreName + ',' + from + ')'))
|
|
);
|
|
}
|
|
|
|
getArtistSongs(artistName: string, from: number = 0): Observable<Song[]> {
|
|
console.info('getArtistSongs- Artist name: ' + artistName + ' - from: ' + from);
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'query': {
|
|
'bool': {
|
|
'should': [
|
|
{'match_phrase' : { 'Album Artist' : artistName }},
|
|
{'match_phrase' : { 'Artist' : artistName }}
|
|
]
|
|
}
|
|
},
|
|
'size': ElsService.DEFAULT_SIZE,
|
|
'from': from
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToSongs(res)),
|
|
catchError(error => this.handleError(error, 'getArtistSongs(' + artistName + ',' + from + ')'))
|
|
);
|
|
}
|
|
|
|
getAlbum(albumName: string): Observable<Album> {
|
|
// TODO Why this is used on album pages?
|
|
// When it's commented, the album information goes up correctly */
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'query': {
|
|
'match_phrase': { 'Album': albumName }
|
|
},
|
|
'size': ElsService.DEFAULT_SIZE
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToOneTypedResult<Album>(res, albumName)),
|
|
catchError(error => this.handleError(error, 'getAlbum(' + albumName + ')'))
|
|
);
|
|
}
|
|
|
|
getArtist(artistName: string): Observable<Artist> {
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'query': {
|
|
'match_phrase': { 'Artist': artistName }
|
|
},
|
|
'size': ElsService.DEFAULT_SIZE
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseToOneTypedResult<Artist>(res, artistName)),
|
|
catchError(error => this.handleError(error, 'getArtist(' + artistName + ')'))
|
|
);
|
|
}
|
|
|
|
getGenres(ordering: string = 'desc'): Observable<Bucket[]> {
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'aggs' : {
|
|
'genres' : {
|
|
'terms' : {
|
|
'field' : 'Genre',
|
|
'size' : 10,
|
|
'missing': 'N/A',
|
|
'order': { '_count' : ordering }
|
|
}
|
|
}
|
|
},
|
|
'size': 0
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseAggregationToBucket(res, 'genres')),
|
|
catchError(error => this.handleError(error, 'getGenres(' + ordering + ')'))
|
|
);
|
|
}
|
|
|
|
// getGenreCount(): Observable<number> {
|
|
// return this.http
|
|
// .post<any>(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
|
|
// JSON.stringify({
|
|
// 'aggs' : {
|
|
// 'genres' : {
|
|
// 'cardinality' : {
|
|
// 'field' : 'Genre.original',
|
|
// 'missing': 'N/A',
|
|
// }
|
|
// }
|
|
// },
|
|
// 'size': 0
|
|
// }), {headers: this.headers})
|
|
// .pipe(
|
|
// map(res => res.aggregations.genres.value)
|
|
// );
|
|
// }
|
|
|
|
getLastAddedAlbums(month: number): Observable<Bucket[]> {
|
|
return this.http
|
|
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'query': {
|
|
'range' : {
|
|
'Date Added' : {
|
|
'gte' : 'now-' + month + 'M/d',
|
|
'lte' : 'now/d'
|
|
}
|
|
}
|
|
},
|
|
'size': 0,
|
|
'aggs': {
|
|
'date' : {
|
|
'terms': {
|
|
'field' : 'Date Added',
|
|
'order': { '_key': 'desc' },
|
|
'size': 20
|
|
},
|
|
'aggs': {
|
|
'album': {
|
|
'terms': {
|
|
'field': 'Album.raw'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseSubAggregationToBucket(res, 'date')),
|
|
catchError(error => this.handleError(error, 'getLastAddedAlbums(' + month + ')' ))
|
|
);
|
|
// TODO Take in consideration "sum_other_doc_count"
|
|
}
|
|
|
|
getArtistFromAlbumName(albumname: string): Observable<Album[]> { // TODO Rename ?
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'query': {
|
|
'match_phrase' : {
|
|
'Album' : albumname
|
|
}
|
|
}
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => res.hits.hits),
|
|
map((hits: Array<any>) => {
|
|
// TODO Use a method (duplicated code ?)
|
|
const result: Array<Album> = [];
|
|
hits.forEach((hit) => {
|
|
result.push(hit._source);
|
|
});
|
|
return result;
|
|
}),
|
|
catchError(error => this.handleError(error, 'getArtistFromAlbumName' + albumname + ')'))
|
|
);
|
|
}
|
|
|
|
getCountArtistSong(artistName: string): Observable<number> {
|
|
console.log('artistname: ' + artistName);
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_COUNT,
|
|
JSON.stringify({
|
|
'query': {
|
|
'bool': {
|
|
'should': [
|
|
{'match_phrase' : { 'Album Artist' : artistName }},
|
|
{'match_phrase' : { 'Artist' : artistName }}
|
|
]
|
|
}
|
|
}
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => res.count as number),
|
|
catchError(error => this.handleError(error, 'getCountArtistSong' + artistName + ')'))
|
|
);
|
|
}
|
|
|
|
getSuggest(text: string): Observable<Suggested[]> {
|
|
console.log('search sugget: ' + text);
|
|
return this.http
|
|
.post<any>(this.elsUrl + ElsService.SUGGEST_INDEX_NAME + ElsService.ACTION_SEARCH,
|
|
JSON.stringify({
|
|
'_source': ['album', 'artist'],
|
|
'suggest': {
|
|
'album-suggest': {
|
|
'prefix': text,
|
|
'completion': {
|
|
'field': 'album_suggest'
|
|
}
|
|
},
|
|
'artist-suggest': {
|
|
'prefix': text,
|
|
'completion': {
|
|
'field': 'artist_suggest'
|
|
}
|
|
}
|
|
}
|
|
}), {headers: this.headers})
|
|
.pipe(
|
|
map(res => this.responseSuggesterToSuggested(res, 'album-suggest', 'artist-suggest')),
|
|
catchError(error => this.handleError(error, 'getSuggest(' + text + ')'))
|
|
);
|
|
|
|
}
|
|
|
|
/** Process a result to return just one result.
|
|
* Used to get an album or an artist.
|
|
* Take a name to put in console output if no result or more than one result.
|
|
*
|
|
* @param res Response to process
|
|
* @param name The searched name - for console output
|
|
*/
|
|
private responseToOneTypedResult<T>(res: any, name: string): T {
|
|
const hits = res.hits.hits;
|
|
|
|
if (hits.length < 1) {
|
|
console.info('No result found for name: "' + name);
|
|
return undefined;
|
|
}
|
|
if (hits.length > 1) {
|
|
// TODO Cumul results (for album)
|
|
console.error('More than one result for name: "' + name + '". Found (' + hits.length + '), return the first.');
|
|
}
|
|
return hits[0]._source;
|
|
}
|
|
|
|
/** Process a response to a array of songs.
|
|
*
|
|
* @param res Response to process
|
|
*/
|
|
protected responseToSongs(res: any): Song[] {
|
|
const result: Array<Song> = [];
|
|
res.hits.hits.forEach((hit) => {
|
|
result.push(hit._source);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/** Process a response to a array of songs.
|
|
*
|
|
* @param res Response to process
|
|
*/
|
|
protected responseToAlbums(res: any): Album[] {
|
|
const result: Array<Album> = [];
|
|
res.hits.hits.forEach((hit) => {
|
|
result.push(hit._source);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/** Process a response to a array of songs.
|
|
*
|
|
* @param res Response to process
|
|
*/
|
|
private responseToArtists(res: any): Artist[] {
|
|
const result: Array<Artist> = [];
|
|
res.hits.hits.forEach((hit) => {
|
|
result.push(hit._source);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
|
|
/** Process an aggregation response to an array of Bucket.
|
|
*
|
|
* @param res Response to process
|
|
* @param name Name of aggregation
|
|
*/
|
|
protected responseAggregationToBucket(res: any, name: string): Bucket[] {
|
|
const result: Array<Bucket> = [];
|
|
res.aggregations[name].buckets.forEach((bucket) => {
|
|
result.push(bucket);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
private responseSubAggregationToBucket(res: any, name: string): Bucket[] {
|
|
const result: Array<Bucket> = [];
|
|
res.aggregations[name].buckets.forEach((bucket) => {
|
|
bucket['album'].buckets.forEach((subBucket) => {
|
|
result.push(subBucket);
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
protected responseSuggesterToSuggested(res: any, ...suggestName: string[]): Suggested[] {
|
|
const result: Array<Suggested> = []
|
|
suggestName.forEach(sname => {
|
|
res['suggest'][sname][0]['options'].forEach(option => {
|
|
let suggest = new Suggested()
|
|
// TODO If more than one key, raise exception
|
|
suggest.type = String(Object.keys(option['_source']))
|
|
suggest.name = option['_source'][suggest.type]
|
|
result.push(suggest)
|
|
});
|
|
});
|
|
return result;
|
|
}
|
|
|
|
protected handleError(error: any, origin: string): Promise<any> {
|
|
console.error('An error occurred!');
|
|
console.error('Origin function: ', origin);
|
|
console.error('An error occurred!', error); // for demo purposes only
|
|
console.error(error); // for demo purposes only
|
|
return Promise.reject(error.message || error);
|
|
}
|
|
}
|