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 = ['*']
CURRENT_HOST = 'http://127.0.0.1:8000' # Needs to be set for spotify callback to work.
INSTALLED_APPS = [
'playlist.apps.PlaylistConfig',
+1
View File
@@ -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
+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.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)
+6
View File
@@ -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)
+1 -2
View File
@@ -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()
+50 -9
View File
@@ -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()
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;
}
.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;
}
+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" />
</div>
</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 %}
<form method="post">
{% csrf_token %}
@@ -32,9 +50,8 @@ Instellingen
</div>
{% endif %}
</form>
<div class="field-row settings">
<label>Nominatie quota:</label>
<span>{{user.profile.quota|floatformat:3}}</span>
</div>
</form>
<div class="field-row settings">
<label>Nominatie quota:</label>
<span>{{user.profile.quota|floatformat:3}}</span>
</div>
{% endblock %}
+13 -13
View File
@@ -18,18 +18,6 @@ Stemmen
<fieldset>
<legend>Hoeveel sterren geef jij dit?</legend>
<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
hx-vals='{"vote": "1"}'
hx-post="{% url 'vote' %}"
@@ -38,13 +26,25 @@ Stemmen
hx-vals='{"vote": "2"}'
hx-post="{% url 'vote' %}"
></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>
</fieldset>
{% if undo %}
<!--<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 }}"}' >
{% 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>
</div>
{% endif %}
+3
View File
@@ -8,6 +8,9 @@ urlpatterns = [
path('vote/', views.VoteView.as_view(), name="vote"),
path('vote/undo', views.UndoView.as_view(), name="undo"),
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('spotify/', views.SpotifyView.as_view(), name="spotify"),
path('spotify/callback', views.SpotifyCallbackView.as_view(), name="spotify-callback"),
path('', views.AuthView.as_view()),
] + 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.template.response import TemplateResponse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Avg
from django.urls import reverse
import re
from collections import OrderedDict
from .models import Track, Artist, Album, Vote, Background, Profile
from .utils import from_json, get_unvoted
from . import spotify
from .spotify import spt
from .tasks import get_banter, get_and_dither_image
spt = spotify.Spotify()
class AuthView(View):
def get(self, request):
if request.user.is_authenticated:
@@ -29,7 +29,7 @@ class VoteView(LoginRequiredMixin, View):
try:
spotify_id = self.request.POST['spotify_id']
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)
try:
Vote.objects.get(user=request.user, track=track)
@@ -82,7 +82,8 @@ class NominateView(LoginRequiredMixin, View):
except Track.DoesNotExist:
try:
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."})
# Album artists
@@ -133,12 +134,22 @@ class SettingsView(LoginRequiredMixin, View):
"backgrounds": backgrounds,
"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)
def post(self, request):
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.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()
except:
pass
@@ -183,4 +194,46 @@ class OverView(LoginRequiredMixin, View):
context = {
"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})