adding play count graphs to now playing page
This commit is contained in:
parent
597db1a42b
commit
2a8b739f8d
@ -43,6 +43,10 @@ $shadow-color: #1e1e1e;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 768px) {
|
@media only screen and (min-width: 768px) {
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.SqlTypes;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Data.SqlTypes;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using SpotifyAPI.Web;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
using Selector.Cache;
|
using Selector.Cache;
|
||||||
using Selector.Model;
|
using Selector.Model;
|
||||||
using Selector.Model.Extensions;
|
using Selector.Model.Extensions;
|
||||||
using Selector.Web.NowPlaying;
|
using Selector.Web.NowPlaying;
|
||||||
using Microsoft.Extensions.Options;
|
using SpotifyAPI.Web;
|
||||||
using System.Collections.Generic;
|
using StackExchange.Redis;
|
||||||
|
|
||||||
namespace Selector.Web.Hubs
|
namespace Selector.Web.Hubs
|
||||||
{
|
{
|
||||||
@ -70,6 +69,8 @@ namespace Selector.Web.Hubs
|
|||||||
|
|
||||||
public async Task SendAudioFeatures(string trackId)
|
public async Task SendAudioFeatures(string trackId)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(trackId)) return;
|
||||||
|
|
||||||
var user = Db.Users
|
var user = Db.Users
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(u => u.Id == Context.UserIdentifier)
|
.Where(u => u.Id == Context.UserIdentifier)
|
||||||
@ -106,16 +107,37 @@ namespace Selector.Web.Hubs
|
|||||||
|
|
||||||
if (user.ScrobbleSavingEnabled())
|
if (user.ScrobbleSavingEnabled())
|
||||||
{
|
{
|
||||||
playCount.Artist = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist).Count();
|
var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist).ToArray();
|
||||||
|
|
||||||
|
playCount.Artist = artistScrobbles.Length;
|
||||||
|
|
||||||
|
playCount.ArtistCountData = artistScrobbles
|
||||||
|
//.Resample(nowOptions.Value.ArtistResampleWindow)
|
||||||
|
.ResampleByMonth()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var postCalc = playCount.ArtistCountData.Select(s => s.Value).Sum();
|
||||||
|
Debug.Assert(postCalc == artistScrobbles.Count());
|
||||||
|
|
||||||
|
playCount.AlbumCountData = artistScrobbles
|
||||||
|
.Where(s => s.AlbumName.Equals(album, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
//.Resample(nowOptions.Value.AlbumResampleWindow)
|
||||||
|
.ResampleByMonth()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
playCount.TrackCountData = artistScrobbles
|
||||||
|
.Where(s => s.TrackName.Equals(track, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
//.Resample(nowOptions.Value.TrackResampleWindow)
|
||||||
|
.ResampleByMonth()
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playCount is not null)
|
|
||||||
{
|
|
||||||
await Clients.Caller.OnNewPlayCount(playCount);
|
await Clients.Caller.OnNewPlayCount(playCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendFacts(string track, string artist, string album, string albumArtist)
|
public async Task SendFacts(string track, string artist, string album, string albumArtist)
|
||||||
{
|
{
|
||||||
@ -125,37 +147,49 @@ namespace Selector.Web.Hubs
|
|||||||
.SingleOrDefault()
|
.SingleOrDefault()
|
||||||
?? throw new SqlNullValueException("No user returned");
|
?? throw new SqlNullValueException("No user returned");
|
||||||
|
|
||||||
|
await PlayDensityFacts(user, track, artist, album, albumArtist);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PlayDensityFacts(ApplicationUser user, string track, string artist, string album, string albumArtist)
|
||||||
|
{
|
||||||
if (user.ScrobbleSavingEnabled())
|
if (user.ScrobbleSavingEnabled())
|
||||||
{
|
{
|
||||||
var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray();
|
var artistScrobbles = ScrobbleRepository.GetAll(userId: user.Id, artistName: artist, from: GetMaximumWindow()).ToArray();
|
||||||
var artistDensity = artistScrobbles.Density(DateTime.UtcNow - nowOptions.Value.ArtistDensityWindow, DateTime.UtcNow);
|
var artistDensity = artistScrobbles.Density(nowOptions.Value.ArtistDensityWindow);
|
||||||
|
|
||||||
|
var tasks = new List<Task>(3);
|
||||||
|
|
||||||
if (artistDensity > nowOptions.Value.ArtistDensityThreshold)
|
if (artistDensity > nowOptions.Value.ArtistDensityThreshold)
|
||||||
{
|
{
|
||||||
await Clients.Caller.OnNewCard(new()
|
tasks.Add(Clients.Caller.OnNewCard(new()
|
||||||
{
|
{
|
||||||
Content = $"You're on a {artist} binge! {artistDensity} plays/day recently"
|
Content = $"You're on a {artist} binge! {artistDensity} plays/day recently"
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
var albumDensity = artistScrobbles.Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase)).Density(DateTime.UtcNow - nowOptions.Value.AlbumDensityWindow, DateTime.UtcNow);
|
var albumDensity = artistScrobbles.Where(s => s.AlbumName.Equals(album, StringComparison.InvariantCultureIgnoreCase)).Density(nowOptions.Value.AlbumDensityWindow);
|
||||||
|
|
||||||
if (albumDensity > nowOptions.Value.AlbumDensityThreshold)
|
if (albumDensity > nowOptions.Value.AlbumDensityThreshold)
|
||||||
{
|
{
|
||||||
await Clients.Caller.OnNewCard(new()
|
tasks.Add(Clients.Caller.OnNewCard(new()
|
||||||
{
|
{
|
||||||
Content = $"You're on a {album} binge! {albumDensity} plays/day recently"
|
Content = $"You're on a {album} binge! {albumDensity} plays/day recently"
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
var trackDensity = artistScrobbles.Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase)).Density(DateTime.UtcNow - nowOptions.Value.TrackDensityWindow, DateTime.UtcNow);
|
var trackDensity = artistScrobbles.Where(s => s.TrackName.Equals(track, StringComparison.InvariantCultureIgnoreCase)).Density(nowOptions.Value.TrackDensityWindow);
|
||||||
|
|
||||||
if (albumDensity > nowOptions.Value.TrackDensityThreshold)
|
if (albumDensity > nowOptions.Value.TrackDensityThreshold)
|
||||||
{
|
{
|
||||||
await Clients.Caller.OnNewCard(new()
|
tasks.Add(Clients.Caller.OnNewCard(new()
|
||||||
{
|
{
|
||||||
Content = $"You're on a {track} binge! {trackDensity} plays/day recently"
|
Content = $"You're on a {track} binge! {trackDensity} plays/day recently"
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tasks.Any())
|
||||||
|
{
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,10 @@ namespace Selector.Web
|
|||||||
{
|
{
|
||||||
public const string Key = "Now";
|
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 ArtistDensityWindow { get; set; } = TimeSpan.FromDays(10);
|
public TimeSpan ArtistDensityWindow { get; set; } = TimeSpan.FromDays(10);
|
||||||
public decimal ArtistDensityThreshold { get; set; } = 5;
|
public decimal ArtistDensityThreshold { get; set; } = 5;
|
||||||
|
|
||||||
|
@ -10,10 +10,30 @@
|
|||||||
<now-playing-card :track="currentlyPlaying.track" v-if="currentlyPlaying !== null && currentlyPlaying !== undefined"></now-playing-card>
|
<now-playing-card :track="currentlyPlaying.track" v-if="currentlyPlaying !== null && currentlyPlaying !== undefined"></now-playing-card>
|
||||||
<now-playing-card v-else></now-playing-card>
|
<now-playing-card v-else></now-playing-card>
|
||||||
|
|
||||||
<popularity :track="currentlyPlaying.track" v-if="currentlyPlaying !== null && currentlyPlaying !== undefined && currentlyPlaying.track != null && currentlyPlaying.track != undefined" ></popularity>
|
<popularity :track="currentlyPlaying.track"
|
||||||
<audio-feature-card :feature="trackFeatures" v-if="trackFeatures !== null && trackFeatures !== undefined" /></audio-feature-card>
|
v-if="currentlyPlaying !== null && currentlyPlaying !== undefined && currentlyPlaying.track != null && currentlyPlaying.track != undefined"></popularity>
|
||||||
<audio-feature-chart-card :feature="trackFeatures" v-if="trackFeatures !== null && trackFeatures !== undefined" /></audio-feature-chart-card>
|
|
||||||
<play-count-card :count="playCount" :track="lastfmTrack" :username="playCount.username" v-if="playCount !== null && playCount !== undefined" /></play-count-card>
|
<audio-feature-card :feature="trackFeatures"
|
||||||
|
v-if="trackFeatures !== null && trackFeatures !== undefined"></audio-feature-card>
|
||||||
|
<audio-feature-chart-card :feature="trackFeatures"
|
||||||
|
v-if="trackFeatures !== null && trackFeatures !== undefined"></audio-feature-chart-card>
|
||||||
|
|
||||||
|
<play-count-card :count="playCount"
|
||||||
|
:track="lastfmTrack"
|
||||||
|
:username="playCount.username"
|
||||||
|
v-if="playCount !== null && playCount !== undefined"></play-count-card>
|
||||||
|
<play-count-chart-card :data_points="playCount.trackCountData"
|
||||||
|
:title="currentlyPlaying.track.name"
|
||||||
|
:chart_id="'track'"
|
||||||
|
v-if="showTrackChart"></play-count-chart-card>
|
||||||
|
<play-count-chart-card :data_points="playCount.albumCountData"
|
||||||
|
:title="currentlyPlaying.track.album.name"
|
||||||
|
:chart_id="'album'"
|
||||||
|
v-if="showAlbumChart"></play-count-chart-card>
|
||||||
|
<play-count-chart-card :data_points="playCount.artistCountData"
|
||||||
|
:title="lastfmArtist"
|
||||||
|
:chart_id="'artist'"
|
||||||
|
v-if="showArtistChart"></play-count-chart-card>
|
||||||
<info-card v-for="card in cards" :html="card.content"></info-card>
|
<info-card v-for="card in cards" :html="card.content"></info-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,9 +23,17 @@ export interface PlayCount {
|
|||||||
artist: number | null;
|
artist: number | null;
|
||||||
user: number | null;
|
user: number | null;
|
||||||
username: string;
|
username: string;
|
||||||
|
trackCountData: CountSample[];
|
||||||
|
albumCountData: CountSample[];
|
||||||
|
artistCountData: CountSample[];
|
||||||
listeningEvent: ListeningChangeEventArgs;
|
listeningEvent: ListeningChangeEventArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CountSample {
|
||||||
|
timeStamp: Date;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CurrentlyPlayingDTO {
|
export interface CurrentlyPlayingDTO {
|
||||||
context: CurrentlyPlayingContextDTO;
|
context: CurrentlyPlayingContextDTO;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -8,12 +8,21 @@ let component: Vue.Component = {
|
|||||||
},
|
},
|
||||||
IsEpisodePlaying() {
|
IsEpisodePlaying() {
|
||||||
return this.episode !== null && this.episode !== undefined;
|
return this.episode !== null && this.episode !== undefined;
|
||||||
|
},
|
||||||
|
ImageUrl() {
|
||||||
|
if(this.track.album.images.length > 0)
|
||||||
|
{
|
||||||
|
return this.track.album.images[0].url;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template:
|
template:
|
||||||
`
|
`
|
||||||
<div class="card now-playing-card" v-if="IsTrackPlaying">
|
<div class="card now-playing-card" v-if="IsTrackPlaying">
|
||||||
<img :src="track.album.images[0].url" class="cover-art">
|
<img :src="ImageUrl" class="cover-art">
|
||||||
<h4>{{ track.name }}</h4>
|
<h4>{{ track.name }}</h4>
|
||||||
<h6>
|
<h6>
|
||||||
{{ track.album.name }}
|
{{ track.album.name }}
|
||||||
|
63
Selector.Web/scripts/Now/PlayCountGraph.ts
Normal file
63
Selector.Web/scripts/Now/PlayCountGraph.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import * as Vue from "vue";
|
||||||
|
import { Chart, PointElement, LineElement, LineController, CategoryScale, LinearScale, TimeSeriesScale } from "chart.js";
|
||||||
|
import { CountSample } from "scripts/HubInterfaces";
|
||||||
|
|
||||||
|
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'],
|
||||||
|
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),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chartId() {
|
||||||
|
return "play-count-chart-" + this.chart_id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
`
|
||||||
|
<div class="card info-card chart-card">
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<canvas :id="chartId"></canvas>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mounted() {
|
||||||
|
new Chart(`play-count-chart-${this.chart_id}`, {
|
||||||
|
type: "line",
|
||||||
|
data: this.chartData,
|
||||||
|
options: {
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
borderWidth: 4,
|
||||||
|
borderColor: "#a34c77",
|
||||||
|
backgroundColor: "#727272",
|
||||||
|
borderCapStyle: "round",
|
||||||
|
borderJoinStyle: "round"
|
||||||
|
},
|
||||||
|
// point: {
|
||||||
|
// radius: 4,
|
||||||
|
// pointStyle: "circle",
|
||||||
|
// borderColor: "black",
|
||||||
|
// backgroundColor: "white"
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import * as Vue from "vue";
|
|||||||
import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces";
|
import { TrackAudioFeatures, PlayCount, CurrentlyPlayingDTO } from "./HubInterfaces";
|
||||||
import NowPlayingCard from "./Now/NowPlayingCard";
|
import NowPlayingCard from "./Now/NowPlayingCard";
|
||||||
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
|
import { AudioFeatureCard, AudioFeatureChartCard, PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
|
||||||
|
import { PlayCountChartCard } from "./Now/PlayCountGraph";
|
||||||
import { PlayCountCard, LastFmLogoLink } from "./Now/LastFm";
|
import { PlayCountCard, LastFmLogoLink } from "./Now/LastFm";
|
||||||
import BaseInfoCard from "./Now/BaseInfoCard";
|
import BaseInfoCard from "./Now/BaseInfoCard";
|
||||||
|
|
||||||
@ -44,6 +45,23 @@ const app = Vue.createApp({
|
|||||||
album: this.currentlyPlaying.track.album.name,
|
album: this.currentlyPlaying.track.album.name,
|
||||||
album_artist: this.currentlyPlaying.track.album.artists[0].name,
|
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 > 0;
|
||||||
|
},
|
||||||
|
showAlbumChart() {
|
||||||
|
return this.playCount !== null && this.playCount !== undefined && this.playCount.albumCountData.length > 0;
|
||||||
|
},
|
||||||
|
showTrackChart(){
|
||||||
|
return this.playCount !== null && this.playCount !== undefined && this.playCount.trackCountData.length > 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@ -56,8 +74,11 @@ const app = Vue.createApp({
|
|||||||
this.cards = [];
|
this.cards = [];
|
||||||
|
|
||||||
if(context.track !== null && context.track !== undefined)
|
if(context.track !== null && context.track !== undefined)
|
||||||
|
{
|
||||||
|
if(context.track.id !== null)
|
||||||
{
|
{
|
||||||
connection.invoke("SendAudioFeatures", context.track.id);
|
connection.invoke("SendAudioFeatures", context.track.id);
|
||||||
|
}
|
||||||
connection.invoke("SendPlayCount",
|
connection.invoke("SendPlayCount",
|
||||||
context.track.name,
|
context.track.name,
|
||||||
context.track.artists[0].name,
|
context.track.artists[0].name,
|
||||||
@ -101,4 +122,5 @@ app.component("popularity", PopularityCard);
|
|||||||
app.component("spotify-logo", SpotifyLogoLink);
|
app.component("spotify-logo", SpotifyLogoLink);
|
||||||
app.component("lastfm-logo", LastFmLogoLink);
|
app.component("lastfm-logo", LastFmLogoLink);
|
||||||
app.component("play-count-card", PlayCountCard);
|
app.component("play-count-card", PlayCountCard);
|
||||||
|
app.component("play-count-chart-card", PlayCountChartCard);
|
||||||
const vm = app.mount('#app');
|
const vm = app.mount('#app');
|
@ -53,6 +53,8 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
if (e.Current.Item is FullTrack track)
|
if (e.Current.Item is FullTrack track)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(track.Id)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Logger.LogTrace("Making Spotify call");
|
Logger.LogTrace("Making Spotify call");
|
||||||
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
|
var audioFeatures = await TrackClient.GetAudioFeatures(track.Id);
|
||||||
@ -81,6 +83,8 @@ namespace Selector
|
|||||||
}
|
}
|
||||||
else if (e.Current.Item is FullEpisode episode)
|
else if (e.Current.Item is FullEpisode episode)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(episode.Id)) return;
|
||||||
|
|
||||||
Logger.LogDebug($"Ignoring podcast episdoe [{episode.DisplayString()}]");
|
Logger.LogDebug($"Ignoring podcast episdoe [{episode.DisplayString()}]");
|
||||||
}
|
}
|
||||||
else if (e.Current.Item is null)
|
else if (e.Current.Item is null)
|
||||||
|
@ -208,6 +208,9 @@ namespace Selector
|
|||||||
public int? Artist { get; set; }
|
public int? Artist { get; set; }
|
||||||
public int? User { get; set; }
|
public int? User { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
|
public IEnumerable<CountSample> TrackCountData { get; set; }
|
||||||
|
public IEnumerable<CountSample> AlbumCountData { get; set; }
|
||||||
|
public IEnumerable<CountSample> ArtistCountData { get; set; }
|
||||||
public ListeningChangeEventArgs ListeningEvent { get; set; }
|
public ListeningChangeEventArgs ListeningEvent { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ namespace Selector
|
|||||||
{
|
{
|
||||||
public static class PlayDensity
|
public static class PlayDensity
|
||||||
{
|
{
|
||||||
|
public static decimal Density(this IEnumerable<Scrobble> scrobbles, TimeSpan window) => scrobbles.Density(DateTime.UtcNow - window, DateTime.UtcNow);
|
||||||
|
|
||||||
public static decimal Density(this IEnumerable<Scrobble> scrobbles, DateTime from, DateTime to)
|
public static decimal Density(this IEnumerable<Scrobble> scrobbles, DateTime from, DateTime to)
|
||||||
{
|
{
|
||||||
var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to);
|
var filteredScrobbles = scrobbles.Where(s => s.Timestamp > from && s.Timestamp < to);
|
||||||
|
97
Selector/Scrobble/Resampler.cs
Normal file
97
Selector/Scrobble/Resampler.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Selector
|
||||||
|
{
|
||||||
|
public record struct CountSample {
|
||||||
|
public DateTime TimeStamp { get; set; }
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Resampler
|
||||||
|
{
|
||||||
|
public static IEnumerable<CountSample> Resample(this IEnumerable<Scrobble> scrobbles, TimeSpan window)
|
||||||
|
{
|
||||||
|
var sortedScrobbles = scrobbles.OrderBy(s => s.Timestamp).ToList();
|
||||||
|
|
||||||
|
if (!sortedScrobbles.Any())
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortedScrobblesIter = sortedScrobbles.GetEnumerator();
|
||||||
|
sortedScrobblesIter.MoveNext();
|
||||||
|
|
||||||
|
var earliest = sortedScrobbles.First().Timestamp;
|
||||||
|
var latest = sortedScrobbles.Last().Timestamp;
|
||||||
|
|
||||||
|
for (var counter = earliest; counter <= latest; counter += window)
|
||||||
|
{
|
||||||
|
var windowEnd = counter + window;
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
if (sortedScrobblesIter.Current is not null)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (sortedScrobblesIter.MoveNext() && counter <= sortedScrobblesIter.Current.Timestamp && sortedScrobblesIter.Current.Timestamp < windowEnd)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new CountSample()
|
||||||
|
{
|
||||||
|
TimeStamp = counter + (window / 2),
|
||||||
|
Value = count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<CountSample> ResampleByMonth(this IEnumerable<Scrobble> scrobbles)
|
||||||
|
{
|
||||||
|
var sortedScrobbles = scrobbles.OrderBy(s => s.Timestamp).ToList();
|
||||||
|
|
||||||
|
if (!sortedScrobbles.Any())
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortedScrobblesIter = sortedScrobbles.GetEnumerator();
|
||||||
|
sortedScrobblesIter.MoveNext();
|
||||||
|
|
||||||
|
var earliest = sortedScrobbles.First().Timestamp;
|
||||||
|
var latest = sortedScrobbles.Last().Timestamp;
|
||||||
|
var latestPlusMonth = latest.AddMonths(1);
|
||||||
|
|
||||||
|
var periodStart = new DateTime(earliest.Year, earliest.Month, 1);
|
||||||
|
var periodEnd = new DateTime(latestPlusMonth.Year, latestPlusMonth.Month, 1);
|
||||||
|
|
||||||
|
for (var counter = periodStart; counter <= periodEnd; counter = counter.AddMonths(1))
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
if (sortedScrobblesIter.Current is not null)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (sortedScrobblesIter.MoveNext()
|
||||||
|
&& sortedScrobblesIter.Current.Timestamp.Year == counter.Year
|
||||||
|
&& sortedScrobblesIter.Current.Timestamp.Month == counter.Month)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new CountSample()
|
||||||
|
{
|
||||||
|
TimeStamp = counter,
|
||||||
|
Value = count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user