adding past page skeleton with hub
This commit is contained in:
parent
79da0108d3
commit
c49e561ae0
@ -1,4 +1,5 @@
|
||||
@import "now.scss";
|
||||
@import "past.scss";
|
||||
|
||||
body {
|
||||
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(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();
|
||||
|
||||
}
|
||||
|
||||
|
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">
|
||||
<div class="container">
|
||||
<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">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" asp-area="" asp-page="/Now">Now</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" asp-area="" asp-page="/Past">Past</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@*</div>*@
|
||||
<partial name="_LoginPartial" />
|
||||
</div>
|
||||
</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>
|
||||
<Folder Include="wwwroot\" />
|
||||
<Folder Include="NowPlaying\" />
|
||||
<Folder Include="Past\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="NowPlaying\" />
|
||||
<None Remove="Past\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.Development.json" Condition="Exists('appsettings.Development.json')">
|
||||
|
@ -159,7 +159,8 @@ namespace Selector.Web
|
||||
{
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapHub<NowPlayingHub>("/hub");
|
||||
endpoints.MapHub<NowPlayingHub>("/nowhub");
|
||||
endpoints.MapHub<PastHub>("/pasthub");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
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";
|
||||
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl("/hub")
|
||||
.withUrl("/nowhub")
|
||||
.build();
|
||||
|
||||
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 = {
|
||||
entry: {
|
||||
now: './scripts/now.ts',
|
||||
past: './scripts/past.ts',
|
||||
nowCss: './CSS/index.scss',
|
||||
},
|
||||
module: {
|
||||
|
@ -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";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user