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
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
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 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
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