From 69d4f25f2900f137bf8540a949abfcc8e796f996 Mon Sep 17 00:00:00 2001 From: aj Date: Thu, 23 Jan 2020 12:01:29 +0000 Subject: [PATCH] added stats obj and some js --- music/api/spotfm.py | 2 +- music/api/spotify.py | 26 ++++ music/db/database.py | 105 ++++++++++++++ music/model/stats.py | 82 +++++++++++ ...ts.py => refresh_playlist_lastfm_stats.py} | 0 music/tasks/refresh_spotify_playlist_stats.py | 40 ++++++ src/js/Maths/Maths.js | 3 + src/js/Maths/Stats.js | 128 ++++++++++++++++++ 8 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 music/model/stats.py rename music/tasks/{refresh_lastfm_stats.py => refresh_playlist_lastfm_stats.py} (100%) create mode 100644 music/tasks/refresh_spotify_playlist_stats.py create mode 100644 src/js/Maths/Stats.js diff --git a/music/api/spotfm.py b/music/api/spotfm.py index 3a90e57..2085d2b 100644 --- a/music/api/spotfm.py +++ b/music/api/spotfm.py @@ -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 diff --git a/music/api/spotify.py b/music/api/spotify.py index 027aff8..83a557a 100644 --- a/music/api/spotify.py +++ b/music/api/spotify.py @@ -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 diff --git a/music/db/database.py b/music/db/database.py index 9338553..4cfd920 100644 --- a/music/db/database.py +++ b/music/db/database.py @@ -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') diff --git a/music/model/stats.py b/music/model/stats.py new file mode 100644 index 0000000..d155ac5 --- /dev/null +++ b/music/model/stats.py @@ -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 diff --git a/music/tasks/refresh_lastfm_stats.py b/music/tasks/refresh_playlist_lastfm_stats.py similarity index 100% rename from music/tasks/refresh_lastfm_stats.py rename to music/tasks/refresh_playlist_lastfm_stats.py diff --git a/music/tasks/refresh_spotify_playlist_stats.py b/music/tasks/refresh_spotify_playlist_stats.py new file mode 100644 index 0000000..e133458 --- /dev/null +++ b/music/tasks/refresh_spotify_playlist_stats.py @@ -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({}) diff --git a/src/js/Maths/Maths.js b/src/js/Maths/Maths.js index f8f9c42..7e023d6 100644 --- a/src/js/Maths/Maths.js +++ b/src/js/Maths/Maths.js @@ -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 {
} /> + } />
); } diff --git a/src/js/Maths/Stats.js b/src/js/Maths/Stats.js new file mode 100644 index 0000000..13ecb9a --- /dev/null +++ b/src/js/Maths/Stats.js @@ -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 =
+ + + + + + + + + + { this.state.isLoadingPlaylist == false && this.state.currentPlaylist != null && + + } +
+

playlist stats

+
+
; + + const loadingMessage =

loading...

; + + return this.state.isLoading ? loadingMessage : table; + } +} + +class PlaylistView extends Component { + + constructor(props){ + super(props); + this.state = { + currentPlaylist: props.currentPlaylist + } + } + + render() { + return + + {this.state.currentPlaylist.name} + + ; + } + +} + +function PlaylistNameEntry(props) { + return ; +} + +export default Stats; \ No newline at end of file