diff --git a/music/api/api.py b/music/api/api.py index 0de9dc8..2f97bca 100644 --- a/music/api/api.py +++ b/music/api/api.py @@ -95,9 +95,14 @@ def playlist(username=None): playlist_add_this_month = request_json.get('add_this_month', None) playlist_add_last_month = request_json.get('add_last_month', None) + playlist_library_tracks = request_json.get('include_library_tracks', None) + playlist_recommendation = request_json.get('include_recommendations', None) playlist_recommendation_sample = request_json.get('recommendation_sample', None) + playlist_chart_range = request_json.get('chart_range', None) + playlist_chart_limit = request_json.get('chart_limit', None) + queried_playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()] if request.method == 'PUT': @@ -111,6 +116,7 @@ def playlist(username=None): 'name': playlist_name, 'parts': playlist_parts if playlist_parts is not None else [], 'playlist_references': playlist_references if playlist_references is not None else [], + 'include_library_tracks': playlist_library_tracks if playlist_library_tracks is not None else False, 'include_recommendations': playlist_recommendation if playlist_recommendation is not None else False, 'recommendation_sample': playlist_recommendation_sample if playlist_recommendation_sample is not None else 10, 'uri': None, @@ -127,6 +133,10 @@ def playlist(username=None): to_add['add_this_month'] = playlist_add_this_month if playlist_add_this_month is not None else False to_add['add_last_month'] = playlist_add_last_month if playlist_add_last_month is not None else False + if playlist_type == 'fmchart': + to_add['chart_range'] = playlist_chart_range + to_add['chart_limit'] = playlist_chart_limit if playlist_chart_limit is not None else 50 + playlists.document().set(to_add) logger.info(f'added {username} / {playlist_name}') @@ -171,15 +181,28 @@ def playlist(username=None): if playlist_add_last_month is not None: dic['add_last_month'] = playlist_add_last_month + if playlist_library_tracks is not None: + dic['include_library_tracks'] = playlist_library_tracks + if playlist_recommendation is not None: dic['include_recommendations'] = playlist_recommendation if playlist_recommendation_sample is not None: dic['recommendation_sample'] = playlist_recommendation_sample + if playlist_chart_range is not None: + dic['chart_range'] = playlist_chart_range + + if playlist_chart_limit is not None: + dic['chart_limit'] = playlist_chart_limit + if playlist_type is not None: dic['type'] = playlist_type + if playlist_type == 'fmchart': + dic['chart_range'] = 'YEAR' + dic['chart_limit'] = 50 + if len(dic) == 0: logger.warning(f'no changes to make for {username} / {playlist_name}') return jsonify({"message": 'no changes to make', "status": "error"}), 400 diff --git a/music/db/database.py b/music/db/database.py index 904087d..9338553 100644 --- a/music/db/database.py +++ b/music/db/database.py @@ -8,7 +8,7 @@ from spotframework.net.network import Network as SpotifyNetwork 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, Sort +from music.model.playlist import Playlist, RecentsPlaylist, LastFMChartPlaylist, Sort db = firestore.Client() @@ -260,6 +260,39 @@ def parse_playlist_reference(username, playlist_ref=None, playlist_snapshot=None add_this_month=playlist_dict.get('add_this_month'), day_boundary=playlist_dict.get('day_boundary')) + elif playlist_dict.get('type') == 'fmchart': + return LastFMChartPlaylist(uri=playlist_dict.get('uri'), + name=playlist_dict.get('name'), + username=username, + + db_ref=playlist_ref, + + include_recommendations=playlist_dict.get('include_recommendations', False), + recommendation_sample=playlist_dict.get('recommendation_sample', 0), + include_library_tracks=playlist_dict.get('include_library_tracks', False), + + parts=playlist_dict.get('parts'), + playlist_references=playlist_dict.get('playlist_references'), + shuffle=playlist_dict.get('shuffle'), + + sort=Sort[playlist_dict.get('sort', 'release_date')], + + description_overwrite=playlist_dict.get('description_overwrite'), + description_suffix=playlist_dict.get('description_suffix'), + + lastfm_stat_count=playlist_dict.get('lastfm_stat_count', 0), + lastfm_stat_album_count=playlist_dict.get('lastfm_stat_album_count', 0), + lastfm_stat_artist_count=playlist_dict.get('lastfm_stat_artist_count', 0), + + lastfm_stat_percent=playlist_dict.get('lastfm_stat_percent', 0), + lastfm_stat_album_percent=playlist_dict.get('lastfm_stat_album_percent', 0), + lastfm_stat_artist_percent=playlist_dict.get('lastfm_stat_artist_percent', 0), + + lastfm_stat_last_refresh=playlist_dict.get('lastfm_stat_last_refresh'), + + chart_limit=playlist_dict.get('chart_limit'), + chart_range=FmNetwork.Range[playlist_dict.get('chart_range')]) + def update_playlist(username: str, name: str, updates: dict) -> None: if len(updates) > 0: diff --git a/music/model/playlist.py b/music/model/playlist.py index a01f39f..b1cd4b6 100644 --- a/music/model/playlist.py +++ b/music/model/playlist.py @@ -3,6 +3,8 @@ from enum import Enum from datetime import datetime from google.cloud.firestore import DocumentReference +from fmframework.net.network import Network + import music.db.database as database @@ -73,6 +75,7 @@ class Playlist: return { 'uri': self.uri, 'name': self.name, + 'type': 'default', 'include_recommendations': self.include_recommendations, 'recommendation_sample': self.recommendation_sample, @@ -325,7 +328,8 @@ class RecentsPlaylist(Playlist): response.update({ 'add_last_month': self.add_last_month, 'add_this_month': self.add_this_month, - 'day_boundary': self.day_boundary + 'day_boundary': self.day_boundary, + 'type': 'recents' }) return response @@ -355,3 +359,97 @@ class RecentsPlaylist(Playlist): def day_boundary(self, value): database.update_playlist(self.username, self.name, {'day_boundary': value}) self._day_boundary = value + + +class LastFMChartPlaylist(Playlist): + def __init__(self, + uri: str, + name: str, + username: str, + + chart_range: Network.Range, + + db_ref: DocumentReference, + + + include_recommendations: bool, + recommendation_sample: int, + include_library_tracks: bool, + + parts: List[str], + playlist_references: List[DocumentReference], + shuffle: bool, + + chart_limit: int = 50, + + sort: Sort = None, + + description_overwrite: str = None, + description_suffix: str = None, + + lastfm_stat_count: int = None, + lastfm_stat_album_count: int = None, + lastfm_stat_artist_count: int = None, + + lastfm_stat_percent: int = None, + lastfm_stat_album_percent: int = None, + lastfm_stat_artist_percent: int = None, + + lastfm_stat_last_refresh: datetime = None): + super().__init__(uri=uri, + name=name, + username=username, + + db_ref=db_ref, + + include_recommendations=include_recommendations, + recommendation_sample=recommendation_sample, + include_library_tracks=include_library_tracks, + + parts=parts, + playlist_references=playlist_references, + shuffle=shuffle, + + sort=sort, + + description_overwrite=description_overwrite, + description_suffix=description_suffix, + + lastfm_stat_count=lastfm_stat_count, + lastfm_stat_album_count=lastfm_stat_album_count, + lastfm_stat_artist_count=lastfm_stat_artist_count, + + lastfm_stat_percent=lastfm_stat_percent, + lastfm_stat_album_percent=lastfm_stat_album_percent, + lastfm_stat_artist_percent=lastfm_stat_artist_percent, + + lastfm_stat_last_refresh=lastfm_stat_last_refresh) + self._chart_range = chart_range + self._chart_limit = chart_limit + + def to_dict(self): + response = super().to_dict() + response.update({ + 'chart_limit': self.chart_limit, + 'chart_range': self.chart_range.name, + 'type': 'fmchart' + }) + return response + + @property + def chart_range(self): + return self._chart_range + + @chart_range.setter + def chart_range(self, value): + database.update_playlist(self.username, self.name, {'chart_range': value.name}) + self._chart_range = value + + @property + def chart_limit(self): + return self._chart_limit + + @chart_limit.setter + def chart_limit(self, value): + database.update_playlist(self.username, self.name, {'chart_limit': value}) + self._chart_limit = value diff --git a/music/tasks/run_user_playlist.py b/music/tasks/run_user_playlist.py index 034b2e1..52d51be 100644 --- a/music/tasks/run_user_playlist.py +++ b/music/tasks/run_user_playlist.py @@ -10,9 +10,11 @@ from spotframework.engine.processor.deduplicate import DeduplicateByID from spotframework.model.uri import Uri +from spotfm.engine.chart_source import ChartSource + import music.db.database as database from music.db.part_generator import PartGenerator -from music.model.playlist import RecentsPlaylist +from music.model.playlist import RecentsPlaylist, LastFMChartPlaylist db = firestore.Client() @@ -34,20 +36,20 @@ def run_user_playlist(username, playlist_name): logger.critical(f'no playlist id to populate ({username}/{playlist_name})') return None - if len(playlist.parts) == 0 and len(playlist.playlist_references) == 0: - logger.critical(f'no playlists to use for creation ({username}/{playlist_name})') - return None - net = database.get_authed_spotify_network(username) engine = PlaylistEngine(net) + if isinstance(playlist, LastFMChartPlaylist) and user.lastfm_username is not None: + engine.sources.append(ChartSource(spotnet=net, fmnet=database.get_authed_lastfm_network(user.username))) + processors = [DeduplicateByID()] - if playlist.shuffle is True: - processors.append(Shuffle()) - else: - processors.append(SortReleaseDate(reverse=True)) + if not isinstance(playlist, LastFMChartPlaylist): + if playlist.shuffle is True: + processors.append(Shuffle()) + else: + processors.append(SortReleaseDate(reverse=True)) part_generator = PartGenerator(user=user) submit_parts = part_generator.get_recursive_parts(playlist.name) @@ -62,6 +64,9 @@ def run_user_playlist(username, playlist_name): if playlist.include_library_tracks: params.append(LibraryTrackSource.Params()) + if isinstance(playlist, LastFMChartPlaylist): + params.append(ChartSource.Params(chart_range=playlist.chart_range, limit=playlist.chart_limit)) + if isinstance(playlist, RecentsPlaylist): boundary_date = datetime.datetime.now(datetime.timezone.utc) - \ datetime.timedelta(days=int(playlist.day_boundary)) diff --git a/src/js/Playlist/View/Edit.js b/src/js/Playlist/View/Edit.js index 1a41133..32a06ca 100644 --- a/src/js/Playlist/View/Edit.js +++ b/src/js/Playlist/View/Edit.js @@ -45,6 +45,9 @@ class Edit extends Component{ playlist_references: [], type: 'default', + chart_limit: '', + chart_range: '', + day_boundary: '', recommendation_sample: '', newPlaylistName: '', @@ -66,9 +69,15 @@ class Edit extends Component{ this.handleRun = this.handleRun.bind(this); this.handleShuffleChange = this.handleShuffleChange.bind(this); + + this.handleIncludeLibraryTracksChange = this.handleIncludeLibraryTracksChange.bind(this); + this.handleIncludeRecommendationsChange = this.handleIncludeRecommendationsChange.bind(this); this.handleThisMonthChange = this.handleThisMonthChange.bind(this); this.handleLastMonthChange = this.handleLastMonthChange.bind(this); + + this.handleChartLimitChange = this.handleChartLimitChange.bind(this); + this.handleChartRangeChange = this.handleChartRangeChange.bind(this); } componentDidMount(){ @@ -124,6 +133,12 @@ class Edit extends Component{ if(event.target.name == 'type'){ this.handleTypeChange(event.target.value); } + if(event.target.name == 'chart_range'){ + this.handleChartRangeChange(event.target.value); + } + if(event.target.name == 'chart_limit'){ + this.handleChartLimitChange(event.target.value); + } } handleDayBoundaryChange(boundary) { @@ -213,6 +228,36 @@ class Edit extends Component{ }); } + handleIncludeLibraryTracksChange(event) { + this.setState({ + include_library_tracks: event.target.checked + }); + axios.post('/api/playlist', { + name: this.state.name, + include_library_tracks: event.target.checked + }).catch((error) => { + showMessage(`error updating library tracks (${error.response.status})`); + }); + } + + handleChartRangeChange(value) { + axios.post('/api/playlist', { + name: this.state.name, + chart_range: value + }).catch((error) => { + showMessage(`error updating chart range (${error.response.status})`); + }); + } + + handleChartLimitChange(value) { + axios.post('/api/playlist', { + name: this.state.name, + chart_limit: parseInt(value) + }).catch((error) => { + showMessage(`error updating limit (${error.response.status})`); + }); + } + handleAddPart(event){ if(this.state.newPlaylistName.length != 0){ @@ -325,26 +370,22 @@ class Edit extends Component{ } handleRun(event){ - if(this.state.playlist_references.length > 0 || this.state.parts.length > 0){ - axios.get('/api/user') - .then((response) => { - if(response.data.spotify_linked == true){ - axios.get('/api/playlist/run', {params: {name: this.state.name}}) - .then((reponse) => { - showMessage(`${this.state.name} ran`); - }) - .catch((error) => { - showMessage(`error running ${this.state.name} (${error.response.status})`); - }); - }else{ - showMessage(`link spotify before running`); - } - }).catch((error) => { - showMessage(`error running ${this.state.name} (${error.response.status})`); - }); - }else{ - showMessage(`add either playlists or parts`); - } + axios.get('/api/user') + .then((response) => { + if(response.data.spotify_linked == true){ + axios.get('/api/playlist/run', {params: {name: this.state.name}}) + .then((reponse) => { + showMessage(`${this.state.name} ran`); + }) + .catch((error) => { + showMessage(`error running ${this.state.name} (${error.response.status})`); + }); + }else{ + showMessage(`link spotify before running`); + } + }).catch((error) => { + showMessage(`error running ${this.state.name} (${error.response.status})`); + }); } render(){ @@ -411,6 +452,16 @@ class Edit extends Component{ onChange={this.handleIncludeRecommendationsChange}> + + + include library tracks? + + + + + number of recommendations @@ -423,6 +474,42 @@ class Edit extends Component{ onChange={this.handleInputChange}> + + { this.state.type == 'fmchart' && + + + limit + + + + + + } + { this.state.type == 'fmchart' && + + + chart range + + + + + + } + { this.state.type == 'recents' && @@ -461,6 +548,8 @@ class Edit extends Component{ } + + playlist type @@ -472,6 +561,7 @@ class Edit extends Component{ value={this.state.type}> +