Spotify user api integration

En een begin aan een playlist maker. Doet op het moment niet veel.
This commit is contained in:
2024-03-13 11:01:23 +01:00
parent aba8d64bed
commit 8dca25f861
15 changed files with 258 additions and 39 deletions
+1
View File
@@ -14,6 +14,7 @@ DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
INSTALLED_APPS = [ INSTALLED_APPS = [
'playlist.apps.PlaylistConfig', 'playlist.apps.PlaylistConfig',
+1
View File
@@ -3,3 +3,4 @@ from .artist import Artist
from .album import Album from .album import Album
from .vote import Vote from .vote import Vote
from .profile import Profile, Background from .profile import Profile, Background
from .playlist import Playlist
+8
View File
@@ -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)
+52
View File
@@ -5,6 +5,7 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ..spotify import spt
NOMINATIONS_PER_DAY = 10 NOMINATIONS_PER_DAY = 10
MAX_QUOTA = 22 MAX_QUOTA = 22
@@ -25,6 +26,10 @@ class Profile(models.Model):
quota = models.FloatField(default=10.0) quota = models.FloatField(default=10.0)
quota_last_updated = models.DateTimeField(default=timezone.now, null=False) 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 @property
def quota_display(self): def quota_display(self):
return int(self.quota) 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}") print(f"Er zijn {elapsed.seconds}s voorbij, je nieuwe quota is {self.quota}")
self.quota_last_updated = timezone.now() 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): def save(self, *args, **kwargs):
self.update_quota() self.update_quota()
super().save(*args, **kwargs) super().save(*args, **kwargs)
+6
View File
@@ -2,6 +2,7 @@ from django.db import models
from random import choice from random import choice
from .album import Album from .album import Album
from .artist import Artist from .artist import Artist
from .vote import Vote
import re import re
class TrackManager(models.Manager): class TrackManager(models.Manager):
@@ -30,6 +31,10 @@ class Track(models.Model):
def link(self): def link(self):
return f'https://open.spotify.com/track/{self.id}' 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): def is_duplicate_of(self):
tracks = Track.objects.exclude(id=self.id).filter(name__startswith=self.name_sanitized) tracks = Track.objects.exclude(id=self.id).filter(name__startswith=self.name_sanitized)
artists = [artist.name for artist in self.artists.all()] artists = [artist.name for artist in self.artists.all()]
@@ -37,6 +42,7 @@ class Track(models.Model):
tracks = tracks.filter(artists__name__exact=artist) tracks = tracks.filter(artists__name__exact=artist)
return tracks.first() return tracks.first()
id = models.CharField(primary_key=True, max_length=128) id = models.CharField(primary_key=True, max_length=128)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
artists = models.ManyToManyField(Artist) artists = models.ManyToManyField(Artist)
+1 -2
View File
@@ -1,10 +1,9 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from . import Track
class Vote(models.Model): class Vote(models.Model):
class Meta: class Meta:
unique_together = ["user", "track"] 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) user = models.ForeignKey(User, on_delete=models.CASCADE)
points = models.IntegerField() points = models.IntegerField()
+50 -9
View File
@@ -3,46 +3,75 @@ import os
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dotenv import load_dotenv from dotenv import load_dotenv
from django.urls import reverse
from django.conf import settings
load_dotenv() load_dotenv()
class Spotify(): class Spotify():
def __init__(self): def __init__(self):
self.token = None self.token = None
self.valid_until = None self.valid_until = None
def redirect_uri(self):
return settings.CURRENT_HOST + reverse('spotify-callback')
def token_valid(self): def token_valid(self):
if self.token and self.valid_until and (datetime.now() < self.valid_until): if self.token and self.valid_until and (datetime.now() < self.valid_until):
return True return True
return False return False
def get_token(self): def get_token_json(self, grant_type, code=None, refresh_token=None):
if self.token_valid():
print("Token still valid")
return self.token
print("New token")
url = 'https://accounts.spotify.com/api/token' url = 'https://accounts.spotify.com/api/token'
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + os.getenv('SPOTIFY_ENCODED_ID') 'Authorization': 'Basic ' + os.getenv('SPOTIFY_ENCODED_ID')
} }
data = { 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) 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.token = data["access_token"]
self.valid_until = datetime.now() + timedelta(seconds=data["expires_in"]-300) self.valid_until = datetime.now() + timedelta(seconds=data["expires_in"]-300)
return self.token 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): 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 = requests.get(url, allow_redirects=False)
r.raise_for_status() r.raise_for_status()
return r.headers["Location"] return r.headers["Location"]
def get_song_info(self, spotify_id): 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 url = 'https://api.spotify.com/v1/tracks/' + spotify_id
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -51,3 +80,15 @@ class Spotify():
r = requests.get(url, headers=headers) r = requests.get(url, headers=headers)
r.raise_for_status() r.raise_for_status()
return r.json() 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()
Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+18 -2
View File
@@ -477,11 +477,27 @@ input[type="range"]#volume::-webkit-slider-thumb {
margin-bottom: 8px; margin-bottom: 8px;
} }
.settings .admin-panel { .settings .icon {
background-image: url(img/gears.png);
height: 40px; height: 40px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
width: 40px; width: 40px;
min-width: 0; 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;
} }
+22
View File
@@ -0,0 +1,22 @@
{% extends "Base.html" %}
{% block window-title %}
Nomineren
{% endblock %}
{% block main-content %}
{% if tracks %}
{% for track in tracks %}
<p>{{track}}: {{track.score}}</p>
{% endfor %}
{% else %}
<form method="post">
{% csrf_token %}
Hoe veel nummers wil je?
<input type="number" id="count" name="count">
<input type="submit" value="OK" >
</form>
{% endif %}
{% endblock %}
+22 -5
View File
@@ -13,6 +13,24 @@ Instellingen
<input type="submit" value="" class="icon admin-panel" /> <input type="submit" value="" class="icon admin-panel" />
</div> </div>
</form> </form>
{% if sptDisplayName %}
<form method="post">
{% csrf_token %}
<div class="field-row settings">
<label>Spotify gekoppeld:</label>
<span>{{sptDisplayName}}</span>
<input type="hidden" id="unlink-spotify" name="unlink-spotify" value="true" />
<input type="submit" id="spotify-cancel" value=""/>
</div>
</form>
{% else %}
<form action="{% url 'spotify' %}">
<div class="field-row settings">
<label>Link je spotify:</label>
<input type="submit" value="" class="icon spotify" />
</div>
</form>
{% endif %}
{% endif %} {% endif %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@@ -32,9 +50,8 @@ Instellingen
</div> </div>
{% endif %} {% endif %}
</form> </form>
<div class="field-row settings"> <div class="field-row settings">
<label>Nominatie quota:</label> <label>Nominatie quota:</label>
<span>{{user.profile.quota|floatformat:3}}</span> <span>{{user.profile.quota|floatformat:3}}</span>
</div> </div>
</form>
{% endblock %} {% endblock %}
+13 -13
View File
@@ -18,18 +18,6 @@ Stemmen
<fieldset> <fieldset>
<legend>Hoeveel sterren geef jij dit?</legend> <legend>Hoeveel sterren geef jij dit?</legend>
<div class="star-wrapper"> <div class="star-wrapper">
<a
hx-vals='{"vote": "-2"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "-1"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "0"}'
hx-post="{% url 'vote' %}"
></a>
<a <a
hx-vals='{"vote": "1"}' hx-vals='{"vote": "1"}'
hx-post="{% url 'vote' %}" hx-post="{% url 'vote' %}"
@@ -38,13 +26,25 @@ Stemmen
hx-vals='{"vote": "2"}' hx-vals='{"vote": "2"}'
hx-post="{% url 'vote' %}" hx-post="{% url 'vote' %}"
></a> ></a>
<a
hx-vals='{"vote": "3"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "4"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "5"}'
hx-post="{% url 'vote' %}"
></a>
</div> </div>
</fieldset> </fieldset>
{% if undo %} {% if undo %}
<!--<form class="undo-wrapper" method="post" action="{% url 'undo' %}" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' >--> <!--<form class="undo-wrapper" method="post" action="{% url 'undo' %}" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' >-->
<div class="undo-wrapper" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' > <div class="undo-wrapper" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' >
{% csrf_token %} {% csrf_token %}
<p>Je hebt {{undo.track}} een score van {{undo.points | add:3}} sterren gegeven!</p> <p>Je hebt {{undo.track}} een score van {{undo.points}} sterren gegeven!</p>
<button class="undo" hx-post="{% url 'undo' %}" >Undo</button> <button class="undo" hx-post="{% url 'undo' %}" >Undo</button>
</div> </div>
{% endif %} {% endif %}
+3
View File
@@ -8,6 +8,9 @@ urlpatterns = [
path('vote/', views.VoteView.as_view(), name="vote"), path('vote/', views.VoteView.as_view(), name="vote"),
path('vote/undo', views.UndoView.as_view(), name="undo"), path('vote/undo', views.UndoView.as_view(), name="undo"),
path('settings/', views.SettingsView.as_view(), name="settings"), path('settings/', views.SettingsView.as_view(), name="settings"),
path('settings/make-playlist', views.PlaylistMake.as_view(), name="playlist"),
path('overview/', views.OverView.as_view(), name="overview"), path('overview/', views.OverView.as_view(), name="overview"),
path('spotify/', views.SpotifyView.as_view(), name="spotify"),
path('spotify/callback', views.SpotifyCallbackView.as_view(), name="spotify-callback"),
path('', views.AuthView.as_view()), path('', views.AuthView.as_view()),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+61 -8
View File
@@ -3,15 +3,15 @@ from django.views import View
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg
from django.urls import reverse
import re import re
from collections import OrderedDict from collections import OrderedDict
from .models import Track, Artist, Album, Vote, Background, Profile from .models import Track, Artist, Album, Vote, Background, Profile
from .utils import from_json, get_unvoted from .utils import from_json, get_unvoted
from . import spotify from .spotify import spt
from .tasks import get_banter, get_and_dither_image from .tasks import get_banter, get_and_dither_image
spt = spotify.Spotify()
class AuthView(View): class AuthView(View):
def get(self, request): def get(self, request):
if request.user.is_authenticated: if request.user.is_authenticated:
@@ -29,7 +29,7 @@ class VoteView(LoginRequiredMixin, View):
try: try:
spotify_id = self.request.POST['spotify_id'] spotify_id = self.request.POST['spotify_id']
score = int(self.request.POST['vote']) score = int(self.request.POST['vote'])
score = min(max(-2, score), 2) score = min(max(1, score), 5)
track = Track.objects.get(pk=spotify_id) track = Track.objects.get(pk=spotify_id)
try: try:
Vote.objects.get(user=request.user, track=track) Vote.objects.get(user=request.user, track=track)
@@ -82,7 +82,8 @@ class NominateView(LoginRequiredMixin, View):
except Track.DoesNotExist: except Track.DoesNotExist:
try: try:
json = spt.get_song_info(spotify_id) json = spt.get_song_info(spotify_id)
except: except Exception as e:
print(e)
return TemplateResponse(request, "Nominate.html", {"error": "Fout bij het ophalen van het liedje."}) return TemplateResponse(request, "Nominate.html", {"error": "Fout bij het ophalen van het liedje."})
# Album artists # Album artists
@@ -133,12 +134,22 @@ class SettingsView(LoginRequiredMixin, View):
"backgrounds": backgrounds, "backgrounds": backgrounds,
"currentBackground": (profile.background.id if profile.background else 1) "currentBackground": (profile.background.id if profile.background else 1)
} }
spt_token = profile.get_or_update_spt()
if spt_token:
context["sptDisplayName"] = spt.get_profile(spt_token).get('display_name')
return TemplateResponse(request, "Settings.html", context) return TemplateResponse(request, "Settings.html", context)
def post(self, request): def post(self, request):
try: try:
bg = Background.objects.get(pk=request.POST['background']) bg = request.POST.get('background', None)
unlink_spotify = request.POST.get('unlink-spotify', None)
(profile,_) = Profile.objects.get_or_create(user=request.user) (profile,_) = Profile.objects.get_or_create(user=request.user)
profile.background = bg if bg:
bg = Background.objects.get(pk=request.POST['background'])
profile.background = bg
if unlink_spotify:
print("unlink spt")
profile.unlink_spt()
profile.save() profile.save()
except: except:
pass pass
@@ -183,4 +194,46 @@ class OverView(LoginRequiredMixin, View):
context = { context = {
"artists": artists_ordered "artists": artists_ordered
} }
return TemplateResponse(request, "Overview.html", context) return TemplateResponse(request, "Overview.html", context)
class PlaylistMake(LoginRequiredMixin, View):
def get(self, request, count=None):
context = {}
if count:
#tracks = Track.objects.order_by('score')[count]
tracks = Track.objects.annotate(average_points=Avg('vote__points')).order_by('-average_points')[:count]
context["tracks"] = tracks
return TemplateResponse(request, "PlaylistMaker.html", context)
def post(self, request):
try:
count = int(self.request.POST['count'])
return self.get(request, count=count)
except Exception as e:
print(e)
return self.get(request)
class SpotifyView(LoginRequiredMixin, View):
def get(self, request):
url = spt.get_oauth_redirect()
return HttpResponseRedirect(url)
class SpotifyCallbackView(LoginRequiredMixin, View):
def get(self, request):
error = self.request.GET.get('error')
code = self.request.GET.get('code')
if error:
return TemplateResponse(request, "Nominate.html", {"error": "Je moet wel accepteren, pannenkoek."})
elif code:
(profile,_) = Profile.objects.get_or_create(user=request.user)
profile.init_spt(code)
# data = spt.get_user_token(code)
# access = data.get('access_token', None)
# refresh = data.get('refresh_token', None)
# expires = data.get('expires_in', 3600)
# if access and refresh:
# profile.set_spotify(access, refresh, expires)
# else:
# return TemplateResponse(request, "Nominate.html", {"error": "No."})
return HttpResponseRedirect(reverse('settings'))
#return TemplateResponse(request, "Nominate.html", {"warning": code})