125 Commits

Author SHA1 Message Date
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
f9acea1e9c Go to dashboard button floating - always visible 2017-10-11 22:39:04 +02:00
0a73002871 TO SQUASH with migration angular 4 2017-10-11 22:07:40 +02:00
94480a27d2 fix bad td title 2017-10-11 00:34:25 +02:00
11c64405eb Quickly add genre component 2017-10-11 00:33:43 +02:00
331458edbd Get last added album improved
- Take number of month
- Extract method to treat bucket
2017-10-07 11:50:57 +02:00
94e59930ee Last album section with artist & link 2017-10-07 11:14:49 +02:00
50bc795d5f ELS Service Refactor: extract method to treat hits buckets 2017-10-07 11:13:54 +02:00
95f80b6b11 Show last added albums 2017-10-05 18:53:56 +02:00
31fd9f27e9 Migrate to Angular 4 2017-10-01 03:59:08 +02:00
b9bcc26258 Remove unused hero part 2017-10-01 02:40:38 +02:00
7b3c236416 TSLint acceptance 2017-10-01 02:38:04 +02:00
95438ec7a0 TSLint fix 2017-10-01 01:57:09 +02:00
60f839f698 Change port 2017-10-01 01:56:47 +02:00
28f4ea41d1 Put Mapping, error handling 2017-05-14 01:25:57 +02:00
40f2f4fc5e Var for index name 2017-05-14 01:25:26 +02:00
28641848e5 [TOSQUASH] pipe 2017-05-14 01:24:32 +02:00
fb092f3b9b Add Genre tables 2017-05-14 01:24:16 +02:00
8cb2d91af0 Add time for Album + pipe to convert Ms 2017-05-14 01:23:42 +02:00
bf274e08d6 Improve artist page + ELS service var 2017-05-13 03:20:45 +02:00
29841820e6 Improve lock system + syle
Try to add a loadAll button without success...
2017-05-07 17:48:05 +02:00
11348abb81 Improve sort button 2017-05-06 14:53:22 +02:00
8693c2ec7e Fix sort bug 2017-05-06 14:47:47 +02:00
d099353aed Little improve service (code style) 2017-05-05 05:03:25 +02:00
f58113ca22 Stats for Album 2017-05-05 05:03:06 +02:00
c0d6459d0b Add an amazing (and bugged) sort button for artist
Just for test purpose - need to be improved
2017-05-05 04:26:12 +02:00
6c55117c0d Add Artist stats 2017-05-05 04:25:38 +02:00
879647564c Get Artist & details + lock loading system
Because with stats, scroll launch an undesired loading of data
So need to lock when loading data
2017-05-05 03:27:04 +02:00
c9a55c8217 Really same artist/album + stats 2017-05-05 02:52:43 +02:00
9cdd69bfd0 Add never list. song + fix color red panel 2017-05-05 02:52:07 +02:00
cba0f110ff TOSQUASH Bad console output 2017-05-05 02:37:19 +02:00
ed9f6020dc Top 5 Style + use sortPipe in album 2017-05-05 00:05:18 +02:00
6f38b9ddd3 Same for artist & album 2017-05-04 23:58:22 +02:00
9ff2e86c65 Improve sort pipe to take multiple args 2017-05-04 23:47:55 +02:00
87bcdf48a5 Add artist part 2017-05-04 23:46:44 +02:00
7b796dceaf TOSQUASH Thank for pipe 2017-05-04 23:16:18 +02:00
ae02d37385 Beautiful Go to Top button :) 2017-05-04 23:16:04 +02:00
1abf0b6a0f Autoload when reach bottom 2017-05-04 21:45:38 +02:00
9793bc8763 Improve load more + scrollToTop 2017-05-04 19:21:37 +02:00
264356e5a1 Load more song for album (+ fake size)
Note how to have static var!
2017-05-04 19:07:56 +02:00
302afb6675 Album + slow service + other 2017-05-04 18:41:55 +02:00
fe9638bcb5 Improve Style + link 2017-05-04 18:40:53 +02:00
ecc216d15d Start to show album 2017-05-04 04:45:44 +02:00
87df9af88a Calc size 2017-05-04 03:44:54 +02:00
84f254d378 Most played song - good way to retrieve data 2017-05-04 03:18:53 +02:00
49aed4575b Start dashboard on ELS data 2017-05-04 02:52:02 +02:00
726cf70a84 Start dashboard from tuto 2017-04-28 02:24:21 +02:00
134 changed files with 34391 additions and 25699 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)

13
dashboard/.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

43
dashboard/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
yarn-error.log
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db

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

@@ -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

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

44
dashboard/karma.conf.js Normal file
View File

@@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
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
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/dashboard'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

30205
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
dashboard/package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "dashboard",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@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",
"rxjs": "~6.6.0",
"zone.js": "~0.10.2",
"bootstrap": "^3.3.7",
"tslib": "^2.0.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1100.4",
"@angular/cli": "~11.0.4",
"@angular/compiler-cli": "~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

@@ -0,0 +1,63 @@
POST http://localhost:9200/itunessongs/album/_search
content-type: application/json
User-Agent: vscode-restclient
{
"query": {
"match" : {
"Album" : {
"query": "Crystal Castles",
"operator" : "and"
}
}
},
"size": 20
}
POST http://localhost:9200/itunessongs/song/_search
content-type: application/json
User-Agent: vscode-restclient
{
"query": {
"match_phrase" : {
"Album" : "Crystal Castles"
}
},
"size": 20
}
POST http://localhost:9200/itunessongs/song/_search
content-type: application/json
User-Agent: vscode-restclient
{
"query": {
"bool": {
"must": [
{"match_phrase" : { "Album" : "Crystal Castles" }},
{"match_phrase" : { "Artist" : "Crystal Castles" }}
]
}
},
"size": 20
}
POST http://localhost:9200/itunessongs/song/_search
content-type: application/json
User-Agent: vscode-restclient
{
"query": {
"bool": {
"must": {
"match_phrase" : { "Album" : "Crystal Castles" }
},
"should" : [
{"match_phrase" : { "Artist" : "Crystal Castles" }},
{"match_phrase" : { "Album Artist" : "Crystal Castles" }}
]
}
},
"size": 20
}

View File

@@ -0,0 +1,92 @@
<div class="container">
<h1>{{albumName}}</h1>
<div class="alert alert-warning">
<h3>Debug Zone</h3>
Returned song: {{songs.length}}<br />
Theorical number song: {{ album ? album['Track Count'] : "" }}
<span *ngIf="album && (songs.length == album['Track Count'])" class="glyphicon glyphicon-ok" style="color:green"></span>
<span *ngIf="album && (songs.length != album['Track Count'])" class="glyphicon glyphicon-remove" style="color:red"></span>
</div>
<div class="row cardAdmin">
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-star stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!album"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="album">{{album.Rating}}/100</h3>
</div>
<div><br>Rating</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-list-alt stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!album"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="album">{{album['Track Count']}}</h3>
</div>
<div><br>Songs</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-purple">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-headphones stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!album"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="album">{{album['Play Count']}} list.</h3>
</div>
<div><br>~{{album['Play Count'] / album['Track Count'] | number:'1.0-0'}} listening avg.</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-blue">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-time stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!album"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="album">{{album['Total Time'] | convertMs}}</h3>
</div>
<div><br>Album time</div>
</div>
</div>
</div>
</div>
</div>
</div>
<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>
</div>

View File

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

View File

@@ -0,0 +1,72 @@
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 './../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/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'
// Prevent useless data load + activate button in interface var
moreDataAvailable = true;
lockLoadData = false;
constructor(
private elsService: ElsService,
private route: ActivatedRoute,
private location: Location
) { }
ngOnInit(): void {
this.route.params
.subscribe((params: Params) => this.albumName = params['name']);
this.loadSongs();
this.elsService.getAlbum(this.albumName).subscribe(data => this.album = data);
}
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;
// Erase song array with result for first load, then add elements one by one
// instead use concat => concat will sort table at each load, very consuming! and not user friendly
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;
}
);
}
}

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

@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
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: 'top-played', component: TopPlayedComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}

View File

@@ -0,0 +1,45 @@
h1 {
font-size: 1.2em;
color: #999;
margin-bottom: 0;
}
h2 {
font-size: 2em;
margin-top: 0;
padding-top: 0;
}
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
margin-left: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
/*
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
} */
/* FIXME Code Repetition */
.btn-top {
display: inline-block;
position: fixed;
z-index: 1;
-webkit-transition: opacity .3s 0s, visibility 0s 0s;
-moz-transition: opacity .3s 0s, visibility 0s 0s;
transition: opacity .3s 0s, visibility 0s 0s;
}

View File

@@ -0,0 +1,5 @@
<nav>
<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

@@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
}

View File

@@ -0,0 +1,54 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
import { AppComponent } from './app.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 './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';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
FormsModule,
AppRoutingModule
],
declarations: [
AppComponent,
DashboardComponent,
AlbumComponent,
AlbumsComponent,
ArtistComponent,
GenreComponent,
SongTableComponent,
ConvertMsPipe,
ConvertMoreExactPipe,
SortByPipe,
ConvertSizeToStringPipe,
TopPlayedComponent,
RoundPipe
],
providers: [
ElsService,
ElsAlbumService
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@@ -0,0 +1,97 @@
<div class="container">
<h1>{{artistName}}</h1>
<div class="alert alert-warning">
<h3>Debug Zone</h3>
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 col-sm-6">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-star stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!artist"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="artist">{{artist.Rating}}/100</h3>
</div>
<div>Rating</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-blue">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-cd stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!artist"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="artist">{{artist.Album?.length}}</h3>
</div>
<div>Albums</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-list-alt stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!artist"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="artist">{{artist['Track Count']}}</h3>
</div>
<div>Songs</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3 col-sm-6">
<div class="panel panel-purple">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-headphones stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!artist"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="artist">{{artist['Play Count']}} list.</h3>
</div>
<div *ngIf="artist">~{{artist['Play Count'] / artist['Track Count'] | number:'1.0-0'}} listening avg.</div>
<div *ngIf="!artist">Listening</div>
</div>
</div>
</div>
</div>
</div>
</div>
<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>
</div>

View File

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

View File

@@ -0,0 +1,76 @@
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 './../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/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();
lockLoadData = false;
countSong: number;
constructor(
private elsService: ElsService,
private route: ActivatedRoute,
private location: Location
) { }
ngOnInit(): void {
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();
}
// TODO Duplicate code!
loadSongs(): void {
if (this.lockLoadData) {
console.log('Loading data locked');
return;
}
if (!this.moreDataAvailable) {
return;
}
this.lockLoadData = true;
this.elsService.getArtistSongs(this.artistName, this.songs.length).subscribe(
data => {
this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE;
// Erase song array with result for first load, then add elements one by one
// instead use concat => concat will sort table at each load, very consuming! and not user friendly
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;
}
);
}
}

View File

@@ -0,0 +1,38 @@
.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

@@ -0,0 +1,177 @@
<div class="container">
<h1>iTunes stats</h1>
<br />
<div class="row cardAdmin">
<div class="col-lg-3 col-md-3">
<div class="panel panel-blue">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-time stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!totalTime"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="totalTime">{{totalTime | convertMs}}</h3>
</div>
<div><br>Total time ({{totalTime | convertMoreExact}})</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="panel panel-green">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-hdd stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!totalSize"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="totalSize">{{totalSize | convertSizeToString}}</h3>
</div>
<div><br>Total size</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="panel panel-yellow">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-play stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!trackCountSong"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="trackCountSong">{{trackCountSong}}</h3>
</div>
<div><br>Total Songs</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="panel panel-red">
<div class="panel-heading">
<div class="row">
<div class="col-xs-3">
<span class="glyphicon glyphicon-stop stats_icon"></span>
</div>
<div class="col-xs-9 text-right">
<div>
<h3 *ngIf="!neverListenSong"><span class="glyphicon glyphicon-refresh loading"></span></h3>
<h3 *ngIf="neverListenSong">{{neverListenSong}}</h3>
</div>
<div><br>Never list. songs (~{{neverListenSong / trackCountSong * 100 | round}}%)</div>
</div>
</div>
</div>
</div>
</div>
</div>
<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"
list="dynmicUserIds"
[(ngModel)]="searchTerm"
(keyup)="onSearchChange()"
(change)="onSearchSelected($event)">
<datalist id="dynmicUserIds">
<option *ngFor="let item of suggested" [value]="item.name" [label]="item.type" [id]="item">{{item}}</option>
</datalist>
</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>
<tr *ngFor="let album of lastAddedAlbums">
<td><a [routerLink]="['/album', album.key]">{{album.key}}</a></td>
<td>{{album.doc_count}}</td>
<td><a [routerLink]="['/artist', albumArtists[album.key]]">{{albumArtists[album.key]}}</a></td>
</tr>
</tbody>
</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>
<h3><a [routerLink]="['/top-played']">Top Played page</a></h3>
</div>
<div class="col-md-6">
<h3>Top Genres</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Genre</th>
<th>Track Count</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let genre of topGenres">
<td><a [routerLink]="['/genre', genre.key]">{{genre.key}}</a></td>
<td>{{genre.doc_count}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<h3>Bottom Genres</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Genre</th>
<th>Track Count</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let genre of bottomGenres">
<td><a [routerLink]="['/genre', genre.key]">{{genre.key}}</a></td>
<td>{{genre.doc_count}}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,136 @@
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 { Album } from './../model/album';
import { Artist } from './../model/artist';
import { Suggested } from '../model/suggested';
@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) + '...';
}
});
}
onSearchChange() {
this.elsService.getSuggest(this.searchTerm).subscribe(result => this.suggested = result);
}
onSearchSelected($event) {
let selected = $event.target.value
// FIXME Not possible to get good element (just value)
// Need to use a plugin to do this correctly
this.suggested.forEach(element => {
if (element.name == selected) {
this.route.navigate(['/' + element.type + '/' + element.name])
}
});
}
}

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

@@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { ElsService } from './els.service';
describe('ElsService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ElsService]
});
});
it('should be created', inject([ElsService], (service: ElsService) => {
expect(service).toBeTruthy();
}));
});

View File

@@ -0,0 +1,516 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http'
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Song } from './model/song';
import { Album } from './model/album';
import { Artist } from './model/artist';
import { Bucket } from './model/bucket';
import { Suggested } from './model/suggested';
@Injectable()
export class ElsService {
public static readonly DEFAULT_SIZE: number = 50;
public static readonly SONG_INDEX_NAME = '/itunes-songs';
public static readonly ARTIST_INDEX_NAME = '/itunes-artists';
public static readonly ALBUM_INDEX_NAME = '/itunes-albums';
public static readonly SUGGEST_INDEX_NAME = '/itunes-suggest';
protected static readonly ACTION_SEARCH = '/_search';
protected static readonly ACTION_COUNT = '/_count';
protected elsUrl = 'http://localhost:9200';
protected headers = new HttpHeaders({'Content-Type': 'application/json'});
constructor(protected http: HttpClient) { }
getTime(): Promise<number> {
return this.http
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
aggs: {
sum_time: {
sum: { field: 'Total Time'}
}
},
'size': 0
}), {headers: this.headers})
.toPromise()
.then(res => res.aggregations.sum_time.value as number)
.catch(error => this.handleError(error, 'getTime()'));
}
getSize(): Promise<number> {
return this.http
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
aggs: {
sum_time: {
sum: { field: 'Size' }
}
},
'size': 0
}), {headers: this.headers})
.toPromise()
.then(res => res.aggregations.sum_time.value as number)
.catch(error => this.handleError(error, 'getSize()'));
}
getCountSong(index: string): Promise<number> {
return this.http
.get<any>(this.elsUrl + index + ElsService.ACTION_COUNT)
.toPromise()
.then(res => res.count as number)
.catch(error => this.handleError(error, 'getCountSong(' + index + ')'));
}
getCountNeverListenSong(): Promise<number> {
return this.http
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_COUNT,
JSON.stringify({
'query': {
'bool': {
'must_not': {
'exists': { 'field': 'Play Count'}
}
}
}
}), {headers: this.headers})
.toPromise()
.then(res => res.count as number)
.catch(error => this.handleError(error, 'getCountNeverListenSong()'));
}
getMostPlayedTrack(): Observable<Song[]> {
// Thank to http://chariotsolutions.com/blog/post/angular2-observables-http-separating-services-components/
// for the map part
return this.http
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [ {
'Play Count': {
'order': 'desc'
}
} ],
'size': 5
}), {headers: this.headers})
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getMostPlayedTrack()'))
);
}
/**
* A basic get of albums ordered by 'Play Count' field.
*/
getMostPlayedAlbumNaive(): Promise<Album[]> {
return this.http
.get(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH + '?sort=Play Count:desc&size=20')
.toPromise()
.then(res => this.responseToAlbums(res))
.catch(error => this.handleError(error, 'getMostPlayedAlbumNaive'));
// TODO Excluse 'Divers' + compilation
}
/**
* More complicated query to calculate Avg. Play and get top ordrered by this results.
* Result is in '.hits.hits[].sort' field - not casted by conversion method
*/
getMostPlayedAlbum(): Observable<Album[]> {
return this.http
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [
{
'_script': {
'type': 'number',
'script': {
'inline': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
},
'order': 'desc'
}
}
]
}), {headers: this.headers})
.pipe(
map(res => this.responseToAlbums(res)),
catchError(error => this.handleError(error, 'getMostPlayedAlbum()'))
);
}
getMostPlayedArtistNaive(): Promise<Artist[]> {
return this.http
.get(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH + '?sort=Play Count:desc&size=20')
.toPromise()
.then(res => this.responseToAlbums(res))
.catch(error => this.handleError(error, 'getMostPlayedArtistNaive'));
// TODO Excluse 'Divers' + compilation
}
getMostPlayedArtist(): Observable<Artist[]> {
return this.http
.post(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'sort': [
{
'_script': {
'type': 'number',
'script': {
'inline': 'doc[\'Play Count\'].value / doc[\'Track Count\'].value'
},
'order': 'desc'
}
},
],
'size': 100
}), {headers: this.headers})
.pipe(
map(res => this.responseToArtists(res)),
catchError(error => this.handleError(error, 'getMostPlayedArtist()'))
);
}
getAlbumSongs(albumName: string, from: number = 0): Observable<Song[]> {
console.info('getAlbumSongs- Album name: ' + albumName + ' - from: ' + from);
return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Album': albumName }
},
'size': ElsService.DEFAULT_SIZE,
'from': from
}), {headers: this.headers})
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getAlbumSongs(' + albumName + ',' + from + ')'))
);
}
getGenreSongs(genreName: string, from: number = 0): Observable<Song[]> {
console.info('getGenreSongs- Genre name: ' + genreName + ' - from: ' + from);
// TODO Code repetition => to refactor
return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Genre': genreName }
},
'size': ElsService.DEFAULT_SIZE,
'from': from
}), {headers: this.headers})
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getAlbumSongs(' + genreName + ',' + from + ')'))
);
}
getArtistSongs(artistName: string, from: number = 0): Observable<Song[]> {
console.info('getArtistSongs- Artist name: ' + artistName + ' - from: ' + from);
return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'bool': {
'should': [
{'match_phrase' : { 'Album Artist' : artistName }},
{'match_phrase' : { 'Artist' : artistName }}
]
}
},
'size': ElsService.DEFAULT_SIZE,
'from': from
}), {headers: this.headers})
.pipe(
map(res => this.responseToSongs(res)),
catchError(error => this.handleError(error, 'getArtistSongs(' + artistName + ',' + from + ')'))
);
}
getAlbum(albumName: string): Observable<Album> {
// TODO Why this is used on album pages?
// When it's commented, the album information goes up correctly */
return this.http
.post(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Album': albumName }
},
'size': ElsService.DEFAULT_SIZE
}), {headers: this.headers})
.pipe(
map(res => this.responseToOneTypedResult<Album>(res, albumName)),
catchError(error => this.handleError(error, 'getAlbum(' + albumName + ')'))
);
}
getArtist(artistName: string): Observable<Artist> {
return this.http
.post(this.elsUrl + ElsService.ARTIST_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase': { 'Artist': artistName }
},
'size': ElsService.DEFAULT_SIZE
}), {headers: this.headers})
.pipe(
map(res => this.responseToOneTypedResult<Artist>(res, artistName)),
catchError(error => this.handleError(error, 'getArtist(' + artistName + ')'))
);
}
getGenres(ordering: string = 'desc'): Observable<Bucket[]> {
return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'aggs' : {
'genres' : {
'terms' : {
'field' : 'Genre',
'size' : 10,
'missing': 'N/A',
'order': { '_count' : ordering }
}
}
},
'size': 0
}), {headers: this.headers})
.pipe(
map(res => this.responseAggregationToBucket(res, 'genres')),
catchError(error => this.handleError(error, 'getGenres(' + ordering + ')'))
);
}
// getGenreCount(): Observable<number> {
// return this.http
// .post<any>(this.elsUrl + 'song' + ElsService.ACTION_SEARCH,
// JSON.stringify({
// 'aggs' : {
// 'genres' : {
// 'cardinality' : {
// 'field' : 'Genre.original',
// 'missing': 'N/A',
// }
// }
// },
// 'size': 0
// }), {headers: this.headers})
// .pipe(
// map(res => res.aggregations.genres.value)
// );
// }
getLastAddedAlbums(month: number): Observable<Bucket[]> {
return this.http
.post(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'range' : {
'Date Added' : {
'gte' : 'now-' + month + 'M/d',
'lte' : 'now/d'
}
}
},
'size': 0,
'aggs': {
'date' : {
'terms': {
'field' : 'Date Added',
'order': { '_term': 'desc' },
'size': 20
},
'aggs': {
'album': {
'terms': {
'field': 'Album.raw'
}
}
}
}
}
}), {headers: this.headers})
.pipe(
map(res => this.responseSubAggregationToBucket(res, 'date')),
catchError(error => this.handleError(error, 'getLastAddedAlbums(' + month + ')' ))
);
// TODO Take in consideration "sum_other_doc_count"
}
getArtistFromAlbumName(albumname: string): Observable<Album[]> {
return this.http
.post<any>(this.elsUrl + ElsService.ALBUM_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'query': {
'match_phrase' : {
'Album' : albumname
}
}
}), {headers: this.headers})
.pipe(
map(res => res.hits.hits),
map((hits: Array<any>) => {
// TODO Use a method (duplicated code ?)
const result: Array<Album> = [];
hits.forEach((hit) => {
result.push(hit._source);
});
return result;
}),
catchError(error => this.handleError(error, 'getArtistFromAlbumName' + albumname + ')'))
);
}
getCountArtistSong(artistName: string): Observable<number> {
console.log('artistname: ' + artistName);
return this.http
.post<any>(this.elsUrl + ElsService.SONG_INDEX_NAME + ElsService.ACTION_COUNT,
JSON.stringify({
'query': {
'bool': {
'should': [
{'match_phrase' : { 'Album Artist' : artistName }},
{'match_phrase' : { 'Artist' : artistName }}
]
}
}
}), {headers: this.headers})
.pipe(
map(res => res.count as number),
catchError(error => this.handleError(error, 'getCountArtistSong' + artistName + ')'))
);
}
getSuggest(text: string): Observable<Suggested[]> {
console.log('search sugget: ' + text);
return this.http
.post<any>(this.elsUrl + ElsService.SUGGEST_INDEX_NAME + ElsService.ACTION_SEARCH,
JSON.stringify({
'_source': ['album', 'artist'],
'suggest': {
'album-suggest': {
'prefix': text,
'completion': {
'field': 'album_suggest'
}
},
'artist-suggest': {
'prefix': text,
'completion': {
'field': 'artist_suggest'
}
}
}
}), {headers: this.headers})
.pipe(
map(res => this.responseSuggesterToSuggested(res, 'album-suggest', 'artist-suggest')),
catchError(error => this.handleError(error, 'getSuggest(' + text + ')'))
);
}
/** Process a result to return just one result.
* Used to get an album or an artist.
* Take a name to put in console output if no result or more than one result.
*
* @param res Response to process
* @param name The searched name - for console output
*/
private responseToOneTypedResult<T>(res: any, name: string): T {
const hits = res.hits.hits;
if (hits.length < 1) {
console.info('No result found for name: "' + name);
return undefined;
}
if (hits.length > 1) {
// TODO Cumul results (for album)
console.error('More than one result for name: "' + name + '". Found (' + hits.length + '), return the first.');
}
return hits[0]._source;
}
/** Process a response to a array of songs.
*
* @param res Response to process
*/
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

@@ -0,0 +1,5 @@
<div class="container">
<h1>{{genreName}}</h1>
<app-song-table [songs]=songs (atBottom)=loadSongs()></app-song-table>
</div>

View File

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

View File

@@ -0,0 +1,50 @@
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 '../model/song';
@Component({
selector: 'app-genre',
templateUrl: './genre.component.html',
styleUrls: ['./genre.component.css']
})
export class GenreComponent implements OnInit {
@ViewChild(SongTableComponent) songtable: SongTableComponent;
genreName = '';
songs: Array<Song> = [];
constructor(
private elsService: ElsService,
private route: ActivatedRoute
) { }
ngOnInit() {
this.route.params
.subscribe((params: Params) => this.genreName = params['name']);
this.loadSongs();
}
loadSongs(): any {
this.elsService.getGenreSongs(this.genreName, this.songs.length).subscribe(
data => {
// this.moreDataAvailable = data.length === ElsService.DEFAULT_SIZE;
// Erase song array with result for first load, then add elements one by one
// instead use concat => concat will sort table at each load, very consuming! and not user friendly
if (this.songs.length === 0) {
this.songs = data;
} else {
data.forEach(song => {
this.songtable.setSortable(true);
this.songs.push(song);
});
}
}
);
}
}

View File

@@ -0,0 +1,14 @@
export class Album {
Name: string;
Album: string;
Artist: Array<string>;
Rating: number;
Genre: Array<string>;
'Track Count': number;
'Album Rating': number;
'Persistent ID': string;
'Album Rating Computed': boolean;
'Play Count': number;
'Total Time': number;
'Album Artist': string;
}

View File

@@ -0,0 +1,10 @@
export class Artist {
Name: string;
Album: string[];
Artist: string;
Rating: number;
Genre: Array<string>;
'Track Count': number;
'Persistent ID': string;
'Play Count': number;
}

View File

@@ -0,0 +1,4 @@
export class Bucket {
key: string;
doc_count: number;
}

View File

@@ -0,0 +1,8 @@
export class Song {
Name: string;
Artist: string;
'Play Count': number;
Album: string;
'Track Number': number; // TODO Default property
'Disc Number': number;
}

View File

@@ -0,0 +1,8 @@
export class Suggested {
name: string;
type: string;
public toString() : string {
return `${this.name} (${this.type})`;
}
}

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,8 @@
import { SortByPipe } from './sort-by.pipe';
describe('SortByPipe', () => {
it('create an instance', () => {
const pipe = new SortByPipe();
expect(pipe).toBeTruthy();
});
});

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

@@ -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);
}
}

View File

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

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