diff --git a/music/api/decorators.py b/music/api/decorators.py index 10fa67a..9259332 100644 --- a/music/api/decorators.py +++ b/music/api/decorators.py @@ -91,7 +91,7 @@ def lastfm_username_required(func): user_dict = database.get_user_doc_ref(kwargs.get('username')).get().to_dict() if user_dict: - if user_dict.get('lastfm_username'): + if user_dict.get('lastfm_username') and len(user_dict.get('lastfm_username')) > 0: return func(*args, **kwargs) else: logger.warning(f'no last.fm username for {user_dict["username"]}') diff --git a/music/api/spotfm.py b/music/api/spotfm.py index 1215409..3026b1b 100644 --- a/music/api/spotfm.py +++ b/music/api/spotfm.py @@ -1,15 +1,23 @@ from flask import Blueprint, jsonify, request import logging +import json +import os -from music.api.decorators import login_or_basic_auth, lastfm_username_required, spotify_link_required +from music.api.decorators import login_or_basic_auth, lastfm_username_required, spotify_link_required, cloud_task import music.db.database as database +from music.tasks.refresh_lastfm_stats import refresh_lastfm_stats from spotfm.maths.counter import Counter from spotframework.model.uri import Uri +from google.cloud import tasks_v2 + blueprint = Blueprint('spotfm-api', __name__) logger = logging.getLogger(__name__) +tasker = tasks_v2.CloudTasksClient() +task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions') + @blueprint.route('/count', methods=['GET']) @login_or_basic_auth @@ -55,3 +63,56 @@ def count(username=None): }), 200 else: return jsonify({'error': f'playlist {playlist_name} not found'}), 404 + + +@blueprint.route('/playlist/refresh', methods=['GET']) +@login_or_basic_auth +@spotify_link_required +@lastfm_username_required +def playlist_refresh(username=None): + + playlist_name = request.args.get('name', None) + + if playlist_name: + + if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': + create_refresh_playlist_task(username, playlist_name) + else: + refresh_lastfm_stats(username, playlist_name) + + return jsonify({'message': 'execution requested', 'status': 'success'}), 200 + + else: + logger.warning('no playlist requested') + return jsonify({"error": 'no name requested'}), 400 + + +@blueprint.route('/playlist/refresh/task', methods=['POST']) +@cloud_task +def run_playlist_task(): + + payload = request.get_data(as_text=True) + if payload: + payload = json.loads(payload) + + logger.info(f'running {payload["username"]} / {payload["name"]}') + + refresh_lastfm_stats(payload['username'], payload['name']) + + return jsonify({'message': 'executed playlist', 'status': 'success'}), 200 + + +def create_refresh_playlist_task(username, playlist_name): + + task = { + 'app_engine_http_request': { # Specify the type of request. + 'http_method': 'POST', + 'relative_uri': '/api/spotfm/playlist/refresh/task', + 'body': json.dumps({ + 'username': username, + 'name': playlist_name + }).encode() + } + } + + tasker.create_task(task_path, task) diff --git a/music/db/database.py b/music/db/database.py index a08fea1..a656b74 100644 --- a/music/db/database.py +++ b/music/db/database.py @@ -123,7 +123,7 @@ def get_user_playlists_collection(user_id: str) -> firestore.CollectionReference return playlists -def get_user_playlist_ref_by_username(user: str, playlist: str) -> Optional[firestore.CollectionReference]: +def get_user_playlist_ref_by_username(user: str, playlist: str) -> Optional[firestore.DocumentReference]: user_ref = get_user_doc_ref(user) @@ -137,7 +137,7 @@ def get_user_playlist_ref_by_username(user: str, playlist: str) -> Optional[fire def get_user_playlist_ref_by_user_ref(user_ref: firestore.DocumentReference, - playlist: str) -> Optional[firestore.CollectionReference]: + playlist: str) -> Optional[firestore.DocumentReference]: playlist_collection = get_user_playlists_collection(user_ref.id) diff --git a/music/tasks/refresh_lastfm_stats.py b/music/tasks/refresh_lastfm_stats.py new file mode 100644 index 0000000..ba71f32 --- /dev/null +++ b/music/tasks/refresh_lastfm_stats.py @@ -0,0 +1,36 @@ +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_lastfm_stats(username, playlist_name): + + logger.info(f'refreshing {playlist_name} 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) + + database_ref = database.get_user_playlist_ref_by_username(user=username, playlist=playlist_name) + + playlist_dict = database_ref.get().to_dict() + count = counter.count(Uri(playlist_dict['uri'])) + + user_count = fmnet.get_user_scrobble_count() + percent = round((count * 100) / user_count, 2) + + database_ref.update({ + 'lastfm_stat_count': count, + 'lastfm_stat_percent': percent, + 'lastfm_stat_last_refresh': datetime.utcnow() + }) diff --git a/src/js/Playlist/PlaylistsView.js b/src/js/Playlist/PlaylistsView.js index b235346..6dc6fdc 100644 --- a/src/js/Playlist/PlaylistsView.js +++ b/src/js/Playlist/PlaylistsView.js @@ -146,7 +146,7 @@ function PlaylistLink(props){ } function getPlaylistLink(playlistName){ - return '/app/playlist/' + playlistName; + return `/app/playlist/${playlistName}/edit`; } export default PlaylistsView; \ No newline at end of file diff --git a/src/js/Playlist/View/Count.js b/src/js/Playlist/View/Count.js new file mode 100644 index 0000000..d50cd95 --- /dev/null +++ b/src/js/Playlist/View/Count.js @@ -0,0 +1,79 @@ +import React, { Component } from "react"; +const axios = require('axios'); + +import showMessage from "../../Toast.js" + +class Count extends Component { + + constructor(props){ + super(props); + this.state = { + name: props.name, + lastfm_refresh: 'never', + lastfm_percent: 0, + count: 0, + isLoading: true + } + this.getUserInfo(); + + this.updateStats = this.updateStats.bind(this); + } + + getUserInfo(){ + axios.get(`/api/playlist?name=${ this.state.name }`) + .then((response) => { + if(response.data.lastfm_stat_last_refresh != undefined){ + this.setState({ + count: response.data.lastfm_stat_count, + lastfm_refresh: response.data.lastfm_stat_last_refresh, + lastfm_percent: response.data.lastfm_stat_percent, + isLoading: false + }) + }else{ + showMessage('no stats for this playlist'); + } + }) + .catch((error) => { + showMessage(`error getting playlist info (${error.response.status})`); + }); + } + + updateStats(){ + axios.get(`/api/spotfm/playlist/refresh?name=${ this.state.name }`) + .then((response) => { + showMessage('stats refresh queued'); + }) + .catch((error) => { + if(error.response.status == 401){ + showMessage('missing either spotify or last.fm link'); + }else{ + showMessage(`error refreshing (${error.response.status})`); + } + }); + } + + render() { + return ( +
+{ this.state.name } |
- |
---|---|
- spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive - |
- |
- - | -- - | -
- - | -- - | -
- shuffle output? - | -- - | -
- include recommendations? - | -- - | -
- number of recommendations - | -+ |
managed | |
spotify | |
+ spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive + |
+ |
+ + | ++ + | +
+ + | ++ + | +
+ shuffle output? + | ++ + | +
+ include recommendations? + | ++ + | +
+ number of recommendations + | ++ + | +
+ added since (days) + | +- | -
- added since (days) - | -- - | -
- include {thisMonth[date.getMonth()]} playlist - | -- - | -
- include {lastMonth[date.getMonth()]} playlist - | -- - | -
- playlist type - | -- - | -
- - | -
{ this.state.error_text }
; - const loadingMessage =loading...
; + const loadingMessage = + +loading...
+{ this.props.match.params.name } |
+ |
---|---|
+
+
+
|
+