81 Commits

Author SHA1 Message Date
f37ce410eb (front) Improve search result stype
Template to show results with glyphicon

(cherry picked from commit 550d8e7b343dc262be87182ae13f231ccc995b55)

Improve style
Stealing CSS from the demo site

(cherry picked from commit 75bebac0ac6080a0210d2b156a14f8a0f70067db)
2021-08-22 17:34:56 +02:00
124a30ad8c (front) Suggester with ng-bootstrap
Really "complex" search method, but yea, it's work :s

(cherry picked from commit 0789a92b0a9153b6112ad39b1f61090bdbeab197)
2021-08-22 17:33:33 +02:00
52a0cde401 (front) Autocomplete search on dashboard with Datalist
Create Elasticsearch service to search suggestions

Send to page when choose a suggestion - bad way
It's impossible to use Object with datalist
So it's impossible to distinguish an album from an artist if they have
the same name
2021-08-22 17:04:29 +02:00
5fbcfbae68 (front) Update package-lock 2021-08-22 17:04:25 +02:00
74c6f99fa5 (back) Suggester: Change ELS search analyzer for specific char
Implementing search in dashboard revealed problems when search with specific char (francois perusse)
Note: it's also possible to specify an analyzer for ingestion

(cherry picked from commit 24da127750c995c55d54876a1be8a4ad3bf9ce9c)
2021-08-22 17:02:14 +02:00
60e1fb2e74 (back) Suggester: Improve processor - more generic
Process album & artist with a calculated fields
Separate main
Show progress

(cherry picked from commit fc8407cc6a51fe18b14169b3a3f0e4fc363beb4f)
2021-08-22 17:01:19 +02:00
520d0be595 (back) Suggester V3: Process album in a separate way
(cherry picked from commit ebbeeccfb8535dbb67240d2c68c7dc9a4da7e7f8)
2021-08-22 17:01:04 +02:00
8121f3d751 (back) Suggester V2: Process album data
(cherry picked from commit dd322405d047d49e51d528341cbd008d7a98b6ab)
2021-08-22 17:01:00 +02:00
436edaf3f2 (back) Suggester V1: Process artist data
(cherry picked from commit 02f5705fde37e1aaef5c68de62aafe45fc86d490)
2021-08-22 17:00:01 +02:00
797c88f946 (mapping) Tokenize location fields 2021-02-12 20:16:25 +01:00
1f427749cf (front) Albums: Improve query 2021-02-12 20:16:06 +01:00
153ca835cb (front) Albums: reset query button 2021-01-24 00:14:00 +01:00
8e79bee1c4 (front) Albums: changing layout with templating
Still not satisfied with this
2021-01-19 19:20:38 +01:00
66dc29be92 (front) Albums: Add select button
Improve edit ELS Query method
2021-01-19 19:18:23 +01:00
fb620f582f (front) Albums: link to artist/albums + Play count
Improve ngIf part
Use a script in ELS query to sort on avg play count
2021-01-19 18:47:24 +01:00
57e1c18a7f (front) Improve show rating stars
A big simplification of the display of rating
2020-12-23 00:39:13 +01:00
050270e890 (scripts) Improve retrieve rating for albums 2020-12-23 00:01:30 +01:00
541fd96a45 (front) Improve presentation
Bit rate presentation
Improve page + if for artist/album artist
2020-12-22 19:28:29 +01:00
93272d5894 (front) Albums: exlude some results and reload
Create a new service for albums to split big ElsService
2020-12-22 19:28:28 +01:00
7e6cc36750 (front) Add "All albums" page
Add nav button + button on dashboard
2020-12-22 19:27:56 +01:00
285ab197e8 (scripts) Put Bit rate (min & avg) in albums
Calc avg Bit Rate for a album
Rename average calc function
2020-12-20 19:33:22 +01:00
c92fb73e91 (send_data) Improve script output 2020-12-20 18:39:04 +01:00
748e3b8c38 (front) Remove last added link 2020-12-20 14:36:07 +01:00
d2cca5107b Update Angular Version
Use a new Angular application and move files to achieve this!
2020-12-20 14:12:11 +01:00
9b522866ea Why this files? 2020-10-17 18:57:48 +02:00
6e74f6aea5 (help) Help file: configure ELS for CORS + remote work 2020-04-12 19:28:21 +02:00
95afad4003 (front) Show percent of never played songs 2020-04-12 19:07:46 +02:00
7a2706f8fd (front) Fix pipe to convert ms to day/hour/min/sec 2020-04-12 19:04:29 +02:00
fac3629d14 (front) Fix ELS services for multiple indexes
Remove or comment unused methods
Better handle of errors
2020-04-12 19:04:29 +02:00
0c8a17febe (scripts) Adapat generation & sending scripts 2020-04-12 19:04:08 +02:00
4e5de730c5 (mapping) Use three mappings file instead one unique 2020-04-12 19:03:50 +02:00
9ca7888189 Clean & ignore working files 2020-04-11 01:47:01 +02:00
1d5a51f79a (send_data) Don't open an already open file 2020-04-10 23:41:43 +02:00
1430566c6c (send_data) Refactor script
Change structure to have a clean Main function
Simplify arguments and operation
2020-04-10 23:29:56 +02:00
fb3af9507b (send_data) Script set settings: replicas at 0 2020-04-10 23:29:56 +02:00
fc35397883 (send_data) POC: ensure all documents are in ELS
Create a script to check ID's to help find problems
2020-04-10 23:29:55 +02:00
1728b2a922 (mapping) Update mapping to ELS 7 2020-04-10 23:27:23 +02:00
3d13e19b0b (send_data) Fix data file names 2020-04-10 23:22:11 +02:00
491c765d7e package lock 2018-06-02 18:27:53 +02:00
9b4efcbfb5 npm fix 2 2018-06-02 18:10:33 +02:00
a5559d9113 npm fix 1 2018-06-02 18:08:09 +02:00
5aa9e72ab0 Update to Angular 6
Fix little bug on last added query
2018-06-02 15:49:20 +02:00
f8662894b3 Update to HttpClient 2018-06-02 15:22:19 +02:00
330adb4b9f Improve top played part
Sort by avg. play + Slice
Add round pipe
Comment ELS Service
2018-03-21 00:50:06 +01:00
6300e9cfc7 Refactoring
Move Top Played part.
Move pipes and dashboard
Rename object folder to model
Doc pipes
2018-03-20 23:32:21 +01:00
efac03a1c7 Experiment other ways to get most played artist 2018-03-20 22:33:05 +01:00
92d9281c35 Update Angular version 2018-03-20 21:50:50 +01:00
594328525e Sort by Disc Number + improve sort-by pipe algo 2018-03-20 00:47:27 +01:00
3867d9513f Add a better way for most listen album 2018-02-10 02:52:34 +01:00
b307b88ef1 Naive approche to get most played album 2018-02-10 01:22:02 +01:00
6e323c39c7 Fix div for last played song
Little commit - to squash?
2018-02-10 00:16:50 +01:00
f0fce02e06 WIP Last added album 2018-02-10 00:06:42 +01:00
b362ddf8c0 WIP Last added more precise
Translate correclty this commit message
2018-01-14 05:20:50 +01:00
66a1cd12e0 Improve debug for artist 2018-01-14 03:32:26 +01:00
9e81d42cb3 Text for icons 2018-01-14 01:09:17 +01:00
39d7f5f556 Convert size with a pipe 2017-10-29 02:57:35 +01:00
60877540f7 Finish to refactor 2017-10-29 02:33:02 +01:00
3876a45c28 Continue to refactor service 2017-10-29 02:21:56 +01:00
cd21d3bdb1 TOSQUASH Continue refactoring with function 2017-10-29 02:02:22 +01:00
8fd9466fb1 TOSQUASH Use a function to process response 2017-10-29 02:47:49 +02:00
ab493346a5 WIP Simplify ELS Service
Next step: use function
2017-10-29 02:39:59 +02:00
4cb5dc44e3 Fix load data pbl 2017-10-29 02:27:38 +02:00
6611667a49 TOSQASH Extract Table 2017-10-29 01:51:04 +02:00
597d9b64ed Fix sortable problem 2017-10-19 00:54:06 +02:00
b420f32838 Extract table
Problem on sortable
2017-10-19 00:31:51 +02:00
ae76f916ae Improve Bit Rate translate processing 2017-10-18 23:00:46 +02:00
b772584c56 Refactor CSS 2017-10-18 01:05:55 +02:00
fcceb69aa4 Use Signal icons for Bit Rate 2017-10-18 00:51:21 +02:00
8b59396e2c Different try to import SVG 2017-10-18 00:30:26 +02:00
3c8654ceaa Signal SVG+PNG Icons 2017-10-18 00:30:10 +02:00
8024d44061 gitgnore - why not commited before this moment??? 2017-10-17 01:56:04 +02:00
cf48487a03 TOSQUASH I think I forgot this files previously... 2017-10-17 01:55:39 +02:00
b88a873635 ELS mapping! 2017-10-17 01:54:49 +02:00
7a1a1c7c17 Last added: Refactor identification of good album 2017-10-17 01:54:22 +02:00
e0f17c24b8 FIX Bad var affect. artist name 2017-10-17 01:47:57 +02:00
e4cf58528a Improve icon on table (heart+stars) 2017-10-17 00:50:58 +02:00
20ca082d9d Add rating + love for artist view
NOTE: Need to refactor to have same layout for album/artist
2017-10-17 00:24:34 +02:00
ab792b6e63 Add Album Artist info for last added songs 2017-10-17 00:23:57 +02:00
cb934b20b7 Fix stats card size for small screen 2017-10-17 00:22:52 +02:00
778fb2d701 Convert Ms unit + fun with approximative pipe 2017-10-14 04:01:54 +02:00
6034b79749 Update angular to 4.4.4 2017-10-14 04:00:46 +02:00
130 changed files with 33270 additions and 26777 deletions

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
node_modules/
.vscode/
/es-albums.json
/es-artists.json
/es-songs.json
/iTunesLibrary.xml
# Working files
*.txt
*.code-workspace
# Snippets for tests
snippet.py
snippet.js
dashboard/resultToStudy.json
sand_box.py
rating_test.py
iTunesGraphParser.my.py

26
check_id.py Normal file
View File

@@ -0,0 +1,26 @@
import json
files = ['es-songs.json', 'es-artists.json', 'es-albums.json']
ids = []
bad_lines = {}
for file in files:
with open(file) as fp:
line = fp.readline()
while line:
content = json.loads(line)
if 'index' in content:
id = content['index']['_id']
if id in ids:
bad_lines[id] = content
else:
ids.append(id)
line = fp.readline()
if not bad_lines:
print("No duplicate ID's found, everything's fine!!")
else:
print('KO')
print(bad_lines)

View File

@@ -1,63 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "dashboard"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": [
"../node_modules/angular4-word-cloud/d3.min.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}

27
dashboard/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Dashboard
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.0.4.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

126
dashboard/angular.json Normal file
View File

@@ -0,0 +1,126 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"dashboard": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/dashboard",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"aot": true,
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "dashboard:build"
},
"configurations": {
"production": {
"browserTarget": "dashboard:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "dashboard:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "dashboard:serve"
},
"configurations": {
"production": {
"devServerTarget": "dashboard:serve:production"
}
}
}
}
}
},
"defaultProject": "dashboard"
}

View File

@@ -1,14 +0,0 @@
import { AppPage } from './app.po';
describe('dashboard App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});

View File

@@ -1,11 +0,0 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

View File

@@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', async () => {
await page.navigateTo();
expect(await page.getTitleText()).toEqual('dashboard app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
async navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl);
}
async getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText();
}
}

View File

@@ -1,13 +1,12 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}

View File

@@ -4,23 +4,33 @@
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
angularCli: {
environment: 'dev'
coverageReporter: {
dir: require('path').join(__dirname, './coverage/dashboard'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
@@ -28,6 +38,7 @@ module.exports = function (config) {
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
singleRun: false,
restartOnFileChange: true
});
};

30317
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{
"name": "dashboard",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -12,42 +11,38 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^4.2.4",
"@angular/common": "^4.2.4",
"@angular/compiler": "^4.2.4",
"@angular/core": "^4.2.4",
"@angular/forms": "^4.2.4",
"@angular/http": "^4.2.4",
"@angular/platform-browser": "^4.2.4",
"@angular/platform-browser-dynamic": "^4.2.4",
"@angular/router": "^4.2.4",
"angular-tag-cloud-module": "^1.4.0",
"angular4-word-cloud": "^0.1.2",
"bootstrap": "3.3.7",
"core-js": "^2.4.1",
"d3": "^4.11.0",
"rxjs": "^5.4.2",
"zone.js": "^0.8.14"
"@angular/animations": "~11.0.4",
"@angular/common": "~11.0.4",
"@angular/compiler": "~11.0.4",
"@angular/core": "~11.0.4",
"@angular/forms": "~11.0.4",
"@angular/platform-browser": "~11.0.4",
"@angular/platform-browser-dynamic": "~11.0.4",
"@angular/router": "~11.0.4",
"@ng-bootstrap/ng-bootstrap": "^9.1.3",
"bootstrap": "^3.3.7",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular/cli": "1.3.2",
"@angular/compiler-cli": "^4.2.4",
"@angular/language-service": "^4.2.4",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "~3.1.1",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.2.0",
"tslint": "~5.3.2",
"typescript": "~2.3.3"
"@angular-devkit/build-angular": "~0.1100.4",
"@angular/cli": "~11.0.4",
"@angular/compiler-cli": "~11.0.4",
"@angular/localize": "^11.0.4",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.1.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.0.2"
}
}

View File

@@ -1,28 +0,0 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@@ -1,37 +0,0 @@
/* Thank to https://codyhouse.co/gem/back-to-top/ */
.btn-top {
display: inline-block;
height: 40px;
width: 40px;
position: fixed;
bottom: 20px;
right: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
/* image replacement properties */
/*overflow: hidden;*/
/*text-indent: 100%;*/
/*white-space: nowrap;*/
/*background: rgba(232, 98, 86, 0.8);*/
/*background: #ff9000;*/
visibility: hidden;
opacity: 0;
-webkit-transition: opacity .3s 0s, visibility 0s .3s;
-moz-transition: opacity .3s 0s, visibility 0s .3s;
transition: opacity .3s 0s, visibility 0s .3s;
}
.btn-top.btn-top-is-visible, .no-touch .btn-top:hover {
-webkit-transition: opacity .3s 0s, visibility 0s 0s;
-moz-transition: opacity .3s 0s, visibility 0s 0s;
transition: opacity .3s 0s, visibility 0s 0s;
}
.btn-top.btn-top-is-visible {
/* the button becomes visible */
visibility: visible;
opacity: 1;
}
.btn-sort {
bottom: 20px;
right: 70px;
}

View File

@@ -10,7 +10,7 @@
</div>
<div class="row cardAdmin">
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
@@ -28,7 +28,7 @@
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
@@ -46,7 +46,7 @@
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-purple">
<div class="panel-heading">
<div class="row">
@@ -64,7 +64,7 @@
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-blue">
<div class="panel-heading">
<div class="row">
@@ -84,38 +84,9 @@
</div>
</div>
<table class="table table-striped" (window:scroll)="onScroll($event)">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Artist</th>
<th>Album</th>
<th>Album Artist</th>
<th>Play Count</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let song of songs | sortBy : 'Track Number'">
<td>{{song['Track Number'] ? (("0" + song['Track Number']).slice(-2)) : "--"}}</td>
<td>{{song.Name}}</td>
<td><a [routerLink]="['/artist', song.Artist]">{{song.Artist}}</a></td>
<td><a [routerLink]="['/album', song.Album]">{{song.Album}}</a></td>
<td>{{song['Album Artist']}}</td>
<td>{{song['Play Count']}}</td>
<td><a [routerLink]="['/genre', song.Genre]">{{song.Genre}}</a></td>
</tr>
</tbody>
</table>
<app-song-table [songs]=songs (atBottom)=loadSongs()></app-song-table>
<button type="button" *ngIf="moreDataAvailable" class="btn btn-default" aria-label="More" (click)="loadSongs()">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> More...
</button>
<!-- Go to Top Button-->
<button type="button" class="btn btn-warning btn-top" [class.btn-top-is-visible]="atBottom"
aria-label="Top" (click)="scrollTop()">
<span class="glyphicon glyphicon-menu-up" aria-hidden="true"></span>
</button>
</div>

View File

@@ -1,23 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { ElsService } from './../els.service';
import { Song } from './../object/song';
import { Album } from './../object/album';
import { Song } from './../model/song';
import { Album } from './../model/album';
import { SongTableComponent } from '../song-table/song-table.component';
@Component({
selector: 'app-album',
templateUrl: './album.component.html',
styleUrls: [ './album.component.css', './../dashboard.component.css' ]
styleUrls: [ './album.component.css', './../dashboard/dashboard.component.css' ]
})
export class AlbumComponent implements OnInit {
@ViewChild(SongTableComponent) private songtable: SongTableComponent;
albumName = '';
songs: Array<Song> = [];
album: Album = new Album(); // If album not found, will be replaced by 'undefined'
moreDataAvailable = false;
atBottom = false;
// Prevent useless data load + activate button in interface var
moreDataAvailable = true;
lockLoadData = false;
constructor(
private elsService: ElsService,
@@ -35,6 +40,16 @@ export class AlbumComponent implements OnInit {
}
loadSongs(): void {
if (this.lockLoadData) {
console.log('Loading data locked');
return;
}
if (!this.moreDataAvailable) {
return;
}
this.lockLoadData = true;
this.elsService.getAlbumSongs(this.albumName, this.songs.length).subscribe(
data => {
this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE;
@@ -44,37 +59,14 @@ export class AlbumComponent implements OnInit {
if (this.songs.length === 0) {
this.songs = data;
} else {
this.songtable.setSortable(true);
data.forEach(song => {
this.songs.push(song);
});
}
console.log('Unlock load data');
this.lockLoadData = false;
}
);
}
scrollTop(): void {
window.scrollTo(0, 0);
}
/**
* Handle scroll:
* - load data if at bottom of screen (if needed)
* - hide/show "go to top" button
*
* @param event scroll event
*/
onScroll(event: any) {
if (this.moreDataAvailable &&
(window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.loadSongs();
}
if (window.scrollY > window.innerHeight) {
this.atBottom = true;
}
if (window.scrollY === 0) {
this.atBottom = false;
}
}
}

View File

@@ -0,0 +1,34 @@
/*** BOTTOM BUTTON PART ***/
/* Thank to https://codyhouse.co/gem/back-to-top/ */
.btn-top {
display: inline-block;
height: 40px;
width: 40px;
position: fixed;
bottom: 20px;
right: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
/* image replacement properties */
/*overflow: hidden;*/
/*text-indent: 100%;*/
/*white-space: nowrap;*/
/*background: rgba(232, 98, 86, 0.8);*/
/*background: #ff9000;*/
visibility: hidden;
opacity: 0;
-webkit-transition: opacity .3s 0s, visibility 0s .3s;
-moz-transition: opacity .3s 0s, visibility 0s .3s;
transition: opacity .3s 0s, visibility 0s .3s;
}
.btn-top.btn-top-is-visible, .no-touch .btn-top:hover {
-webkit-transition: opacity .3s 0s, visibility 0s 0s;
-moz-transition: opacity .3s 0s, visibility 0s 0s;
transition: opacity .3s 0s, visibility 0s 0s;
}
.btn-top.btn-top-is-visible {
/* the button becomes visible */
visibility: visible;
opacity: 1;
}
/*** END BOTTOM BUTTON PART ***/

View File

@@ -0,0 +1,74 @@
<div class="container">
<h1>Albums - {{this.albums.length}}</h1>
<div class="col-md-12">
<table class="table table-striped" style="white-space: nowrap;">
<thead>
<tr>
<th>Album</th>
<th>Track Count</th>
<th></th>
<th></th>
<th>Artist/Album Artist</th>
<th>Avg Bit Rate (min)</th>
<th>Play Count</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let album of albums">
<td>
<a [routerLink]="['/album', album.Album]">{{album.Album}}</a>&nbsp;
<span class="glyphicon glyphicon-remove-circle" style="cursor:pointer;color:red;" (click)="exlude('Album', album)"></span>
</td>
<td>{{album['Track Count']}}</td>
<ng-template [ngIf]="album['Album Artist']" [ngIfElse]="artistSection">
<td>
<span class="glyphicon glyphicon-ban-circle" style="cursor:pointer;color:red;" (click)="exlude('Album Artist', album)"></span>
</td>
<td>
<span class="glyphicon glyphicon-zoom-in" style="cursor:pointer;" (click)="select('Album Artist', album)"></span>
</td>
<td>
<a [routerLink]="['/artist', album['Album Artist']]">{{album['Album Artist']}}</a>&nbsp;
</td>
</ng-template>
<ng-template #artistSection>
<td>
<span class="glyphicon glyphicon-ban-circle" style="cursor:pointer;color:red;" (click)="exlude('Artist', album)"></span>
</td>
<td>
<span class="glyphicon glyphicon-zoom-in" style="cursor:pointer;" (click)="select('Artist', album)"></span>
</td>
<td>
<a [routerLink]="['/artist', album.Artist[0]]">{{album.Artist}}</a>&nbsp;
</td>
</ng-template>
<td>
{{album['Avg Bit Rate']}}
<span *ngIf="album['Avg Bit Rate'] != album['Min Bit Rate']">({{album['Min Bit Rate']}})</span>
</td>
<td>
{{album['Play Count']}} ({{album['Play Count']/album['Track Count'] | number:'1.0-0'}}/songs)
</td>
<td class="star" [title]="(album['Album Rating Computed']?'Computed Rating: ':'Rating: ') + album['Album Rating']">
<span *ngFor="let item of numberToArray(album['Album Rating'], 20)">
<span class="glyphicon" [ngClass]="album['Album Rating Computed']?'glyphicon-star-empty':'glyphicon-star'"></span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-danger btn-top" [class.btn-top-is-visible]="queryEdited"
aria-label="Reset filters" (click)="resetQuery()" title="Reset filters and reload datas">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
</button>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlbumsComponent } from './albums.component';
describe('AlbumsComponent', () => {
let component: AlbumsComponent;
let fixture: ComponentFixture<AlbumsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AlbumsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AlbumsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,71 @@
import { Component, OnInit } from '@angular/core';
import { ElsAlbumService } from '../els-album.service';
import { Album } from '../model/album';
import { Utils } from '../utils';
enum query_edit_type {
exclude = 'must_not',
select = 'must'
}
@Component({
selector: 'app-albums',
templateUrl: './albums.component.html',
styleUrls: ['./albums.component.css']
})
export class AlbumsComponent implements OnInit {
numberToArray = Utils.numberToArray;
albums: Album[] = [];
filterQuery = Object.assign({}, ElsAlbumService.GET_ALBUMS_DEFAULT_QUERY);
queryEdited = false;
constructor(private elsService : ElsAlbumService) { }
ngOnInit(): void {
this.loadData();
}
private editQuery(field: string, value: string, type: query_edit_type): void {
// TODO Move this method to a service
if (value[field] instanceof Array) {
value[field] = value[field][0]
}
// If firt edit, add needed fields in ELS Query
if (!this.filterQuery['query']) {
this.filterQuery['query']['bool'][type].push({ 'must': [] })
this.filterQuery['query']['bool'][type].push({ 'must_not': [] })
}
this.filterQuery['query']['bool'][type].push({
'match_phrase': {
[field]: value[field]
}
})
this.queryEdited = true;
}
exlude(field: string, value: string): void {
this.editQuery(field, value, query_edit_type.exclude)
this.loadData()
}
select(field: string, value: string): void {
this.editQuery(field, value, query_edit_type.select)
this.loadData()
}
resetQuery(): void {
this.filterQuery = Object.assign({}, ElsAlbumService.GET_ALBUMS_DEFAULT_QUERY);
this.loadData();
}
loadData(): void {
// console.log(JSON.stringify(this.filterQuery))
this.elsService.getAlbums(this.filterQuery).subscribe(data => this.albums = data);
}
}

View File

@@ -1,17 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { AlbumComponent } from './album/album.component';
import { ArtistComponent } from './artist/artist.component';
import { GenreComponent } from './genre/genre.component';
import { TopPlayedComponent } from './top-played/top-played.component';
import { AlbumsComponent } from './albums/albums.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'album/:name', component: AlbumComponent },
{ path: 'album', component: AlbumsComponent },
{ path: 'artist/:name', component: ArtistComponent },
{ path: 'genre/:name', component: GenreComponent }
{ path: 'genre/:name', component: GenreComponent },
{ path: 'top-played', component: TopPlayedComponent }
];
@NgModule({

View File

@@ -1,4 +1,5 @@
<nav>
<a class="btn-top" routerLink="/dashboard" routerLinkActive="active"><span class="glyphicon glyphicon-home"></span></a>
<a class="btn-top" routerLink="/dashboard" routerLinkActive="active"><span class="glyphicon glyphicon-home"></span></a><br />
<a class="btn-top" routerLink="/album" routerLinkActive="active"><span class="glyphicon glyphicon-cd"></span></a>
</nav>
<router-outlet></router-outlet>

View File

@@ -1,42 +1,56 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { TagCloudModule } from 'angular-tag-cloud-module';
import { AgWordCloudModule } from 'angular4-word-cloud';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { AlbumComponent } from './album/album.component';
import { ArtistComponent } from './artist/artist.component';
import { GenreComponent } from './genre/genre.component';
import { SongTableComponent } from './song-table/song-table.component';
import { TopPlayedComponent } from './top-played/top-played.component';
import { ElsService } from './els.service';
import { ElsAlbumService } from './els-album.service';
import { AppRoutingModule } from './app-routing.module';
import { ConvertMsPipe } from './convertms.pipe';
import { SortByPipe } from './sort-by.pipe';
import { ConvertMsPipe } from './pipes/convertms.pipe';
import { ConvertMoreExactPipe } from './pipes/convert-more-exact.pipe';
import { SortByPipe } from './pipes/sort-by.pipe';
import { ConvertSizeToStringPipe } from './pipes/convert-size-to-string.pipe';
import { RoundPipe } from './pipes/round.pipe';
import { AlbumsComponent } from './albums/albums.component';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
@NgModule({
imports: [
BrowserModule,
HttpModule,
TagCloudModule,
AgWordCloudModule.forRoot(),
AppRoutingModule
HttpClientModule,
FormsModule,
AppRoutingModule,
NgbModule
],
declarations: [
AppComponent,
DashboardComponent,
AlbumComponent,
AlbumsComponent,
ArtistComponent,
GenreComponent,
SongTableComponent,
ConvertMsPipe,
SortByPipe
ConvertMoreExactPipe,
SortByPipe,
ConvertSizeToStringPipe,
TopPlayedComponent,
RoundPipe
],
providers: [
ElsService
ElsService,
ElsAlbumService
],
bootstrap: [ AppComponent ]
})

View File

@@ -3,14 +3,18 @@
<div class="alert alert-warning">
<h3>Debug Zone</h3>
Returned song: {{songs.length}}<br />
Theorical song: {{ artist ? artist['Track Count'] : "" }}
Returned song:&emsp;{{songs.length}}<br />
Theorical song:&emsp;{{ artist ? artist['Track Count'] : "" }}
<span *ngIf="artist && (songs.length == artist['Track Count'])" class="glyphicon glyphicon-ok" style="color:green"></span>
<span *ngIf="artist && (songs.length != artist['Track Count'])" class="glyphicon glyphicon-remove" style="color:red"></span>
<br />
Artist song:&emsp;&emsp;&emsp;{{countSong}}
<span *ngIf="artist && (songs.length == countSong)" class="glyphicon glyphicon-ok" style="color:green"></span>
<span *ngIf="artist && (songs.length != countSong)" class="glyphicon glyphicon-remove" style="color:red"></span>
</div>
<div class="row cardAdmin">
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
@@ -28,7 +32,7 @@
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-blue">
<div class="panel-heading">
<div class="row">
@@ -46,7 +50,7 @@
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
@@ -64,7 +68,7 @@
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-purple">
<div class="panel-heading">
<div class="row">
@@ -85,44 +89,9 @@
</div>
</div>
<table class="table table-striped" (window:scroll)="onScroll($event)">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Album</th>
<th>Year</th>
<th>Artist</th>
<th>Album Artist</th>
<th>Play Count</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let song of songs | sortBy : 'Year':'Album':'Track Number'">
<td>{{song['Track Number'] ? (("0" + song['Track Number']).slice(-2)) : "--"}}</td>
<td>{{song.Name}}</td>
<td><a [routerLink]="['/album', song.Album]">{{song.Album}}</a></td>
<td>{{song.Year}}</td>
<td><a [routerLink]="['/artist', song.Artist]">{{song.Artist}}</a></td>
<td>{{song['Album Artist'] ? song['Album Artist'] : "-" }}</td>
<td>{{song['Play Count']}}</td>
<td><a [routerLink]="['/genre', song.Genre]">{{song.Genre}}</a></td>
</tr>
</tbody>
</table>
<app-song-table [songs]=songs (atBottom)=loadSongs()></app-song-table>
<button type="button" *ngIf="moreDataAvailable" class="btn btn-default" aria-label="More" (click)="loadSongs()">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> More...
</button>
<!-- Go to Top Button-->
<button type="button" class="btn btn-warning btn-top" [class.btn-top-is-visible]="atBottom"
aria-label="Top" (click)="scrollTop()" title="Go back to Top!">
<span class="glyphicon glyphicon-menu-up" aria-hidden="true"></span>
</button>
<button type="button" class="btn btn-primary btn-top btn-sort" [class.btn-top-is-visible]="sortable"
aria-label="Top" (click)="sort()" title="Sort by Album & Track Number">
<span class="glyphicon glyphicon-sort-by-attributes" aria-hidden="true"></span>
</button>
</div>

View File

@@ -1,29 +1,30 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
import { ElsService } from './../els.service';
import { Song } from './../object/song';
import { Artist } from './../object/artist';
import { SortByPipe } from './../sort-by.pipe';
import { Song } from './../model/song';
import { Artist } from './../model/artist';
import { SongTableComponent } from '../song-table/song-table.component';
@Component({
selector: 'app-artist',
templateUrl: './artist.component.html',
styleUrls: [ './../album/album.component.css', './../dashboard.component.css' ]
styleUrls: [ './../album/album.component.css', './../dashboard/dashboard.component.css', './artist.component.css' ]
})
export class ArtistComponent implements OnInit {
// Interacte with table to set sortable
@ViewChild(SongTableComponent) songtable: SongTableComponent;
// Prevent useless data load + activate button in interface var
moreDataAvailable = true;
artistName = '';
songs: Array<Song> = [];
artist: Artist = new Artist();
// To activate button in interface var
moreDataAvailable = false;
atBottom = false;
sortable = false;
lockLoadData = false;
countSong: number;
constructor(
private elsService: ElsService,
@@ -35,6 +36,9 @@ export class ArtistComponent implements OnInit {
this.route.params.subscribe((params: Params) => this.artistName = params['name']);
this.elsService.getArtist(this.artistName).subscribe(data => this.artist = data);
this.elsService.getCountArtistSong(this.artistName).subscribe(data => this.countSong = data);
this.loadSongs();
}
@@ -45,6 +49,10 @@ export class ArtistComponent implements OnInit {
return;
}
if (!this.moreDataAvailable) {
return;
}
this.lockLoadData = true;
this.elsService.getArtistSongs(this.artistName, this.songs.length).subscribe(
data => {
@@ -55,7 +63,7 @@ export class ArtistComponent implements OnInit {
if (this.songs.length === 0) {
this.songs = data;
} else {
this.sortable = true;
this.songtable.setSortable(true);
data.forEach(song => {
this.songs.push(song);
});
@@ -65,35 +73,4 @@ export class ArtistComponent implements OnInit {
}
);
}
scrollTop(): void {
window.scrollTo(0, 0);
}
/**
* Handle scroll:
* - load data if at bottom of screen (if needed)
* - hide/show "go to top" button
*
* @param event scroll event
*/
onScroll(event: any) {
if (this.moreDataAvailable &&
(window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.loadSongs();
}
if (window.scrollY > window.innerHeight) {
this.atBottom = true;
}
if (window.scrollY === 0) {
this.atBottom = false;
}
}
sort(): void {
this.songs = new SortByPipe().transform(this.songs, 'Year', 'Album', 'Track Number', 'Play Count');
this.sortable = false;
}
}

View File

@@ -1,8 +0,0 @@
import { ConvertmsPipe } from './convertms.pipe';
describe('ConvertmsPipe', () => {
it('create an instance', () => {
const pipe = new ConvertmsPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -1,34 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'convertMs'
})
export class ConvertMsPipe implements PipeTransform {
transform(timeMs: number): string {
let x = timeMs / 1000;
const seconds = Math.round(x % 60);
x /= 60;
let minutes = 0;
if (x > 1) { minutes = Math.round(x % 60); }
x /= 60;
let hours = 0;
if (x > 1) { hours = Math.round(x % 24); }
// TODO Enable/disable day
x /= 24;
const days = Math.round(x);
// Final string
let ret = '';
if (days > 0) { ret += ('0' + days).slice(-2) + ':'; }
if (hours > 0) { ret += ('0' + hours).slice(-2) + ':'; }
if (minutes > 0) { ret += ('0' + minutes).slice(-2) + ':'; }
if (seconds > 0) { ret += ('0' + seconds).slice(-2); }
return ret;
// return ('0' + days).slice(-2) + ':' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2) + ':' + ('0' + seconds).slice(-2);
}
}

View File

@@ -1,38 +0,0 @@
.panel-stat {
min-height: 102px;
}
.panel-green {
background-color: #16a085;
color:white;
}
.panel-yellow {
background-color: #f1c40f;
color:white;
}
.panel-red {
background-color: #d9534f;
color:white;
}
.panel-purple {
background-color: #9b59b6;
color:white;
}
.panel-blue {
background-color: #3498db;
color:white;
}
.stats_icon {
font-size: 3em;
}
.circle {
border-radius: 50%;
background-color: #9b59b6;
margin: 0 auto;
}

View File

@@ -1,148 +0,0 @@
import { Observable } from 'rxjs/Rx';
import { Component, OnInit, ViewChild, ViewChildren } from '@angular/core';
import { CloudData, CloudOptions } from 'angular-tag-cloud-module';
import { AgWordCloudData, AgWordCloudDirective } from 'angular4-word-cloud';
import { ElsService } from './els.service';
import { Song } from './object/song';
import { Bucket } from './object/bucket';
import { Album } from './object/album';
import { Artist } from './object/artist';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
totalTime = 0;
totalSize = 0;
totalSizeSt = '';
trackCountSong = 0;
trackCountArtist = 0;
trackCountAlbum = 0;
neverListenSong = 0;
albumArtistCount = 0;
topGenres: Bucket[] = [];
bottomGenres: Bucket[] = [];
mostPlayedSongs: Song[] = [];
lastAddedAlbums: Bucket[] = [];
albumArtists = [];
/**
* Test of angular-tag-cloud-module (https://www.npmjs.com/package/angular-tag-cloud-module)
*/
options: CloudOptions = {
// if width is between 0 and 1 it will be set to the size of the upper element multiplied by the value
width : 1,
height : 400,
overflow: false,
};
wordCloud: Array<CloudData> = [];
/**
* Test of angular4-word-cloud (https://www.npmjs.com/package/angular4-word-cloud)
*/
word_cloud: Array<AgWordCloudData> = [];
@ViewChild('word_cloud_chart') testTruc: AgWordCloudDirective;
constructor(private elsService: ElsService) { }
ngOnInit(): void {
this.elsService.getTime().then(result => {
this.totalTime = result;
});
this.elsService.getSize().then(result => {
this.totalSize = result;
this.totalSizeSt = this.convertSizeToString(result);
});
this.elsService.getCountSong('song')
.then(result => this.trackCountSong = result);
this.elsService.getCountSong('artist')
.then(result => this.trackCountArtist = result);
this.elsService.getCountSong('album')
.then(result => this.trackCountAlbum = result);
this.elsService.getCountNeverListenSong()
.then(result => this.neverListenSong = result);
this.elsService.getMostPlayedTrack().subscribe(
data => this.mostPlayedSongs = data
);
this.elsService.getGenres().subscribe(data => {
this.topGenres = data.slice(0, 10);
/** angular-tag-cloud-module **/
const changedData: Observable<Array<CloudData>> = Observable.of(
data.map(element => {
return {weight: element.doc_count, text: element.key, link: '/genre/' + element.key};
})
);
changedData.subscribe(res => this.wordCloud = res);
data.forEach((el) => {
this.word_cloud.push({size: el.doc_count, text: el.key});
});
this.testTruc.update();
});
this.elsService.getGenres('asc').subscribe(data => this.bottomGenres = data.slice(0, 10).reverse());
// this.elsService.getGenreCount().subscribe(data => console.log(data));
this.elsService.getLastAddedAlbums(6).subscribe(buckets => {
this.lastAddedAlbums = buckets;
buckets.forEach(bucket => this.getArtistName(bucket));
});
}
private getArtistName(albumBucket: Bucket) {
// For each bucket.key (album name), search artist.
// Use track count to compare
this.elsService.getArtistFromAlbumName(albumBucket.key).subscribe(albums => {
if (albums.length > 1) {
// More than one result for an album name
// Search good artist by track count
albums.forEach(album => {
if (album['Track Count'] === albumBucket.doc_count) {
this.albumArtists[album.Name] = album.Artist.toString();
}
});
} else {
// Just one artistresult for album name
this.albumArtists[albums[0].Name] = albums[0].Artist.toString();
}
if (this.albumArtists[albums[0].Name].length > 50) {
this.albumArtists[albums[0].Name] = this.albumArtists[albums[0].Name].substring(0, 50) + '...';
}
});
}
/**
* UTILS FUNCTION - TODO MOVE
*/
convertSizeToString(size: number) {
const units = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];
if (size === 0) {
return '0 Byte';
}
const i = Math.floor(Math.log(size) / Math.log(1024));
let calcSize = size / Math.pow(1024, i);
calcSize = Math.round(calcSize * 100) / 100;
return calcSize + ' ' + units[i];
}
}

View File

@@ -0,0 +1,77 @@
.panel-stat {
min-height: 102px;
}
.panel-green {
background-color: #16a085;
color:white;
}
.panel-yellow {
background-color: #f1c40f;
color:white;
}
.panel-red {
background-color: #d9534f;
color:white;
}
.panel-purple {
background-color: #9b59b6;
color:white;
}
.panel-blue {
background-color: #3498db;
color:white;
}
.stats_icon {
font-size: 3em;
}
.circle {
border-radius: 50%;
background-color: #9b59b6;
margin: 0 auto;
}
/* Style for autocompletion */
/* Stolen from https://ng-bootstrap.github.io/#/components/typeahead/examples */
::ng-deep .dropdown-item {
display: block;
width: 100%;
padding: .25rem 1.5rem;
clear: both;
font-weight: 400;
color: #212529;
text-align: inherit;
white-space: nowrap;
background-color: transparent;
border: 0
}
::ng-deep .dropdown-item:focus,
::ng-deep .dropdown-item:hover {
color: #16181b;
text-decoration: none;
background-color: #f8f9fa
}
::ng-deep .dropdown-item.active,
::ng-deep .dropdown-item:active {
color: #fff;
text-decoration: none;
background-color: #007bff
}
::ng-deep .dropdown-item.disabled,
::ng-deep .dropdown-item:disabled {
color: #6c757d;
pointer-events: none;
background-color: transparent
}
::ng-deep .dropdown-menu.show {
display: block
}

View File

@@ -15,7 +15,7 @@
<h3 *ngIf="!totalTime"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="totalTime">{{totalTime | convertMs}}</h3>
</div>
<div><br>Total time</div>
<div><br>Total time ({{totalTime | convertMoreExact}})</div>
</div>
</div>
</div>
@@ -30,8 +30,8 @@
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!totalSizeSt"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="totalSizeSt">{{totalSizeSt}}</h3>
<h3 *ngIf="!totalSize"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="totalSize">{{totalSize | convertSizeToString}}</h3>
</div>
<div><br>Total size</div>
</div>
@@ -69,7 +69,7 @@
<h3 *ngIf="!neverListenSong"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="neverListenSong">{{neverListenSong}}</h3>
</div>
<div><br>Never list. songs</div>
<div><br>Never list. songs (~{{neverListenSong / trackCountSong * 100 | round}}%)</div>
</div>
</div>
</div>
@@ -77,13 +77,37 @@
</div>
</div>
<div class="col-md-12">
<h3>Last added albums</h3>
<ng-template #rt let-r="result" let-t="term">
<!-- glyphicon glyphicon-cd -->
<span *ngIf="r.type == 'artist'" class="glyphicon glyphicon-user"></span>
<span *ngIf="r.type == 'album'" class="glyphicon glyphicon-cd"></span>
&nbsp; <ngb-highlight [result]="r.name" [term]="t"></ngb-highlight>
</ng-template>
<form class="navbar-form">
<div class="form-group" style="display:inline;">
<div class="input-group" style="display:table;">
<span class="input-group-addon" style="width:1%;">
<span class="glyphicon glyphicon-search"></span>
</span>
<input class="form-control" type="text" name="search" placeholder="Search Here" autocomplete="off" autofocus="autofocus"
id="searchSuggest"
[(ngModel)]="searchTerm"
[ngbTypeahead]="search"
[resultTemplate]="rt"
(selectItem)="onSelectSearch($event)">
</div>
</div>
</form>
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<th>Album</th>
<th>Track Count</th>
<th>Album Artist</th>
</tr>
</thead>
<tbody>
@@ -96,25 +120,30 @@
</table>
</div>
<input type="button" class="btn btn-primary btn-lg btn-block" value="All Albums" routerLink="/album">
<div class="col-md-12">
<h3>Top Played Songs</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Track Name</th>
<th>Artist</th>
<th>Album</th>
<th>Play Count</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let song of mostPlayedSongs">
<td><b>{{song.Name}}</b></td>
<td><a [routerLink]="['/artist', song.Artist]">{{song.Artist}}</a></td>
<td><a [routerLink]="['/album', song.Album]">{{song.Album}}</a></td>
<td>{{song['Play Count']}}</td>
</tr>
</tbody>
</table>
<table class="table table-striped">
<thead>
<tr>
<th>Track Name</th>
<th>Artist</th>
<th>Album</th>
<th>Play Count</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let song of mostPlayedSongs">
<td><b>{{song.Name}}</b></td>
<td><a [routerLink]="['/artist', song.Artist]">{{song.Artist}}</a></td>
<td><a [routerLink]="['/album', song.Album]">{{song.Album}}</a></td>
<td>{{song['Play Count']}}</td>
</tr>
</tbody>
</table>
<h3><a [routerLink]="['/top-played']">Top Played page</a></h3>
</div>
<div class="col-md-6">
<h3>Top Genres</h3>
@@ -150,22 +179,4 @@
</tbody>
</table>
</div>
<div>
<angular-tag-cloud
[data]="wordCloud"
[width]="options.width"
[height]="options.height"
[overflow]="options.overflow">
</angular-tag-cloud>
</div>
<div AgWordCloud
#word_cloud_chart=ag-word-cloud
[wordData]="word_cloud">
</div>
<button (click)="word_cloud_chart.update()">Toggle tooltip</button>
</div>

View File

@@ -0,0 +1,144 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ElsService } from './../els.service';
import { Song } from './../model/song';
import { Bucket } from './../model/bucket';
import { Suggested } from '../model/suggested';
import {Observable, of, OperatorFunction} from 'rxjs';
import {catchError, debounceTime, distinctUntilChanged, map, tap, switchMap} from 'rxjs/operators';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
totalTime = 0;
totalSize = 0;
trackCountSong = 0;
trackCountArtist = 0;
trackCountAlbum = 0;
neverListenSong = 0;
albumArtistCount = 0;
topGenres: Bucket[] = [];
bottomGenres: Bucket[] = [];
mostPlayedSongs: Song[] = [];
lastAddedAlbums: Bucket[] = [];
albumArtists = [];
searchTerm = ''
suggested : Suggested[] = []
constructor(private elsService: ElsService, private route: Router) { }
ngOnInit(): void {
this.elsService.getTime().then(result => {
this.totalTime = result;
});
this.elsService.getSize().then(result => this.totalSize = result);
this.elsService.getCountSong(ElsService.SONG_INDEX_NAME)
.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()
.then(result => this.neverListenSong = result);
this.elsService.getMostPlayedTrack().subscribe(
data => this.mostPlayedSongs = data
);
this.elsService.getGenres().subscribe(data => this.topGenres = data);
this.elsService.getGenres('asc').subscribe(data => this.bottomGenres = data);
// this.elsService.getGenreCount().subscribe(data => console.log(data));
const lastAddedAlbumsTemp: Bucket[] = [];
const BreakException = {};
this.elsService.getLastAddedAlbums(6).subscribe(buckets => {
buckets.forEach(bucket => {
// console.log(bucket);
if (lastAddedAlbumsTemp.length === 0) {
lastAddedAlbumsTemp.push(bucket);
} else {
let found = false;
lastAddedAlbumsTemp.forEach(element => {
if (element.key === bucket.key) {
element.doc_count += bucket.doc_count;
found = true;
}
});
if (!found) {
lastAddedAlbumsTemp.push(bucket);
}
}
});
// console.log("alors");
// console.log(lastAddedAlbumsTemp);
this.lastAddedAlbums = lastAddedAlbumsTemp;
this.lastAddedAlbums.forEach(bucket => this.getArtistName(bucket));
});
}
private getArtistName(albumBucket: Bucket) {
// For each bucket.key (album name), search artist.
// 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];
}
this.albumArtists[goodAlbum.Name] = goodAlbum['Album Artist'] ? goodAlbum['Album Artist'].toString() : goodAlbum.Artist.toString();
// If an album with multiple artist doesn't have 'Album Artist' field - this case should not happen
if (this.albumArtists[goodAlbum.Name].length > 50) {
this.albumArtists[goodAlbum.Name] = this.albumArtists[goodAlbum.Name].substring(0, 50) + '...';
}
});
}
onSelectSearch($event) {
this.route.navigate(['/' + $event.item.type + '/' + $event.item.name])
}
searching = false;
searchFailed = false;
search: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() => this.searching = true),
switchMap(term =>
this.elsService.getSuggest(term).pipe(
tap(() => this.searchFailed = false),
catchError(() => {
this.searchFailed = true;
return of([]);
}))
),
tap(() => this.searching = false)
)
}

View File

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

View File

@@ -0,0 +1,96 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Album } from './model/album';
import { ElsService } from './els.service';
@Injectable()
export class ElsAlbumService extends ElsService {
public static readonly GET_ALBUMS_DEFAULT_QUERY = {
'query': {
'bool': {
'must': [],
'must_not': [],
'filter': [
{ 'range': { 'Avg Bit Rate': { 'lte': '128'}}}
]
}
},
'sort': [ {
'_script' : {
'type' : 'number',
'script' : {
'lang': 'painless',
'source': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
},
'order' : 'desc'
}
}, {
'Avg Bit Rate': {
'order': 'asc'
}
} ],
'size': 500
}
constructor(protected http: HttpClient) {
super(http);
}
getAlbums(query: any): Observable<Album[]> {
console.info('getAlbums');
console.info(query);
return this.http
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify(query), {headers: this.headers})
.pipe(
map(res => this.responseToAlbums(res)),
catchError(error => this.handleError(error, 'getAlbums'))
);
}
getAlbumsFiltered(size: number): Observable<Album[]> {
// http://localhost:9200/itunes-albums/_search
console.info('getAlbums');
return this.http
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [ {
'Avg Bit Rate': {
'order': 'asc'
}
} ],
'size': size,
'query': {
'bool': {
'must': [],
'filter': [
{
'match_all': {}
}
],
'should': [],
'must_not': [
{
'match_phrase': {
'Artist': 'François Pérusse'
}
},
{
'match_phrase': {
'Album Artist': 'Comédiens'
}
}
]
}
}
}), {headers: this.headers})
.pipe(
map(res => this.responseToAlbums(res)),
catchError(error => this.handleError(error, 'getAlbums'))
);
}
}

View File

@@ -1,31 +1,34 @@
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import { HttpClient, HttpHeaders } from '@angular/common/http'
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Song } from './object/song';
import { Album } from './object/album';
import { Artist } from './object/artist';
import { Bucket } from './object/bucket';
import { Song } from './model/song';
import { Album } from './model/album';
import { Artist } from './model/artist';
import { Bucket } from './model/bucket';
import { Suggested } from './model/suggested';
@Injectable()
export class ElsService {
public static readonly DEFAULT_SIZE: number = 50;
private static readonly INDEX_NAME = 'itunessongs';
public static readonly SONG_INDEX_NAME = '/itunes-songs';
public static readonly ARTIST_INDEX_NAME = '/itunes-artists';
public static readonly ALBUM_INDEX_NAME = '/itunes-albums';
public static readonly SUGGEST_INDEX_NAME = '/itunes-suggest';
private static readonly ACTION_SEARCH = '/_search';
private static readonly ACTION_COUNT = '/_count';
protected static readonly ACTION_SEARCH = '/_search';
protected static readonly ACTION_COUNT = '/_count';
private elsUrl = 'http://localhost:9200/' + ElsService.INDEX_NAME + '/';
private headers = new Headers({'Content-Type': 'application/json'});
protected elsUrl = 'http://localhost:9200';
protected headers = new HttpHeaders({'Content-Type': 'application/json'});
constructor(private http: Http) { }
constructor(protected http: HttpClient) { }
getTime(): Promise<number> {
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
aggs: {
sum_time: {
@@ -35,19 +38,13 @@ export class ElsService {
'size': 0
}), {headers: this.headers})
.toPromise()
.then(res => res.json().aggregations.sum_time.value as number)
.catch(this.handleError);
}
getTimeSlowly(): Promise<number> {
return new Promise(resolve => {
setTimeout(() => resolve(this.getTime()), 2000);
});
.then(res => res.aggregations.sum_time.value as number)
.catch(error => this.handleError(error, 'getTime()'));
}
getSize(): Promise<number> {
return this.http
.post(this.elsUrl + ElsService.ACTION_SEARCH,
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
aggs: {
sum_time: {
@@ -57,27 +54,21 @@ export class ElsService {
'size': 0
}), {headers: this.headers})
.toPromise()
.then(res => res.json().aggregations.sum_time.value as number)
.catch(this.handleError);
.then(res => res.aggregations.sum_time.value as number)
.catch(error => this.handleError(error, 'getSize()'));
}
getSizeSlowly(): Promise<number> {
return new Promise(resolve => {
setTimeout(() => resolve(this.getSize()), 2000);
});
}
getCountSong(type: string): Promise<number> {
getCountSong(index: string): Promise<number> {
return this.http
.get(this.elsUrl + type + ElsService.ACTION_COUNT)
.get<any>(this.elsUrl + index + ElsService.ACTION_COUNT)
.toPromise()
.then(res => res.json().count as number)
.catch(this.handleError);
.then(res => res.count as number)
.catch(error => this.handleError(error, 'getCountSong(' + index + ')'));
}
getCountNeverListenSong(): Promise<number> {
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_COUNT,
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_COUNT,
JSON.stringify({
'query': {
'bool': {
@@ -88,23 +79,15 @@ export class ElsService {
}
}), {headers: this.headers})
.toPromise()
.then(res => res.json().count as number)
.catch(this.handleError);
}
getTrackCountSlowly(type: string): Promise<number> {
return new Promise(resolve => {
setTimeout(() => resolve(this.getCountSong(type)), 2000);
});
.then(res => res.count as number)
.catch(error => this.handleError(error, 'getCountNeverListenSong()'));
}
getMostPlayedTrack(): Observable<Song[]> {
// Thank to http://chariotsolutions.com/blog/post/angular2-observables-http-separating-services-components/
// for the map part
// Could be shorter but I think it's more readable like this.
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [ {
'Play Count': {
@@ -113,30 +96,86 @@ export class ElsService {
} ],
'size': 5
}), {headers: this.headers})
.map(res => {
return res.json().hits.hits;
})
.map((hits: Array<any>) => {
const result: Array<Song> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
});
// Shorter way:
// .map(res => {
// let result: Array<Song> = [];
// res.json().hits.hits.forEach(element => {
// result.push(element._source);
// });
// return result;
// });
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getMostPlayedTrack()'))
);
}
/**
* A basic get of albums ordered by 'Play Count' field.
*/
getMostPlayedAlbumNaive(): Promise<Album[]> {
return this.http
.get(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH + '?sort=Play Count:desc&size=20')
.toPromise()
.then(res => this.responseToAlbums(res))
.catch(error => this.handleError(error, 'getMostPlayedAlbumNaive'));
// TODO Excluse 'Divers' + compilation
}
/**
* More complicated query to calculate Avg. Play and get top ordrered by this results.
* Result is in '.hits.hits[].sort' field - not casted by conversion method
*/
getMostPlayedAlbum(): Observable<Album[]> {
return this.http
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [
{
'_script': {
'type': 'number',
'script': {
'inline': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
},
'order': 'desc'
}
}
]
}), {headers: this.headers})
.pipe(
map(res => this.responseToAlbums(res)),
catchError(error => this.handleError(error, 'getMostPlayedAlbum()'))
);
}
getMostPlayedArtistNaive(): Promise<Artist[]> {
return this.http
.get(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH + '?sort=Play Count:desc&size=20')
.toPromise()
.then(res => this.responseToAlbums(res))
.catch(error => this.handleError(error, 'getMostPlayedArtistNaive'));
// TODO Excluse 'Divers' + compilation
}
getMostPlayedArtist(): Observable<Artist[]> {
return this.http
.post(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [
{
'_script': {
'type': 'number',
'script': {
'inline': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
},
'order': 'desc'
}
},
],
'size': 100
}), {headers: this.headers})
.pipe(
map(res => this.responseToArtists(res)),
catchError(error => this.handleError(error, 'getMostPlayedArtist()'))
);
}
getAlbumSongs(albumName: string, from: number = 0): Observable<Song[]> {
console.info('getAlbumSongs- Album name: ' + albumName + ' - from: ' + from);
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Album': albumName }
@@ -144,23 +183,17 @@ export class ElsService {
'size': ElsService.DEFAULT_SIZE,
'from': from
}), {headers: this.headers})
.map(res => {
return res.json().hits.hits;
})
.map((hits: Array<any>) => {
const result: Array<Song> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
});
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getAlbumSongs(' + albumName + ',' + from + ')'))
);
}
getGenreSongs(genreName: string, from: number = 0): Observable<Song[]> {
console.info('getGenreSongs- Genre name: ' + genreName + ' - from: ' + from);
// TODO Code repetition => to refactor
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Genre': genreName }
@@ -168,22 +201,16 @@ export class ElsService {
'size': ElsService.DEFAULT_SIZE,
'from': from
}), {headers: this.headers})
.map(res => {
return res.json().hits.hits;
})
.map((hits: Array<any>) => {
const result: Array<Song> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
});
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getAlbumSongs(' + genreName + ',' + from + ')'))
);
}
getArtistSongs(artistName: string, from: number = 0): Observable<Song[]> {
console.info('getArtistSongs- Artist name: ' + artistName + ' - from: ' + from);
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'bool': {
@@ -196,76 +223,53 @@ export class ElsService {
'size': ElsService.DEFAULT_SIZE,
'from': from
}), {headers: this.headers})
.map(res => {
return res.json().hits.hits;
})
.map((hits: Array<any>) => {
const result: Array<Song> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
});
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getArtistSongs(' + artistName + ',' + from + ')'))
);
}
getAlbum(albumName: string): Observable<Album> {
// TODO Why this is used on album pages?
// When it's commented, the album information goes up correctly */
return this.http
.post(this.elsUrl + 'album' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Album': albumName }
},
'size': ElsService.DEFAULT_SIZE
}), {headers: this.headers})
.map(res => {
return res.json().hits.hits;
})
.map((hits: Array<any>) => {
if (hits.length < 1) {
console.info('No album "' + albumName + '" found.');
return undefined;
}
if (hits.length > 1) {
console.error('More than one album "' + albumName + '" found (' + hits.length + '), return the first.');
}
return hits[0]._source;
});
.pipe(
map(res => this.responseToOneTypedResult<Album>(res, albumName)),
catchError(error => this.handleError(error, 'getAlbum(' + albumName + ')'))
);
}
getArtist(artistName: string): Observable<Artist> {
return this.http
.post(this.elsUrl + 'artist' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Artist': artistName }
},
'size': ElsService.DEFAULT_SIZE
}), {headers: this.headers})
.map(res => res.json().hits.hits)
.map((hits: Array<any>) => {
// Theorically, my script prevent to found two documents with this query.
// But Prevention is better than cure as Shakespeare said
if (hits.length < 1) {
console.info('No artist "' + artistName + '" found.');
return undefined;
}
if (hits.length > 1) {
console.error('More than one artist "' + artistName + '" found (' + hits.length + '), return the first.');
console.error('This is not normal!');
}
return hits[0]._source;
});
.pipe(
map(res => this.responseToOneTypedResult<Artist>(res, artistName)),
catchError(error => this.handleError(error, 'getArtist(' + artistName + ')'))
);
}
getGenres(ordering: string = 'desc'): Observable<Bucket[]> {
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'aggs' : {
'genres' : {
'terms' : {
'field' : 'Genre.original',
'size' : 100,
'field' : 'Genre',
'size' : 10,
'missing': 'N/A',
'order': { '_count' : ordering }
}
@@ -273,90 +277,71 @@ export class ElsService {
},
'size': 0
}), {headers: this.headers})
.map(res => res.json().aggregations.genres.buckets)
.map((hits: Array<any>) => this.hitsToBuckets(hits));
.pipe(
map(res => this.responseAggregationToBucket(res, 'genres')),
catchError(error => this.handleError(error, 'getGenres(' + ordering + ')'))
);
}
getArtists(): Observable<Bucket[]> {
return this.http
.post(this.elsUrl + 'artist' + ElsService.ACTION_SEARCH,
JSON.stringify({
'size': 100,
'_source': [
'Artist',
'Track Count'
],
'sort': [
{
'Track Count': {
'order': 'desc'
}
}
]
}), {headers: this.headers})
.map(res => {
return res.json().hits.hits;
})
.map((hits: Array<any>) => {
const result: Array<Bucket> = [];
hits.forEach((hit) => {
const bucket = new Bucket;
bucket.doc_count = hit._source['Track Count'];
bucket.key = hit._source.Artist;
result.push(bucket);
});
console.log(result);
return result;
});
}
getGenreCount(): Observable<number> {
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
JSON.stringify({
'aggs' : {
'genres' : {
'cardinality' : {
'field' : 'Genre.original',
'missing': 'N/A',
}
}
},
'size': 0
}), {headers: this.headers})
.map(res => res.json().aggregations.genres.value);
}
// getGenreCount(): Observable<number> {
// return this.http
// .post<any>(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
// JSON.stringify({
// 'aggs' : {
// 'genres' : {
// 'cardinality' : {
// 'field' : 'Genre.original',
// 'missing': 'N/A',
// }
// }
// },
// 'size': 0
// }), {headers: this.headers})
// .pipe(
// map(res => res.aggregations.genres.value)
// );
// }
getLastAddedAlbums(month: number): Observable<Bucket[]> {
return this.http
.post(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'range' : {
'Date Added' : {
'gte' : 'now-' + month + 'M/d',
'lte' : 'now/d'
}
'range' : {
'Date Added' : {
'gte' : 'now-' + month + 'M/d',
'lte' : 'now/d'
}
},
'aggs' : {
'album' : {
'terms' : {
'field' : 'Album.original',
'size': 10
}
},
'size': 0,
'aggs': {
'date' : {
'terms': {
'field' : 'Date Added',
'order': { '_term': 'desc' },
'size': 20
},
'aggs': {
'album': {
'terms': {
'field': 'Album.raw'
}
}
}
},
'size': 0
}), {headers: this.headers})
.map(res => res.json().aggregations.album.buckets)
}
}
}), {headers: this.headers})
.pipe(
map(res => this.responseSubAggregationToBucket(res, 'date')),
catchError(error => this.handleError(error, 'getLastAddedAlbums(' + month + ')' ))
);
// TODO Take in consideration "sum_other_doc_count"
.map((hits: Array<any>) => this.hitsToBuckets(hits));
}
getArtistFromAlbumName(albumname: string): Observable<Album[]> {
return this.http
.post(this.elsUrl + 'album' + ElsService.ACTION_SEARCH,
.post<any>(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase' : {
@@ -364,27 +349,168 @@ export class ElsService {
}
}
}), {headers: this.headers})
.map(res => res.json().hits.hits)
.map((hits: Array<any>) => {
// TODO Use a method (duplicated code ?)
const result: Array<Album> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
});
.pipe(
map(res => res.hits.hits),
map((hits: Array<any>) => {
// TODO Use a method (duplicated code ?)
const result: Array<Album> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
}),
catchError(error => this.handleError(error, 'getArtistFromAlbumName' + albumname + ')'))
);
}
private hitsToBuckets(hits: Array<any>): Bucket[] {
const result: Array<Bucket> = [];
hits.forEach((bucket) => {
result.push(bucket);
});
return result;
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 + ')'))
);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
getSuggest(text: string): Observable<Suggested[]> {
console.log('search sugget: ' + text);
return this.http
.post<any>(this.elsUrl + ElsService.SUGGEST_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'_source': ['album', 'artist'],
'suggest': {
'album-suggest': {
'prefix': text,
'completion': {
'field': 'album_suggest'
}
},
'artist-suggest': {
'prefix': text,
'completion': {
'field': 'artist_suggest'
}
}
}
}), {headers: this.headers})
.pipe(
map(res => this.responseSuggesterToSuggested(res, 'album-suggest', 'artist-suggest')),
catchError(error => this.handleError(error, 'getSuggest(' + text + ')'))
);
}
/** Process a result to return just one result.
* Used to get an album or an artist.
* Take a name to put in console output if no result or more than one result.
*
* @param res Response to process
* @param name The searched name - for console output
*/
private responseToOneTypedResult<T>(res: any, name: string): T {
const hits = res.hits.hits;
if (hits.length < 1) {
console.info('No result found for name: "' + name);
return undefined;
}
if (hits.length > 1) {
// TODO Cumul results (for album)
console.error('More than one result for name: "' + name + '". Found (' + hits.length + '), return the first.');
}
return hits[0]._source;
}
/** Process a response to a array of songs.
*
* @param res Response to process
*/
private responseToSongs(res: any): Song[] {
const result: Array<Song> = [];
res.hits.hits.forEach((hit) => {
result.push(hit._source);
});
return result;
}
/** Process a response to a array of songs.
*
* @param res Response to process
*/
protected responseToAlbums(res: any): Album[] {
const result: Array<Album> = [];
res.hits.hits.forEach((hit) => {
result.push(hit._source);
});
return result;
}
/** Process a response to a array of songs.
*
* @param res Response to process
*/
private responseToArtists(res: any): Artist[] {
const result: Array<Artist> = [];
res.hits.hits.forEach((hit) => {
result.push(hit._source);
});
return result;
}
/** Process an aggregation response to an array of Bucket.
*
* @param res Response to process
* @param name Name of aggregation
*/
private responseAggregationToBucket(res: any, name: string): Bucket[] {
const result: Array<Bucket> = [];
res.aggregations[name].buckets.forEach((bucket) => {
result.push(bucket);
});
return result;
}
private responseSubAggregationToBucket(res: any, name: string): Bucket[] {
const result: Array<Bucket> = [];
res.aggregations[name].buckets.forEach((bucket) => {
bucket['album'].buckets.forEach((subBucket) => {
result.push(subBucket);
});
});
return result;
}
protected responseSuggesterToSuggested(res: any, ...suggestName: string[]): Suggested[] {
const result: Array<Suggested> = []
suggestName.forEach(sname => {
res['suggest'][sname][0]['options'].forEach(option => {
let suggest = new Suggested()
// TODO If more than one key, raise exception
suggest.type = String(Object.keys(option['_source']))
suggest.name = option['_source'][suggest.type]
result.push(suggest)
});
});
return result;
}
protected handleError(error: any, origin: string): Promise<any> {
console.error('An error occurred!');
console.error('Origin function: ', origin);
console.error('An error occurred!', error); // for demo purposes only
console.error(error); // for demo purposes only
return Promise.reject(error.message || error);
}
}

View File

@@ -1,28 +1,5 @@
<div class="container">
<h1>{{genreName}}</h1>
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Artist</th>
<th>Album</th>
<th>Album Artist</th>
<th>Play Count</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let song of songs | sortBy : 'Track Number'">
<td>{{song['Track Number'] ? (("0" + song['Track Number']).slice(-2)) : "--"}}</td>
<td>{{song.Name}}</td>
<td><a [routerLink]="['/artist', song.Artist]">{{song.Artist}}</a></td>
<td><a [routerLink]="['/album', song.Album]">{{song.Album}}</a></td>
<td>{{song['Album Artist']}}</td>
<td>{{song['Play Count']}}</td>
<td>{{song.Genre}}</td>
</tr>
</tbody>
</table>
<app-song-table [songs]=songs (atBottom)=loadSongs()></app-song-table>
</div>

View File

@@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { ElsService } from '../els.service';
import { SongTableComponent } from '../song-table/song-table.component';
import { Song } from '../object/song';
import { Song } from '../model/song';
@Component({
selector: 'app-genre',
@@ -11,6 +12,7 @@ import { Song } from '../object/song';
styleUrls: ['./genre.component.css']
})
export class GenreComponent implements OnInit {
@ViewChild(SongTableComponent) songtable: SongTableComponent;
genreName = '';
songs: Array<Song> = [];
@@ -37,6 +39,7 @@ export class GenreComponent implements OnInit {
this.songs = data;
} else {
data.forEach(song => {
this.songtable.setSortable(true);
this.songs.push(song);
});
}

View File

@@ -10,4 +10,5 @@ export class Album {
'Album Rating Computed': boolean;
'Play Count': number;
'Total Time': number;
'Album Artist': string;
}

View File

@@ -4,4 +4,5 @@ export class Song {
'Play Count': number;
Album: string;
'Track Number': number; // TODO Default property
'Disc Number': number;
}

View File

@@ -0,0 +1,4 @@
export class Suggested {
name: string;
type: string;
}

View File

@@ -0,0 +1,8 @@
import { ConvertMoreExactPipe } from './convert-more-exact.pipe';
describe('ConvertMoreExactPipe', () => {
it('create an instance', () => {
const pipe = new ConvertMoreExactPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,56 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'convertMoreExact'
})
export class ConvertMoreExactPipe implements PipeTransform {
/**
* Convert milliseconds to rounded time e.g. ~84d
* @param timeMs time in milliseconds
*/
transform(timeMs: number): string {
let x = timeMs / 1000;
const seconds = Math.floor(x % 60);
x /= 60;
let minutes = 0;
if (x > 1) { minutes = Math.floor(x % 60); }
x /= 60;
let hours = 0;
if (x > 1) { hours = Math.floor(x % 24); }
// TODO Enable/disable day
x /= 24;
const days = Math.floor(x);
// Final string
let ret = '~';
if (days > 0) {
return '~' + (days + (hours > 12 ? 1 : 0)) + 'd';
}
if (hours > 0) {
if (minutes > 45) {
ret += (hours + 1) + 'h';
} else if (minutes < 15) {
ret += hours + 'h';
} else {
ret += hours + 'h 1/2';
}
return ret;
}
if (minutes > 0) {
ret += (minutes + (seconds > 35 ? 1 : 0)) + 'min';
return ret;
}
if (seconds > 0) {
if (seconds > 35) {
return '~1min';
} else {
return seconds + 's';
}
}
}
}

View File

@@ -0,0 +1,8 @@
import { ConvertSizeToStringPipe } from './convert-size-to-string.pipe';
describe('ConvertSizeToStringPipe', () => {
it('create an instance', () => {
const pipe = new ConvertSizeToStringPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'convertSizeToString'
})
export class ConvertSizeToStringPipe implements PipeTransform {
/**
* Convert size to human readable size
* @param size size in byte
*/
transform(size: number): any {
const units = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];
if (size === 0) {
return '0 Byte';
}
const i = Math.floor(Math.log(size) / Math.log(1024));
let calcSize = size / Math.pow(1024, i);
calcSize = Math.round(calcSize * 100) / 100;
return calcSize + ' ' + units[i];
}
}

View File

@@ -0,0 +1,8 @@
import { ConvertMsPipe } from './convertms.pipe';
describe('ConvertMsPipe', () => {
it('create an instance', () => {
const pipe = new ConvertMsPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,38 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'convertMs'
})
export class ConvertMsPipe implements PipeTransform {
/**
* Convert milliseconds to "readable" duration e.g. 1d21h35m24s
* @param timeMs time in milliseconds
*/
transform(timeMs: number): string {
let x = timeMs / 1000;
const seconds = Math.floor(x % 60);
x /= 60;
let minutes = 0;
if (x > 1) { minutes = Math.floor(x % 60); }
x /= 60;
let hours = 0;
if (x > 1) { hours = Math.floor(x % 24); }
// TODO Enable/disable day
x /= 24;
const days = Math.floor(x);
// Final string
let ret = '';
if (days > 0) { ret += parseInt('0' + days) + 'd'; }
if (hours > 0) { ret += ('0' + hours).slice(-2) + 'h'; }
if (minutes > 0) { ret += ('0' + minutes).slice(-2) + 'm'; }
if (seconds > 0) { ret += ('0' + seconds).slice(-2) + 's'; }
return ret;
// return ('0' + days).slice(-2) + ':' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2) + ':' + ('0' + seconds).slice(-2);
}
}

View File

@@ -0,0 +1,8 @@
import { RoundPipe } from './round.pipe';
describe('RoundPipe', () => {
it('create an instance', () => {
const pipe = new RoundPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'round'
})
export class RoundPipe implements PipeTransform {
/**
* Round a value
* @param value value to round
* @param precision number of numbers to keep after coma - 1 if unspecified
*/
transform(value: number, precision?: number): number {
if (precision === undefined) {
precision = 1;
}
const factor = Math.pow(10, precision);
return Math.round(value * factor) / factor;
}
}

View File

@@ -0,0 +1,40 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sortBy'
})
export class SortByPipe implements PipeTransform {
/**
* Sort an array by multiple elements.
*
* Example to sort an array respectively by Year, Album, Disc Number, etc.
* Typescript usage:
* <pre>
* new SortByPipe().transform(this.songs, 'Year', 'Album', 'Disc Number', 'Track Number', 'Play Count');
* </pre>
* In HTML usage:
* <pre>
* <tr *ngFor="let song of songs | sortBy : 'Year':'Album':'Disc Number':'Track Number'">
* </pre>
*
* @param array array to sort
* @param args elements used to sort in oder
*/
transform(array: Array<any>, ...args: any[]): Array<any> {
array.sort((a: any, b: any) => {
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (a[arg] === undefined && b[arg] !== undefined) { return -1; }
if (a[arg] !== undefined && b[arg] === undefined) { return 1; }
if (a[arg] < b[arg]) { return -1; }
if (a[arg] > b[arg]) { return 1; }
}
return 0;
});
return array;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,55 @@
<table class="table table-striped" (window:scroll)="onScroll($event)">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Album</th>
<th>Year</th>
<th>Artist</th>
<th>Album Artist</th>
<th>Play Count</th>
<th>Genre</th>
<th>Bit Rate</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let song of songs | sortBy : 'Year':'Album':'Disc Number':'Track Number'">
<td>{{song['Track Number'] ? (("0" + song['Track Number']).slice(-2)) : "--"}}</td>
<td>{{song.Name}}</td>
<td>
<a [routerLink]="['/album', song.Album]">{{song.Album}}</a>
</td>
<td>{{song.Year}}</td>
<td>
<a [routerLink]="['/artist', song.Artist]">{{song.Artist}}</a>
</td>
<td>{{song['Album Artist'] ? song['Album Artist'] : "-" }}</td>
<td>{{song['Play Count']}}</td>
<td>
<a [routerLink]="['/genre', song.Genre]">{{song.Genre}}</a>
</td>
<td [title]="song['Bit Rate']">
<span [class]="'signal signal-' + roundBitRate(song['Bit Rate'])"></span>
</td>
<td class="star" [title]="(song['Rating Computed']?'Computed Rating: ':'Rating: ') + song.Rating">
<span *ngFor="let item of numberToArray(song.Rating, 20)">
<span class="glyphicon" [ngClass]="song['Rating Computed']?'glyphicon-star-empty':'glyphicon-star'"></span>
</span>
</td>
<td *ngIf="song.Loved" class="loved">
<span class="glyphicon glyphicon-heart heart"></span>
</td>
</tr>
</tbody>
</table>
<!-- Go to Top Button-->
<button type="button" class="btn btn-warning btn-top" [class.btn-top-is-visible]="bottomReached" aria-label="Top" (click)="scrollTop()"
title="Go back to Top!">
<span class="glyphicon glyphicon-menu-up" aria-hidden="true"></span>
</button>
<button type="button" class="btn btn-primary btn-top btn-sort" [class.btn-top-is-visible]="sortable" aria-label="Top" (click)="sort()"
title="Sort by Album & Track Number">
<span class="glyphicon glyphicon-sort-by-attributes" aria-hidden="true"></span>
</button>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SongTableComponent } from './song-table.component';
describe('SongTableComponent', () => {
let component: SongTableComponent;
let fixture: ComponentFixture<SongTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SongTableComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SongTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,61 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Utils } from '../utils';
import { Song } from './../model/song';
import { SortByPipe } from './../pipes/sort-by.pipe';
@Component({
selector: 'app-song-table',
templateUrl: './song-table.component.html',
styleUrls: ['./song-table.component.css']
})
export class SongTableComponent {
numberToArray = Utils.numberToArray;
@Input() songs: Array<Song> = [];
@Output() private atBottom = new EventEmitter();
// To activate button in interface var
bottomReached = false;
sortable: boolean;
constructor() { }
/**
* Handle scroll:
* - load data if at bottom of screen (if needed)
* - hide/show "go to top" button
*
* @param event scroll event
*/
onScroll(event: any) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.atBottom.emit();
}
// Show/hide 'at top' button
if (window.scrollY > window.innerHeight) {
this.bottomReached = true;
}
if (window.scrollY === 0) {
this.bottomReached = false;
}
}
scrollTop(): void {
window.scrollTo(0, 0);
}
roundBitRate(bitRate: number) {
return Math.round(bitRate / 64);
}
sort(): void {
this.songs = new SortByPipe().transform(this.songs, 'Year', 'Album', 'Disc Number', 'Track Number', 'Play Count');
this.sortable = false;
}
setSortable(sortable: boolean) {
this.sortable = sortable;
}
}

View File

@@ -1,24 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sortBy'
})
export class SortByPipe implements PipeTransform {
transform(array: Array<any>, ...args: any[]): Array<any> {
array.sort((a: any, b: any) => {
for (let i = 0; i < array.length; i++) {
const arg = args[i];
if (a[arg] === undefined && b[arg] !== undefined) { return -1; }
if (a[arg] !== undefined && b[arg] === undefined) { return 1; }
if (a[arg] < b[arg]) { return -1; }
if (a[arg] > b[arg]) { return 1; }
}
return 0;
});
return array;
}
}

View File

@@ -0,0 +1,111 @@
<div class="container">
<div class="col-md-12">
<h3>Top Played Album (Naive Way)</h3>
<div class="alert alert-info">
<strong>Caution:</strong> In this list, albums with more than 10 artists are ignored.
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Album</th>
<th>Artist Name</th>
<th>Track Count</th>
<th>Play Count</th>
<th>Average Play</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let album of mostPlayedAlbumsNaive">
<td>
<b>{{album.Name}}</b>
</td>
<td>{{album['Album Artist'] ? album['Album Artist'] : album.Artist}}</td>
<td>{{album['Track Count']}}</td>
<td>{{album['Play Count']}}</td>
<td>{{album['Play Count'] / album['Track Count'] | round}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12">
<h3>Top Played Album (avg. play way)</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Album</th>
<th>Artist Name</th>
<th>Track Count</th>
<th>Play Count</th>
<th>Average Play</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let album of mostPlayedAlbums">
<td>
<b>{{album.Name}}</b>
</td>
<td>{{album['Album Artist'] ? album['Album Artist'] : album.Artist}}</td>
<td>{{album['Track Count']}}</td>
<td>{{album['Play Count']}}</td>
<td>{{album['Play Count'] / album['Track Count'] | round}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12">
<h3>Top Played Artist (Naive Way)</h3>
<!-- <div class="alert alert-info"> -->
<!-- <strong>Caution:</strong> In this list, artist with more than 10 artists are ignored. -->
<!-- </div> -->
<table class="table table-striped">
<thead>
<tr>
<th>Artist</th>
<th>Album Count</th>
<th>Track Count</th>
<th>Play Count</th>
<th>Average Play</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let artist of mostPlayedArtistsNaive">
<td>
<b>{{artist.Name}}</b>
</td>
<td>{{artist.Album.length}}</td>
<td>{{artist['Track Count']}}</td>
<td>{{artist['Play Count']}}</td>
<td>{{artist['Play Count'] / artist['Track Count'] | round}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-12">
<h3>Top Played Artist (avg. play way)</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Artist</th>
<th>Album Count</th>
<th>Track Count</th>
<th>Play Count</th>
<th>Average Play</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let artist of mostPlayedArtists">
<td>
<b>{{artist.Name}}</b>
</td>
<td>{{artist.Album.length}}</td>
<td>{{artist['Track Count']}}</td>
<td>{{artist['Play Count']}}</td>
<td>{{artist['Play Count'] / artist['Track Count'] | round}}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TopPlayedComponent } from './top-played.component';
describe('TopPlayedComponent', () => {
let component: TopPlayedComponent;
let fixture: ComponentFixture<TopPlayedComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TopPlayedComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TopPlayedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,65 @@
import { Component, OnInit } from '@angular/core';
import { ElsService } from '../els.service';
import { Album } from '../model/album';
import { Artist } from '../model/artist';
@Component({
selector: 'app-top-played',
templateUrl: './top-played.component.html',
styleUrls: ['./top-played.component.css']
})
export class TopPlayedComponent implements OnInit {
mostPlayedAlbumsNaive: Album[] = [];
mostPlayedAlbums: Album[] = [];
mostPlayedArtistsNaive: Artist[] = [];
mostPlayedArtists: Artist[] = [];
constructor(private elsService: ElsService) { }
ngOnInit() {
this.elsService.getMostPlayedAlbumNaive()
.then(result => {
result.forEach(album => {
if (album.Artist.length <= 10) {
this.mostPlayedAlbumsNaive.push(album);
}
});
this.mostPlayedAlbumsNaive.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
});
this.elsService.getMostPlayedAlbum().subscribe(result => {
this.mostPlayedAlbums = result;
this.mostPlayedAlbums.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
// TODO Load more! (Use a )
});
this.elsService.getMostPlayedArtistNaive()
.then(result => {
this.mostPlayedArtistsNaive = result;
this.mostPlayedArtistsNaive.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
});
this.elsService.getMostPlayedArtist().subscribe(result => {
result.forEach(artist => {
if (artist['Track Count'] > 10) {
this.mostPlayedArtists.push(artist);
}
});
this.mostPlayedArtists.sort((a: any, b: any) => this.sortByAveragePlay(a, b)).splice(10);
});
}
sortByAveragePlay(a: any, b: any) {
if ((a['Play Count'] / a['Track Count']) < (b['Play Count'] / b['Track Count'])) {
return 1;
} else {
return -1;
}
}
}

View File

@@ -0,0 +1,7 @@
import { Utils } from './utils';
describe('Utils', () => {
it('should create an instance', () => {
expect(new Utils()).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
export class Utils {
/**
* Convert a number to an array.
* Mainly used for rating: need to convert the number to use a ngFor
* @param n number to convert on a array of n cases
* @param d if you want to divide number, e.g. by 20 for rating (rating is one value out of one hundred)
*/
public static numberToArray(n: number, d: number = 1): any[] {
if (!n) {
return Array(0)
}
return Array(n / d);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><rect x="45" y="5" display="inline" width="10" height="10"></rect><rect x="45" y="45" display="inline" width="10" height="10"></rect><rect x="35" y="35" display="inline" width="10" height="10"></rect><rect x="25" y="25" display="inline" width="10" height="10"></rect><rect x="55" y="35" display="inline" width="10" height="10"></rect><rect x="65" y="15" display="inline" width="10" height="10"></rect><rect x="35" y="5" display="inline" width="10" height="10"></rect><rect x="15" y="15" display="inline" width="10" height="10"></rect><rect x="5" y="45" display="inline" width="10" height="10"></rect><rect x="5" y="55" display="inline" width="10" height="10"></rect><rect x="5" y="35" display="inline" width="10" height="10"></rect><rect x="15" y="65" display="inline" width="10" height="10"></rect><rect x="15" y="75" display="inline" width="10" height="10"></rect><rect x="25" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="85" display="inline" width="10" height="10"></rect><rect x="35" y="85" display="inline" width="10" height="10"></rect><rect x="55" y="85" display="inline" width="10" height="10"></rect><rect x="65" y="75" display="inline" width="10" height="10"></rect><rect x="75" y="65" display="inline" width="10" height="10"></rect><rect x="75" y="75" display="inline" width="10" height="10"></rect><rect x="85" y="45" display="inline" width="10" height="10"></rect><rect x="85" y="55" display="inline" width="10" height="10"></rect><rect x="85" y="35" display="inline" width="10" height="10"></rect><rect x="75" y="25" display="inline" width="10" height="10"></rect><rect x="75" y="15" display="inline" width="10" height="10"></rect><rect x="55" y="5" display="inline" width="10" height="10"></rect></g><g><path d="M5.675,93.515h9V61.651h-9V93.515z M6.675,62.651h7v29.864h-7V62.651z"></path><path d="M18.175,93.515h9V52.803h-9V93.515z M19.175,53.803h7v38.712h-7V53.803z"></path><path d="M30.675,93.515h9v-47.06h-9V93.515z M31.675,47.455h7v45.06h-7V47.455z"></path><path d="M43.175,93.515h9V37.607h-9V93.515z M44.175,38.607h7v53.908h-7V38.607z"></path><path d="M55.675,93.515h9V28.759h-9V93.515z M56.675,29.759h7v62.756h-7V29.759z"></path><path d="M68.175,17.411v76.104h9V17.411H68.175z M76.175,92.515h-7V18.411h7V92.515z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><rect x="25" y="25" display="inline" width="10" height="10"></rect><rect x="75" y="75" display="inline" width="10" height="10"></rect><rect x="85" y="75" display="inline" width="10" height="10"></rect><rect x="35" y="15" display="inline" width="10" height="10"></rect><rect x="45" y="5" display="inline" width="10" height="10"></rect><rect x="55" y="15" display="inline" width="10" height="10"></rect><rect x="5" y="65" display="inline" width="10" height="10"></rect><rect x="15" y="55" display="inline" width="10" height="10"></rect><rect x="15" y="45" display="inline" width="10" height="10"></rect><rect x="25" y="35" display="inline" width="10" height="10"></rect><rect x="65" y="25" display="inline" width="10" height="10"></rect><rect x="65" y="35" display="inline" width="10" height="10"></rect><rect x="75" y="45" display="inline" width="10" height="10"></rect><rect x="75" y="55" display="inline" width="10" height="10"></rect><rect x="85" y="65" display="inline" width="10" height="10"></rect><rect x="65" y="75" display="inline" width="10" height="10"></rect><rect x="55" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="85" display="inline" width="10" height="10"></rect><rect x="35" y="75" display="inline" width="10" height="10"></rect><rect x="25" y="75" display="inline" width="10" height="10"></rect><rect x="15" y="75" display="inline" width="10" height="10"></rect><rect x="5" y="75" display="inline" width="10" height="10"></rect></g><g><polygon points="5.675,93.515 14.675,93.515 14.675,93.015 14.675,62.152 14.675,61.652 5.675,61.652 "></polygon><path d="M18.175,93.515h9V52.804h-9V93.515z M19.175,53.804h7v38.711h-7V53.804z"></path><path d="M30.675,93.515h9v-47.06h-9V93.515z M31.675,47.456h7v45.06h-7V47.456z"></path><path d="M43.175,93.515h9V37.607h-9V93.515z M44.175,38.607h7v53.908h-7V38.607z"></path><path d="M55.675,93.515h9V28.759h-9V93.515z M56.675,29.759h7v62.756h-7V29.759z"></path><path d="M68.175,93.515h9V17.412h-9V93.515z M69.175,18.412h7v74.104h-7V18.412z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><rect x="85" y="25" display="inline" width="10" height="10"></rect><rect x="65" y="35" display="inline" width="10" height="10"></rect><rect x="75" y="35" display="inline" width="10" height="10"></rect><rect x="55" y="45" display="inline" width="10" height="10"></rect><rect x="45" y="45" display="inline" width="10" height="10"></rect><rect x="35" y="45" display="inline" width="10" height="10"></rect><rect x="35" y="55" display="inline" width="10" height="10"></rect><rect x="35" y="65" display="inline" width="10" height="10"></rect><rect x="35" y="75" display="inline" width="10" height="10"></rect><rect x="25" y="75" display="inline" width="10" height="10"></rect><rect x="15" y="75" display="inline" width="10" height="10"></rect><rect x="5" y="75" display="inline" width="10" height="10"></rect><rect x="5" y="65" display="inline" width="10" height="10"></rect><rect x="5" y="55" display="inline" width="10" height="10"></rect><rect x="5" y="45" display="inline" width="10" height="10"></rect><rect x="15" y="45" display="inline" width="10" height="10"></rect><rect x="25" y="45" display="inline" width="10" height="10"></rect></g><g><polygon points="5.675,93.516 14.675,93.516 14.675,93.016 14.675,62.151 14.675,61.651 5.675,61.651 "></polygon><polygon points="18.175,93.516 27.175,93.516 27.175,93.016 27.175,53.304 27.175,52.804 18.175,52.804 "></polygon><path d="M30.675,93.516h9v-47.06h-9V93.516z M31.675,47.456h7v45.06h-7V47.456z"></path><path d="M43.175,93.516h9V37.607h-9V93.516z M44.175,38.607h7v53.908h-7V38.607z"></path><path d="M55.675,93.516h9V28.76h-9V93.516z M56.675,29.76h7v62.756h-7V29.76z"></path><path d="M68.175,93.516h9V17.412h-9V93.516z M69.175,18.412h7v74.104h-7V18.412z"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><rect x="45" y="5" display="inline" width="10" height="10"></rect><rect x="45" y="45" display="inline" width="10" height="10"></rect><rect x="35" y="5" display="inline" width="10" height="10"></rect><rect x="25" y="15" display="inline" width="10" height="10"></rect><rect x="15" y="15" display="inline" width="10" height="10"></rect><rect x="15" y="25" display="inline" width="10" height="10"></rect><rect x="5" y="45" display="inline" width="10" height="10"></rect><rect x="5" y="55" display="inline" width="10" height="10"></rect><rect x="5" y="35" display="inline" width="10" height="10"></rect><rect x="15" y="65" display="inline" width="10" height="10"></rect><rect x="15" y="75" display="inline" width="10" height="10"></rect><rect x="25" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="85" display="inline" width="10" height="10"></rect><rect x="35" y="85" display="inline" width="10" height="10"></rect><rect x="55" y="85" display="inline" width="10" height="10"></rect><rect x="65" y="75" display="inline" width="10" height="10"></rect><rect x="75" y="65" display="inline" width="10" height="10"></rect><rect x="75" y="75" display="inline" width="10" height="10"></rect><rect x="85" y="45" display="inline" width="10" height="10"></rect><rect x="85" y="55" display="inline" width="10" height="10"></rect><rect x="85" y="35" display="inline" width="10" height="10"></rect><rect x="75" y="25" display="inline" width="10" height="10"></rect><rect x="65" y="15" display="inline" width="10" height="10"></rect><rect x="75" y="15" display="inline" width="10" height="10"></rect><rect x="55" y="5" display="inline" width="10" height="10"></rect></g><g><polygon points="5.427,93.516 14.427,93.516 14.427,93.016 14.427,62.151 14.427,61.651 5.427,61.651 "></polygon><polygon points="17.927,93.516 26.927,93.516 26.927,93.016 26.927,53.304 26.927,52.804 17.927,52.804 "></polygon><polygon points="30.427,93.516 39.427,93.516 39.427,93.016 39.427,46.956 39.427,46.456 30.427,46.456 "></polygon><path d="M42.927,93.516h9V37.607h-9V93.516z M43.927,38.607h7v53.908h-7V38.607z"></path><path d="M55.427,93.516h9V28.76h-9V93.516z M56.427,29.76h7v62.756h-7V29.76z"></path><path d="M67.927,93.516h9V17.412h-9V93.516z M68.927,18.412h7v74.104h-7V18.412z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><rect x="15" y="15" display="inline" width="10" height="10"></rect><rect x="25" y="15" display="inline" width="10" height="10"></rect><rect x="35" y="15" display="inline" width="10" height="10"></rect><rect x="45" y="15" display="inline" width="10" height="10"></rect><rect x="55" y="15" display="inline" width="10" height="10"></rect><rect x="65" y="15" display="inline" width="10" height="10"></rect><rect x="75" y="15" display="inline" width="10" height="10"></rect><rect x="85" y="25" display="inline" width="10" height="10"></rect><rect x="85" y="35" display="inline" width="10" height="10"></rect><rect x="85" y="45" display="inline" width="10" height="10"></rect><rect x="85" y="55" display="inline" width="10" height="10"></rect><rect x="85" y="65" display="inline" width="10" height="10"></rect><rect x="75" y="75" display="inline" width="10" height="10"></rect><rect x="65" y="75" display="inline" width="10" height="10"></rect><rect x="55" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="75" display="inline" width="10" height="10"></rect><rect x="35" y="75" display="inline" width="10" height="10"></rect><rect x="25" y="75" display="inline" width="10" height="10"></rect><rect x="15" y="75" display="inline" width="10" height="10"></rect><rect x="5" y="65" display="inline" width="10" height="10"></rect><rect x="5" y="55" display="inline" width="10" height="10"></rect><rect x="5" y="45" display="inline" width="10" height="10"></rect><rect x="5" y="35" display="inline" width="10" height="10"></rect><rect x="5" y="25" display="inline" width="10" height="10"></rect></g><g><polygon points="4,94.5 13,94.5 13,94 13,63.136 13,62.636 4,62.636 "></polygon><polygon points="16.5,94.5 25.5,94.5 25.5,94 25.5,54.288 25.5,53.788 16.5,53.788 "></polygon><polygon points="29,94.5 38,94.5 38,94 38,47.94 38,47.44 29,47.44 "></polygon><polygon points="41.5,94.5 50.5,94.5 50.5,94 50.5,39.092 50.5,38.592 41.5,38.592 "></polygon><path d="M54,94.5h9V29.744h-9V94.5z M55,30.744h7V93.5h-7V30.744z"></path><path d="M66.5,94.5h9V18.396h-9V94.5z M67.5,19.396h7V93.5h-7V19.396z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><circle display="inline" fill="#000000" cx="50" cy="50" r="45"></circle></g><g display="none"><rect x="45" y="5" display="inline" width="10" height="10"></rect><rect x="35" y="5" display="inline" width="10" height="10"></rect><rect x="25" y="15" display="inline" width="10" height="10"></rect><rect x="15" y="15" display="inline" width="10" height="10"></rect><rect x="15" y="25" display="inline" width="10" height="10"></rect><rect x="5" y="45" display="inline" width="10" height="10"></rect><rect x="5" y="55" display="inline" width="10" height="10"></rect><rect x="5" y="35" display="inline" width="10" height="10"></rect><rect x="15" y="65" display="inline" width="10" height="10"></rect><rect x="15" y="75" display="inline" width="10" height="10"></rect><rect x="25" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="85" display="inline" width="10" height="10"></rect><rect x="35" y="85" display="inline" width="10" height="10"></rect><rect x="55" y="85" display="inline" width="10" height="10"></rect><rect x="65" y="75" display="inline" width="10" height="10"></rect><rect x="75" y="65" display="inline" width="10" height="10"></rect><rect x="75" y="75" display="inline" width="10" height="10"></rect><rect x="85" y="45" display="inline" width="10" height="10"></rect><rect x="85" y="55" display="inline" width="10" height="10"></rect><rect x="85" y="35" display="inline" width="10" height="10"></rect><rect x="75" y="25" display="inline" width="10" height="10"></rect><rect x="65" y="15" display="inline" width="10" height="10"></rect><rect x="75" y="15" display="inline" width="10" height="10"></rect><rect x="55" y="5" display="inline" width="10" height="10"></rect></g><g><polygon points="5.675,94.5 14.675,94.5 14.675,94 14.675,63.136 14.675,62.636 5.675,62.636 "></polygon><polygon points="18.175,94.5 27.175,94.5 27.175,94 27.175,54.288 27.175,53.788 18.175,53.788 "></polygon><polygon points="30.675,94.5 39.675,94.5 39.675,94 39.675,47.94 39.675,47.44 30.675,47.44 "></polygon><polygon points="43.175,94.5 52.175,94.5 52.175,94 52.175,39.092 52.175,38.592 43.175,38.592 "></polygon><polygon points="55.675,94.5 64.675,94.5 64.675,94 64.675,30.244 64.675,29.744 55.675,29.744 "></polygon><path d="M68.175,94.5h9V18.396h-9V94.5z M69.175,19.396h7V93.5h-7V19.396z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><g display="none"><rect x="45" y="25" display="inline" width="10" height="10"></rect><rect x="35" y="15" display="inline" width="10" height="10"></rect><rect x="25" y="15" display="inline" width="10" height="10"></rect><rect x="15" y="25" display="inline" width="10" height="10"></rect><rect x="5" y="35" display="inline" width="10" height="10"></rect><rect x="5" y="45" display="inline" width="10" height="10"></rect><rect x="15" y="55" display="inline" width="10" height="10"></rect><rect x="25" y="65" display="inline" width="10" height="10"></rect><rect x="35" y="75" display="inline" width="10" height="10"></rect><rect x="45" y="85" display="inline" width="10" height="10"></rect><rect x="55" y="15" display="inline" width="10" height="10"></rect><rect x="65" y="15" display="inline" width="10" height="10"></rect><rect x="75" y="25" display="inline" width="10" height="10"></rect><rect x="85" y="35" display="inline" width="10" height="10"></rect><rect x="85" y="45" display="inline" width="10" height="10"></rect><rect x="75" y="55" display="inline" width="10" height="10"></rect><rect x="65" y="65" display="inline" width="10" height="10"></rect><rect x="55" y="75" display="inline" width="10" height="10"></rect></g><g><rect x="4.895" y="64.136" width="8" height="30.864"></rect><rect x="17.395" y="55.288" width="8" height="39.712"></rect><rect x="29.895" y="48.94" width="8" height="46.06"></rect><rect x="42.395" y="40.092" width="8" height="54.908"></rect><rect x="54.895" y="31.244" width="8" height="63.756"></rect><rect x="67.395" y="19.896" width="8" height="75.104"></rect></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M18.404,83.511H5V70.106h13.404V83.511z M7,81.511h9.404v-9.404H7V81.511z"></path><path d="M37.553,83.511H24.149V56.702h13.404V83.511z M26.149,81.511h9.404V58.702h-9.404V81.511z"></path><path d="M56.702,83.511H43.298V43.298h13.404V83.511z M45.298,81.511h9.404V45.298h-9.404V81.511z"></path><path d="M75.851,83.511H62.447V29.894h13.403V83.511z M64.447,81.511h9.403V31.894h-9.403V81.511z"></path><path d="M95,83.511H81.596V16.489H95V83.511z M83.596,81.511H93V18.489h-9.404V81.511z"></path></svg>

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><rect x="5" y="70.106" width="13.404" height="13.404"></rect><path d="M37.553,83.511H24.149V56.702h13.404V83.511z M26.149,81.511h9.404V58.702h-9.404V81.511z"></path><path d="M56.702,83.511H43.298V43.298h13.404V83.511z M45.298,81.511h9.404V45.298h-9.404V81.511z"></path><path d="M75.851,83.511H62.447V29.894h13.403V83.511z M64.447,81.511h9.403V31.894h-9.403V81.511z"></path><path d="M95,83.511H81.596V16.489H95V83.511z M83.596,81.511H93V18.489h-9.404V81.511z"></path></svg>

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><rect x="5" y="70.106" width="13.404" height="13.404"></rect><rect x="24.149" y="56.702" width="13.404" height="26.809"></rect><path d="M56.702,83.511H43.298V43.298h13.404V83.511z M45.298,81.511h9.404V45.298h-9.404V81.511z"></path><path d="M75.851,83.511H62.447V29.894h13.403V83.511z M64.447,81.511h9.403V31.894h-9.403V81.511z"></path><path d="M95,83.511H81.596V16.489H95V83.511z M83.596,81.511H93V18.489h-9.404V81.511z"></path></svg>

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><rect x="5" y="70.106" width="13.404" height="13.404"></rect><rect x="24.149" y="56.702" width="13.404" height="26.809"></rect><rect x="43.298" y="43.298" width="13.404" height="40.213"></rect><path d="M75.851,83.511H62.447V29.894h13.403V83.511z M64.447,81.511h9.403V31.894h-9.403V81.511z"></path><path d="M95,83.511H81.596V16.489H95V83.511z M83.596,81.511H93V18.489h-9.404V81.511z"></path></svg>

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><rect x="5" y="70.106" width="13.404" height="13.404"></rect><rect x="24.149" y="56.702" width="13.404" height="26.809"></rect><rect x="43.298" y="43.298" width="13.404" height="40.213"></rect><rect x="62.447" y="29.894" width="13.403" height="53.617"></rect><path d="M95,83.511H81.596V16.489H95V83.511z M83.596,81.511H93V18.489h-9.404V81.511z"></path></svg>

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><rect x="5" y="70.106" width="13.404" height="13.404"></rect><rect x="24.149" y="56.702" width="13.404" height="26.809"></rect><rect x="43.298" y="43.298" width="13.404" height="40.213"></rect><rect x="62.447" y="29.894" width="13.403" height="53.617"></rect><rect x="81.596" y="16.489" width="13.404" height="67.021"></rect></svg>

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 512 512" x="0px" y="0px"><defs><style>.cls-1{fill:none;}</style></defs><title>wifi20</title><path class="cls-1" d="M475.76,117.66c-0.11-.13-0.93-0.77-6.22-0.78-7.64,0-15.39-.15-22.89-0.29-8.19-.15-16.65-0.31-25-0.29-3.61,0-4.84.26-5.26,0.44-0.13.32-.4,1.34-0.4,4.37q0,55.17,0,110.28c0,52.37.05,106.52,0,159.82,0,3.08.6,4,.86,4.26s1.15,0.82,4.08.76C432.28,396,443.7,396,454.74,396c4.26,0,8.51,0,12.78,0h0c1.85,0,3.45.09,4.84,0.13s2.86,0.1,3.79,0c0.18-1.42,0-4.13,0-6.23,0-30.92-.17-60.64-0.17-90.88V256h0.5V225.41c0-33.43,0-67.76,0-100.56C476.48,123.29,476.48,118.51,475.76,117.66Z"></path><path class="cls-1" d="M380.16,172.31c-0.52-.51-1.89-0.78-3.78-0.75-11.11.19-22.39,0.16-33.29,0.12-5.36,0-10.71,0-16.07,0-5.68,0-5.68,0-5.69,7.76,0,25.75,0,52.27,0,78.71V283l0.06,4.5c0,9.29,0,18.41,0,27.45,0,26.37,0,51.27-.11,77.85a8.8,8.8,0,0,0,.33,3.19,10.17,10.17,0,0,0,3.83.34l6.25-.15c14.88-.35,30.26-0.71,45.44-0.4,2.51,0,3.08-.47,3.14-0.53s0.68-.83.67-4c-0.08-66.1-.07-136.42.05-215C381,173.45,380.41,172.56,380.16,172.31Z"></path><path class="cls-1" d="M284.44,228.07c-0.18-.17-1-0.73-3.67-0.68-11.41.21-23,.17-34.2,0.13q-7.58,0-15.17,0c-4.88,0-5.64.75-5.74,0.9-0.79,1.14-.79,5.7-0.8,8.42,0,18.36,0,37.33,0,56.49V312H225v24.91c0,16.72,0,33.21,0,50.68,0,4,0,8.2.4,9.53a41.48,41.48,0,0,0,7.07.26h3.12c6.43,0,13-.15,19.34-0.29,8.38-.19,17-0.39,25.6-0.25,1.89,0,3.24-.25,3.77-0.77s0.89-2.05.88-4.1c-0.12-52.9-.1-106.67.06-159.8C285.24,229.18,284.68,228.31,284.44,228.07Z"></path><path d="M469.54,107.88c-7.56,0-15.27-.15-22.73-0.29-8.24-.15-16.75-0.31-25.15-0.29-9.91,0-14.63,2.1-14.63,13.81q0,55.17,0,110.29c0,52.36.05,106.51,0,159.81,0,3.09.37,7.56,3.56,10.69,2.29,2.24,5.56,3.34,10,3.34,0.19,0,.38,0,0.58,0,11.14-.23,22.5-0.22,33.49-0.19,4.11,0,8.22,0,12.33,0h0.51c1.76,0,3.24.09,4.56,0.12,4.8,0.12,7.94.26,10.45-2.19,2.71-2.64,2.62-6.53,2.62-13,0-30.92-.17-60.65-0.17-90.89V265h0.5V225.41c0-33.43,0-67.76,0-100.57C485.48,112.9,483.26,107.9,469.54,107.88Zm7,117.53V256H476v43.07c0,30.24.18,60,.17,90.88,0,2.1.18,4.81,0,6.23-0.94.05-2.43,0-3.79,0s-3-.13-4.84-0.13h0c-4.27,0-8.52,0-12.78,0-11,0-22.46,0-33.71.21-2.94.06-3.83-.51-4.08-0.76s-0.86-1.18-.86-4.26c0-53.3,0-107.45,0-159.82q0-55.13,0-110.28c0-3,.26-4,0.4-4.37,0.42-.18,1.65-0.43,5.26-0.44,8.31,0,16.77.14,25,.29,7.49,0.14,15.24.29,22.89,0.29,5.29,0,6.11.65,6.22,0.78,0.72,0.85.73,5.62,0.73,7.19C476.5,157.66,476.5,192,476.5,225.41Z"></path><path d="M386.47,165.89c-2.36-2.32-5.69-3.4-10.25-3.34-11,.19-22.25.16-33.11,0.12q-8.06,0-16.11,0c-13.56,0-14.67,7.6-14.67,16.76,0,25.75,0,52.28,0,78.71V287.5l0.06,4.5h0c0,7.74,0,15.38,0,23,0,26.35,0,51.24-.11,77.79,0,3.3.32,7,3,9.67,2.51,2.47,6.08,2.92,9.4,2.92h1l6.26-.15c14.78-.35,30.06-0.71,45-0.4,4.26,0.09,7.41-.93,9.63-3.1,3-2.94,3.38-7.1,3.37-10.4-0.08-66.09-.07-136.41.05-215C390,171.65,388.85,168.24,386.47,165.89Zm-5.53,225.4c0,3.13-.56,3.85-0.67,4s-0.64.58-3.14,0.53c-15.17-.31-30.56,0-45.44.4l-6.25.15a10.17,10.17,0,0,1-3.83-.34,8.8,8.8,0,0,1-.33-3.19c0.16-26.58.13-51.48,0.11-77.85,0-9,0-18.16,0-27.45l-0.06-4.5V258.12c0-26.43,0-53,0-78.71,0-7.75,0-7.75,5.69-7.76,5.36,0,10.71,0,16.07,0,10.9,0,22.18.07,33.29-.12,1.89,0,3.26.24,3.78,0.75,0.25,0.25.85,1.14,0.84,4C380.88,254.87,380.86,325.19,380.95,391.3Z"></path><path d="M290.75,221.66c-2.32-2.28-5.66-3.37-10.15-3.26-11.31.21-22.84,0.17-34,.13q-7.6,0-15.21,0c-14.46,0-15.52,6.92-15.54,18.3,0,18.36,0,37.34,0,56.51V321H216v15.91c0,16.72,0,33.21,0,50.69,0,8.63,0,13,3,16,2.56,2.54,6,2.82,11.31,2.82h5.26c6.53,0,13.15-.15,19.55-0.3,8.29-.19,16.86-0.38,25.25-0.25,4.53,0.1,7.87-1,10.24-3.36s3.57-5.82,3.56-10.53c-0.12-52.89-.1-106.63.06-159.75C294.24,229.14,293.89,224.74,290.75,221.66ZM285.17,392c0,2-.32,3.54-0.88,4.1s-1.88.8-3.77,0.77c-8.56-.14-17.22.06-25.6,0.25-6.35.14-12.91,0.29-19.34,0.29h-3.12a41.48,41.48,0,0,1-7.07-.26c-0.42-1.33-.4-5.49-0.4-9.53,0-17.47,0-34,0-50.68V312h-0.16V293.3c0-19.16,0-38.13,0-56.49,0-2.72,0-7.27.8-8.42,0.1-.15.87-0.9,5.74-0.9q7.58,0,15.17,0c11.2,0,22.78.08,34.2-.13,2.72-.05,3.5.51,3.67,0.68s0.8,1.11.79,4.08C285.07,285.29,285.05,339.05,285.17,392Z"></path><path d="M195,277.32c-1.57-1.6-4.44-3.32-9.17-3.32H186v-0.16c-17,.1-34-0.06-51.54-0.15-4.31,0-7.61,1.08-9.9,3.38s-3.48,5.62-3.46,10.07c0.14,36,.14,71.12,0,104.39,0,4.65,1.11,8,3.46,10.36s5.67,3.41,10.26,3.34c17.36-.26,34.38-0.25,50.6,0h1c3.39,0,7-.36,9.59-2.92s2.87-6.34,2.82-9.31c-0.22-12.28-.18-23.48-0.14-35.35,0-3.48,0-7,0-10.65l0-4.49c0-4.37,0-9.06,0-13.94,0-12.74-.09-27.15.15-39.74C198.92,283.79,197.65,280,195,277.32Z"></path><path d="M100.94,330.66c-2.18-2.13-5.21-3.14-9.31-3h0c-4.7.1-9.27,0.11-13.14,0.1l-5.38,0q-7.91,0-15.82,0l-5.32,0h0c-3.85,0-8.42,0-13.1-.12-4.05-.12-7.05.86-9.2,3s-3.22,5.26-3.17,9.53c0.19,18.44.19,35.5,0,52.15,0,4.22,1,7.35,3.18,9.55s5.07,3.2,9,3.2H91.74c4,0,7.07-1.1,9.2-3.24s3.13-5,3.12-8.7c-0.05-18.82,0-37.71,0-53.7C104.08,335.63,103,332.71,100.94,330.66Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More