Skip to content

Commit

Permalink
Version 1.2 (#80)
Browse files Browse the repository at this point in the history
* Rework config and save to playlist arg (#68)

* rework config file & make config-modifying arguments more uniform

* add custom config parser

* finalize new config structure

* clean up conf file and add some print output

* allow adding and removing currently playing track to input playlist

* update readme

* reformat some older functions

* minor bug fixes

* add and update comments

* update intro text in readme

* remove redundant non-positional parameter assignments in function calls

* Update conf.py

Co-Authored-By: Falke Carlsen <[email protected]>

* update readme

Co-authored-by: Falke Carlsen <[email protected]>

* resolve conflicts

* resolve conflicts

* resolve conflicts

* Track feature printing (#75)

* rework config file & make config-modifying arguments more uniform

* add custom config parser

* finalize new config structure

* clean up conf file and add some print output

* allow adding and removing currently playing track to input playlist

* update readme

* reformat some older functions

* minor bug fixes

* add and update comments

* update intro text in readme

* remove redundant non-positional parameter assignments in function calls

* allow printing track features

* Enhance tune validation (#74)

* rework config file & make config-modifying arguments more uniform

* add custom config parser

* finalize new config structure

* clean up conf file and add some print output

* allow adding and removing currently playing track to input playlist

* update readme

* reformat some older functions

* minor bug fixes

* add and update comments

* update intro text in readme

* remove redundant non-positional parameter assignments in function calls

* enhance validity check of tunining

* add version flag (#61)

* remove whitespace error in custom schemes (#62)

* Single playlist (#65)

* use single playlist

* update readme

* Add tuning option to print (#67)

* add tuning option to print

* update readme to reflect install changes

* spelling

* Catch keyboard interrupts (#60)

* catch keyboard interrupts

* catch interrupt on new input prompts

* add flag for changing playback device (#77)

* notify when recommendation yields few different tracks (#78)

* fix minor things before release

Co-authored-by: Falke Carlsen <[email protected]>
  • Loading branch information
Badgie and falkecarlsen authored Mar 11, 2020
1 parent 17cac19 commit 9a5d665
Show file tree
Hide file tree
Showing 6 changed files with 991 additions and 334 deletions.
127 changes: 95 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
</p>

# Spotirec
Script that creates a playlist of recommendations based on the user's top artists or tracks, or genres extracted from top artists. A sort of Discover Weekly on demand.
A tool that can create a playlist of recommendations based on the user's top artists or tracks, or genres extracted from top artists with various parameters - a sort of Discover Weekly on demand. Also includes functionality for various other Spotify-related actions, such as saving the currently playing track.

## Table of Contents
- [Installation](#installation)
- [AUR](#aur-helper)
- [Manual](#manual)
- [Usage](#usage)
- [Recommendation Schemes](#recommendation-schemes)
- [Preserving Playlists](#preserving-playlists)
- [Limits](#limits)
- [Presets](#presets)
- [Tuning](#tuning)
- [Blacklists](#blacklists)
- [Autoplay](#autoplay)
- [Devices](#devices)
- [Saving Playlists](#saving-playlists)
- [Saving Tracks](#saving-tracks)
- [Printing](#printing)
- [Troubleshooting](#troubleshooting)

Expand All @@ -33,7 +36,7 @@ yay -S spotirec-git
```

#### Manual
On any other distribution you need to install Spotirec manually. Spotirec has two dependencies
On any other distribution you need to install Spotirec manually. Spotirec has three dependencies
```
bottle>=0.12.17
requests>=2.22.0
Expand All @@ -50,7 +53,8 @@ mkdir -p /usr/lib/spotirec
mkdir -p /usr/bin
mkdir -p $HOME/.config/spotirec
install spotirec.py oauth2.py recommendation.py api.py -t /usr/lib/spotirec
install tuning-opts -t $HOME/.config/spotirec
install spotirec.py oauth2.py conf.py recommendation.py api.py -t /usr/lib/spotirec
ln -s /usr/lib/spotirec/spotirec.py /usr/bin/spotirec
```
Expand Down Expand Up @@ -100,6 +104,9 @@ $ spotirec -t 4
```
Note that if this option is used with no-arg, it **must** be the very first argument

### Preserving Playlists
By default, Spotirec caches the id of the first playlist created and uses this every time new recommendations are requested, meaning that any old tracks are overwritten. To avoid this and create a new playlist instead, pass the `--preserve` flag.

### Limits
You can add a limit as an integer value with the `-l` argument
```
Expand All @@ -108,17 +115,22 @@ $ spotirec -l 50
This option determines how many tracks should be added to your new playlist. The default value is 20, the minimum value is 1, and the max value is 100.

### Presets
You can save the settings for a recommendation with the `-ps` argument followed by a name
You can save the settings for a recommendation with the `--save-preset` flag followed by a name
```
$ spotirec -t --save-preset preset_name -l 50 --tune prefix_attribute=value prefix_attribute=value
```
$ spotirec -t -ps name -l 50 --tune prefix_attribute=value prefix_attribute=value
To load and use a saved preset, pass the `--load-preset` flag followed by the name of the preset
```
To load and use a saved preset, pass the `-p` argument followed by the name of the preset
$ spotirec --load-preset preset_name
```
$ spotirec -p name
To remove one or more saved presets, pass the `--remove-presets` flag followed by a sequence of preset names
```
$ spotirec --remove-presets preset_name0 preset_name1 preset_name2
```
If you forgot which presets you have saved, see [printing](#printing)

### Tuning
You can also specify tunable attributes with the `--tune` option, followed by any number of whitespace separated arguments on the form `prefix_attribute=value`
You can also specify tunable attributes with the `--tune` flag, followed by any number of whitespace separated inputs on the form `prefix_attribute=value`
```
$ spotirec --tune prefix_attribute=value prefix_attribute=value
```
Expand All @@ -138,67 +150,118 @@ $ spotirec --tune prefix_attribute=value prefix_attribute=value
| key | int | 0-11 | N/A | [Pitch class](https://en.wikipedia.org/wiki/Pitch_class#Other_ways_to_label_pitch_classes) of the track. |
| mode | int | 0-1 | N/A | Modality of the track. 1 is major, 0 is minor. |
| time_signature | int | N/A | N/A | Estimated overall [time signature](https://en.wikipedia.org/wiki/Time_signature) of the track. |
| popularity | int | 0-100 | 0-100 | Popularity of the track. High is popular, low is barely known |
| acousticness | float | 0.0-1.0 | Any | Confidence measure for whether or not the track is acoustic. High value is acoustic. |
| popularity | int | 0-100 | 0-100 | Popularity of the track. High is popular, low is barely known. |
| acousticness | float | 0.0-1.0 | 0.0-1.0 | Confidence measure for whether or not the track is acoustic. High value is acoustic. |
| danceability | float | 0.0-1.0 | 0.1-0.9 | How well fit a track is for dancing. Measurement includes among others tempo, rhythm stability, and beat strength. High value is suitable for dancing. |
| energy | float | 0.0-1.0 | Any | Perceptual measure of intensity and activity. High energy is fast, loud, and noisy, and low is slow and mellow. |
| energy | float | 0.0-1.0 | 0.0-1.0 | Perceptual measure of intensity and activity. High energy is fast, loud, and noisy, and low is slow and mellow. |
| instrumentalness | float | 0.0-1.0 | 0.0-1.0 | Whether or not a track contains vocals. Low contains vocals, high is purely instrumental. |
| liveness | float | 0.0-1.0 | 0.0-0.4 | Predicts whether or not a track is live. High value is live. |
| loudness | float | -60-0 | -20-0 | Overall loudness of the track, measured in decibels. |
| speechiness | float | 0.0-1.0 | 0.0-0.3 | Presence of spoken words. Low is a song, and high is likely to be a talk show or podcast. |
| valence | float | 0.0-1.0 | Any | Positivity of the track. High value is positive, and low value is negative. |
| speechiness | float | 0.0-1.0 | 0.0-0.3 | Presence of spoken words. Low is a song, high is likely to be a talk show or podcast. |
| valence | float | 0.0-1.0 | 0.0-1.0 | Positivity of the track. High value is positive, low value is negative. |
| tempo | float | 0.0-220.0 | 60.0-210.0 | Overall estimated beats per minute of the track. |

Recommendations may be sparce outside the recommended range.
Recommendations may be scarce outside the recommended range.

### Blacklists
To blacklist tracks or artists, pass the `-b` option followed by an arbitrary number of whitespace separated Spotify URIs
To blacklist tracks or artists, pass the `-b` argument followed by an arbitrary number of whitespace separated Spotify URIs
```
$ spotirec -b spotify:track:id spotify:track:id spotify:artist:id
```
To remove entries from your blacklist, pass the `-br` option followed by an arbitrary number of whitespace separated Spotify URIs
To remove entries from your blacklist, pass the `-br` argument followed by an arbitrary number of whitespace separated Spotify URIs
```
$ spotirec -br spotify:track:id spotify:track:id spotify:artist:id
```
To blacklist the currently playing track, or the artists that created the track, pass the `-bc` argument followed by either 'artist' or 'track'
```
$ spotirec -bc track
$ spotirec -bc artist
```
If you forgot which tracks and artists you have blacklisted, see [printing](#printing)

### Autoplay
You can also automatically play your new playlist upon creation using the `--play` option - here you will be prompted to select which device you want to start the playback on
You can also automatically play your new playlist upon creation using the `--play` flag followed by a name of a saved device - see [devices](#devices)
```
$ spotirec --play device_name
```
$ spotirec --play
Available devices:

### Devices
You can save devices using the `--save-device` flag, whereafter you will be prompted to select a device from your currently connected devices, and to input a name that will serve as an identifier
```
$ spotirec --save-device
Name Type
----------------------------------------
0. Phone Smartphone
1. Laptop Computer
Please select a device by index number [default: Phone]: 1
Would you like to save "Laptop" for later use? [y/n] y
Select a device by index [0]: 1
Enter an identifier for your device: laptop
Saved device "Laptop" as "laptop"
Added device laptop to config
```
You will also be asked if you want to save the device in your config for later use. If you choose to do so, you can use the `--play-device` option followed by an identifier for a device to play on a saved device
To remove one or more saved devices, pass the `--remove-devices` flag followed by a sequence of names for devices
```
$ spotirec --play-device laptop
$ spotirec --remove-devices device_name0 device_name1 device_name2
```
If you forgot which devices you have saved, see [printing](#printing)

### Devices
You can also manually save devices using the `-d` option, which provides the same functionality as `--play` without creating a playlist
### Saving playlists
You can save playlists using the `--save-playlist` flag, whereafter you will be prompted to input an identifier for the playlist, and then a URI for the playlist. For further usage of this, see [saving tracks](#saving-tracks)
```
$ spotirec --save-playlist
Please input an identifier for your playlist: test
Please input the URI for your playlist: spotify:playlist:0Vu97Y7WoJgBlFzAwbrZ8h
Added playlist test to config
```
To remove one or more saved playlists, pass the `--remove-playlists` flag followed by a sequence of names for playlists
```
$ spotirec --remove-playlists playlist_name0 playlist_name1 playlist_name2
```
If you forgot which playlists you have saved, see [printing](#printing)

### Saving Tracks
To like the currently playing track, pass the `-s` argument
```
$ spotirec -d
$ spotirec -s
```
To remove a saved device, pass the `-dr` option followed by an identifier for a device
To remove the currently playing track from liked tracks, pass the `-sr` argument
```
$ spotirec -dr laptop
$ spotirec -sr
```

To add the currently playing track to a specific playlist, pass the `--add-to` flag followed by a name for a saved playlist, or a playlist URI
```
$ spotirec --add-to playlist_name
$ spotirec --add-to spotify:playlist:0Vu97Y7WoJgBlFzAwbrZ8h
```

To remove the currently playing track from a specific playlist, pass the `--remove-from` flag followed by a name for a saved playlist, or a playlist URI
```
$ spotirec --remove-from playlist_name
$ spotirec --remove-from spotify:playlist:0Vu97Y7WoJgBlFzAwbrZ8h
```

### Printing
You can print lists of various data contained within your Spotify account and config files using the `--print` argument followed by `[artists|tracks|genres|genre-seeds|blacklist|devices]`
You can print lists of various data contained within your Spotify account and config files using the `--print` flag followed by any of the following strings, depending on what you would like to print
```
$ spotirec --print artists
$ spotirec --print tracks
$ spotirec --print genres
$ spotirec --print genre-seeds
$ spotirec --print blacklist
$ spotirec --print devices
$ spotirec --print blacklist
$ spotirec --print presets
$ spotirec --print playlists
$ spotirec --print tuning
```

You can also print various features of a track with the `--track-features` flag followed by either a URI or 'current' if you want information about the currently playing track. Features include track attributes (as used in [tuning](#tuning)) and URIs.
```
$ spotirec --track-features current
$ spotirec --track-features spotify:track:4uLU6hMCjMI75M1A2tKUQC
```

### Playback
You can change playback to a different device by passing the `--transfer-playback` device followed by an identifier for a saved device
```
$ spotirec --transfer-playback phone
```

## Troubleshooting
Expand Down
99 changes: 97 additions & 2 deletions api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
import json
import requests
import conf

url_base = 'https://api.spotify.com/v1'

Expand Down Expand Up @@ -47,9 +48,10 @@ def get_user_id(headers: dict) -> str:
return json.loads(response.content.decode('utf-8'))['id']


def create_playlist(playlist_name: str, playlist_description: str, headers: dict) -> str:
def create_playlist(playlist_name: str, playlist_description: str, headers: dict, cache_id=False) -> str:
"""
Creates playlist on user's account.
:param cache_id: whether playlist id should be saved as default or not
:param playlist_name: name of the playlist
:param playlist_description: description of the playlist
:param headers: request headers
Expand All @@ -60,7 +62,10 @@ def create_playlist(playlist_name: str, playlist_description: str, headers: dict
print('Creating playlist')
response = requests.post(f'{url_base}/users/{get_user_id(headers)}/playlists', json=data, headers=headers)
error_handle('playlist creation', 201, 'POST', response=response)
return json.loads(response.content.decode('utf-8'))['id']
playlist = json.loads(response.content.decode('utf-8'))
if cache_id:
conf.save_playlist({'name': playlist['name'], 'uri': playlist['uri']}, 'spotirec-default')
return playlist['id']


def upload_image(playlist_id: str, data: str, img_headers: dict):
Expand Down Expand Up @@ -187,3 +192,93 @@ def unlike_track(headers: dict):
response = requests.delete(f'{url_base}/me/tracks', headers=headers, params=track)
error_handle('remove liked track', 200, 'DELETE', response=response)


def update_playlist_details(name: str, description: str, playlist_id: str, headers: dict):
"""
Update the details of a playlist
:param playlist_id: id of the playlist
:param name: new name of the playlist
:param description: new description of the playlist
:param headers: request headers
:return:
"""
data = {'name': name, 'description': description}
response = requests.put(f'{url_base}/playlists/{playlist_id}', headers=headers, json=data)
error_handle('update playlist details', 200, 'PUT', response=response)


def replace_playlist_tracks(playlist_id: str, tracks: list, headers: dict):
"""
Remove the tracks from a playlist
:param tracks: list of track uris
:param playlist_id: id of the playlist
:param headers: request headers
:return:
"""
data = {'uris': tracks}
response = requests.put(f'{url_base}/playlists/{playlist_id}/tracks', headers=headers, json=data)
error_handle('remove tracks from playlist', 201, 'PUT', response=response)


def get_playlist(headers: dict, playlist_id: str):
"""
Retrieve playlist from API
:param headers: request headers
:param playlist_id: ID of the playlist
:return: playlist object
"""
response = requests.get(f'{url_base}/playlists/{playlist_id}', headers=headers)
error_handle('retrieve playlist', 200, 'GET', response=response)
return json.loads(response.content.decode('utf-8'))


def remove_from_playlist(tracks: list, playlist_id: str, headers: dict):
"""
Remove track(s) from a playlist
:param tracks: the tracks to remove
:param playlist_id: identifier of the playlist to remove tracks from
:param headers: request headers
"""
data = {'tracks': [{'uri': x} for x in tracks]}
response = requests.delete(f'{url_base}/playlists/{playlist_id}/tracks', headers=headers, json=data)
error_handle('delete track from playlist', 200, 'DELETE', response=response)


def get_audio_features(track_id: str, headers: dict) -> json:
"""
Get audio features of a track
:param track_id: id of the track
:param headers: request headers
:return: audio features object
"""
response = requests.get(f'{url_base}/audio-features/{track_id}', headers=headers)
error_handle('retrieve audio features', 200, 'GET', response=response)
return json.loads(response.content.decode('utf-8'))


def check_if_playlist_exists(playlist_id: str, headers: dict) -> bool:
"""
Checks whether a playlist exists
:param playlist_id: id of playlist
:param headers: request headers
:return: bool determining if playlist exists
"""
response = requests.get(f'{url_base}/playlists/{playlist_id}', headers=headers)
# If playlist is public, return true (if playlist has been deleted, this value is false)
if json.loads(response.content.decode('utf-8'))['public']:
return True
else:
print('Playlist has either been deleted, or made private, creating new...')
return False


def transfer_playback(device_id: str, headers: dict, start_playback=True):
"""
Transfer playback to device
:param device_id: id to transfer playback to
:param headers: request headers
:param start_playback: if music should start playing or not
"""
data = {'device_ids': [device_id], 'play': start_playback}
response = requests.put(f'{url_base}/me/player', headers=headers, json=data)
error_handle('transfer playback', 204, 'PUT', response=response)
Loading

0 comments on commit 9a5d665

Please sign in to comment.