diff --git a/Selector.Web/Pages/Now.cshtml b/Selector.Web/Pages/Now.cshtml
index f75e347..5622181 100644
--- a/Selector.Web/Pages/Now.cshtml
+++ b/Selector.Web/Pages/Now.cshtml
@@ -22,19 +22,37 @@
:track="lastfmTrack"
:username="playCount.username"
v-if="playCount !== null && playCount !== undefined">
+
+
+
+
+
+
-
diff --git a/Selector.Web/appsettings.json b/Selector.Web/appsettings.json
index 1e809b2..2ba4c58 100644
--- a/Selector.Web/appsettings.json
+++ b/Selector.Web/appsettings.json
@@ -9,6 +9,11 @@
"Selector": {
"Redis": {
"enabled": true
+ },
+ "Now": {
+ "ArtistResampleWindow": "14.00:00:00",
+ "AlbumResampleWindow": "14.00:00:00",
+ "TrackResampleWindow": "14.00:00:00"
}
},
"AllowedHosts": "*"
diff --git a/Selector.Web/package-lock.json b/Selector.Web/package-lock.json
index a5198e2..d864995 100644
--- a/Selector.Web/package-lock.json
+++ b/Selector.Web/package-lock.json
@@ -12,9 +12,12 @@
"@microsoft/signalr": "^6.0.0",
"bootstrap": "^5.1.3",
"chart.js": "^3.6.0",
+ "chartjs-adapter-luxon": "^1.1.0",
+ "luxon": "^2.4.0",
"vue": "^3.2.22"
},
"devDependencies": {
+ "@types/luxon": "^2.3.2",
"clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0",
"sass": "^1.44.0",
@@ -111,6 +114,12 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
+ "node_modules/@types/luxon": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz",
+ "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
+ "dev": true
+ },
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -707,6 +716,15 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
},
+ "node_modules/chartjs-adapter-luxon": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.1.0.tgz",
+ "integrity": "sha512-CS+xBWEyXYVLBZ3dSY/MwlSXhz8er4JjkApazY84ft/++oOLsmkt6TaXBCsUFudum7QdoYmpxiL/gSp20+emkw==",
+ "peerDependencies": {
+ "chart.js": "^3.0.0",
+ "luxon": "^1.0.0 || ^2.0.0"
+ }
+ },
"node_modules/chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@@ -1568,6 +1586,14 @@
"node": ">=10"
}
},
+ "node_modules/luxon": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz",
+ "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@@ -2879,6 +2905,12 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
+ "@types/luxon": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz",
+ "integrity": "sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA==",
+ "dev": true
+ },
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
@@ -3385,6 +3417,12 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
},
+ "chartjs-adapter-luxon": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.1.0.tgz",
+ "integrity": "sha512-CS+xBWEyXYVLBZ3dSY/MwlSXhz8er4JjkApazY84ft/++oOLsmkt6TaXBCsUFudum7QdoYmpxiL/gSp20+emkw==",
+ "requires": {}
+ },
"chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@@ -4026,6 +4064,11 @@
"yallist": "^4.0.0"
}
},
+ "luxon": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz",
+ "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA=="
+ },
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
diff --git a/Selector.Web/package.json b/Selector.Web/package.json
index bd596b1..66b02d2 100644
--- a/Selector.Web/package.json
+++ b/Selector.Web/package.json
@@ -23,9 +23,12 @@
"@microsoft/signalr": "^6.0.0",
"bootstrap": "^5.1.3",
"chart.js": "^3.6.0",
+ "chartjs-adapter-luxon": "^1.1.0",
+ "luxon": "^2.4.0",
"vue": "^3.2.22"
},
"devDependencies": {
+ "@types/luxon": "^2.3.2",
"clean-webpack-plugin": "^4.0.0",
"file-loader": "^6.2.0",
"sass": "^1.44.0",
diff --git a/Selector.Web/scripts/Now/ArtistBreakdownGraph.ts b/Selector.Web/scripts/Now/ArtistBreakdownGraph.ts
new file mode 100644
index 0000000..506c254
--- /dev/null
+++ b/Selector.Web/scripts/Now/ArtistBreakdownGraph.ts
@@ -0,0 +1,71 @@
+import * as Vue from "vue";
+import { Chart, DoughnutController, ArcElement } from "chart.js";
+import { PlayCount } from "scripts/HubInterfaces";
+
+Chart.register(DoughnutController, ArcElement);
+
+const pieColours = ['#7a99c2',
+ '#a34c77',
+ '#598556',
+];
+
+export let ArtistBreakdownChartCard: Vue.Component = {
+ props: ['play_count'],
+ data() {
+ return {
+
+ }
+ },
+ computed: {
+ trackPercent() {
+ return this.play_count.track * 100 / this.play_count.artist
+ },
+ albumPercent() {
+ return this.play_count.album * 100 / this.play_count.artist
+ },
+ albumDiff() {
+ return this.albumPercent - this.trackPercent;
+ },
+ artistPercent() {
+ return 100 - this.albumDiff + this.trackPercent;
+ }
+ },
+ template:
+ `
+
+
+
+
+ `,
+ mounted() {
+ new Chart(`artist-breakdown`, {
+ type: "doughnut",
+ data: {
+ labels: [ "track", "album", "artist" ],
+ datasets: [{
+ data: [ this.trackPercent, this.albumDiff, this.artistPercent ],
+ }]
+ },
+ options: {
+ plugins: {
+ legend : {
+ display : true,
+ labels: {
+ color: 'white'
+ }
+ }
+ },
+ layout: {
+ padding: 20
+ },
+ elements: {
+ arc : {
+ backgroundColor: pieColours,
+ borderWidth: 2,
+ borderColor: 'rgb(0, 0, 0)'
+ }
+ }
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/Selector.Web/scripts/Now/PlayCountGraph.ts b/Selector.Web/scripts/Now/PlayCountGraph.ts
index ab09b11..aa7182c 100644
--- a/Selector.Web/scripts/Now/PlayCountGraph.ts
+++ b/Selector.Web/scripts/Now/PlayCountGraph.ts
@@ -1,24 +1,22 @@
import * as Vue from "vue";
import { Chart, PointElement, LineElement, LineController, CategoryScale, LinearScale, TimeSeriesScale } from "chart.js";
+import 'chartjs-adapter-luxon';
import { CountSample } from "scripts/HubInterfaces";
+import { ScrobbleDataSeries } from "scripts/now";
Chart.register(LineController, CategoryScale, LinearScale, TimeSeriesScale, PointElement, LineElement);
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
export let PlayCountChartCard: Vue.Component = {
- props: ['data_points', 'title', 'chart_id', 'link'],
+ props: ['data_points', 'title', 'chart_id', 'link', 'earliest_date', 'latest_date', 'colour'],
data() {
return {
chartData: {
- labels: this.data_points.map((e: CountSample) => {
- var date = new Date(e.timeStamp);
-
- return `${months[date.getMonth()]} ${date.getFullYear()}`;
- }),
datasets: [{
- // label: '# of Votes',
- data: this.data_points.map((e: CountSample) => e.value),
+ data: this.data_points.map((e: CountSample) => {
+ return {x: e.timeStamp, y: e.value};
+ }),
}]
}
}
@@ -43,25 +41,85 @@ export let PlayCountChartCard: Vue.Component = {
options: {
elements: {
line: {
- borderWidth: 4,
- borderColor: "#a34c77",
- backgroundColor: "#727272",
+ borderWidth: 5,
+ borderColor: this.colour,
borderCapStyle: "round",
borderJoinStyle: "round"
},
- // point: {
- // radius: 4,
- // pointStyle: "circle",
- // borderColor: "black",
- // backgroundColor: "white"
- // }
+ point: {
+ radius: 0
+ }
},
scales: {
yAxis: {
suggestedMin: 0
+ },
+ xAxis: {
+ type: 'time',
+ min: this.earliest_date,
+ max: this.latest_date
}
}
}
})
}
-}
\ No newline at end of file
+}
+
+export let CombinedPlayCountChartCard: Vue.Component = {
+ props: ['data_points', 'title', 'chart_id', 'link', 'earliest_date'],
+ data() {
+ return {
+ chartData: {
+ datasets: this.data_points.map((series: ScrobbleDataSeries) => {
+ return {
+ label: series.label,
+ borderColor: series.colour,
+ data: series.data.map((e: CountSample) => {
+ return {x: e.timeStamp, y: e.value};
+ })
+ }
+ })
+ }
+ }
+ },
+ computed: {
+ chartId() {
+ return "play-count-chart-" + this.chart_id;
+ }
+ },
+ template:
+ `
+
+
+
+
+ `,
+ mounted() {
+ new Chart(`play-count-chart-${this.chart_id}`, {
+ type: "line",
+ data: this.chartData,
+ options: {
+ elements: {
+ line: {
+ borderWidth: 5,
+ borderColor: "#a34c77",
+ borderCapStyle: "round",
+ borderJoinStyle: "round"
+ },
+ point: {
+ radius: 0
+ }
+ },
+ scales: {
+ yAxis: {
+ suggestedMin: 0
+ },
+ xAxis: {
+ type: 'time',
+ min: this.earliest_date
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/Selector.Web/scripts/now.ts b/Selector.Web/scripts/now.ts
index c799958..c6a1d0c 100644
--- a/Selector.Web/scripts/now.ts
+++ b/Selector.Web/scripts/now.ts
@@ -1,11 +1,13 @@
import * as signalR from "@microsoft/signalr";
import * as Vue from "vue";
-import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces";
+import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO, CountSample } from "./HubInterfaces";
import NowPlayingCard from "./Now/NowPlayingCard";
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
-import { PlayCountChartCard } from "./Now/PlayCountGraph";
+import { PlayCountChartCard, CombinedPlayCountChartCard } from "./Now/PlayCountGraph";
+import { ArtistBreakdownChartCard } from "./Now/ArtistBreakdownGraph";
import { PlayCountCard, LastFmLogoLink } from "./Now/LastFm";
import BaseInfoCard from "./Now/BaseInfoCard";
+import { DateTime } from "luxon";
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub")
@@ -28,6 +30,12 @@ interface NowPlaying {
cards: InfoCard[]
}
+export interface ScrobbleDataSeries {
+ label: string,
+ colour: string,
+ data: CountSample[]
+}
+
const app = Vue.createApp({
data() {
return {
@@ -46,14 +54,6 @@ const app = Vue.createApp({
album_artist: this.currentlyPlaying.track.album.artists[0].name,
};
},
- lastfmArtist(){
-
- // if(this.currentlyPlaying.track.artists[0].length > 0)
- {
- return this.currentlyPlaying.track.artists[0].name;
- }
- return "";
- },
showArtistChart(){
return this.playCount !== null && this.playCount !== undefined && this.playCount.artistCountData.length > 3;
},
@@ -62,6 +62,33 @@ const app = Vue.createApp({
},
showTrackChart(){
return this.playCount !== null && this.playCount !== undefined && this.playCount.trackCountData.length > 3;
+ },
+ earliestDate(){
+ return this.playCount.artistCountData[0].timeStamp;
+ },
+ latestDate(){
+ return this.playCount.artistCountData.at(-1).timeStamp;
+ },
+ trackGraphTitle() { return `${this.currentlyPlaying.track.name} 🎵`},
+ albumGraphTitle() { return `${this.currentlyPlaying.track.album.name} 💿`},
+ artistGraphTitle() { return `${this.currentlyPlaying.track.artists[0].name} 🎤`},
+ combinedData(){
+ return [
+ {
+ label: "artist",
+ colour: "#598556",
+ data: this.playCount.artistCountData
+ },
+ {
+ label: "album",
+ colour: "#a34c77",
+ data: this.playCount.albumCountData
+ },
+ {
+ label: "track",
+ colour: "#7a99c2",
+ data: this.playCount.trackCountData
+ }];
}
},
created() {
@@ -123,4 +150,6 @@ app.component("spotify-logo", SpotifyLogoLink);
app.component("lastfm-logo", LastFmLogoLink);
app.component("play-count-card", PlayCountCard);
app.component("play-count-chart-card", PlayCountChartCard);
+app.component("play-count-chart-card-comb", CombinedPlayCountChartCard);
+app.component("artist-breakdown", ArtistBreakdownChartCard);
const vm = app.mount('#app');
\ No newline at end of file
diff --git a/Selector/Options.cs b/Selector/Options.cs
index 5bd0279..a135b6a 100644
--- a/Selector/Options.cs
+++ b/Selector/Options.cs
@@ -5,9 +5,9 @@ namespace Selector
{
public const string Key = "Now";
- public TimeSpan ArtistResampleWindow { get; set; } = TimeSpan.FromDays(30);
- public TimeSpan AlbumResampleWindow { get; set; } = TimeSpan.FromDays(30);
- public TimeSpan TrackResampleWindow { get; set; } = TimeSpan.FromDays(30);
+ public TimeSpan ArtistResampleWindow { get; set; } = TimeSpan.FromDays(7);
+ public TimeSpan AlbumResampleWindow { get; set; } = TimeSpan.FromDays(7);
+ public TimeSpan TrackResampleWindow { get; set; } = TimeSpan.FromDays(7);
public TimeSpan ArtistDensityWindow { get; set; } = TimeSpan.FromDays(10);
public decimal ArtistDensityThreshold { get; set; } = 5;