Spotify user api integration
En een begin aan een playlist maker. Doet op het moment niet veel.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user