added stats obj and some js

This commit is contained in:
aj 2020-01-23 12:01:29 +00:00
parent 4c9efbd614
commit 69d4f25f29
8 changed files with 385 additions and 1 deletions

View File

@ -6,7 +6,7 @@ import datetime
from music.api.decorators import admin_required, login_or_basic_auth, lastfm_username_required, spotify_link_required, cloud_task, gae_cron
import music.db.database as database
from music.tasks.refresh_lastfm_stats import refresh_lastfm_track_stats, \
from music.tasks.refresh_playlist_lastfm_stats import refresh_lastfm_track_stats, \
refresh_lastfm_album_stats, \
refresh_lastfm_artist_stats

View File

@ -34,3 +34,29 @@ def play(username=None):
return jsonify({'error': "no uris provided"}), 400
return jsonify({'message': 'sorted', 'status': 'success'}), 200
@blueprint.route('/playlist', methods=['GET'])
@login_or_basic_auth
@spotify_link_required
def playlist(username=None):
net = database.get_authed_spotify_network(username)
playlists = net.get_user_playlists()
return jsonify({'playlists': [i.name for i in playlists], 'status': 'success'}), 200
@blueprint.route('/playlist/stats', methods=['GET'])
@login_or_basic_auth
@spotify_link_required
def stats(username=None):
name = request.args.get('name')
if name is not None:
stats_obj = database.get_stat(username=username, name=name)
if stats_obj is not None:
return jsonify({'stats': stats_obj.to_dict(), 'status': 'success'}), 200
else:
return jsonify({'message': 'stat not found', 'status': 'error'}), 404
else:
return jsonify({'message': 'no name provided', 'status': 'error'}), 400

View File

@ -5,10 +5,12 @@ from typing import List, Optional
from werkzeug.security import generate_password_hash
from spotframework.net.network import Network as SpotifyNetwork
from spotframework.model.uri import Uri
from fmframework.net.network import Network as FmNetwork
from music.db.user import DatabaseUser
from music.model.user import User
from music.model.playlist import Playlist, RecentsPlaylist, LastFMChartPlaylist, Sort
from music.model.stats import Stats
db = firestore.Client()
@ -324,3 +326,106 @@ def delete_playlist(username: str, name: str) -> None:
playlist.db_ref.delete()
else:
logger.error(f'playlist {name} not found for {username}')
def get_user_stats(username):
logger.info(f'retrieving stats for {username}')
user = get_user(username)
if user:
stats_refs = [i for i in user.db_ref.collection(u'stats').stream()]
return [parse_user_stats_reference(username=username, stats_snapshot=i) for i in stats_refs]
else:
logger.error(f'user {username} not found')
def get_stat(username: str = None, uri: Uri = None, name: str = None) -> Optional[Stats]:
logger.info(f'retrieving {uri}/{name} stats for {username}')
user = get_user(username)
if user:
if uri is not None:
stats = [i for i in user.db_ref.collection(u'stats').where(u'uri', u'==', str(uri)).stream()]
else:
stats = [i for i in user.db_ref.collection(u'stats').where(u'name', u'==', name).stream()]
if len(stats) == 0:
logger.error(f'stat {uri} for {user} not found')
return None
if len(stats) > 1:
logger.critical(f"multiple {uri} stats for {user} found")
return None
return parse_user_stats_reference(username=username, stats_snapshot=stats[0])
else:
logger.error(f'user {username} not found')
def parse_user_stats_reference(username, stats_ref=None, stats_snapshot=None) -> Stats:
if stats_ref is None and stats_snapshot is None:
raise ValueError('no user object supplied')
if stats_ref is None:
stats_ref = stats_snapshot.reference
if stats_snapshot is None:
stats_snapshot = stats_ref.get()
stats_dict = stats_snapshot.to_dict()
return Stats(name=stats_dict.get('name'),
username=username,
uri=stats_dict.get('uri'),
artists=stats_dict.get('artists'),
albums=stats_dict.get('albums'),
tracks=stats_dict.get('tracks'),
user_total=stats_dict.get('user_total'),
db_ref=stats_ref)
def update_stats(username: str, uri: Uri, updates: dict) -> None:
if len(updates) > 0:
logger.debug(f'updating {uri} stat for {username}')
user = get_user(username)
stats = [i for i in user.db_ref.collection(u'stats').where(u'uri', u'==', str(uri)).stream()]
if len(stats) == 0:
logger.error(f'stat {uri} for {username} not found')
return None
if len(stats) > 1:
logger.critical(f"multiple {uri} stats for {username} found")
return None
stats[0].reference.update(updates)
else:
logger.debug(f'nothing to update for {uri} for {username}')
def create_stat(username: str, uri: Uri):
logger.info(f'creating {uri} stat for {username}')
user = get_user(username=username)
net = get_authed_spotify_network(username)
playlist = net.get_playlist(uri=uri)
if playlist is not None:
if user is not None:
db_ref = user.db_ref.collection(u'stats').document()
db_ref.set({
'uri': str(uri),
'name': playlist.name,
'artists': {},
'albums': {},
'tracks': {},
'user_total': 0
})
return parse_user_stats_reference(stats_ref=db_ref)
else:
logger.error(f'no {username} user returned')

82
music/model/stats.py Normal file
View File

@ -0,0 +1,82 @@
from google.cloud.firestore import DocumentReference
import music.db.database as database
from spotframework.model.uri import Uri
class Stats:
def __init__(self,
name: str,
username: str,
uri: str,
artists,
albums,
tracks,
user_total,
db_ref: DocumentReference):
self.name = name
self.username = username
self.uri = Uri(uri)
self._artists = artists
self._albums = albums
self._tracks = tracks
self._user_total = user_total
self.db_ref = db_ref
def to_dict(self):
return {
'uri': str(self.uri),
'name': self.name,
'username': self.username,
'artists': self.artists,
'albums': self.albums,
'tracks': self.tracks
}
def update_database(self, updates):
database.update_stats(username=self.username, uri=self.uri, updates=updates)
@property
def artists(self):
return self._artists
@artists.setter
def artists(self, value):
database.update_stats(self.username, uri=self.uri, updates={'artists': value})
self._artists = value
@property
def albums(self):
return self._albums
@albums.setter
def albums(self, value):
database.update_stats(self.username, uri=self.uri, updates={'albums': value})
self._albums = value
@property
def tracks(self):
return self._tracks
@tracks.setter
def tracks(self, value):
database.update_stats(self.username, uri=self.uri, updates={'tracks': value})
self._tracks = value
@property
def user_total(self):
return self._user_total
@user_total.setter
def user_total(self, value):
database.update_stats(self.username, uri=self.uri, updates={'user_total': value})
self._user_total = value

View File

@ -0,0 +1,40 @@
from google.cloud import firestore
import logging
from datetime import datetime
import music.db.database as database
from spotfm.maths.counter import Counter
from spotframework.model.uri import Uri
db = firestore.Client()
logger = logging.getLogger(__name__)
def refresh_stats(username, uri: Uri = None, uri_string: str = None):
if uri is None and uri_string is None:
raise ValueError('no uri to analyse')
if uri is None:
uri = Uri(uri_string)
logger.info(f'refreshing {uri} stats for {username}')
fmnet = database.get_authed_lastfm_network(username=username)
spotnet = database.get_authed_spotify_network(username=username)
counter = Counter(fmnet=fmnet, spotnet=spotnet)
playlist = spotnet.get_playlist(uri=uri)
track_count = counter.count_playlist(playlist=playlist)
user_count = fmnet.get_user_scrobble_count()
stat = database.get_stat(username=username, uri=uri)
if stat is None:
stat = database.create_stat(username=username, uri=uri)
stat.update_database({})

View File

@ -2,6 +2,7 @@ import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
import Count from "./Count.js";
import Stats from "./Stats.js";
class Maths extends Component {
@ -11,9 +12,11 @@ class Maths extends Component {
<div>
<ul className="navbar" style={{width: "100%"}}>
<li><Link to={`${this.props.match.url}/count`}>count</Link></li>
<li><Link to={`${this.props.match.url}/stats`}>stats</Link></li>
</ul>
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
<Route path={`${this.props.match.url}/stats`} render={(props) => <Stats {...props} name={this.props.match.params.name}/>} />
</div>
);
}

128
src/js/Maths/Stats.js Normal file
View File

@ -0,0 +1,128 @@
import React, { Component } from "react";
const axios = require('axios');
import showMessage from "../Toast.js";
import BarChart from "./BarChart.js";
import PieChart from "./PieChart.js";
class Stats extends Component {
constructor(props){
super(props);
this.state = {
playlists: [],
isLoading: true,
isLoadingPlaylist: false,
subjectPlaylistName: '',
currentPlaylist: null
}
this.getPlaylists();
this.handleInputChange = this.handleInputChange.bind(this);
}
getPlaylists(){
axios.get('/api/spotify/playlist')
.then((response) => {
var playlists = response.data.playlists;
playlists.sort(function(a, b){
if(a.toLowerCase() < b.toLowerCase()) { return -1; }
if(a.toLowerCase() > b.toLowerCase()) { return 1; }
return 0;
});
this.setState({
playlists: playlists,
isLoading: false
});
})
.catch((error) => {
console.log(error);
showMessage(`error getting playlists (${error.response.status})`);
});
}
getPlaylist(){
axios.get(`/api/spotify/playlist/stats?name=\'${this.state.subjectPlaylistName}\'`)
.then((response) => {
this.setState({
currentPlaylist: response.data.playlist,
isLoadingPlaylist: false
});
})
.catch((error) => {
console.log(error);
showMessage(`error getting ${this.state.subjectPlaylistName} (${error.response.status})`);
});
}
handleInputChange(event){
this.setState({
[event.target.name]: event.target.value
});
if (event.target.name == "subjectPlaylistName"){
this.setState({
isLoadingPlaylist: true
})
this.getPlaylist();
}
}
render() {
var table = <div>
<table className="app-table max-width">
<thead>
<tr>
<th colSpan='3'>
<h1 className="ui-text center-text text-no-select">playlist stats</h1>
</th>
</tr>
<tr>
<th><select name="subjectPlaylistName"
className="full-width"
value={this.state.subjectPlaylistName}
onChange={this.handleInputChange}>
{ this.state.playlists.map((entry) => <PlaylistNameEntry name={entry} key={entry} />) }
</select></th>
</tr>
</thead>
{ this.state.isLoadingPlaylist == false && this.state.currentPlaylist != null &&
<PlaylistView playlist={this.state.currentPlaylist}/>
}
</table>
</div>;
const loadingMessage = <p className="center-text">loading...</p>;
return this.state.isLoading ? loadingMessage : table;
}
}
class PlaylistView extends Component {
constructor(props){
super(props);
this.state = {
currentPlaylist: props.currentPlaylist
}
}
render() {
return <tbody>
<tr>
<td>{this.state.currentPlaylist.name}</td>
</tr>
</tbody>;
}
}
function PlaylistNameEntry(props) {
return <option value={props.name}>{props.name}</option>;
}
export default Stats;