From c49e561ae0cb8ac96b59e9ecf268eae7a5d6068b Mon Sep 17 00:00:00 2001 From: Andy Pack Date: Mon, 10 Oct 2022 11:44:47 +0100 Subject: [PATCH 1/2] adding past page skeleton with hub --- Selector.Web/CSS/index.scss | 1 + Selector.Web/CSS/past.scss | 26 ++++++ Selector.Web/Hubs/PastHub.cs | 113 +++++++++++++++++++++++ Selector.Web/Options.cs | 2 + Selector.Web/Pages/Past.cshtml | 37 ++++++++ Selector.Web/Pages/Past.cshtml.cs | 20 ++++ Selector.Web/Pages/Shared/_Layout.cshtml | 7 +- Selector.Web/Past/ChartEntry.cs | 10 ++ Selector.Web/Past/ChartResult.cs | 12 +++ Selector.Web/Past/PastParams.cs | 14 +++ Selector.Web/Selector.Web.csproj | 2 + Selector.Web/Startup.cs | 3 +- Selector.Web/scripts/HubInterfaces.ts | 19 ++++ Selector.Web/scripts/Past/RankCard.ts | 19 ++++ Selector.Web/scripts/now.ts | 2 +- Selector.Web/scripts/past.ts | 65 +++++++++++++ Selector.Web/webpack.common.js | 1 + Selector/Options.cs | 7 ++ 18 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 Selector.Web/CSS/past.scss create mode 100644 Selector.Web/Hubs/PastHub.cs create mode 100644 Selector.Web/Pages/Past.cshtml create mode 100644 Selector.Web/Pages/Past.cshtml.cs create mode 100644 Selector.Web/Past/ChartEntry.cs create mode 100644 Selector.Web/Past/ChartResult.cs create mode 100644 Selector.Web/Past/PastParams.cs create mode 100644 Selector.Web/scripts/Past/RankCard.ts create mode 100644 Selector.Web/scripts/past.ts diff --git a/Selector.Web/CSS/index.scss b/Selector.Web/CSS/index.scss index b8fcadc..3f30786 100644 --- a/Selector.Web/CSS/index.scss +++ b/Selector.Web/CSS/index.scss @@ -1,4 +1,5 @@ @import "now.scss"; +@import "past.scss"; body { background-color: #121212; diff --git a/Selector.Web/CSS/past.scss b/Selector.Web/CSS/past.scss new file mode 100644 index 0000000..3337708 --- /dev/null +++ b/Selector.Web/CSS/past.scss @@ -0,0 +1,26 @@ +.form-input { + width: calc(100% - 20px); + margin: 10px; + display: block; +} + +@media only screen and (min-width: 768px) { + .form-input { + width: 30%; + display: inline; + } +} + +.rank-card { + width: calc(100% - 10px); + display: block; + margin-top: 20px !important; + margin-bottom: 20px !important; +} + +@media only screen and (min-width: 768px) { + .rank-card { + width: calc(100% - 10px); + // display: inline; + } +} \ No newline at end of file diff --git a/Selector.Web/Hubs/PastHub.cs b/Selector.Web/Hubs/PastHub.cs new file mode 100644 index 0000000..5055090 --- /dev/null +++ b/Selector.Web/Hubs/PastHub.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; +using Selector.Cache; +using Selector.Model; +using StackExchange.Redis; + +namespace Selector.Web.Hubs +{ + public interface IPastHubClient + { + public Task OnRankResult(RankResult result); + } + + public class PastHub: Hub + { + private readonly IDatabaseAsync Cache; + private readonly AudioFeaturePuller AudioFeaturePuller; + private readonly PlayCountPuller PlayCountPuller; + private readonly DBPlayCountPuller DBPlayCountPuller; + private readonly ApplicationDbContext Db; + private readonly IListenRepository ListenRepository; + + private readonly IOptions pastOptions; + + public PastHub( + IDatabaseAsync cache, + AudioFeaturePuller featurePuller, + ApplicationDbContext db, + IListenRepository listenRepository, + IOptions options, + DBPlayCountPuller dbPlayCountPuller, + PlayCountPuller playCountPuller = null + ) + { + Cache = cache; + AudioFeaturePuller = featurePuller; + PlayCountPuller = playCountPuller; + DBPlayCountPuller = dbPlayCountPuller; + Db = db; + ListenRepository = listenRepository; + pastOptions = options; + } + + public async Task OnConnected() + { + + } + + public async Task OnSubmitted(PastParams param) + { + param.Track = string.IsNullOrWhiteSpace(param.Track) ? null : param.Track; + param.Album = string.IsNullOrWhiteSpace(param.Album) ? null : param.Album; + param.Artist = string.IsNullOrWhiteSpace(param.Artist) ? null : param.Artist; + + DateTime? from = param.From is string f && DateTime.TryParse(f, out var fromDate) ? fromDate.ToUniversalTime() : null; + DateTime? to = param.To is string t && DateTime.TryParse(t, out var toDate) ? toDate.ToUniversalTime() : null; + + var listenQuery = ListenRepository.GetAll( + userId: Context.UserIdentifier, + trackName: param.Track, + albumName: param.Album, + artistName: param.Artist, + from: from, + to: to + ).ToArray(); + + var artistGrouped = listenQuery + .GroupBy(x => x.ArtistName) + .Select(x => (x.Key, x.Count())) + .OrderByDescending(x => x.Item2) + .Take(20) + .ToArray(); + + var albumGrouped = listenQuery + .GroupBy(x => (x.AlbumName, x.ArtistName)) + .Select(x => (x.Key, x.Count())) + .OrderByDescending(x => x.Item2) + .Take(20) + .ToArray(); + + var trackGrouped = listenQuery + .GroupBy(x => (x.TrackName, x.ArtistName)) + .Select(x => (x.Key, x.Count())) + .OrderByDescending(x => x.Item2) + .Take(20) + .ToArray(); + + await Clients.Caller.OnRankResult(new() + { + TrackEntries = trackGrouped.Select(x => new ChartEntry() + { + Name = $"{x.Key.TrackName} - {x.Key.ArtistName}", + Value = x.Item2 + }).ToArray(), + + AlbumEntries = albumGrouped.Select(x => new ChartEntry() + { + Name = $"{x.Key.AlbumName} - {x.Key.ArtistName}", + Value = x.Item2 + }).ToArray(), + + ArtistEntries = artistGrouped.Select(x => new ChartEntry() + { + Name = x.Key, + Value = x.Item2 + }).ToArray(), + }); + } + } +} \ No newline at end of file diff --git a/Selector.Web/Options.cs b/Selector.Web/Options.cs index 388c7e7..ae4850c 100644 --- a/Selector.Web/Options.cs +++ b/Selector.Web/Options.cs @@ -10,6 +10,7 @@ namespace Selector.Web config.GetSection(RootOptions.Key).Bind(options); config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions); config.GetSection(FormatKeys(new[] { RootOptions.Key, NowPlayingOptions.Key })).Bind(options.NowOptions); + config.GetSection(FormatKeys(new[] { RootOptions.Key, PastOptions.Key })).Bind(options.PastOptions); } public static RootOptions ConfigureOptions(IConfiguration config) @@ -45,6 +46,7 @@ namespace Selector.Web public RedisOptions RedisOptions { get; set; } = new(); public NowPlayingOptions NowOptions { get; set; } = new(); + public PastOptions PastOptions { get; set; } = new(); } diff --git a/Selector.Web/Pages/Past.cshtml b/Selector.Web/Pages/Past.cshtml new file mode 100644 index 0000000..4ecc242 --- /dev/null +++ b/Selector.Web/Pages/Past.cshtml @@ -0,0 +1,37 @@ +@page +@model PastModel +@{ + ViewData["Title"] = "Past - Selector"; +} + +
+

Past

+
+
+
+ + + +
+
+ + + + +
+
+ +
+
+ +
+ + + +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/Selector.Web/Pages/Past.cshtml.cs b/Selector.Web/Pages/Past.cshtml.cs new file mode 100644 index 0000000..45adc13 --- /dev/null +++ b/Selector.Web/Pages/Past.cshtml.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +namespace Selector.Web.Pages +{ + public class PastModel : PageModel + { + private readonly ILogger Logger; + + public PastModel(ILogger logger) + { + Logger = logger; + } + + public void OnGet() + { + + } + } +} diff --git a/Selector.Web/Pages/Shared/_Layout.cshtml b/Selector.Web/Pages/Shared/_Layout.cshtml index 237c220..5c348e9 100644 --- a/Selector.Web/Pages/Shared/_Layout.cshtml +++ b/Selector.Web/Pages/Shared/_Layout.cshtml @@ -12,13 +12,16 @@ diff --git a/Selector.Web/Past/ChartEntry.cs b/Selector.Web/Past/ChartEntry.cs new file mode 100644 index 0000000..af3fed8 --- /dev/null +++ b/Selector.Web/Past/ChartEntry.cs @@ -0,0 +1,10 @@ +using System; + +namespace Selector.Web; + +public class ChartEntry +{ + public string Name { get; set; } + public int Value { get; set; } +} + diff --git a/Selector.Web/Past/ChartResult.cs b/Selector.Web/Past/ChartResult.cs new file mode 100644 index 0000000..b3fd206 --- /dev/null +++ b/Selector.Web/Past/ChartResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Selector.Web; + +public class RankResult +{ + public IEnumerable TrackEntries { get; set; } + public IEnumerable AlbumEntries { get; set; } + public IEnumerable ArtistEntries { get; set; } +} + diff --git a/Selector.Web/Past/PastParams.cs b/Selector.Web/Past/PastParams.cs new file mode 100644 index 0000000..ae5993d --- /dev/null +++ b/Selector.Web/Past/PastParams.cs @@ -0,0 +1,14 @@ +using System; + +namespace Selector.Web; + +public class PastParams +{ + public string Track { get; set; } + public string Album { get; set; } + public string Artist { get; set; } + + public string From { get; set; } + public string To { get; set; } +} + diff --git a/Selector.Web/Selector.Web.csproj b/Selector.Web/Selector.Web.csproj index 17f5ea6..362469d 100644 --- a/Selector.Web/Selector.Web.csproj +++ b/Selector.Web/Selector.Web.csproj @@ -37,10 +37,12 @@ + + diff --git a/Selector.Web/Startup.cs b/Selector.Web/Startup.cs index aea55db..90d6b49 100644 --- a/Selector.Web/Startup.cs +++ b/Selector.Web/Startup.cs @@ -159,7 +159,8 @@ namespace Selector.Web { endpoints.MapRazorPages(); endpoints.MapControllers(); - endpoints.MapHub("/hub"); + endpoints.MapHub("/nowhub"); + endpoints.MapHub("/pasthub"); }); } diff --git a/Selector.Web/scripts/HubInterfaces.ts b/Selector.Web/scripts/HubInterfaces.ts index 9f3906b..9d1102b 100644 --- a/Selector.Web/scripts/HubInterfaces.ts +++ b/Selector.Web/scripts/HubInterfaces.ts @@ -29,6 +29,25 @@ export interface PlayCount { listeningEvent: ListeningChangeEventArgs; } +export interface PastParams { + track: string; + album: string; + artist: string; + from: string; + to: string; +} + +export interface RankResult { + trackEntries: RankEntry[]; + albumEntries: RankEntry[]; + artistEntries: RankEntry[]; +} + +export interface RankEntry { + name: string; + value: number; +} + export interface CountSample { timeStamp: Date; value: number; diff --git a/Selector.Web/scripts/Past/RankCard.ts b/Selector.Web/scripts/Past/RankCard.ts new file mode 100644 index 0000000..87fa276 --- /dev/null +++ b/Selector.Web/scripts/Past/RankCard.ts @@ -0,0 +1,19 @@ +import * as Vue from "vue"; + +export let RankCard: Vue.Component = { + props: ['title', 'entries'], + computed: { + + }, + template: + ` +
+

{{ title }}

+
    +
  1. + {{ entry.name }} - {{entry.value}} +
  2. +
+
+ ` +} \ No newline at end of file diff --git a/Selector.Web/scripts/now.ts b/Selector.Web/scripts/now.ts index 8435d4e..574696f 100644 --- a/Selector.Web/scripts/now.ts +++ b/Selector.Web/scripts/now.ts @@ -9,7 +9,7 @@ import { PlayCountCard, LastFmLogoLink } from "./Now/LastFm"; import BaseInfoCard from "./Now/BaseInfoCard"; const connection = new signalR.HubConnectionBuilder() - .withUrl("/hub") + .withUrl("/nowhub") .build(); connection.start() diff --git a/Selector.Web/scripts/past.ts b/Selector.Web/scripts/past.ts new file mode 100644 index 0000000..635f347 --- /dev/null +++ b/Selector.Web/scripts/past.ts @@ -0,0 +1,65 @@ +import * as signalR from "@microsoft/signalr"; +// import { stringifyStyle } from "@vue/shared"; +import * as Vue from "vue"; +import { RankResult, RankEntry, PastParams } from "./HubInterfaces"; +import { RankCard } from "./Past/RankCard"; + +const connection = new signalR.HubConnectionBuilder() + .withUrl("/pasthub") + .build(); + +connection.start() +.then(val => { + connection.invoke("OnConnected"); +}) +.catch(err => console.error(err)); + +const app = Vue.createApp({ + data() { + return { + track: "", + album: "", + artist: "", + + from: null, + to: null, + + trackEntries: [], + albumEntries: [], + artistEntries: [], + } + }, + created() { + connection.on("OnRankResult", (result: RankResult) => + { + console.log(result); + + this.trackEntries = result.trackEntries; + this.albumEntries = result.albumEntries; + this.artistEntries = result.artistEntries; + }); + }, + methods: { + submit() { + console.log({ + "track": this.track, + "album": this.album, + "artist": this.artist, + "from": this.from, + "to": this.to, + }); + + connection.invoke("OnSubmitted", { + track: this.track, + album: this.album, + artist: this.artist, + from: this.from, + to: this.to, + } as PastParams); + } + } +}); + +app.component("rank-card", RankCard); + +const vm = app.mount('#pastapp'); \ No newline at end of file diff --git a/Selector.Web/webpack.common.js b/Selector.Web/webpack.common.js index b880012..698ae87 100644 --- a/Selector.Web/webpack.common.js +++ b/Selector.Web/webpack.common.js @@ -4,6 +4,7 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: { now: './scripts/now.ts', + past: './scripts/past.ts', nowCss: './CSS/index.scss', }, module: { diff --git a/Selector/Options.cs b/Selector/Options.cs index a135b6a..7e00573 100644 --- a/Selector/Options.cs +++ b/Selector/Options.cs @@ -18,5 +18,12 @@ namespace Selector public TimeSpan TrackDensityWindow { get; set; } = TimeSpan.FromDays(10); public decimal TrackDensityThreshold { get; set; } = 5; } + + public class PastOptions + { + public const string Key = "Past"; + + + } } -- 2.45.2 From 28fb783432f236dffd5206502a35fe6d1693d9fd Mon Sep 17 00:00:00 2001 From: Andy Pack Date: Mon, 10 Oct 2022 22:29:47 +0100 Subject: [PATCH 2/2] adding count and chart to past page --- Selector.Web/Hubs/PastHub.cs | 8 ++++++ Selector.Web/Pages/Past.cshtml | 14 ++++++++-- .../Past/{ChartResult.cs => RankResult.cs} | 4 +++ Selector.Web/scripts/HubInterfaces.ts | 2 ++ Selector.Web/scripts/Now/PlayCountGraph.ts | 6 ++-- Selector.Web/scripts/Past/CountCard.ts | 14 ++++++++++ Selector.Web/scripts/past.ts | 28 ++++++++++++------- Selector/Options.cs | 2 +- 8 files changed, 61 insertions(+), 17 deletions(-) rename Selector.Web/Past/{ChartResult.cs => RankResult.cs} (73%) create mode 100644 Selector.Web/scripts/Past/CountCard.ts diff --git a/Selector.Web/Hubs/PastHub.cs b/Selector.Web/Hubs/PastHub.cs index 5055090..4144680 100644 --- a/Selector.Web/Hubs/PastHub.cs +++ b/Selector.Web/Hubs/PastHub.cs @@ -107,6 +107,14 @@ namespace Selector.Web.Hubs Name = x.Key, Value = x.Item2 }).ToArray(), + + ResampledSeries = listenQuery + .Resample(pastOptions.Value.ResampleWindow) + //.ResampleByMonth() + .CumulativeSum() + .ToArray(), + + TotalCount = listenQuery.Length }); } } diff --git a/Selector.Web/Pages/Past.cshtml b/Selector.Web/Pages/Past.cshtml index 4ecc242..8d865a2 100644 --- a/Selector.Web/Pages/Past.cshtml +++ b/Selector.Web/Pages/Past.cshtml @@ -24,10 +24,18 @@ + + + +
- - - + + +
diff --git a/Selector.Web/Past/ChartResult.cs b/Selector.Web/Past/RankResult.cs similarity index 73% rename from Selector.Web/Past/ChartResult.cs rename to Selector.Web/Past/RankResult.cs index b3fd206..984dd5c 100644 --- a/Selector.Web/Past/ChartResult.cs +++ b/Selector.Web/Past/RankResult.cs @@ -8,5 +8,9 @@ public class RankResult public IEnumerable TrackEntries { get; set; } public IEnumerable AlbumEntries { get; set; } public IEnumerable ArtistEntries { get; set; } + + public IEnumerable ResampledSeries { get; set; } + + public int TotalCount { get; set; } } diff --git a/Selector.Web/scripts/HubInterfaces.ts b/Selector.Web/scripts/HubInterfaces.ts index 9d1102b..58a551e 100644 --- a/Selector.Web/scripts/HubInterfaces.ts +++ b/Selector.Web/scripts/HubInterfaces.ts @@ -41,6 +41,8 @@ export interface RankResult { trackEntries: RankEntry[]; albumEntries: RankEntry[]; artistEntries: RankEntry[]; + totalCount: number; + resampledSeries: CountSample[]; } export interface RankEntry { diff --git a/Selector.Web/scripts/Now/PlayCountGraph.ts b/Selector.Web/scripts/Now/PlayCountGraph.ts index aa7182c..128b210 100644 --- a/Selector.Web/scripts/Now/PlayCountGraph.ts +++ b/Selector.Web/scripts/Now/PlayCountGraph.ts @@ -31,7 +31,7 @@ export let PlayCountChartCard: Vue.Component = {

{{ title }}

- +
`, mounted() { @@ -56,8 +56,8 @@ export let PlayCountChartCard: Vue.Component = { }, xAxis: { type: 'time', - min: this.earliest_date, - max: this.latest_date + // min: this.earliest_date, + // max: this.latest_date } } } diff --git a/Selector.Web/scripts/Past/CountCard.ts b/Selector.Web/scripts/Past/CountCard.ts new file mode 100644 index 0000000..b39657e --- /dev/null +++ b/Selector.Web/scripts/Past/CountCard.ts @@ -0,0 +1,14 @@ +import * as Vue from "vue"; + +export let CountCard: Vue.Component = { + props: ['count'], + computed: { + + }, + template: + ` +
+

{{ count }}

+
+ ` +} \ No newline at end of file diff --git a/Selector.Web/scripts/past.ts b/Selector.Web/scripts/past.ts index 635f347..f8b2505 100644 --- a/Selector.Web/scripts/past.ts +++ b/Selector.Web/scripts/past.ts @@ -3,6 +3,9 @@ import * as signalR from "@microsoft/signalr"; import * as Vue from "vue"; import { RankResult, RankEntry, PastParams } from "./HubInterfaces"; import { RankCard } from "./Past/RankCard"; +import { CountCard } from "./Past/CountCard"; +import { PlayCountChartCard } from "./Now/PlayCountGraph"; +import { LastFmLogoLink } from "./Now/LastFm"; const connection = new signalR.HubConnectionBuilder() .withUrl("/pasthub") @@ -27,6 +30,10 @@ const app = Vue.createApp({ trackEntries: [], albumEntries: [], artistEntries: [], + + resampledSeries: [], + + totalCount: 0 } }, created() { @@ -37,29 +44,30 @@ const app = Vue.createApp({ this.trackEntries = result.trackEntries; this.albumEntries = result.albumEntries; this.artistEntries = result.artistEntries; + this.resampledSeries = result.resampledSeries; + this.totalCount = result.totalCount; }); }, methods: { submit() { - console.log({ - "track": this.track, - "album": this.album, - "artist": this.artist, - "from": this.from, - "to": this.to, - }); - - connection.invoke("OnSubmitted", { + let context = { track: this.track, album: this.album, artist: this.artist, from: this.from, to: this.to, - } as PastParams); + } as PastParams; + + console.log(context); + + connection.invoke("OnSubmitted", context); } } }); +app.component("play-count-chart-card", PlayCountChartCard); app.component("rank-card", RankCard); +app.component("lastfm-logo", LastFmLogoLink); +app.component("count-card", CountCard); const vm = app.mount('#pastapp'); \ No newline at end of file diff --git a/Selector/Options.cs b/Selector/Options.cs index 7e00573..2f1c930 100644 --- a/Selector/Options.cs +++ b/Selector/Options.cs @@ -23,7 +23,7 @@ namespace Selector { public const string Key = "Past"; - + public TimeSpan ResampleWindow { get; set; } = TimeSpan.FromDays(7); } } -- 2.45.2