adding past page skeleton with hub
This commit is contained in:
parent
79da0108d3
commit
c49e561ae0
@ -1,4 +1,5 @@
|
|||||||
@import "now.scss";
|
@import "now.scss";
|
||||||
|
@import "past.scss";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
|
26
Selector.Web/CSS/past.scss
Normal file
26
Selector.Web/CSS/past.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
113
Selector.Web/Hubs/PastHub.cs
Normal file
113
Selector.Web/Hubs/PastHub.cs
Normal file
@ -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<IPastHubClient>
|
||||||
|
{
|
||||||
|
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> pastOptions;
|
||||||
|
|
||||||
|
public PastHub(
|
||||||
|
IDatabaseAsync cache,
|
||||||
|
AudioFeaturePuller featurePuller,
|
||||||
|
ApplicationDbContext db,
|
||||||
|
IListenRepository listenRepository,
|
||||||
|
IOptions<PastOptions> 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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ namespace Selector.Web
|
|||||||
config.GetSection(RootOptions.Key).Bind(options);
|
config.GetSection(RootOptions.Key).Bind(options);
|
||||||
config.GetSection(FormatKeys(new[] { RootOptions.Key, RedisOptions.Key })).Bind(options.RedisOptions);
|
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, NowPlayingOptions.Key })).Bind(options.NowOptions);
|
||||||
|
config.GetSection(FormatKeys(new[] { RootOptions.Key, PastOptions.Key })).Bind(options.PastOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RootOptions ConfigureOptions(IConfiguration config)
|
public static RootOptions ConfigureOptions(IConfiguration config)
|
||||||
@ -45,6 +46,7 @@ namespace Selector.Web
|
|||||||
|
|
||||||
public RedisOptions RedisOptions { get; set; } = new();
|
public RedisOptions RedisOptions { get; set; } = new();
|
||||||
public NowPlayingOptions NowOptions { get; set; } = new();
|
public NowPlayingOptions NowOptions { get; set; } = new();
|
||||||
|
public PastOptions PastOptions { get; set; } = new();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
37
Selector.Web/Pages/Past.cshtml
Normal file
37
Selector.Web/Pages/Past.cshtml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@page
|
||||||
|
@model PastModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Past - Selector";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="display-4">Past</h1>
|
||||||
|
<div id="pastapp" class="app col-12">
|
||||||
|
<div class="card" style="width: 100%">
|
||||||
|
<div>
|
||||||
|
<input v-model="track" class="form-input form-control" placeholder="Track" />
|
||||||
|
<input v-model="album" class="form-input form-control" placeholder="Album" />
|
||||||
|
<input v-model="artist" class="form-input form-control" placeholder="Artist" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="from-picker">From</label>
|
||||||
|
<input type="date" v-model="from" class="form-input form-control" id="from-picker" />
|
||||||
|
<label for="to-picker">To</label>
|
||||||
|
<input type="date" v-model="to" class="form-input form-control" id="to-picker" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="button" v-on:click="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width: 100%">
|
||||||
|
<rank-card :title="'Track'" :entries="trackEntries" v-if="trackEntries.length > 0"></rank-card>
|
||||||
|
<rank-card :title="'Album'" :entries="albumEntries" v-if="albumEntries.length > 0"></rank-card>
|
||||||
|
<rank-card :title="'Artist'" :entries="artistEntries" v-if="artistEntries.length > 0"></rank-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script type="module" src="~/js/past.bundle.js"></script>
|
||||||
|
}
|
20
Selector.Web/Pages/Past.cshtml.cs
Normal file
20
Selector.Web/Pages/Past.cshtml.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Selector.Web.Pages
|
||||||
|
{
|
||||||
|
public class PastModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ILogger<PastModel> Logger;
|
||||||
|
|
||||||
|
public PastModel(ILogger<PastModel> logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,13 +12,16 @@
|
|||||||
<nav class="navbar navbar-expand-sm navbar-dark navbar-bg separator-border box-shadow mb-3">
|
<nav class="navbar navbar-expand-sm navbar-dark navbar-bg separator-border box-shadow mb-3">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" asp-area="" asp-page="/Index">Selector</a>
|
<a class="navbar-brand" asp-area="" asp-page="/Index">Selector</a>
|
||||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
@*<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">*@
|
||||||
<ul class="navbar-nav flex-grow-1">
|
<ul class="navbar-nav flex-grow-1">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" asp-area="" asp-page="/Now">Now</a>
|
<a class="nav-link" asp-area="" asp-page="/Now">Now</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" asp-area="" asp-page="/Past">Past</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
@*</div>*@
|
||||||
<partial name="_LoginPartial" />
|
<partial name="_LoginPartial" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
10
Selector.Web/Past/ChartEntry.cs
Normal file
10
Selector.Web/Past/ChartEntry.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Selector.Web;
|
||||||
|
|
||||||
|
public class ChartEntry
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
|
12
Selector.Web/Past/ChartResult.cs
Normal file
12
Selector.Web/Past/ChartResult.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Selector.Web;
|
||||||
|
|
||||||
|
public class RankResult
|
||||||
|
{
|
||||||
|
public IEnumerable<ChartEntry> TrackEntries { get; set; }
|
||||||
|
public IEnumerable<ChartEntry> AlbumEntries { get; set; }
|
||||||
|
public IEnumerable<ChartEntry> ArtistEntries { get; set; }
|
||||||
|
}
|
||||||
|
|
14
Selector.Web/Past/PastParams.cs
Normal file
14
Selector.Web/Past/PastParams.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -37,10 +37,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="wwwroot\" />
|
<Folder Include="wwwroot\" />
|
||||||
<Folder Include="NowPlaying\" />
|
<Folder Include="NowPlaying\" />
|
||||||
|
<Folder Include="Past\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="NowPlaying\" />
|
<None Remove="NowPlaying\" />
|
||||||
|
<None Remove="Past\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
|
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
|
||||||
|
@ -159,7 +159,8 @@ namespace Selector.Web
|
|||||||
{
|
{
|
||||||
endpoints.MapRazorPages();
|
endpoints.MapRazorPages();
|
||||||
endpoints.MapControllers();
|
endpoints.MapControllers();
|
||||||
endpoints.MapHub<NowPlayingHub>("/hub");
|
endpoints.MapHub<NowPlayingHub>("/nowhub");
|
||||||
|
endpoints.MapHub<PastHub>("/pasthub");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,25 @@ export interface PlayCount {
|
|||||||
listeningEvent: ListeningChangeEventArgs;
|
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 {
|
export interface CountSample {
|
||||||
timeStamp: Date;
|
timeStamp: Date;
|
||||||
value: number;
|
value: number;
|
||||||
|
19
Selector.Web/scripts/Past/RankCard.ts
Normal file
19
Selector.Web/scripts/Past/RankCard.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import * as Vue from "vue";
|
||||||
|
|
||||||
|
export let RankCard: Vue.Component = {
|
||||||
|
props: ['title', 'entries'],
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
`
|
||||||
|
<div class="rank-card card">
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
<ol>
|
||||||
|
<li v-for="entry in entries">
|
||||||
|
{{ entry.name }} - <b>{{entry.value}}</b>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
@ -9,7 +9,7 @@ import { PlayCountCard, LastFmLogoLink } from "./Now/LastFm";
|
|||||||
import BaseInfoCard from "./Now/BaseInfoCard";
|
import BaseInfoCard from "./Now/BaseInfoCard";
|
||||||
|
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl("/hub")
|
.withUrl("/nowhub")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
connection.start()
|
connection.start()
|
||||||
|
65
Selector.Web/scripts/past.ts
Normal file
65
Selector.Web/scripts/past.ts
Normal file
@ -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');
|
@ -4,6 +4,7 @@ const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
now: './scripts/now.ts',
|
now: './scripts/now.ts',
|
||||||
|
past: './scripts/past.ts',
|
||||||
nowCss: './CSS/index.scss',
|
nowCss: './CSS/index.scss',
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
@ -18,5 +18,12 @@ namespace Selector
|
|||||||
public TimeSpan TrackDensityWindow { get; set; } = TimeSpan.FromDays(10);
|
public TimeSpan TrackDensityWindow { get; set; } = TimeSpan.FromDays(10);
|
||||||
public decimal TrackDensityThreshold { get; set; } = 5;
|
public decimal TrackDensityThreshold { get; set; } = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PastOptions
|
||||||
|
{
|
||||||
|
public const string Key = "Past";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user