floating player, popularity card, item change on from or to null context

ready to inject audio features
This commit is contained in:
andy 2021-11-10 01:46:30 +00:00
parent 83bb76a5eb
commit cb8553af60
14 changed files with 233 additions and 52 deletions

View File

@ -0,0 +1,40 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
using SpotifyAPI.Web;
using StackExchange.Redis;
namespace Selector.Cache
{
public class AudioFeaturePuller
{
private readonly IRefreshTokenFactoryProvider SpotifyFactory;
private readonly IDatabaseAsync Cache;
public AudioFeaturePuller(
IRefreshTokenFactoryProvider spotifyFactory,
IDatabaseAsync cache
)
{
SpotifyFactory = spotifyFactory;
Cache = cache;
}
public async Task<TrackAudioFeatures> Get(string userId, string trackId)
{
var track = await Cache.StringGetAsync(Key.AudioFeature(trackId));
if (track == RedisValue.Null)
{
// TODO: finish implementing network pull
// return await SpotifyClient.GetAudioFeatures(trackId);
throw new NotImplementedException("Can't pull over network yet");
}
else
{
var deserialised = JsonSerializer.Deserialize<TrackAudioFeatures>(track);
return deserialised;
}
}
}
}

View File

@ -1,7 +1,7 @@
.card {
background-color: grey;
color: white;
margin: 5px;
box-shadow: 4px 4px 2px #5e5e5e;
}
@ -18,4 +18,37 @@
img {
margin: 15px;
}
}
@media only screen and (min-width: 768px) {
.now-playing-card {
position: fixed;
right: 20px;
bottom: 20px;
width: 300px;
}
}
.spotify-logo {
width: 21px;
img {
width: 21px;
}
}
.info-card {
margin-top: 10px;
margin-bottom: 10px;
img {
margin: 15px;
}
}
@media only screen and (min-width: 768px) {
.info-card {
max-width: 300px;
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using SpotifyAPI.Web;
using StackExchange.Redis;
using Selector.Cache;
@ -12,11 +13,13 @@ namespace Selector.Web.Hubs
public interface INowPlayingHubClient
{
public Task OnNewPlaying(CurrentlyPlayingDTO context);
// public Task OnNewAudioFeature(TrackAudioFeatures features);
}
public class NowPlayingHub: Hub<INowPlayingHubClient>
{
private readonly IDatabaseAsync Cache;
// private readonly AudioFeaturePuller AudioFeaturePuller;
public NowPlayingHub(IDatabaseAsync cache)
{
@ -34,5 +37,10 @@ namespace Selector.Web.Hubs
var deserialised = JsonSerializer.Deserialize<CurrentlyPlayingDTO>(nowPlaying);
await Clients.Caller.OnNewPlaying(deserialised);
}
// public async Task SendAudioFeatures(string trackId)
// {
// await Clients.Caller.OnNewAudioFeature(await AudioFeaturePuller.Get(trackId));
// }
}
}

View File

@ -6,8 +6,12 @@
<div class="text-center">
<h1 class="display-4">Now</h1>
<div id="app" class="col-12">
<now-playing-card :track="currentlyPlaying.track" class="col-md-3"></now-playing-card>
<div id="app">
<now-playing-card :track="currentlyPlaying.track" v-if="currentlyPlaying !== null && currentlyPlaying !== undefined"></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" />
<info-card v-for="card in cards" :html="card.html" />
</div>
</div>

View File

@ -117,6 +117,8 @@ namespace Selector.Web
services.AddTransient<ISubscriber>(services => services.GetService<ConnectionMultiplexer>().GetSubscriber());
}
services.AddSingleton<IRefreshTokenFactoryProvider, CachingRefreshTokenFactoryProvider>();
services.AddSingleton<CacheHubProxy>();
services.AddHostedService<CacheHubProxyService>();

View File

@ -1,7 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Trace",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}

View File

@ -9,6 +9,7 @@ export interface nowPlayingProxy {
export interface NowPlayingHubClient {
OnNewPlaying: (context: CurrentlyPlayingDTO) => void;
OnNewAudioFeature: (features: TrackAudioFeatures) => void;
}
export interface NowPlayingHub {
@ -106,6 +107,27 @@ export interface FullTrack {
isLocal: boolean;
}
export interface TrackAudioFeatures {
type: string;
trackHref: string;
timeSignature: number;
tempo: number;
speechiness: number;
mode: number;
loudness: number;
uri: string;
liveness: number;
instrumentalness: number;
id: string;
energy: number;
durationMs: number;
danceability: number;
analysisUrl: string;
acousticness: number;
key: number;
valence: number;
}
export interface LinkedTrack {
externalUrls: { [key: string]: string; };
href: string;

View File

@ -0,0 +1,12 @@
import * as Vue from "vue";
let BaseInfoCard: Vue.Component = {
props: ['html'],
template:
`
<div class="card info-card" v-html="html">
</div>
`
}
export default BaseInfoCard;

View File

@ -0,0 +1,48 @@
import * as Vue from "vue";
let component: Vue.Component = {
props: ['track', 'episode'],
computed: {
IsTrackPlaying() {
return this.track !== null && this.track !== undefined;
},
IsEpisodePlaying() {
return this.episode !== null && this.episode !== undefined;
}
},
template:
`
<div class="card now-playing-card" v-if="IsTrackPlaying">
<img :src="track.album.images[0].url" class="cover-art">
<h4>{{ track.name }}</h4>
<h6>
{{ track.album.name }}
</h6>
<h6>
<template v-for="(artist, index) in track.artists">
<template v-if="index > 0">, </template>
<span>{{ artist.name }}</span>
</template>
</h6>
<spotify-logo :link="track.externalUrls.spotify" />
</div>
<div class="card now-playing-card" v-else-if="IsEpisodePlaying">
<img :src="episode.show.images[0].url" class="cover-art">
<h4>{{ episode.name }}</h4>
<h6>
{{ episode.show.name }}
</h6>
<h6>
{{ episode.show.publisher }}
</h6>
<spotify-logo :link="episode.externalUrls.spotify" />
</div>
<div class="card now-playing-card" v-else>
<h4>No Playback</h4>
</div>
`
}
export default component;

View File

@ -0,0 +1,25 @@
import * as Vue from "vue";
export let PopularityCard: Vue.Component = {
props: ['track'],
template:
`
<div class="card info-card">
<h3>Popularity</h3>
<h1>{{ track.popularity }}%</h1>
<spotify-logo :link="track.externalUrls.spotify" />
</div>
`
}
export let SpotifyLogoLink: Vue.Component = {
props: ['link'],
template:
`
<a :href="link" class="spotify-logo" v-if="link != null && link != undefined">
<img src="/Spotify_Icon_RGB_White.png">
</a>
<img src="/Spotify_Icon_RGB_White.png" v-else>
`
}

View File

@ -1,26 +0,0 @@
import * as Vue from "vue";
let component: Vue.Component = {
props: ['track'],
template:
`
<div class="card now-playing-card" >
<img :src="track.album.images[0].url" class="cover-art">
<h4>{{ track.name }}</h4>
<h6>
{{ track.album.name }}
</h6>
<h6>
<template v-for="(artist, index) in track.artists">
<template v-if="index > 0">, </template>
<span>{{ artist.name }}</span>
</template>
</h6>
<a :href="track.externalUrls.spotify" style="width: 21px">
<img src="/Spotify_Icon_RGB_White.png" style="width: 21px">
</a>
</div>
`
}
export default component;

View File

@ -1,7 +1,9 @@
import * as signalR from "@microsoft/signalr";
import * as Vue from "vue";
import { FullTrack, CurrentlyPlayingDTO } from "./HubInterfaces";
import NowPlayingCard from "./NowPlayingCard";
import { TrackAudioFeatures, CurrentlyPlayingDTO } from "./HubInterfaces";
import NowPlayingCard from "./Now/NowPlayingCard";
import { PopularityCard, SpotifyLogoLink } from "./Now/Spotify";
import BaseInfoCard from "./Now/BaseInfoCard";
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub")
@ -13,32 +15,22 @@ connection.start()
})
.catch(err => console.error(err));
interface InfoCard {
html: string
}
interface NowPlaying {
currentlyPlaying?: CurrentlyPlayingDTO
currentlyPlaying?: CurrentlyPlayingDTO,
trackFeatures?: TrackAudioFeatures,
cards: InfoCard[]
}
const app = Vue.createApp({
data() {
return {
currentlyPlaying: {
track: {
name: "No Playback",
album: {
name: "",
images: [
{
url: ""
}
]
},
artists: [
{
name: ""
}
],
externalUrls: {}
}
}
currentlyPlaying: undefined,
trackFeatures: undefined,
cards: []
} as NowPlaying
},
created() {
@ -46,9 +38,24 @@ const app = Vue.createApp({
{
console.log(context);
this.currentlyPlaying = context;
this.cards = [];
// if(context.track !== null && context.track !== undefined)
// {
// connection.invoke("SendAudioFeatures", context.track.id);
// }
});
// connection.on("OnNewAudioFeature", (feature: TrackAudioFeatures) =>
// {
// console.log(feature);
// this.trackFeatures = feature;
// });
}
});
app.component("now-playing-card", NowPlayingCard);
app.component("info-card", BaseInfoCard);
app.component("popularity", PopularityCard);
app.component("spotify-logo", SpotifyLogoLink);
const vm = app.mount('#app');

View File

@ -27,6 +27,10 @@ namespace Selector
{
return $"{currentPlaying.IsPlaying}, {episode.DisplayString()}, {currentPlaying.Device?.DisplayString()}";
}
else if (currentPlaying.Item is null)
{
return $"{currentPlaying.IsPlaying}, no item, {currentPlaying.Device?.DisplayString()}";
}
else
{
throw new ArgumentException("Unknown playing type");

View File

@ -75,6 +75,7 @@ namespace Selector
{
Logger.LogDebug($"Playback started: {Live.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
}
// STOPPED PLAYBACK
else if((previous.Item is FullTrack || previous.Item is FullEpisode)
@ -82,6 +83,7 @@ namespace Selector
{
Logger.LogDebug($"Playback stopped: {previous.DisplayString()}");
OnPlayingChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
OnItemChange(ListeningChangeEventArgs.From(previous, Live, Past, id: Id, username: SpotifyUsername));
}
// CONTINUING PLAYBACK
else {