added stats obj and some js
This commit is contained in:
parent
4c9efbd614
commit
69d4f25f29
@ -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
|
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
|
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_album_stats, \
|
||||||
refresh_lastfm_artist_stats
|
refresh_lastfm_artist_stats
|
||||||
|
|
||||||
|
@ -34,3 +34,29 @@ def play(username=None):
|
|||||||
return jsonify({'error': "no uris provided"}), 400
|
return jsonify({'error': "no uris provided"}), 400
|
||||||
|
|
||||||
return jsonify({'message': 'sorted', 'status': 'success'}), 200
|
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
|
||||||
|
@ -5,10 +5,12 @@ from typing import List, Optional
|
|||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
from spotframework.net.network import Network as SpotifyNetwork
|
from spotframework.net.network import Network as SpotifyNetwork
|
||||||
|
from spotframework.model.uri import Uri
|
||||||
from fmframework.net.network import Network as FmNetwork
|
from fmframework.net.network import Network as FmNetwork
|
||||||
from music.db.user import DatabaseUser
|
from music.db.user import DatabaseUser
|
||||||
from music.model.user import User
|
from music.model.user import User
|
||||||
from music.model.playlist import Playlist, RecentsPlaylist, LastFMChartPlaylist, Sort
|
from music.model.playlist import Playlist, RecentsPlaylist, LastFMChartPlaylist, Sort
|
||||||
|
from music.model.stats import Stats
|
||||||
|
|
||||||
db = firestore.Client()
|
db = firestore.Client()
|
||||||
|
|
||||||
@ -324,3 +326,106 @@ def delete_playlist(username: str, name: str) -> None:
|
|||||||
playlist.db_ref.delete()
|
playlist.db_ref.delete()
|
||||||
else:
|
else:
|
||||||
logger.error(f'playlist {name} not found for {username}')
|
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
82
music/model/stats.py
Normal 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
|
40
music/tasks/refresh_spotify_playlist_stats.py
Normal file
40
music/tasks/refresh_spotify_playlist_stats.py
Normal 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({})
|
@ -2,6 +2,7 @@ import React, { Component } from "react";
|
|||||||
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
||||||
|
|
||||||
import Count from "./Count.js";
|
import Count from "./Count.js";
|
||||||
|
import Stats from "./Stats.js";
|
||||||
|
|
||||||
class Maths extends Component {
|
class Maths extends Component {
|
||||||
|
|
||||||
@ -11,9 +12,11 @@ class Maths extends Component {
|
|||||||
<div>
|
<div>
|
||||||
<ul className="navbar" style={{width: "100%"}}>
|
<ul className="navbar" style={{width: "100%"}}>
|
||||||
<li><Link to={`${this.props.match.url}/count`}>count</Link></li>
|
<li><Link to={`${this.props.match.url}/count`}>count</Link></li>
|
||||||
|
<li><Link to={`${this.props.match.url}/stats`}>stats</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
128
src/js/Maths/Stats.js
Normal file
128
src/js/Maths/Stats.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user