diff --git a/muzak/settings.py b/muzak/settings.py index 4682432..67f7ac8 100644 --- a/muzak/settings.py +++ b/muzak/settings.py @@ -14,6 +14,7 @@ DEBUG = True ALLOWED_HOSTS = ['*'] +CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work. INSTALLED_APPS = [ 'playlist.apps.PlaylistConfig', diff --git a/playlist/models/__init__.py b/playlist/models/__init__.py index 2af94c6..7213a1b 100644 --- a/playlist/models/__init__.py +++ b/playlist/models/__init__.py @@ -3,3 +3,4 @@ from .artist import Artist from .album import Album from .vote import Vote from .profile import Profile, Background +from .playlist import Playlist diff --git a/playlist/models/playlist.py b/playlist/models/playlist.py new file mode 100644 index 0000000..ad030ea --- /dev/null +++ b/playlist/models/playlist.py @@ -0,0 +1,8 @@ +from django.db import models +from .track import Track + +class Playlist(models.Model): + def __str__(self): + return self.name + name = models.CharField(max_length=255) + tracks = models.ManyToManyField(Track) \ No newline at end of file diff --git a/playlist/models/profile.py b/playlist/models/profile.py index 192c553..ed701c5 100644 --- a/playlist/models/profile.py +++ b/playlist/models/profile.py @@ -5,6 +5,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone from datetime import datetime, timedelta +from ..spotify import spt NOMINATIONS_PER_DAY = 10 MAX_QUOTA = 22 @@ -25,6 +26,10 @@ class Profile(models.Model): quota = models.FloatField(default=10.0) quota_last_updated = models.DateTimeField(default=timezone.now, null=False) + spt_access_token = models.CharField(max_length=128, default="", null=True) + spt_refresh_token = models.CharField(max_length=128, default="", null=True) + spt_expires = models.DateTimeField(default=None, null=True) + @property def quota_display(self): return int(self.quota) @@ -38,6 +43,53 @@ class Profile(models.Model): print(f"Er zijn {elapsed.seconds}s voorbij, je nieuwe quota is {self.quota}") self.quota_last_updated = timezone.now() + def init_spt(self, code): + """Initial setting of spotify tokens from user accept code""" + data = spt.get_token_json( + grant_type='authorization_code', + code=code + ) + self.save_spt(data) + + def refresh_spt(self): + """Refreshes expired access token using stored refresh token""" + if self.spt_refresh_token: + data = spt.get_token_json( + grant_type='refresh_token', + refresh_token=self.spt_refresh_token + ) + self.save_spt(data) + + def save_spt(self, data): + """Saves tokens from json response""" + access = data.get('access_token', None) + refresh = data.get('refresh_token', self.spt_refresh_token) + expires = data.get('expires_in', 3600) + self.spt_access_token = access + self.spt_refresh_token = refresh + self.spt_expires = timezone.now() + timedelta(seconds=expires - 300) + self.save() + + def unlink_spt(self): + self.spt_access_token = None + self.spt_refresh_token = None + self.spt_expires = None + + @property + def can_spt(self): + return ( + self.spt_access_token and + self.spt_expires and + (timezone.now() < self.spt_expires) + ) + + def get_or_update_spt(self): + if self.spt_refresh_token: + if not self.can_spt: + self.refresh_spt() + return self.spt_access_token + return None + def save(self, *args, **kwargs): self.update_quota() super().save(*args, **kwargs) diff --git a/playlist/models/track.py b/playlist/models/track.py index d12c6d4..7ecbc55 100644 --- a/playlist/models/track.py +++ b/playlist/models/track.py @@ -2,6 +2,7 @@ from django.db import models from random import choice from .album import Album from .artist import Artist +from .vote import Vote import re class TrackManager(models.Manager): @@ -30,6 +31,10 @@ class Track(models.Model): def link(self): return f'https://open.spotify.com/track/{self.id}' + @property + def score(self): + return Vote.objects.filter(track=self.id).aggregate(rating=models.Avg("points"))["rating"] + def is_duplicate_of(self): tracks = Track.objects.exclude(id=self.id).filter(name__startswith=self.name_sanitized) artists = [artist.name for artist in self.artists.all()] @@ -37,6 +42,7 @@ class Track(models.Model): tracks = tracks.filter(artists__name__exact=artist) return tracks.first() + id = models.CharField(primary_key=True, max_length=128) name = models.CharField(max_length=255) artists = models.ManyToManyField(Artist) diff --git a/playlist/models/vote.py b/playlist/models/vote.py index 3586528..d05712c 100644 --- a/playlist/models/vote.py +++ b/playlist/models/vote.py @@ -1,10 +1,9 @@ from django.db import models from django.contrib.auth.models import User -from . import Track class Vote(models.Model): class Meta: unique_together = ["user", "track"] - track = models.ForeignKey(Track, on_delete=models.CASCADE) + track = models.ForeignKey('Track', on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) points = models.IntegerField() diff --git a/playlist/spotify.py b/playlist/spotify.py index 30f7ba4..ee530be 100644 --- a/playlist/spotify.py +++ b/playlist/spotify.py @@ -3,46 +3,75 @@ import os import re from datetime import datetime, timedelta from dotenv import load_dotenv +from django.urls import reverse +from django.conf import settings load_dotenv() + class Spotify(): def __init__(self): self.token = None self.valid_until = None + def redirect_uri(self): + return settings.CURRENT_HOST + reverse('spotify-callback') + def token_valid(self): if self.token and self.valid_until and (datetime.now() < self.valid_until): return True return False - def get_token(self): - if self.token_valid(): - print("Token still valid") - return self.token - print("New token") + def get_token_json(self, grant_type, code=None, refresh_token=None): url = 'https://accounts.spotify.com/api/token' headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + os.getenv('SPOTIFY_ENCODED_ID') } data = { - 'grant_type': 'client_credentials' + 'grant_type': grant_type } + if code: + data['code'] = code + data['redirect_uri'] = self.redirect_uri() + if refresh_token: + data['refresh_token'] = refresh_token r = requests.post(url, headers=headers, data=data) - data = r.json() + return r.json() + + def get_system_token(self): + if self.token_valid(): + return self.token + data = self.get_token_json('client_credentials') self.token = data["access_token"] self.valid_until = datetime.now() + timedelta(seconds=data["expires_in"]-300) return self.token + def get_user_token(self, code): + data = self.get_token_json('authorization_code', code, redirect=True) + print(data) + return data + return (data.get('access_token', None), data.get('refresh_token', None)) + + def get_oauth_redirect(self): + scopes = ['user-read-private', 'user-read-email'] + CLIENT_ID = 'bcc523219d1d4248a7e8892e809a5767' #TODO: delet. + url = f'https://accounts.spotify.com/authorize?' + url += f'client_id={CLIENT_ID}&' + url += f'response_type=code&' + url += f'scope={"%20".join(scopes)}&' + url += f'redirect_uri={self.redirect_uri()}' + print(url) + return url + def unshort(self, url): - """Converts a spotify.link url into a normal spotify ID""" + """Converts a spotify.link url into a normal spotify URL with ID""" r = requests.get(url, allow_redirects=False) r.raise_for_status() return r.headers["Location"] def get_song_info(self, spotify_id): - token = self.get_token() + token = self.get_system_token() url = 'https://api.spotify.com/v1/tracks/' + spotify_id headers = { 'Content-Type': 'application/json', @@ -51,3 +80,15 @@ class Spotify(): r = requests.get(url, headers=headers) r.raise_for_status() return r.json() + + def get_profile(self, access_token): + url = 'https://api.spotify.com/v1/me' + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + access_token + } + r = requests.get(url, headers=headers) + r.raise_for_status() + return r.json() + +spt = Spotify() \ No newline at end of file diff --git a/playlist/static/img/spotify-cancel.png b/playlist/static/img/spotify-cancel.png new file mode 100644 index 0000000..827afbd Binary files /dev/null and b/playlist/static/img/spotify-cancel.png differ diff --git a/playlist/static/img/spotify.png b/playlist/static/img/spotify.png new file mode 100644 index 0000000..01417df Binary files /dev/null and b/playlist/static/img/spotify.png differ diff --git a/playlist/static/style.css b/playlist/static/style.css index e4aeb29..b5d850d 100644 --- a/playlist/static/style.css +++ b/playlist/static/style.css @@ -477,11 +477,27 @@ input[type="range"]#volume::-webkit-slider-thumb { margin-bottom: 8px; } -.settings .admin-panel { - background-image: url(img/gears.png); +.settings .icon { height: 40px; background-repeat: no-repeat; background-position: 50% 50%; width: 40px; min-width: 0; +} + +.settings .admin-panel { + background-image: url(img/gears.png); +} + +.settings .spotify { + background-image: url(img/spotify.png); + background-size: 32px; +} + +.settings #spotify-cancel { + background-image: url(img/spotify-cancel.png); + background-size: 16px; + background-position: 50% 50%; + background-repeat: no-repeat; + min-width: 0; } \ No newline at end of file diff --git a/playlist/templates/PlaylistMaker.html b/playlist/templates/PlaylistMaker.html new file mode 100644 index 0000000..c3ac988 --- /dev/null +++ b/playlist/templates/PlaylistMaker.html @@ -0,0 +1,22 @@ +{% extends "Base.html" %} + +{% block window-title %} +Nomineren +{% endblock %} + +{% block main-content %} + +{% if tracks %} + {% for track in tracks %} +
{{track}}: {{track.score}}
+ {% endfor %} +{% else %} + +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/playlist/templates/Settings.html b/playlist/templates/Settings.html index 2654aa1..93da805 100644 --- a/playlist/templates/Settings.html +++ b/playlist/templates/Settings.html @@ -13,6 +13,24 @@ Instellingen + {% if sptDisplayName %} + + {% else %} + + {% endif %} {% endif %} -Je hebt {{undo.track}} een score van {{undo.points | add:3}} sterren gegeven!
+Je hebt {{undo.track}} een score van {{undo.points}} sterren gegeven!