17 Commits

Author SHA1 Message Date
20f4fdbd39 (typesense) Suggester 2024-01-23 23:54:46 +01:00
b789c925c4 (typesense) Main dashboard
Lot of duplicate to have a simple improvment
2024-01-23 23:54:36 +01:00
1ff074aac3 (front) Use a compatible ng-bootstrap version 2023-09-20 01:17:35 +02:00
f6eb99fa67 (front) Update to Angular 15 2023-02-06 23:58:43 +01:00
46c4a0b40c (front) Update to Angular 14 2023-02-06 23:51:56 +01:00
05cbe19b4d (front) Update package-lock
Why?...
2023-02-06 23:36:50 +01:00
003ad4917e (front) NVM update node version 2023-02-06 23:27:24 +01:00
25828bb789 (front) Service refactoring for sorting operations
- Create an ElsArtistService to use common query
- Move services in a subfolder

Use a function to add sort filter
2023-02-06 23:08:56 +01:00
29350c36cd (front) Another way to get toSort boolean at Service 2023-01-30 01:30:38 +01:00
9f4ef952b3 (front) Use a 'tosort' filter 2023-01-30 01:23:35 +01:00
2ddafaff1b (front) Improve ToSort using album index 2023-01-29 22:34:23 +01:00
7e0a791fbe (back) Add location for albums 2023-01-29 19:38:24 +01:00
cf8cd046f4 (front) To sort: reorganize stats panels 2023-01-09 00:06:56 +01:00
a10c9ae060 (front) Update to Angular 13 2023-01-09 00:06:56 +01:00
754ee9213c (front) Adapt query for ELS 8.5 2023-01-09 00:06:56 +01:00
40aed6039d (front) Run after a long time
Use a nvmrc file to keep in memory node version used
2023-01-09 00:06:56 +01:00
4399018b4a (back) Readme udpate 2023-01-09 00:06:56 +01:00
42 changed files with 9130 additions and 19915 deletions

21
README.md Normal file
View File

@@ -0,0 +1,21 @@
# Classic data
1. Analyze data :
```sh
python3 iTunesParser.py
```
2. Send data to ELS :
```sh
python3 send_data.py -D -A
```
# Suggester
To use suggester, create index with data in `suggester.es` file.
Then :
```sh
python3 suggester.py
```

View File

@@ -25,6 +25,7 @@
!.vscode/extensions.json !.vscode/extensions.json
# misc # misc
/.angular/cache
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage

1
dashboard/.nvmrc Normal file
View File

@@ -0,0 +1 @@
v18.12.1

View File

@@ -100,19 +100,6 @@
"scripts": [] "scripts": []
} }
}, },
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": { "e2e": {
"builder": "@angular-devkit/build-angular:protractor", "builder": "@angular-devkit/build-angular:protractor",
"options": { "options": {
@@ -127,6 +114,5 @@
} }
} }
} }
}, }
"defaultProject": "dashboard"
} }

26254
dashboard/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,26 +11,25 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~12.2.2", "@angular/animations": "^15.1.3",
"@angular/common": "~12.2.2", "@angular/common": "^15.1.3",
"@angular/compiler": "~12.2.2", "@angular/compiler": "^15.1.3",
"@angular/core": "~12.2.2", "@angular/core": "^15.1.3",
"@angular/forms": "~12.2.2", "@angular/forms": "^15.1.3",
"@angular/localize": "~12.2.2", "@angular/localize": "^15.1.3",
"@angular/platform-browser": "~12.2.2", "@angular/platform-browser": "^15.1.3",
"@angular/platform-browser-dynamic": "~12.2.2", "@angular/platform-browser-dynamic": "^15.1.3",
"@angular/router": "~12.2.2", "@angular/router": "^15.1.3",
"@ng-bootstrap/ng-bootstrap": "^10.0.0", "@ng-bootstrap/ng-bootstrap": "^14.0.0",
"bootstrap": "^3.3.7", "bootstrap": "^3.3.7",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~12.2.2", "@angular-devkit/build-angular": "^15.1.4",
"@angular/cli": "~12.2.2", "@angular/cli": "^15.1.4",
"@angular/compiler-cli": "~12.2.2", "@angular/compiler-cli": "^15.1.3",
"@angular/localize": "^12.2.2",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
@@ -44,6 +43,6 @@
"protractor": "~7.0.0", "protractor": "~7.0.0",
"ts-node": "~8.3.0", "ts-node": "~8.3.0",
"tslint": "~6.1.0", "tslint": "~6.1.0",
"typescript": "~4.3.5" "typescript": "~4.9.5"
} }
} }

View File

@@ -1,6 +1,11 @@
<div class="container"> <div class="container">
<h1>{{albumName}}</h1> <h1>{{albumName}}</h1>
<div *ngIf="sortFilter" class="alert alert-danger">
To sort filter applyed. Songs are filter on location. <br/>
Album folder: {{album['Location']}}
</div>
<div class="alert alert-warning"> <div class="alert alert-warning">
<h3>Debug Zone</h3> <h3>Debug Zone</h3>
Returned song: {{songs.length}}<br /> Returned song: {{songs.length}}<br />
@@ -20,7 +25,7 @@
<div class="col-xs-9 text-right"> <div class="col-xs-9 text-right">
<div> <div>
<h3 *ngIf="!album"><span class="glyphicon glyphicon-refresh loading"></span></h3> <h3 *ngIf="!album"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="album">{{album.Rating}}/100</h3> <h3 *ngIf="album">{{album['Album Rating']}}/100</h3>
</div> </div>
<div><br>Rating</div> <div><br>Rating</div>
</div> </div>

View File

@@ -1,12 +1,13 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { ElsService } from './../els.service';
import { Song } from './../model/song'; import { Song } from './../model/song';
import { Album } from './../model/album'; import { Album } from './../model/album';
import { SongTableComponent } from '../song-table/song-table.component'; import { SongTableComponent } from '../song-table/song-table.component';
import { TsService } from '../ts-service/ts.service';
import { TsAlbumService } from '../ts-service/ts-album.service';
@Component({ @Component({
selector: 'app-album', selector: 'app-album',
templateUrl: './album.component.html', templateUrl: './album.component.html',
@@ -19,24 +20,24 @@ export class AlbumComponent implements OnInit {
albumName = ''; albumName = '';
songs: Array<Song> = []; songs: Array<Song> = [];
album: Album = new Album(); // If album not found, will be replaced by 'undefined' album: Album = new Album(); // If album not found, will be replaced by 'undefined'
sortFilter: boolean = false; // Show only song to sort
// Prevent useless data load + activate button in interface var // Prevent useless data load + activate button in interface var
moreDataAvailable = true; moreDataAvailable = true;
lockLoadData = false; lockLoadData = false;
constructor( constructor(
private elsService: ElsService, private tsAlbumService: TsAlbumService,
private route: ActivatedRoute, private route: ActivatedRoute
private location: Location
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.route.params this.route.params.subscribe((params: Params) => this.albumName = params['name']);
.subscribe((params: Params) => this.albumName = params['name']); this.route.queryParams.subscribe(params => { this.sortFilter = params.tosort ?? false; });
this.loadSongs(); this.loadSongs();
this.elsService.getAlbum(this.albumName).subscribe(data => this.album = data); this.tsAlbumService.getAlbum(this.albumName).subscribe(data => this.album = data);
} }
loadSongs(): void { loadSongs(): void {
@@ -50,9 +51,9 @@ export class AlbumComponent implements OnInit {
} }
this.lockLoadData = true; this.lockLoadData = true;
this.elsService.getAlbumSongs(this.albumName, this.songs.length).subscribe( this.tsAlbumService.getAlbumSongs(this.albumName, this.songs.length, this.sortFilter).subscribe(
data => { data => {
this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE; this.moreDataAvailable = data.length === TsService.DEFAULT_SIZE;
// Erase song array with result for first load, then add elements one by one // Erase song array with result for first load, then add elements one by one
// instead use concat => concat will sort table at each load, very consuming! and not user friendly // instead use concat => concat will sort table at each load, very consuming! and not user friendly

View File

@@ -1,27 +1,28 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from "@angular/core";
import { ElsAlbumService } from '../els-album.service';
import { Album } from '../model/album'; import { Album } from "../model/album";
import { Utils } from '../utils'; import { Utils } from "../utils";
import { TsAlbumService } from "../ts-service/ts-album.service";
enum query_edit_type { enum query_edit_type {
exclude = 'must_not', exclude = "must_not",
select = 'must' select = "must",
} }
@Component({ @Component({
selector: 'app-albums', selector: "app-albums",
templateUrl: './albums.component.html', templateUrl: "./albums.component.html",
styleUrls: ['./albums.component.css'] styleUrls: ["./albums.component.css"],
}) })
export class AlbumsComponent implements OnInit { export class AlbumsComponent implements OnInit {
numberToArray = Utils.numberToArray; numberToArray = Utils.numberToArray; // For star representation
albums: Album[] = []; albums: Album[] = [];
filterQuery = Object.assign({}, ElsAlbumService.GET_ALBUMS_DEFAULT_QUERY); filterParams = TsAlbumService.GET_ALBUMS_DEFAULT_PARAMS();
queryEdited = false; queryEdited = false; // Show reset button if true
constructor(private elsService : ElsAlbumService) { } constructor(private tsService: TsAlbumService) {}
ngOnInit(): void { ngOnInit(): void {
this.loadData(); this.loadData();
@@ -30,42 +31,47 @@ export class AlbumsComponent implements OnInit {
private editQuery(field: string, value: Album, type: query_edit_type): void { private editQuery(field: string, value: Album, type: query_edit_type): void {
// TODO Move this method to a service // TODO Move this method to a service
if (value[field] instanceof Array) { if (value[field] instanceof Array) {
value[field] = value[field][0] value[field] = value[field][0];
} }
// If firt edit, add needed fields in ELS Query if (type == query_edit_type.exclude) {
if (!this.filterQuery['query']) { // Filter can be cumulated
this.filterQuery['query']['bool'][type].push({ 'must': [] }) // TODO Specific treatment for array? https://typesense.org/docs/0.25.1/api/search.html#filter-parameters
this.filterQuery['query']['bool'][type].push({ 'must_not': [] }) this.filterParams = this.filterParams.append(
"filter_by",
field + ":!=`" + value[field] + "`"
);
} }
if (type == query_edit_type.select) {
this.filterQuery['query']['bool'][type].push({ this.filterParams = this.filterParams
'match_phrase': { .delete("q")
[field]: value[field] .append("q", value[field])
.delete("query_by")
.append("query_by", field);
} }
})
this.queryEdited = true; this.queryEdited = true;
} }
exlude(field: string, value: Album): void { exlude(field: string, value: Album): void {
this.editQuery(field, value, query_edit_type.exclude) this.editQuery(field, value, query_edit_type.exclude);
this.loadData() this.loadData();
} }
select(field: string, value: Album): void { select(field: string, value: Album): void {
this.editQuery(field, value, query_edit_type.select) this.editQuery(field, value, query_edit_type.select);
this.loadData() this.loadData();
} }
resetQuery(): void { resetQuery(): void {
this.filterQuery = Object.assign({}, ElsAlbumService.GET_ALBUMS_DEFAULT_QUERY); this.filterParams = TsAlbumService.GET_ALBUMS_DEFAULT_PARAMS();
this.loadData(); this.loadData();
} }
loadData(): void { loadData(): void {
// console.log(JSON.stringify(this.filterQuery)) console.log(this.filterParams.toString());
this.elsService.getAlbums(this.filterQuery).subscribe(data => this.albums = data); this.tsService
.getAlbums(this.filterParams)
.subscribe((data) => (this.albums = data));
} }
} }

View File

@@ -11,8 +11,9 @@ import { GenreComponent } from './genre/genre.component';
import { SongTableComponent } from './song-table/song-table.component'; import { SongTableComponent } from './song-table/song-table.component';
import { TopPlayedComponent } from './top-played/top-played.component'; import { TopPlayedComponent } from './top-played/top-played.component';
import { ElsService } from './els.service'; import { TsService } from './ts-service/ts.service';
import { ElsAlbumService } from './els-album.service'; import { TsAlbumService } from './ts-service/ts-album.service';
import { TsArtistService } from './ts-service/ts-artist.service';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
@@ -51,8 +52,9 @@ import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
ToSortComponent ToSortComponent
], ],
providers: [ providers: [
ElsService, TsService,
ElsAlbumService TsAlbumService,
TsArtistService
], ],
bootstrap: [ AppComponent ] bootstrap: [ AppComponent ]
}) })

View File

@@ -1,12 +1,13 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { ElsService } from './../els.service';
import { Song } from './../model/song'; import { Song } from './../model/song';
import { Artist } from './../model/artist'; import { Artist } from './../model/artist';
import { SongTableComponent } from '../song-table/song-table.component'; import { SongTableComponent } from '../song-table/song-table.component';
import { TsService } from '../ts-service/ts.service';
import { TsArtistService } from '../ts-service/ts-artist.service';
@Component({ @Component({
selector: 'app-artist', selector: 'app-artist',
templateUrl: './artist.component.html', templateUrl: './artist.component.html',
@@ -25,19 +26,26 @@ export class ArtistComponent implements OnInit {
artist: Artist = new Artist(); artist: Artist = new Artist();
lockLoadData = false; lockLoadData = false;
countSong: number; countSong: number;
toSortFilter: boolean = false; // Show only song to sort
constructor( constructor(
private elsService: ElsService, private elsService: TsArtistService,
private route: ActivatedRoute, private route: ActivatedRoute,
private location: Location
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.route.params.subscribe((params: Params) => this.artistName = params['name']); this.route.params.subscribe((params: Params) => this.artistName = params['name']);
this.route.queryParams.subscribe(params => { this.toSortFilter = params.tosort ?? false; });
this.elsService.getArtist(this.artistName).subscribe(data => this.artist = data); this.elsService.getArtist(this.artistName).subscribe(data => this.artist = data);
this.elsService.getCountArtistSong(this.artistName).subscribe(data => this.countSong = data); this.elsService.getCountArtistSong(this.artistName, this.toSortFilter).subscribe(data => {
this.countSong = data
this.artist['Track Count'] = this.countSong;
// TODO Async problem: some time, get updated data, some time no
// TODO Use only this value?
// TODO ==> Show each time if there are unsorted songs
});
this.loadSongs(); this.loadSongs();
} }
@@ -54,9 +62,11 @@ export class ArtistComponent implements OnInit {
} }
this.lockLoadData = true; this.lockLoadData = true;
this.elsService.getArtistSongs(this.artistName, this.songs.length).subscribe( this.elsService.getArtistSongs(this.artistName, this.songs.length, this.toSortFilter).subscribe(
data => { data => {
this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE; this.moreDataAvailable = data.length === TsService.DEFAULT_SIZE;
console.log(data.length)
console.log(this.moreDataAvailable)
// Erase song array with result for first load, then add elements one by one // Erase song array with result for first load, then add elements one by one
// instead use concat => concat will sort table at each load, very consuming! and not user friendly // instead use concat => concat will sort table at each load, very consuming! and not user friendly

View File

@@ -82,7 +82,7 @@
<span *ngIf="r.type == 'artist'" class="glyphicon glyphicon-user"></span> <span *ngIf="r.type == 'artist'" class="glyphicon glyphicon-user"></span>
<span *ngIf="r.type == 'album'" class="glyphicon glyphicon-cd"></span> <span *ngIf="r.type == 'album'" class="glyphicon glyphicon-cd"></span>
&nbsp; <ngb-highlight [result]="r.name" [term]="t"></ngb-highlight> &nbsp; <ngb-highlight [result]="r.name" [term]="t"></ngb-highlight>
</ng-template> </ng-template>
<form class="navbar-form"> <form class="navbar-form">
@@ -112,9 +112,9 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let album of lastAddedAlbums"> <tr *ngFor="let album of lastAddedAlbums">
<td><a [routerLink]="['/album', album.key]">{{album.key}}</a></td> <td><a [routerLink]="['/album/', album.group_key[0]]">{{album.group_key[0]}}</a></td>
<td>{{album.doc_count}}</td> <td>{{album.found}}</td>
<td><a [routerLink]="['/artist', albumArtists[album.key]]">{{albumArtists[album.key]}}</a></td> <td><a [routerLink]="['/artist', albumArtists[album.group_key[0]]]">{{albumArtists[album.group_key[0]]}}</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -156,8 +156,8 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let genre of topGenres"> <tr *ngFor="let genre of topGenres">
<td><a [routerLink]="['/genre', genre.key]">{{genre.key}}</a></td> <td><a [routerLink]="['/genre', genre.value]">{{genre.value}}</a></td>
<td>{{genre.doc_count}}</td> <td>{{genre.count}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -173,8 +173,8 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let genre of bottomGenres"> <tr *ngFor="let genre of bottomGenres">
<td><a [routerLink]="['/genre', genre.key]">{{genre.key}}</a></td> <td><a [routerLink]="['/genre', genre.value]">{{genre.value}}</a></td>
<td>{{genre.doc_count}}</td> <td>{{genre.count}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -1,13 +1,15 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ElsService } from './../els.service'; import { ElsService } from '../els-service/els.service';
import { Song } from './../model/song'; import { Song } from './../model/song';
import { Bucket } from './../model/bucket'; import { Bucket } from './../model/bucket';
import { Suggested } from '../model/suggested'; import { Suggested } from '../model/suggested';
import {Observable, of, OperatorFunction} from 'rxjs'; import {Observable, of, OperatorFunction} from 'rxjs';
import {catchError, debounceTime, distinctUntilChanged, map, tap, switchMap} from 'rxjs/operators'; import {catchError, debounceTime, distinctUntilChanged, map, tap, switchMap} from 'rxjs/operators';
import { TsService } from '../ts-service/ts.service';
import { TsBucket } from '../model/tsBucket';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
@@ -24,48 +26,42 @@ export class DashboardComponent implements OnInit {
albumArtistCount = 0; albumArtistCount = 0;
topGenres: Bucket[] = []; topGenres: TsBucket[] = [];
bottomGenres: Bucket[] = []; bottomGenres: TsBucket[] = [];
mostPlayedSongs: Song[] = []; mostPlayedSongs: Song[] = [];
lastAddedAlbums: Bucket[] = []; lastAddedAlbums: TsBucket[] = [];
albumArtists = []; albumArtists = [];
searchTerm = '' searchTerm = ''
suggested : Suggested[] = [] suggested : Suggested[] = []
constructor(private elsService: ElsService, private route: Router) { } constructor(private tsService: TsService, private route: Router) { }
ngOnInit(): void { ngOnInit(): void {
this.elsService.getTime().then(result => { this.tsService.getTime().then(result => {
this.totalTime = result; this.totalTime = result;
}); });
this.elsService.getSize().then(result => this.totalSize = result); this.tsService.getSize().then(result => this.totalSize = result);
this.elsService.getCountSong(ElsService.SONG_INDEX_NAME) this.tsService.getCountSong(ElsService.SONG_INDEX_NAME)
.then(result => this.trackCountSong = result); .then(result => this.trackCountSong = result);
// TODO: Unused information
// this.elsService.getCountSong(ElsService.ARTIST_INDEX_NAME)
// .then(result => this.trackCountArtist = result);
// this.elsService.getCountSong(ElsService.ALBUM_INDEX_NAME)
// .then(result => this.trackCountAlbum = result);
this.elsService.getCountNeverListenSong() this.tsService.getCountNeverListenSong()
.then(result => this.neverListenSong = result); .then(result => this.neverListenSong = result);
this.elsService.getMostPlayedTrack().subscribe( this.tsService.getMostPlayedTrack().subscribe(
data => this.mostPlayedSongs = data data => this.mostPlayedSongs = data
); );
this.elsService.getGenres().subscribe(data => this.topGenres = data); this.tsService.getGenres().subscribe(data => this.topGenres = data);
this.elsService.getGenres('asc').subscribe(data => this.bottomGenres = data); this.tsService.getGenres('asc').subscribe(data => this.bottomGenres = data);
// this.elsService.getGenreCount().subscribe(data => console.log(data)); this.tsService.getGenreCount().subscribe(data => console.log(data));
const lastAddedAlbumsTemp: Bucket[] = []; const lastAddedAlbumsTemp: TsBucket[] = [];
const BreakException = {}; this.tsService.getLastAddedAlbums(6).subscribe(buckets => {
this.elsService.getLastAddedAlbums(6).subscribe(buckets => {
buckets.forEach(bucket => { buckets.forEach(bucket => {
// console.log(bucket); // console.log(bucket);
@@ -74,8 +70,8 @@ export class DashboardComponent implements OnInit {
} else { } else {
let found = false; let found = false;
lastAddedAlbumsTemp.forEach(element => { lastAddedAlbumsTemp.forEach(element => {
if (element.key === bucket.key) { if (element.group_key === bucket.group_key) {
element.doc_count += bucket.doc_count; element.found += bucket.found;
found = true; found = true;
} }
}); });
@@ -84,24 +80,22 @@ export class DashboardComponent implements OnInit {
} }
} }
}); });
// console.log("alors");
// console.log(lastAddedAlbumsTemp);
this.lastAddedAlbums = lastAddedAlbumsTemp; this.lastAddedAlbums = lastAddedAlbumsTemp;
this.lastAddedAlbums.forEach(bucket => this.getArtistName(bucket)); this.lastAddedAlbums.forEach(bucket => this.getArtistName(bucket));
}); });
} }
private getArtistName(albumBucket: Bucket) { private getArtistName(albumBucket: TsBucket) {
// For each bucket.key (album name), search artist. // For each bucket.key (album name), search artist.
// Use track count to compare // Use track count to compare
this.elsService.getArtistFromAlbumName(albumBucket.key).subscribe(albums => { this.tsService.getArtistFromAlbumName(albumBucket.group_key[0]).subscribe(albums => {
// Identification of the good album // Identification of the good album
let goodAlbum; let goodAlbum;
if (albums.length > 1) { if (albums.length > 1) {
// More than one result for an album name: search good by track count // More than one result for an album name: search good by track count
albums.forEach(album => { albums.forEach(album => {
if (album['Track Count'] === albumBucket.doc_count) { if (album['Track Count'] === albumBucket.found) {
goodAlbum = album; goodAlbum = album;
} }
}); });
@@ -132,7 +126,7 @@ export class DashboardComponent implements OnInit {
distinctUntilChanged(), distinctUntilChanged(),
tap(() => this.searching = true), tap(() => this.searching = true),
switchMap(term => switchMap(term =>
this.elsService.getSuggest(term).pipe( this.tsService.getSuggest(term).pipe(
tap(() => this.searchFailed = false), tap(() => this.searchFailed = false),
catchError(() => { catchError(() => {
this.searchFailed = true; this.searchFailed = true;

View File

@@ -1,10 +1,10 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators'; import { map, catchError } from 'rxjs/operators';
import { Album } from './model/album'; import { Album } from './../model/album';
import { ElsService } from './els.service'; import { ElsService } from './els.service';
@Injectable() @Injectable()

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ElsArtistService } from './els-artist.service';
describe('ElsArtistService', () => {
let service: ElsArtistService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ElsArtistService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,121 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { Artist } from "../model/artist";
import { Song } from "../model/song";
import { ElsService } from "./els.service";
@Injectable()
export class ElsArtistService extends ElsService {
constructor(protected http: HttpClient) {
super(http);
}
private getQuerySongsWithArtistName(
artistName: string,
sortFilter: boolean = false,
size: number = 0,
from: number = 0
) {
let query = {
query: {
bool: {
should: [
{ match_phrase: { "Album Artist": artistName } },
{ match_phrase: { Artist: artistName } },
],
must_not: [],
},
},
};
if (sortFilter) {
console.log("ElsArtistService- TO SORT filter enabled");
query = this.addSortFilterToQuery(query);
}
if (size) {
query["size"] = size;
}
if (from) {
query["from"] = from;
}
return query;
}
public 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 + ")")
)
);
}
public getArtistSongs(
artistName: string,
from: number = 0,
sortFilter = false
): Observable<Song[]> {
console.info(
"getArtistSongs- Artist name: " + artistName + " - from: " + from
);
let query = this.getQuerySongsWithArtistName(
artistName,
sortFilter,
ElsService.DEFAULT_SIZE,
from
);
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,
"getArtistSongs(" + artistName + "," + from + ")"
)
)
);
}
public getCountArtistSong(
artistName: string,
sortFilter = false
): Observable<number> {
console.log("artistname: " + artistName);
const query = this.getQuerySongsWithArtistName(artistName, sortFilter);
return this.http
.post<any>(
this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_COUNT,
JSON.stringify(query),
{ headers: this.headers }
)
.pipe(
map((res) => res.count as number),
catchError((error) =>
this.handleError(error, "getCountArtistSong" + artistName + ")")
)
);
}
}

View File

@@ -1,11 +1,10 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators'; import { map, catchError } from 'rxjs/operators';
import { ElsService } from './els.service'; import { ElsService } from './els.service';
import { Album } from './model/album'; import { Album } from './../model/album';
import { Bucket } from './model/bucket';
import { Song } from './model/song';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -113,9 +112,38 @@ export class ElsSortService extends ElsService {
.catch(error => this.handleError(error, 'getCountNeverListenSong()')); .catch(error => this.handleError(error, 'getCountNeverListenSong()'));
} }
getAlbums(): Observable<Bucket[]> { getNbAlbums(): Promise<number> {
return this.http return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH, .post<any>(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'bool': {
'must_not': [
{
term: {
"Location.tree": "/F:/Musique"
}
}
]
}
},
size: 0,
"aggs": {
"album_count": {
"cardinality": {
"field": "Album.raw"
}
}
}
}), {headers: this.headers})
.toPromise()
.then(res => res.aggregations.album_count.value as number )
.catch(error => this.handleError(error, 'getNbAlbums()'));
}
getAlbums(): Observable<Album[]> {
return this.http
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({ JSON.stringify({
query: { query: {
bool: { bool: {
@@ -128,19 +156,13 @@ export class ElsSortService extends ElsService {
] ]
} }
}, },
'size': 0, 'size': 550,
'aggs': { "sort": [
'albums' : { { "Play Count": "desc"}
'terms': { ]
'field' : 'Album.raw',
'order': { '_term': 'asc' },
'size': 50
}
}
}
}), {headers: this.headers}) }), {headers: this.headers})
.pipe( .pipe(
map(res => this.responseAggregationToBucket(res, "albums")), map(res => this.responseToAlbums(res)),
catchError(error => this.handleError(error, 'getAlbums')) catchError(error => this.handleError(error, 'getAlbums'))
); );
} }

View File

@@ -1,14 +1,14 @@
import { Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http' import { HttpClient, HttpHeaders } from '@angular/common/http'
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators'; import { map, catchError } from 'rxjs/operators';
import { Song } from './model/song'; import { Song } from '../model/song';
import { Album } from './model/album'; import { Album } from '../model/album';
import { Artist } from './model/artist'; import { Artist } from '../model/artist';
import { Bucket } from './model/bucket'; import { Bucket } from '../model/bucket';
import { Suggested } from './model/suggested'; import { Suggested } from '../model/suggested';
@Injectable() @Injectable()
export class ElsService { export class ElsService {
@@ -21,8 +21,9 @@ export class ElsService {
protected static readonly ACTION_SEARCH = '/_search'; protected static readonly ACTION_SEARCH = '/_search';
protected static readonly ACTION_COUNT = '/_count'; protected static readonly ACTION_COUNT = '/_count';
protected elsUrl = 'http://localhost:9200'; protected elsUrl = 'http://192.168.1.20:9200';
protected headers = new HttpHeaders({'Content-Type': 'application/json'}); protected headers = new HttpHeaders({'Content-Type': 'application/json'});
protected defaultLocation = "/F:/Musique"; // TODO Use conf
constructor(protected http: HttpClient) { } constructor(protected http: HttpClient) { }
@@ -172,20 +173,48 @@ export class ElsService {
); );
} }
getAlbumSongs(albumName: string, from: number = 0): Observable<Song[]> { getAlbumSongs(albumName: string, from: number = 0, toSortFilter = false): Observable<Song[]> {
console.info('getAlbumSongs- Album name: ' + albumName + ' - from: ' + from); // TODO Move in els-album service
return this.http console.info(
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH, "getAlbumSongs- Album name: " + albumName + " - from: " + from
JSON.stringify({ );
'query': {
'match_phrase': { 'Album': albumName } let query = {
query: {
bool: {
must: [
{
match_phrase: {
"Album.raw": albumName,
}, },
'size': ElsService.DEFAULT_SIZE, },
'from': from ],
}), {headers: this.headers}) must_not: [],
},
},
size: ElsService.DEFAULT_SIZE,
from: from,
};
if (toSortFilter) {
console.log("getAlbumSongs- TO SORT filter enabled");
query = this.addSortFilterToQuery(query)
}
return this.http
.post(
this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify(query),
{ headers: this.headers }
)
.pipe( .pipe(
map(res => this.responseToSongs(res)), map((res) => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getAlbumSongs(' + albumName + ',' + from + ')')) catchError((error) =>
this.handleError(
error,
"getAlbumSongs(" + albumName + "," + from + ")"
)
)
); );
} }
@@ -203,31 +232,11 @@ export class ElsService {
}), {headers: this.headers}) }), {headers: this.headers})
.pipe( .pipe(
map(res => this.responseToSongs(res)), map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getAlbumSongs(' + genreName + ',' + from + ')')) catchError(error => this.handleError(error, 'getGenreSongs(' + 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> { getAlbum(albumName: string): Observable<Album> {
// TODO Why this is used on album pages? // TODO Why this is used on album pages?
@@ -246,21 +255,6 @@ export class ElsService {
); );
} }
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[]> { getGenres(ordering: string = 'desc'): Observable<Bucket[]> {
return this.http return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH, .post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
@@ -283,25 +277,6 @@ export class ElsService {
); );
} }
// 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[]> { getLastAddedAlbums(month: number): Observable<Bucket[]> {
return this.http return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH, .post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
@@ -319,7 +294,7 @@ export class ElsService {
'date' : { 'date' : {
'terms': { 'terms': {
'field' : 'Date Added', 'field' : 'Date Added',
'order': { '_term': 'desc' }, 'order': { '_key': 'desc' },
'size': 20 'size': 20
}, },
'aggs': { 'aggs': {
@@ -363,26 +338,6 @@ export class ElsService {
); );
} }
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[]> { getSuggest(text: string): Observable<Suggested[]> {
console.log('search sugget: ' + text); console.log('search sugget: ' + text);
return this.http return this.http
@@ -418,7 +373,7 @@ export class ElsService {
* @param res Response to process * @param res Response to process
* @param name The searched name - for console output * @param name The searched name - for console output
*/ */
private responseToOneTypedResult<T>(res: any, name: string): T { protected responseToOneTypedResult<T>(res: any, name: string): T {
const hits = res.hits.hits; const hits = res.hits.hits;
if (hits.length < 1) { if (hits.length < 1) {
@@ -513,4 +468,13 @@ export class ElsService {
console.error(error); // for demo purposes only console.error(error); // for demo purposes only
return Promise.reject(error.message || error); return Promise.reject(error.message || error);
} }
protected addSortFilterToQuery(query) {
query.query.bool.must_not.push({
term: {
"Location.tree": this.defaultLocation,
},
});
return query;
}
} }

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { ElsService } from '../els.service'; import { TsService } from '../ts-service/ts.service';
import { SongTableComponent } from '../song-table/song-table.component'; import { SongTableComponent } from '../song-table/song-table.component';
import { Song } from '../model/song'; import { Song } from '../model/song';
@@ -17,7 +17,7 @@ export class GenreComponent implements OnInit {
songs: Array<Song> = []; songs: Array<Song> = [];
constructor( constructor(
private elsService: ElsService, private tsService: TsService,
private route: ActivatedRoute private route: ActivatedRoute
) { } ) { }
@@ -29,7 +29,7 @@ export class GenreComponent implements OnInit {
} }
loadSongs(): any { loadSongs(): any {
this.elsService.getGenreSongs(this.genreName, this.songs.length).subscribe( this.tsService.getGenreSongs(this.genreName, this.songs.length).subscribe(
data => { data => {
// this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE; // this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE;

View File

@@ -0,0 +1,6 @@
export class TsBucket {
value: string;
count: number;
group_key: string[];
found: number;
}

View File

@@ -3,19 +3,39 @@
<br /> <br />
<div class="row cardAdmin"> <div class="row cardAdmin">
<div class="col-lg-3 col-md-3">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-play stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!trackCountSong"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="trackCountSong">{{trackCountSong}} songs</h3>
</div>
<div>Total songs
<br>~{{totalTime | convertMs}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3"> <div class="col-lg-3 col-md-3">
<div class="panel panel-blue"> <div class="panel panel-blue">
<div class="panel-heading"> <div class="panel-heading">
<div class="row"> <div class="row">
<div class="col-xs-3"> <div class="col-xs-3">
<span class="glyphicon glyphicon-time stats_icon"></span> <span class="glyphicon glyphicon-cd stats_icon"></span>
</div> </div>
<div class="col-xs-9 text-right"> <div class="col-xs-9 text-right">
<div> <div>
<h3 *ngIf="!totalTime"><span class="glyphicon glyphicon-refresh loading"></span></h3> <h3 *ngIf="!neverListenSong"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="totalTime">{{totalTime | convertMs}}</h3> <h3 *ngIf="neverListenSong">{{nbAlbums}}</h3>
</div> </div>
<div><br>Total time ({{totalTime | convertMoreExact}})</div> <div><br>Albums</div>
</div> </div>
</div> </div>
</div> </div>
@@ -39,24 +59,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-3 col-md-3">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-play stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!trackCountSong"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="trackCountSong">{{trackCountSong}}</h3>
</div>
<div><br>Total Songs</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3"> <div class="col-lg-3 col-md-3">
<div class="panel panel-red"> <div class="panel panel-red">
<div class="panel-heading"> <div class="panel-heading">
@@ -89,22 +91,22 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let album of toSortAlbum"> <tr *ngFor="let album of albums">
<td> <td>
<a [routerLink]="['/album', album.key]">{{album.key}}</a>&nbsp; <a [routerLink]="['/album', album.Name]" [queryParams]="{tosort: true}">{{album.Name}}</a>&nbsp;
</td> </td>
<td>{{albums[album.key]['Track Count']}}</td> <td>{{album['Track Count']}}</td>
<ng-template [ngIf]="albums[album.key]['Album Artist']" [ngIfElse]="artistSection"> <ng-template [ngIf]="album['Album Artist']" [ngIfElse]="artistSection">
<td> <td>
<a [routerLink]="['/artist', albums[album.key]['Album Artist']]">{{albums[album.key]['Album Artist']}}</a>&nbsp; <a [routerLink]="['/artist', album['Album Artist']]" [queryParams]="{tosort: true}">{{album['Album Artist']}}</a>&nbsp;
</td> </td>
</ng-template> </ng-template>
<ng-template #artistSection> <ng-template #artistSection>
<td> <td>
<a [routerLink]="['/artist', albums[album.key].Artist[0]]">{{albums[album.key].Artist}}</a>&nbsp; <a [routerLink]="['/artist', album.Artist[0]]">{{album.Artist}}</a>&nbsp;
</td> </td>
</ng-template> </ng-template>
@@ -115,12 +117,12 @@
</ng-template> </ng-template>
<td> <td>
{{albums[album.key]['Play Count']}} ({{albums[album.key]['Play Count']/albums[album.key]['Track Count'] | number:'1.0-0'}}/songs) {{album['Play Count']}} ({{album['Play Count']/album['Track Count'] | number:'1.0-0'}}/songs)
</td> </td>
<td class="star" [title]="(albums[album.key]['Album Rating Computed']?'Computed Rating: ':'Rating: ') + albums[album.key]['Album Rating']"> <td class="star" [title]="(album['Album Rating Computed']?'Computed Rating: ':'Rating: ') + album['Album Rating']">
<span *ngFor="let item of numberToArray(albums[album.key]['Album Rating'], 20)"> <span *ngFor="let item of numberToArray(album['Album Rating'], 20)">
<span class="glyphicon" [ngClass]="albums[album.key]['Album Rating Computed']?'glyphicon-star-empty':'glyphicon-star'"></span> <span class="glyphicon" [ngClass]="album['Album Rating Computed']?'glyphicon-star-empty':'glyphicon-star'"></span>
</span> </span>
</td> </td>
</tr> </tr>

View File

@@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ElsSortService } from '../els-sort.service'; import { ElsSortService } from '../els-service/els-sort.service';
import { Album } from '../model/album';
import { Bucket } from '../model/bucket'; import { Bucket } from '../model/bucket';
import { Song } from '../model/song';
import { Utils } from '../utils'; import { Utils } from '../utils';
@@ -18,6 +16,7 @@ export class ToSortComponent implements OnInit {
totalSize = 0; totalSize = 0;
trackCountSong = 0; trackCountSong = 0;
neverListenSong = 0; neverListenSong = 0;
nbAlbums = 0;
// Global var // Global var
toSortAlbum: Bucket[] = []; toSortAlbum: Bucket[] = [];
@@ -31,59 +30,9 @@ export class ToSortComponent implements OnInit {
this.elsService.getSize().then(result => this.totalSize = result); this.elsService.getSize().then(result => this.totalSize = result);
this.elsService.getCountSong().then(result => this.trackCountSong = result); this.elsService.getCountSong().then(result => this.trackCountSong = result);
this.elsService.getCountNeverListenSong().then(result => this.neverListenSong = result); this.elsService.getCountNeverListenSong().then(result => this.neverListenSong = result);
this.elsService.getNbAlbums().then(result => this.nbAlbums = result);
// **** GET ALBUMS ****// this.elsService.getAlbums().subscribe(data => this.albums = data)
const tmpToSortAlbums: Bucket[] = [];
this.elsService.getAlbums().subscribe(buckets => {
buckets.forEach(bucket => {
if (tmpToSortAlbums.length === 0) {
tmpToSortAlbums.push(bucket);
} else {
let found = false;
tmpToSortAlbums.forEach(element => {
if (element.key === bucket.key) {
element.doc_count += bucket.doc_count;
found = true;
}
});
if (!found) {
tmpToSortAlbums.push(bucket);
}
}
});
this.toSortAlbum = tmpToSortAlbums;
this.toSortAlbum.forEach(bucket => this.getAlbum(bucket));
// console.log(this.toSortAlbum)
// console.log(this.albums)
});
}
private getAlbum(albumBucket: Bucket) {
// For each bucket.key (album name), get Album document
// Use track count to compare
this.elsService.getArtistFromAlbumName(albumBucket.key).subscribe(albums => {
// Identification of the good album
let goodAlbum;
if (albums.length > 1) {
// More than one result for an album name: search good by track count
albums.forEach(album => {
if (album['Track Count'] === albumBucket.doc_count) {
goodAlbum = album;
}
});
} else {
// Just one result for album name
goodAlbum = albums[0];
}
// TODO Crap security if no good album found
if (goodAlbum == undefined) {
goodAlbum = albums[0]
}
this.albums[albumBucket.key] = goodAlbum;
});
} }
} }

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ElsService } from '../els.service'; import { TsService } from '../ts-service/ts.service';
import { Album } from '../model/album'; import { Album } from '../model/album';
import { Artist } from '../model/artist'; import { Artist } from '../model/artist';
@@ -15,10 +15,10 @@ export class TopPlayedComponent implements OnInit {
mostPlayedArtistsNaive: Artist[] = []; mostPlayedArtistsNaive: Artist[] = [];
mostPlayedArtists: Artist[] = []; mostPlayedArtists: Artist[] = [];
constructor(private elsService: ElsService) { } constructor(private tsService: TsService) { }
ngOnInit() { ngOnInit() {
this.elsService.getMostPlayedAlbumNaive() this.tsService.getMostPlayedAlbumNaive()
.then(result => { .then(result => {
result.forEach(album => { result.forEach(album => {
if (album.Artist.length <= 10) { if (album.Artist.length <= 10) {
@@ -29,29 +29,29 @@ export class TopPlayedComponent implements OnInit {
this.mostPlayedAlbumsNaive.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10); this.mostPlayedAlbumsNaive.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
}); });
this.elsService.getMostPlayedAlbum().subscribe(result => { // this.elsService.getMostPlayedAlbum().subscribe(result => {
this.mostPlayedAlbums = result; // this.mostPlayedAlbums = result;
this.mostPlayedAlbums.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10); // this.mostPlayedAlbums.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
// TODO Load more! (Use a ) // // TODO Load more! (Use a )
}); // });
this.elsService.getMostPlayedArtistNaive() this.tsService.getMostPlayedArtistNaive()
.then(result => { .then(result => {
this.mostPlayedArtistsNaive = result; this.mostPlayedArtistsNaive = result;
this.mostPlayedArtistsNaive.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10); this.mostPlayedArtistsNaive.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
}); });
this.elsService.getMostPlayedArtist().subscribe(result => { // this.tsService.getMostPlayedArtist().subscribe(result => {
result.forEach(artist => { // result.forEach(artist => {
if (artist['Track Count'] > 10) { // if (artist['Track Count'] > 10) {
this.mostPlayedArtists.push(artist); // this.mostPlayedArtists.push(artist);
} // }
}); // });
this.mostPlayedArtists.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10); // this.mostPlayedArtists.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
}); // });
} }
sortByAveragePlay(a: any, b: any) { sortByAveragePlay(a: any, b: any) {

View File

@@ -0,0 +1,101 @@
import { HttpClient, HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { TsService } from "./ts.service";
import { Album } from "../model/album";
import { Song } from "../model/song";
@Injectable()
export class TsAlbumService extends TsService {
constructor(protected http: HttpClient) {
super(http);
}
getAlbum(albumName: string): Observable<Album> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", albumName);
queryParams = queryParams.append("query_by", "Name");
console.log("coucou");
return this.http
.get(
this.tsUrl +
TsService.ALBUM_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToOneTypedResult<Album>(res, albumName)),
catchError((error) =>
this.handleError(error, "getAlbum(" + albumName + ")")
)
);
}
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 queryParams = new HttpParams();
queryParams = queryParams.append("q", albumName);
queryParams = queryParams.append("query_by", "Album");
queryParams = queryParams.append("per_page", TsService.DEFAULT_SIZE);
queryParams = queryParams.append("offset", from);
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToSongs(res)),
catchError((error) =>
this.handleError(
error,
"getAlbumSongs(" + albumName + "," + from + ")"
)
)
);
}
public static GET_ALBUMS_DEFAULT_PARAMS(): HttpParams {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append(
"sort_by",
"Play Count:desc,Avg Bit Rate:desc"
);
queryParams = queryParams.append("filter_by", "Min Bit Rate:<128");
queryParams = queryParams.append("per_page", "100");
return queryParams;
}
getAlbums(queryParams: HttpParams): Observable<Album[]> {
return this.http
.get<any>(
this.tsUrl +
TsService.ALBUM_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToAlbums(res)),
catchError((error) => this.handleError(error, "getAlbums"))
);
}
}

View File

@@ -0,0 +1,140 @@
import { HttpClient, HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { TsService } from "./ts.service";
import { Artist } from "../model/artist";
import { Song } from "../model/song";
@Injectable()
export class TsArtistService extends TsService {
constructor(protected http: HttpClient) {
super(http);
}
private getQuerySongsWithArtistName(
artistName: string,
sortFilter: boolean = false,
size: number = 0,
from: number = 0
) {
let query = {
query: {
bool: {
should: [
{ match_phrase: { "Album Artist": artistName } },
{ match_phrase: { Artist: artistName } },
],
must_not: [],
},
},
};
if (sortFilter) {
console.log("ElsArtistService- TO SORT filter enabled");
query = this.addSortFilterToQuery(query);
}
if (size) {
query["size"] = size;
}
if (from) {
query["from"] = from;
}
return query;
}
public getArtist(artistName: string): Observable<Artist> {
console.log("getArtist");
let queryParams = new HttpParams();
queryParams = queryParams.append("q", artistName);
queryParams = queryParams.append("query_by", "Name");
return this.http
.get<any>(
this.tsUrl +
TsService.ARTIST_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToOneTypedResult<Artist>(res, artistName)),
catchError((error) =>
this.handleError(error, "getArtist(" + artistName + ")")
)
);
}
public getArtistSongs(
artistName: string,
from: number = 0,
sortFilter = false
): Observable<Song[]> {
console.info(
"getArtistSongs- Artist name: " + artistName + " - from: " + from
);
let query = this.getQuerySongsWithArtistName(
artistName,
sortFilter,
TsService.DEFAULT_SIZE,
from
);
let queryParams = new HttpParams();
queryParams = queryParams.append("q", artistName);
queryParams = queryParams.append("query_by", "Artist,Album Artist");
queryParams = queryParams.append("per_page", TsService.DEFAULT_SIZE);
queryParams = queryParams.append("offset", from);
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToSongs(res)),
catchError((error) =>
this.handleError(
error,
"getArtistSongs(" + artistName + "," + from + ")"
)
)
);
}
public getCountArtistSong(
artistName: string,
sortFilter = false
): Observable<number> {
console.log("artistname: " + artistName);
const query = this.getQuerySongsWithArtistName(artistName, sortFilter);
let queryParams = new HttpParams();
queryParams = queryParams.append("q", artistName);
queryParams = queryParams.append("query_by", "Artist"); // QUESTION ArtistName?
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => res.found as number),
catchError((error) =>
this.handleError(error, "getCountArtistSong" + artistName + ")")
)
);
}
}

View File

@@ -0,0 +1,169 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { TsService } from './ts.service';
import { Album } from '../model/album';
@Injectable({
providedIn: 'root'
})
export class TsSortService extends TsService {
constructor(protected http: HttpClient) {
super(http);
}
getTime(): Promise<number> {
return this.http
.post<any>(this.tsUrl + TsService.SONG_INDEX_NAME + TsService.ACTION_SEARCH,
JSON.stringify({
query: {
bool: {
must_not: [
{
term: {
"Location.tree": "/F:/Musique"
}
}
]
}
},
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.tsUrl + TsService.SONG_INDEX_NAME + TsService.ACTION_SEARCH,
JSON.stringify({
query: {
bool: {
must_not: [
{
term: {
"Location.tree": "/F:/Musique"
}
}
]
}
},
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(): Promise<number> {
return this.http
.post<any>(this.tsUrl + TsService.SONG_INDEX_NAME + TsService.ACTION_COUNT,
JSON.stringify({
query: {
bool: {
must_not: [
{
term: {
"Location.tree": "/F:/Musique"
}
}
]
}
}
}), {headers: this.headers})
.toPromise()
.then(res => res.count as number)
.catch(error => this.handleError(error, 'getCountSong()'));
}
getCountNeverListenSong(): Promise<number> {
return this.http
.post<any>(this.tsUrl + TsService.SONG_INDEX_NAME + TsService.ACTION_COUNT,
JSON.stringify({
'query': {
'bool': {
'must_not': [
{
'exists': { 'field': 'Play Count'}
}, {
term: {
"Location.tree": "/F:/Musique"
} }
]
}
}
}), {headers: this.headers})
.toPromise()
.then(res => res.count as number)
.catch(error => this.handleError(error, 'getCountNeverListenSong()'));
}
getNbAlbums(): Promise<number> {
return this.http
.post<any>(this.tsUrl + TsService.ALBUM_INDEX_NAME + TsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'bool': {
'must_not': [
{
term: {
"Location.tree": "/F:/Musique"
}
}
]
}
},
size: 0,
"aggs": {
"album_count": {
"cardinality": {
"field": "Album.raw"
}
}
}
}), {headers: this.headers})
.toPromise()
.then(res => res.aggregations.album_count.value as number )
.catch(error => this.handleError(error, 'getNbAlbums()'));
}
getAlbums(): Observable<Album[]> {
return this.http
.post(this.tsUrl + TsService.ALBUM_INDEX_NAME + TsService.ACTION_SEARCH,
JSON.stringify({
query: {
bool: {
must_not: [
{
term: {
"Location.tree": "/F:/Musique"
}
}
]
}
},
'size': 550,
"sort": [
{ "Play Count": "desc"}
]
}), {headers: this.headers})
.pipe(
map(res => this.responseToAlbums(res)),
catchError(error => this.handleError(error, 'getAlbums'))
);
}
}

View File

@@ -0,0 +1,456 @@
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { Album } from "../model/album";
import { Artist } from "../model/artist";
import { Song } from "../model/song";
import { Suggested } from "../model/suggested";
import { TsBucket } from "../model/tsBucket";
@Injectable()
export class TsService {
public static readonly DEFAULT_SIZE: number = 20;
public static readonly SONG_INDEX_NAME = "/songs";
public static readonly ARTIST_INDEX_NAME = "/artists";
public static readonly ALBUM_INDEX_NAME = "/albums";
public static readonly SUGGEST_INDEX_NAME = "/suggest";
protected static readonly ACTION_SEARCH = "/search";
protected static readonly ACTION_COUNT = "/_count";
protected tsUrl = "http://localhost:8108/collections";
protected tsUrlPure = "http://localhost:8108/";
protected headers = new HttpHeaders({
"Content-Type": "application/json",
"X-TYPESENSE-API-KEY": "toto",
});
protected defaultLocation = "/F:/Musique"; // TODO Use conf
constructor(protected http: HttpClient) {}
getTime(): Promise<number> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append("limit", "0");
queryParams = queryParams.append(
"facet_by",
"Total Time(Range:[0,99999999999999999])"
);
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.toPromise()
.then((res) => res.facet_counts[0].stats.sum as number)
.catch((error) => this.handleError(error, "getTime()"));
}
getSize(): Promise<number> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append("limit", "0");
queryParams = queryParams.append(
"facet_by",
"Size(Range:[0,99999999999999999])"
);
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.toPromise()
.then((res) => res.facet_counts[0].stats.sum as number)
.catch((error) => this.handleError(error, "getSize()"));
}
getCountSong(index: string): Promise<number> {
return this.http
.get<any>(this.tsUrl + TsService.SONG_INDEX_NAME, {
headers: this.headers,
})
.toPromise()
.then((res) => res.num_documents as number)
.catch((error) => this.handleError(error, "getCountSong(" + index + ")"));
}
getCountNeverListenSong(): Promise<number> {
// TODO Impossible sans valeur par defaut dans Play Count
return new Promise((resolve) => {
resolve(0);
});
}
getMostPlayedTrack(): Observable<Song[]> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append("sort_by", "Play Count:desc");
queryParams = queryParams.append("limit", 5);
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToSongs(res)),
catchError((error) => this.handleError(error, "getMostPlayedTrack()"))
);
}
getMostPlayedArtist(): Observable<Artist[]> {
return this.http
.post(
this.tsUrl + TsService.ARTIST_INDEX_NAME + TsService.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()"))
);
}
/**
* A basic get of albums ordered by 'Play Count' field.
*/
getMostPlayedAlbumNaive(): Promise<Album[]> {
return this.http
.get(
this.tsUrl +
TsService.ALBUM_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH +
"?sort_by=Play%20Count%3Adesc,Track%20Count%3Adesc&limit=20&q=*",
{ headers: this.headers }
)
.toPromise()
.then((res) => this.responseToAlbums(res))
.catch((error) => this.handleError(error, "getMostPlayedAlbumNaive"));
// TODO Excluse 'Divers' + compilation
}
getMostPlayedArtistNaive(): Promise<Artist[]> {
return this.http
.get(
this.tsUrl +
TsService.ARTIST_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH +
"?sort_by=Play%20Count%3Adesc&limit=20&q=*",
{ headers: this.headers }
)
.toPromise()
.then((res) => this.responseToAlbums(res))
.catch((error) => this.handleError(error, "getMostPlayedArtistNaive"));
// TODO Excluse 'Divers' + compilation
}
getGenreSongs(genreName: string, from: number = 0): Observable<Song[]> {
console.info(
"getGenreSongs- Genre name: " + genreName + " - from: " + from
);
let queryParams = new HttpParams();
queryParams = queryParams.append("q", genreName);
queryParams = queryParams.append("query_by", "Genre");
queryParams = queryParams.append("per_page", TsService.DEFAULT_SIZE);
queryParams = queryParams.append("offset", from);
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseToSongs(res)),
catchError((error) =>
this.handleError(
error,
"getGenreSongs(" + genreName + "," + from + ")"
)
)
);
}
getGenres(ordering: string = "desc"): Observable<TsBucket[]> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append("facet_by", "Genre");
queryParams = queryParams.append("max_facet_values", "5");
queryParams = queryParams.append("limit", "0");
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseAggregationToBucket(res)),
catchError((error) =>
this.handleError(error, "getGenres(" + ordering + ")")
)
);
}
getGenreCount(ordering: string = "desc"): Observable<number> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append("facet_by", "Genre");
queryParams = queryParams.append("max_facet_values", "0");
queryParams = queryParams.append("limit", "0");
return this.http
.get<any>(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => res.facet_counts[0].stats.total_values as number),
catchError((error) =>
this.handleError(error, "getGenres(" + ordering + ")")
)
);
}
getLastAddedAlbums(month: number): Observable<TsBucket[]> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", "*");
queryParams = queryParams.append("sort_by", "Track ID:desc");
queryParams = queryParams.append("group_by", "Album");
// TODO Deal with mounth?
return this.http
.get(
this.tsUrl +
TsService.SONG_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => this.responseSubAggregationToBucket(res)),
catchError((error) =>
this.handleError(error, "getLastAddedAlbums(" + month + ")")
)
);
}
getArtistFromAlbumName(albumname: string): Observable<Album[]> {
let queryParams = new HttpParams();
queryParams = queryParams.append("q", albumname);
queryParams = queryParams.append("query_by", "Name");
return this.http
.get<any>(
this.tsUrl +
TsService.ALBUM_INDEX_NAME +
"/documents" +
TsService.ACTION_SEARCH,
{ headers: this.headers, params: queryParams }
)
.pipe(
map((res) => res.hits),
map((hits: Array<any>) => {
const result: Array<Album> = [];
hits.forEach((hit) => {
result.push(hit.document);
});
return result;
}),
catchError((error) =>
this.handleError(error, "getArtistFromAlbumName(" + albumname + ")")
)
);
}
getSuggest(text: string): Observable<Suggested[]> {
let queryParameters = new HttpParams();
queryParameters = queryParameters.append("query_by", "Name");
queryParameters = queryParameters.append(
"sort_by",
"_text_match:desc,Play Count:desc"
);
queryParameters = queryParameters.append("limit", 5);
console.log("search sugget: " + text);
return this.http
.post<any>(
this.tsUrlPure + "multi_search",
JSON.stringify({
searches: [
{
collection: "albums",
q: text,
},
{
collection: "artists",
q: text,
},
],
}),
{ headers: this.headers, params: queryParameters }
)
.pipe(
map((res) => this.responseSuggesterToSuggested(res)),
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
*/
protected responseToOneTypedResult<T>(res: any, name: string): T {
const hits = res.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].document;
}
/** Process a response to a array of songs.
*
* @param res Response to process
*/
protected responseToSongs(res: any): Song[] {
const result: Array<Song> = [];
res.hits.forEach((hit) => {
result.push(hit.document);
});
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): TsBucket[] {
const result: Array<TsBucket> = [];
res.facet_counts[0].counts.forEach((bucket) => {
result.push(bucket);
});
return result;
}
private responseSubAggregationToBucket(res: any): TsBucket[] {
const result: Array<TsBucket> = [];
res.grouped_hits.forEach((tmp) => {
result.push(tmp);
});
return result;
}
protected responseSuggesterToSuggested(res: any): Suggested[] {
const suggesteds: Array<Suggested> = [];
res.results.forEach((result) => {
let type = result.request_params.collection_name;
type = type.slice(0, type.length - 1);
result.hits.forEach((hit) => {
let suggested = new Suggested();
suggested.type = type;
suggested.name = hit.document.Name;
suggesteds.push(suggested);
});
});
return suggesteds;
}
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);
}
protected addSortFilterToQuery(query) {
query.query.bool.must_not.push({
term: {
"Location.tree": this.defaultLocation,
},
});
return query;
}
/** Process a response to a array of songs.
*
* @param res Response to process
*/
protected responseToAlbums(res: any): Album[] {
const result: Array<Album> = [];
res.hits.forEach((hit) => {
result.push(hit.document);
});
return result;
}
}

View File

@@ -22,16 +22,6 @@ import '@angular/localize/init';
* BROWSER POLYFILLS * BROWSER POLYFILLS
*/ */
/** IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** /**
* By default, zone.js will patch all possible macroTask and DomEvents * By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags * user can disable parts of macroTask/DomEvents patch by setting following flags

View File

@@ -7,19 +7,10 @@ import {
platformBrowserDynamicTesting platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing'; } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment. // First, initialize the Angular testing environment.
getTestBed().initTestEnvironment( getTestBed().initTestEnvironment(
BrowserDynamicTestingModule, BrowserDynamicTestingModule,
platformBrowserDynamicTesting() platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
}
); );
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -10,12 +10,13 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"target": "es2015", "target": "ES2022",
"module": "es2020", "module": "es2020",
"lib": [ "lib": [
"es2018", "es2018",
"dom" "dom"
] ],
"useDefineForClassFields": false
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"strictTemplates": true "strictTemplates": true

View File

@@ -147,8 +147,7 @@ class ITunesParser:
'Play Count': 0, 'Play Count': 0,
'Rating': 0, 'Rating': 0,
'Genre': set(), 'Genre': set(),
'Album': set(), 'Album': set()
'Album Artist': set()
} }
# Compute information # Compute information
@@ -169,9 +168,6 @@ class ITunesParser:
if 'Album' in track: if 'Album' in track:
self._artists[akey]['Album'].add(track['Album']) self._artists[akey]['Album'].add(track['Album'])
if 'Album Artist' in track:
self._artists[akey]['Album Artist'].add(track['Artist'])
def _process_album(self, track): def _process_album(self, track):
""" """
Process albums in the track part of library and return a JSON formated for a bulk ELS request Process albums in the track part of library and return a JSON formated for a bulk ELS request
@@ -196,7 +192,8 @@ class ITunesParser:
'Avg Bit Rate': track['Bit Rate'], 'Avg Bit Rate': track['Bit Rate'],
'Min Bit Rate': track['Bit Rate'], 'Min Bit Rate': track['Bit Rate'],
# 'Album Artist': '', # 'Album Artist': '',
'Total Time': 0 'Total Time': 0,
'Location': ''
} }
# Compute information # Compute information
@@ -211,6 +208,8 @@ class ITunesParser:
self._albums[akey]['Play Count'] += play_count self._albums[akey]['Play Count'] += play_count
self._albums[akey]['Total Time'] += total_time self._albums[akey]['Total Time'] += total_time
self._albums[akey]['Location'] = os.path.dirname(track['Location'])
if self._albums[akey]['Min Bit Rate'] > track['Bit Rate']: if self._albums[akey]['Min Bit Rate'] > track['Bit Rate']:
self._albums[akey]['Min Bit Rate'] = track['Bit Rate'] self._albums[akey]['Min Bit Rate'] = track['Bit Rate']

View File

@@ -1,4 +1,28 @@
{ {
"settings": {
"analysis": {
"analyzer": {
"custom_path_tree": {
"tokenizer": "custom_hierarchy"
},
"custom_path_tree_reversed": {
"tokenizer": "custom_hierarchy_reversed"
}
},
"tokenizer": {
"custom_hierarchy": {
"type": "path_hierarchy",
"delimiter": "/",
"skip": 3
},
"custom_hierarchy_reversed": {
"type": "path_hierarchy",
"delimiter": "/",
"reverse": "true"
}
}
}
},
"mappings" : { "mappings" : {
"properties": { "properties": {
"Artist": { "Artist": {
@@ -21,6 +45,19 @@
}, },
"Genre": { "Genre": {
"type": "keyword" "type": "keyword"
},
"Location": {
"type": "text",
"fields": {
"tree": {
"type": "text",
"analyzer": "custom_path_tree"
},
"tree_reversed": {
"type": "text",
"analyzer": "custom_path_tree_reversed"
}
}
} }
} }
} }

View File

@@ -1,70 +1,556 @@
{ {
"settings": { "name": "songs",
"analysis": { "token_separators": [":", "/", "."],
"analyzer": { "fields": [
"custom_path_tree": { {
"tokenizer": "custom_hierarchy" "facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Album",
"optional": true,
"sort": false,
"type": "string"
}, },
"custom_path_tree_reversed": { {
"tokenizer": "custom_hierarchy_reversed" "facet": false,
} "index": true,
"infix": false,
"locale": "",
"name": "Album Rating",
"optional": true,
"sort": true,
"type": "int64"
}, },
"tokenizer": { {
"custom_hierarchy": { "facet": false,
"type": "path_hierarchy", "index": true,
"delimiter": "/", "infix": false,
"skip": 3 "locale": "",
"name": "Album Rating Computed",
"optional": true,
"sort": true,
"type": "bool"
}, },
"custom_hierarchy_reversed": { {
"type": "path_hierarchy", "facet": false,
"delimiter": "/", "index": true,
"reverse": "true" "infix": false,
} "locale": "",
} "name": "Artist",
} "optional": true,
"sort": false,
"type": "string"
}, },
"mappings" : { {
"properties": { "facet": false,
"Artist": { "index": true,
"type": "text", "infix": false,
"fields": { "locale": "",
"raw": {"type": "keyword"} "name": "Artwork Count",
} "optional": true,
"sort": true,
"type": "int64"
}, },
"Album Artist": { {
"type": "text", "facet": false,
"fields": { "index": true,
"raw": {"type": "keyword"} "infix": false,
} "locale": "",
"name": "Bit Rate",
"optional": true,
"sort": true,
"type": "int64"
}, },
"Album": { {
"type": "text", "facet": false,
"fields": { "index": true,
"raw": {"type": "keyword"} "infix": false,
} "locale": "",
"name": "Date Added",
"optional": true,
"sort": false,
"type": "string"
}, },
"Bit Rate": { {
"type": "integer" "facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Date Modified",
"optional": true,
"sort": false,
"type": "string"
}, },
"Genre": { {
"type": "keyword" "facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "File Folder Count",
"optional": true,
"sort": true,
"type": "int64"
}, },
"Kind": { {
"type": "keyword" "facet": true,
"index": true,
"infix": false,
"locale": "",
"name": "Genre",
"optional": true,
"sort": false,
"type": "string"
}, },
"Location": { {
"type": "text", "facet": false,
"fields": { "index": true,
"tree": { "infix": false,
"type": "text", "locale": "",
"analyzer": "custom_path_tree" "name": "Kind",
"optional": true,
"sort": false,
"type": "string"
}, },
"tree_reversed": { {
"type": "text", "facet": false,
"analyzer": "custom_path_tree_reversed" "index": true,
} "infix": false,
} "locale": "",
} "name": "Library Folder Count",
} "optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Location",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Name",
"optional": false,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Persistent ID",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Play Count",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Play Date",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Play Date UTC",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Rating",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Rating Computed",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Sample Rate",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": true,
"index": true,
"infix": false,
"locale": "",
"name": "Size",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Skip Count",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Skip Date",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": true,
"index": true,
"infix": false,
"locale": "",
"name": "Total Time",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Track ID",
"optional": false,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Track Number",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Track Type",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Year",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Composer",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Disabled",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Disc Count",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Disc Number",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Album Artist",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Sort Name",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Sort Album",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Comments",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Sort Artist",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Sort Composer",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Loved",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Volume Adjustment",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Compilation",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Part Of Gapless Album",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Track Count",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Sort Album Artist",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Album Loved",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "BPM",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Grouping",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Series",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Purchased",
"optional": true,
"sort": true,
"type": "bool"
},
{
"facet": false,
"index": true,
"infix": false,
"locale": "",
"name": "Release Date",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Movement Count",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Movement Name",
"optional": true,
"sort": false,
"type": "string"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Movement Number",
"optional": true,
"sort": true,
"type": "int64"
},
{
"facet": false,
"index": false,
"infix": false,
"locale": "",
"name": "Work",
"optional": true,
"sort": false,
"type": "string"
} }
]
} }

View File

@@ -1,48 +0,0 @@
{
"settings": {
"index": {
"number_of_replicas": 0
},
"analysis": {
"filter": {
"french_stop": {
"type": "stop",
"stopwords": "_french_"
},
"english_stop": {
"type": "stop",
"stopwords": "_english_"
}
},
"analyzer": {
"names": {
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding",
"french_stop",
"english_stop"
]
}
}
}
},
"mappings": {
"properties": {
"artist_suggest": {
"type": "completion",
"search_analyzer": "names"
},
"artist": {
"type": "keyword"
},
"album_suggest": {
"type": "completion",
"search_analyzer": "names"
},
"album": {
"type": "keyword"
}
}
}
}

View File

@@ -10,8 +10,6 @@ import json
import time import time
import requests import requests
from suggester import process_file
class bcolors: class bcolors:
HEADER = '\033[95m' HEADER = '\033[95m'
OKBLUE = '\033[94m' OKBLUE = '\033[94m'
@@ -24,18 +22,16 @@ class bcolors:
UNDERLINE = '\033[4m' UNDERLINE = '\033[4m'
# Default file names # Default file names
SONG_FILE = 'es-songs.json' DEFAULT_SONG_FILE = 'es-songs.json'
ALBUM_FILE = 'es-albums.json' DEFAULT_ALBUM_FILE = 'es-albums.json'
ARTIST_FILE = 'es-artists.json' DEFAULT_ARTIST_FILE = 'es-artists.json'
MAPPING_SONGS_FILE = 'mapping.songs.json' DEFAULT_MAPPING_SONGS_FILE = 'mapping.songs.json'
MAPPING_ARTISTS_FILE = 'mapping.artists.json' DEFAULT_MAPPING_ARTISTS_FILE = 'mapping.artists.json'
MAPPING_ALBUMS_FILE = 'mapping.albums.json' DEFAULT_MAPPING_ALBUMS_FILE = 'mapping.albums.json'
MAPPING_SUGGEST_FILE = 'mapping.suggest.json'
SONG_INDEX = 'itunes-songs' SONG_INDEX = 'itunes-songs'
ALBUM_INDEX = 'itunes-albums' ALBUM_INDEX = 'itunes-albums'
ARTIST_INDEX = 'itunes-artists' ARTIST_INDEX = 'itunes-artists'
SUGGEST_INDEX = 'itunes-suggest'
# TODO Put variables in a config files or in a python library # TODO Put variables in a config files or in a python library
# Global values / set as default values # Global values / set as default values
@@ -51,8 +47,8 @@ def main():
args = create_args_parser().parse_args() args = create_args_parser().parse_args()
if args.ALL and args.no_song: if not args.song and args.ALL:
print(__file__ + ': error: argument -A/--ALL: not allowed with argument --no-song') print(__file__ + ': error: argument -A/--ALL: not allowed with argument -s/--song')
sys.exit(-1) sys.exit(-1)
# Overloaded setting value # Overloaded setting value
@@ -72,16 +68,16 @@ def main():
check_is_ok = [] check_is_ok = []
# Send song data # Send song data
if not args.no_song: if args.song or args.ALL:
if args.DELETE: if args.DELETE:
mapping_song = load_file(args.mapping_song, MAPPING_SONGS_FILE) mapping_song = load_file(args.mapping_song, DEFAULT_MAPPING_SONGS_FILE)
if not args.quiet: if not args.quiet:
print("Mapping of song index file: '{}'".format(mapping_song.name)) print("Mapping of song index file: '{}'".format(mapping_song.name))
delete_index(SONG_INDEX, args.quiet) delete_index(SONG_INDEX, args.quiet)
put_mapping(SONG_INDEX, mapping_song, args.quiet) put_mapping(SONG_INDEX, mapping_song, args.quiet)
song_file = load_file(args.song_file, SONG_FILE) song_file = load_file(args.song_file, DEFAULT_SONG_FILE)
if not args.quiet: if not args.quiet:
print("Song file: '{}'".format(song_file.name)) print("Song file: '{}'".format(song_file.name))
@@ -100,7 +96,7 @@ def main():
# Send artist data # Send artist data
if args.artist_file or args.ALL: if args.artist_file or args.ALL:
if args.DELETE: if args.DELETE:
mapping_artist = load_file(args.mapping_artist, MAPPING_ARTISTS_FILE) mapping_artist = load_file(args.mapping_artist, DEFAULT_MAPPING_ARTISTS_FILE)
if not args.quiet: if not args.quiet:
print("Mapping of artist index file: '{}'".format(mapping_artist.name)) print("Mapping of artist index file: '{}'".format(mapping_artist.name))
@@ -111,7 +107,7 @@ def main():
if not artist_file: if not artist_file:
if not args.quiet: if not args.quiet:
print('No artist file specified, take default file...') print('No artist file specified, take default file...')
artist_file = open(ARTIST_FILE, 'r') artist_file = open(DEFAULT_ARTIST_FILE, 'r')
if not args.quiet: if not args.quiet:
print("Artist file: '{}'".format(artist_file.name)) print("Artist file: '{}'".format(artist_file.name))
@@ -126,7 +122,7 @@ def main():
if args.album_file or args.ALL: if args.album_file or args.ALL:
if args.DELETE: if args.DELETE:
mapping_album = load_file(args.mapping_album, MAPPING_ALBUMS_FILE) mapping_album = load_file(args.mapping_album, DEFAULT_MAPPING_ALBUMS_FILE)
if not args.quiet: if not args.quiet:
print("Mapping of artist index file: '{}'".format(mapping_album.name)) print("Mapping of artist index file: '{}'".format(mapping_album.name))
@@ -137,7 +133,7 @@ def main():
if not album_file: if not album_file:
if not args.quiet: if not args.quiet:
print('No album file specified, take default file...') print('No album file specified, take default file...')
album_file = open(ALBUM_FILE, 'r') album_file = open(DEFAULT_ALBUM_FILE, 'r')
if not args.quiet: if not args.quiet:
print("Take file '{}' to send song data".format(album_file.name)) print("Take file '{}' to send song data".format(album_file.name))
@@ -149,28 +145,6 @@ def main():
else: else:
print('Album sent') print('Album sent')
if not args.no_suggest:
print("Process suggestion:")
if args.DELETE:
delete_index(SUGGEST_INDEX, args.quiet)
if not args.ALL and not args.album_file and not args.artist_file:
print('Only song file processed. No suggestion to process.')
else:
if args.DELETE:
mapping_suggest = load_file(args.mapping_suggest, MAPPING_SUGGEST_FILE)
if not args.quiet:
print("Mapping of suggest index file: '{}'".format(mapping_suggest.name))
put_mapping(SUGGEST_INDEX, mapping_suggest, args.quiet)
suggs_docs = 0
if args.album_file or args.ALL:
suggs_docs += process_file(ALBUM_FILE, 'Album')
print('Created suggestion documents: ' + str(suggs_docs))
if args.artist_file or args.ALL:
suggs_docs += process_file(ARTIST_FILE, 'Artist', 'Album Artist')
print('Created suggestion documents: ' + str(suggs_docs))
print("I'm done!") print("I'm done!")
if check_is_ok.count(False) > 0: if check_is_ok.count(False) > 0:
print('Some problems occurs') print('Some problems occurs')
@@ -199,14 +173,15 @@ def create_args_parser():
# TODO rewrit description with multi-index phylosophie # TODO rewrit description with multi-index phylosophie
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=''' description='''
Send JSON files formated for bulk Elasticsearch operation to an Elasticsearch. Send JSON files formated for bulk Elasticsearch operation to an Elasticsearch.
By default: send only song data. See option to send album/artist/suggest data. By default: send song data enable, send album & artist data disabled.
Check that all the data has been sent.
Create index if -D option activated with a mapping file (see -map). Detect if index doesn't exist and create it with a mapping file (see -map and -idx argument).
Remeber : it's cumulative! If you want to remove songs/artits/albums,
It's cumulative! If you want to remove songs/artits/albums, you have to delete and re-create the index (use -D option).''', you have to delete and re-create the index (use -D option).
formatter_class=argparse.RawTextHelpFormatter '''
) )
# Bulk # Bulk
parser.add_argument('-q', '--quiet', action='store_true', parser.add_argument('-q', '--quiet', action='store_true',
@@ -215,43 +190,38 @@ It's cumulative! If you want to remove songs/artits/albums, you have to delete a
sending_group = parser.add_argument_group("Sending options") sending_group = parser.add_argument_group("Sending options")
song_group = sending_group.add_mutually_exclusive_group() song_group = sending_group.add_mutually_exclusive_group()
song_group.add_argument('-sf', '--song-file', type=argparse.FileType('r'), song_group.add_argument('-sf', '--song-file', type=argparse.FileType('r'),
help='Song file data to send (default: \'{}\').'.format(SONG_FILE)) help='Song file data to send (default: \'{}\').'.format(DEFAULT_SONG_FILE))
sending_group.add_argument('-al', '--album-file', nargs='?', type=argparse.FileType('r'), const=ALBUM_FILE, sending_group.add_argument('-al', '--album-file', nargs='?', type=argparse.FileType('r'), const=DEFAULT_ALBUM_FILE,
help='Enable sending album data. Optionally, precise the album data file (default: \'{}\')' help='Enable sending album data. Optionally, precise the album data file (default: \'{}\')'
.format(ALBUM_FILE)) .format(DEFAULT_ALBUM_FILE))
sending_group.add_argument('-ar', '--artist-file', nargs='?', type=argparse.FileType('r'), const=ARTIST_FILE, sending_group.add_argument('-ar', '--artist-file', nargs='?', type=argparse.FileType('r'), const=DEFAULT_ARTIST_FILE,
help='Enable sending artist data. Optionally, precise the artist data file (default: \'{}\')' help='Enable sending artist data. Optionally, precise the artist data file (default: \'{}\')'
.format(ARTIST_FILE)) .format(DEFAULT_ARTIST_FILE))
song_group.add_argument('-s', '--song', action='store_false',
help='Disable sending song data')
# Mode # Mode
mode_group = parser.add_argument_group('Mode') mode_group = parser.add_argument_group('Mode')
mode_group.add_argument('-A', '--ALL', action='store_true', mode_group.add_argument('-A', '--ALL', action='store_true',
help='Send all possible data: song, artist, album and suggest. Use default file if not specified') help='Send all possible data: song, artist and album')
mode_group.add_argument('-D', '--DELETE', action='store_true', mode_group.add_argument('-D', '--DELETE', action='store_true',
help='Delete index and create new. See -map arguement to set mapping file') help='''Delete old index and create a new.
mode_group.add_argument('--no-song', action='store_true', See -idx argument to set index name.
help='''Disable sending song data. See -map arguement to set mapping file.''')
Not allowed with -A option.''')
mode_group.add_argument('--no-suggest', action='store_true',
help='Disable sending suggest data. Allowed with -A option')
# Mapping # Mapping
mapping_group = parser.add_argument_group('Mapping files') mapping_group = parser.add_argument_group('Mapping files')
# CAUTION default values cannot be used because they necessarily activate the option mode_group.add_argument('-ms', '--mapping-song', type=argparse.FileType('r'), const=DEFAULT_MAPPING_SONGS_FILE, nargs='?',
# QUESTION Use a for with a list of default mapping file? help='Mapping file for songs (default: \'{}\')'.format(DEFAULT_MAPPING_SONGS_FILE))
mapping_group.add_argument('-ms', '--mapping-song', type=argparse.FileType('r'), const=MAPPING_SONGS_FILE, nargs='?', mode_group.add_argument('-mr', '--mapping-artist', type=argparse.FileType('r'), const=DEFAULT_ARTIST_FILE, nargs='?',
help='Mapping file for songs (default: \'{}\')'.format(MAPPING_SONGS_FILE)) help='Mapping file for artists (default: \'{}\')'.format(DEFAULT_MAPPING_ARTISTS_FILE))
mapping_group.add_argument('-mr', '--mapping-artist', type=argparse.FileType('r'), const=ARTIST_FILE, nargs='?', mode_group.add_argument('-ml', '--mapping-album', type=argparse.FileType('r'), const=DEFAULT_MAPPING_ALBUMS_FILE, nargs='?',
help='Mapping file for artists (default: \'{}\')'.format(MAPPING_ARTISTS_FILE)) help='Mapping file for albums (default: \'{}\')'.format(DEFAULT_MAPPING_ALBUMS_FILE))
mapping_group.add_argument('-ml', '--mapping-album', type=argparse.FileType('r'), const=MAPPING_ALBUMS_FILE, nargs='?',
help='Mapping file for albums (default: \'{}\')'.format(MAPPING_ALBUMS_FILE))
mapping_group.add_argument('-mg', '--mapping-suggest', type=argparse.FileType('r'), const=MAPPING_SUGGEST_FILE, nargs='?',
help='Mapping file for suggest (default: \'{}\')'.format(MAPPING_SUGGEST_FILE))
# Global Settings # Global Settings
g_settings_group = parser.add_argument_group('Global Settings') g_settings_group = parser.add_argument_group('Global Settings')
g_settings_group.add_argument('-els', '--elasticsearch-url', default=ELASTICSEARCH_URL, nargs='?', g_settings_group.add_argument('-els', '--elasticsearch-url', default=ELASTICSEARCH_URL, nargs='?',
help="Elasticsearch URL.") help="Elasticsearch URL (default: \'{}\')".format(ELASTICSEARCH_URL))
return parser return parser
@@ -283,7 +253,7 @@ def delete_index(index_name, quiet=False):
res = requests.delete(url=ELASTICSEARCH_URL + index_name) res = requests.delete(url=ELASTICSEARCH_URL + index_name)
if res.status_code == 200: if res.status_code == 200:
if not quiet: if not quiet:
print(bcolors.OKGREEN + "Index '{}' deleted!".format(index_name) + bcolors.ENDC) print(bcolors.OKGREEN + "Index deleted!" + bcolors.ENDC)
else: else:
print(bcolors.FAIL + "An error occured" + bcolors.ENDC) print(bcolors.FAIL + "An error occured" + bcolors.ENDC)
if res.json()['error']['type'] == 'index_not_found_exception': if res.json()['error']['type'] == 'index_not_found_exception':
@@ -306,7 +276,7 @@ def put_mapping(index_name, mapping_file, quiet=False):
print(res.text + bcolors.ENDC) print(res.text + bcolors.ENDC)
else: else:
if not quiet: if not quiet:
print(bcolors.OKGREEN + "Mapping for '{}' sent".format(index_name) + bcolors.ENDC) print(bcolors.OKGREEN + "Mapping sent" + bcolors.ENDC)
put_setting(index_name, 0, quiet) put_setting(index_name, 0, quiet)

View File

@@ -1,7 +1,51 @@
DELETE itunes-suggest DELETE itunes-suggest
PUT /itunes-suggest PUT /itunes-suggest
!./mapping.suggest.json {
"settings": {
"analysis": {
"filter": {
"french_stop": {
"type": "stop",
"stopwords": "_french_"
},
"english_stop": {
"type": "stop",
"stopwords": "_english_"
}
},
"analyzer": {
"names": {
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding",
"french_stop",
"english_stop"
]
}
}
}
},
"mappings": {
"properties": {
"artist_suggest": {
"type": "completion",
"search_analyzer": "names"
},
"artist": {
"type": "keyword"
},
"album_suggest": {
"type": "completion",
"search_analyzer": "names"
},
"album": {
"type": "keyword"
}
}
}
}
// Also possible to specify analyze for ingesting => https://stackoverflow.com/questions/48304499/elasticsearch-completion-suggester-not-working-with-whitespace-analyzer // Also possible to specify analyze for ingesting => https://stackoverflow.com/questions/48304499/elasticsearch-completion-suggester-not-working-with-whitespace-analyzer
@@ -15,7 +59,7 @@ GET itunes-suggest/_analyze
GET itunes-suggest/_search GET itunes-suggest/_search
GET itunes-suggest/_search POST itunes-suggest/_search
{ {
"_source" : "artist", "_source" : "artist",
"suggest": { "suggest": {
@@ -28,7 +72,7 @@ GET itunes-suggest/_search
} }
} }
GET itunes-suggest/_search POST itunes-suggest/_search
{ {
"_source" : "album", "_source" : "album",
"suggest": { "suggest": {
@@ -42,7 +86,7 @@ GET itunes-suggest/_search
} }
} }
GET itunes-suggest/_search POST itunes-suggest/_search
{ {
"_source": ["album", "artist"], "_source": ["album", "artist"],
"suggest": { "suggest": {
@@ -61,7 +105,7 @@ GET itunes-suggest/_search
} }
} }
GET itunes-suggest/_search POST itunes-suggest/_search
{ {
"_source": ["album", "artist"], "_source": ["album", "artist"],
"suggest": { "suggest": {
@@ -80,7 +124,7 @@ GET itunes-suggest/_search
} }
} }
GET itunes-suggest/_search POST itunes-suggest/_search
{ {
"suggest": { "suggest": {
"ar-suggest": { "ar-suggest": {

View File

@@ -1,77 +1,37 @@
"""
Process files generated by iTunesParser to fill a suggester index.
Suggester index in ELS must be created before use.
Found suggester.es query to create index.
"""
import sys
import json
import requests import requests
import json
import sys
ELS_URL = 'http://localhost:9200' ELS_URL ='http://localhost:9200'
INDEX = 'itunes-suggest' INDEX = 'itunes-suggest'
class NoGoodDataException(Exception): class NoGoodDataException(Exception):
""" Raise when data can't be correctly analyzed """ def __init__(self, message):
super().__init__(message)
def get_tokens(data: str) -> list: def get_tokens(data: str) -> list:
"""
Query Elasticsearch to get token for a string with a specific analyzer.
Throw an exception if no token found in ELS response.
Parameters
----------
data: string
String to be analysed to obtain the tokens
Returns
-------
list
A list of token
Raises
------
NoGoodDataException
If no tokens are found in the ELS responses, consider that the data is not correct for analysis.
"""
if not data: if not data:
return [] return []
query = { query = {
"analyzer": "names", # TODO Parameterize analyzer ? "analyzer": "names",
"text" : data "text" : data
} }
url = '{}/{}/_analyze'.format(ELS_URL, INDEX) url = '{}/{}/_analyze'.format(ELS_URL, INDEX)
req = requests.get(url, json=query) r = requests.get(url, json=query)
if not 'tokens' in req.json(): if not 'tokens' in r.json():
print('ERROR: Not tokens in result') print('ERROR: Not tokens in result')
print('Input: ' + str(data)) print('Input: ' + str(data))
print('Request: ' + str(req.json())) print('Request: ' + str(r.json()))
raise NoGoodDataException('Data is not correct to get tokens') raise NoGoodDataException('Data is not correct to get tokens')
return [t['token'] for t in req.json()['tokens']] return [t['token'] for t in r.json()['tokens']]
def post_document(main_field_value: str, input_terms: list, main_field_name: str) -> str: def post_document(name: str, input: list, field_name: str) -> bool:
""" suggest_name = field_name + '_suggest'
Create suggestion document in Elasticsearch.
Parameters
----------
main_field_value : str
Value to put in the main field named by `main_field_name`
input_terms : list
List of suggestion term to put in document
main_field_name : str
Name of the main field, to fill with `main_field_value`
Returns
-------
str
Success: ID of created document
Fail (ret. status <> 201): None
"""
suggest_name = main_field_name + '_suggest'
element = { element = {
main_field_name: main_field_value, field_name: name,
suggest_name: input_terms suggest_name: input
} }
# Filter empty keys # Filter empty keys
@@ -83,26 +43,13 @@ def post_document(main_field_value: str, input_terms: list, main_field_name: str
print('ELS Response KO') print('ELS Response KO')
print(resp.status_code) print(resp.status_code)
print(resp.text) print(resp.text)
return None return
el_id = resp.json()['_id'] el_id = resp.json()['_id']
# print('Post_element - Element created: ' + el_id) # print('Post_element - Element created: ' + el_id)
return el_id return el_id
def process_file(file_name: str, field_name: str, array_file: str = None) -> int: def process_file(file_name: str, field_name: str) -> int:
"""
Process a JSON file with data
Parameters
----------
file_name: string
Path and name of file to analyze
field_name: string
Name of the field where to find the data to create the suggestion entries
array_file: string, Default: None
Name of an array field to analyze to create more suggestion entries.
Nothing if None
"""
print('Process file: ' + file_name) print('Process file: ' + file_name)
with open(file_name, 'r') as o_file: with open(file_name, 'r') as o_file:
lines = o_file.readlines() lines = o_file.readlines()
@@ -115,28 +62,22 @@ def process_file(file_name: str, field_name: str, array_file: str = None) -> int
sys.stdout.flush() sys.stdout.flush()
sys.stdout.write("\b" * (40+1)) # return to start of line, after '[' sys.stdout.write("\b" * (40+1)) # return to start of line, after '['
data = json.loads(line) data = json.loads(line)
if not "index" in data: # Exclude index line if "Artist" in data:
try: try :
suggests_entries = get_tokens(data[field_name]) input = get_tokens(data[field_name])
post_document(name=data[field_name], input=input, field_name=field_name.lower())
if array_file and array_file in data and data[array_file]:
for key in data[array_file]:
suggests_entries.extend(get_tokens(key))
# TODO Input have the same value several times ==> use to process a score
post_document(main_field_value=data[field_name], input_terms=suggests_entries, main_field_name=field_name.lower())
count += 1 count += 1
except NoGoodDataException: except NoGoodDataException:
print('ERROR WITH DATA') print('ERROR WITH DATA')
print(str(data)) print(str(data))
print('File processed\n') print('File processed\n')
return count return count
if __name__ == '__main__': if __name__ == '__main__':
created_docs = 0 # Using readlines()
created_docs += process_file('/home/budd/workspace/iTunes/es-albums.json', 'Album') count = 0
print('Created documents: ' + str(created_docs)) count += process_file('/home/budd/workspace/iTunes/es-albums.json', 'Album')
created_docs += process_file('/home/budd/workspace/iTunes/es-artists.json', 'Artist', 'Album Artist') count += process_file('/home/budd/workspace/iTunes/es-artists.json', 'Artist')
print('Created documents: ' + str(created_docs)) print('Created documents: ' + str(count))
# TODO Created doc <> nb doc in ELS