From 9df1ac6b58cc707a977351dacba5fe2e4497dcea Mon Sep 17 00:00:00 2001 From: dnns01 <github@dnns01.de> Date: Tue, 7 Dec 2021 12:39:09 +0100 Subject: [PATCH] Add spotify to web interface --- strolchibot/forms.py | 12 +-- strolchibot/migrations/0009_spotify.py | 25 ++++++ strolchibot/models.py | 10 +++ strolchibot/templates/navigation.html | 2 + strolchibot/templates/spotify/edit.html | 9 ++ strolchibot/templates/spotify/list.html | 28 ++++++ strolchibot/urls.py | 4 + strolchibot/views.py | 84 +++++++++++++++++- twitchbot/spotify_cog.py | 108 ++++++++++++++---------- 9 files changed, 231 insertions(+), 51 deletions(-) create mode 100644 strolchibot/migrations/0009_spotify.py create mode 100644 strolchibot/templates/spotify/edit.html create mode 100644 strolchibot/templates/spotify/list.html diff --git a/strolchibot/forms.py b/strolchibot/forms.py index 6e49630..51e24bb 100644 --- a/strolchibot/forms.py +++ b/strolchibot/forms.py @@ -1,6 +1,6 @@ from django import forms -from .models import Config, Command +from .models import Config, Command, Spotify class BaseModelForm(forms.ModelForm): @@ -52,8 +52,8 @@ class CommandForm(BaseModelForm): model = Command exclude = ['active'] -# class ClipEditForm(BaseModelForm): -# class Meta: -# model = Clip -# fields = ['custom_title', 'tags'] -# widgets = {'tags': forms.widgets.CheckboxSelectMultiple()} + +class SpotifyForm(BaseModelForm): + class Meta: + model = Spotify + fields = ['streamer'] diff --git a/strolchibot/migrations/0009_spotify.py b/strolchibot/migrations/0009_spotify.py new file mode 100644 index 0000000..6a8bca3 --- /dev/null +++ b/strolchibot/migrations/0009_spotify.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.8 on 2021-12-04 23:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('strolchibot', '0008_counter'), + ] + + operations = [ + migrations.CreateModel( + name='Spotify', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('streamer', models.TextField(max_length=50, unique=True)), + ('access_token', models.TextField(max_length=200)), + ('token_type', models.TextField(max_length=20)), + ('expires_in', models.IntegerField()), + ('refresh_token', models.TextField(max_length=200)), + ('scope', models.TextField(max_length=100)), + ('user_id', models.TextField(max_length=50)), + ], + ), + ] diff --git a/strolchibot/models.py b/strolchibot/models.py index 4580920..61e3966 100644 --- a/strolchibot/models.py +++ b/strolchibot/models.py @@ -89,3 +89,13 @@ class TwitchUser(models.Model): class Counter(models.Model): name = models.CharField(max_length=50) count = models.IntegerField() + + +class Spotify(models.Model): + streamer = models.TextField(max_length=50, unique=True) + access_token = models.TextField(max_length=200) + token_type = models.TextField(max_length=20) + expires_in = models.IntegerField() + refresh_token = models.TextField(max_length=200) + scope = models.TextField(max_length=100) + user_id = models.TextField(max_length=50) diff --git a/strolchibot/templates/navigation.html b/strolchibot/templates/navigation.html index 8dc5911..f0c9b49 100644 --- a/strolchibot/templates/navigation.html +++ b/strolchibot/templates/navigation.html @@ -12,6 +12,8 @@ class="w3-bar-item w3-button {% if title == "Timers" %}w3-light-gray{% endif %}">Timers</a> <a href="{% url 'link_protection' %}" class="w3-bar-item w3-button {% if title == "Link Protection" %}w3-light-gray{% endif %}">Link Protection</a> + <a href="{% url 'spotify' %}" + class="w3-bar-item w3-button {% if title == "Spotify" %}w3-light-gray{% endif %}">Spotify</a> {% if user.admin %} <a href="{% url 'config' %}" class="w3-bar-item w3-button {% if title == "Config" %}w3-light-gray{% endif %}">Config</a> diff --git a/strolchibot/templates/spotify/edit.html b/strolchibot/templates/spotify/edit.html new file mode 100644 index 0000000..7588a5c --- /dev/null +++ b/strolchibot/templates/spotify/edit.html @@ -0,0 +1,9 @@ +{% extends 'layout.html' %} + +{% block content %} + <form method="post"> + {% csrf_token %} + {% include 'card_form.html' %} + <input type="submit" value="Add/Save" class="w3-button w3-strolchpink w3-block"> + </form> +{% endblock %} \ No newline at end of file diff --git a/strolchibot/templates/spotify/list.html b/strolchibot/templates/spotify/list.html new file mode 100644 index 0000000..deb2a12 --- /dev/null +++ b/strolchibot/templates/spotify/list.html @@ -0,0 +1,28 @@ +{% extends 'layout.html' %} + +{% block content %} + {% csrf_token %} + <div class="w3-card w3-white"> + <div> + <a href="{% url "spotify_login" %}" class="w3-button w3-strolchpink w3-right w3-margin"> + <em class="fas fa-plus"></em>Add/Update</a> + </div> + <div class="w3-padding"> + <table class="w3-table w3-striped"> + <tbody> + <tr> + <th>Streamer</th> + <th></th> + </tr> + {% for spotify_login in spotify_logins %} + <tr> + <td>{{ spotify_login.streamer }}</td> + <td><a href="{% url "spotify_edit" spotify_login.streamer %}"><em + class="fas fa-pencil-alt"></em></a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> +{% endblock %} \ No newline at end of file diff --git a/strolchibot/urls.py b/strolchibot/urls.py index babb13d..be5a8d6 100644 --- a/strolchibot/urls.py +++ b/strolchibot/urls.py @@ -47,6 +47,10 @@ urlpatterns = [ name="link_protection_blacklist_remove"), path('strolchguru/', include('strolchguru.urls')), path('twitter/', include('twitter.urls')), + path('spotify', views.spotify, name="spotify"), + path('spotify/edit/<str:streamer>', views.spotify_edit, name="spotify_edit"), + path('spotify/login', views.spotify_login, name="spotify_login"), + path('spotify/login/redirect', views.spotify_login_redirect, name="spotify_login_redirect"), ] if settings.DEBUG: diff --git a/strolchibot/views.py b/strolchibot/views.py index 86a7cfd..0688dde 100644 --- a/strolchibot/views.py +++ b/strolchibot/views.py @@ -8,8 +8,8 @@ from django.forms import modelformset_factory from django.http import Http404, JsonResponse, HttpResponse, HttpRequest from django.shortcuts import render, redirect, get_object_or_404 -from .forms import BaseModelForm, LinkProtectionConfigForm, CommandForm -from .models import Command, Klassenbuch, Timer, Config, LinkPermit, LinkWhitelist, LinkBlacklist +from .forms import BaseModelForm, LinkProtectionConfigForm, CommandForm, SpotifyForm +from .models import Command, Klassenbuch, Timer, Config, LinkPermit, LinkWhitelist, LinkBlacklist, Spotify def home(request): @@ -296,4 +296,84 @@ def commands_set_active(request: HttpRequest) -> JsonResponse: pass raise Http404 + + +# </editor-fold> + + +# <editor-fold desc="Spotify"> +@login_required(login_url="/login") +def spotify(request: HttpRequest) -> HttpResponse: + spotify_logins = Spotify.objects.all() + + return render(request, "spotify/list.html", + {"title": "Spotify", "spotify_logins": spotify_logins}) + + +def spotify_edit(request: HttpRequest, streamer: str) -> HttpResponse: + user = get_object_or_404(Spotify, streamer=streamer) + + if request.method == "POST": + form = SpotifyForm(request.POST, instance=user) + + if form.is_valid(): + form.save() + return redirect("/spotify") + + form = SpotifyForm(instance=user) + + return render(request, "spotify/edit.html", + {"title": "Spotify", "form": {"form": form}}) + + +@login_required(login_url="/login") +def spotify_login(request: HttpRequest) -> HttpResponse: + url = os.getenv("SPOTIFY_AUTH_URL") + return redirect(url) + + +@login_required(login_url="/login") +def spotify_login_redirect(request: HttpRequest) -> JsonResponse: + code = request.GET.get('code') + data = { + "client_id": os.getenv("SPOTIFY_CLIENT_ID"), + "client_secret": os.getenv("SPOTIFY_CLIENT_SECRET"), + "grant_type": "authorization_code", + "code": code, + "redirect_uri": os.getenv("SPOTIFY_REDIRECT_URI"), + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + response = requests.post("https://accounts.spotify.com/api/token", data=data, headers=headers) + credentials = response.json() + access_token = credentials['access_token'] + token_type = credentials['token_type'] + expires_in = credentials['expires_in'] + refresh_token = credentials['refresh_token'] + scope = credentials['scope'] + + response = requests.get("https://api.spotify.com/v1/me", headers={ + 'Authorization': f'Bearer {access_token}' + }) + + user_id = response.json()['id'] + streamer = response.json()['display_name'].replace(' ', '').lower() + users = Spotify.objects.filter(user_id=user_id) + + if len(list(users)) == 1: + for user in users: + user.access_token = access_token + user.token_type = token_type + user.expires_in = expires_in + user.refresh_token = refresh_token + user.scope = scope + user.save() + return redirect(f"/spotify") + else: + user = Spotify(streamer=streamer, access_token=access_token, token_type=token_type, + expires_in=expires_in, refresh_token=refresh_token, scope=scope, user_id=user_id) + user.save() + return redirect(f"/spotify/edit/{user_id}") + # </editor-fold> diff --git a/twitchbot/spotify_cog.py b/twitchbot/spotify_cog.py index 9388cbf..b1bd517 100644 --- a/twitchbot/spotify_cog.py +++ b/twitchbot/spotify_cog.py @@ -1,5 +1,5 @@ -import json import os +import sqlite3 import requests from twitchio.ext import commands @@ -8,19 +8,37 @@ from twitchio.ext import commands class SpotifyCog(commands.Cog): def __init__(self, bot): self.bot = bot - self.spotify_file = "spotify.json" - self.basic_auth = os.getenv("BASIC_AUTH") - self.spotify = {} - self.load_spotify() + self.basic_auth = os.getenv("SPOTIFY_BASIC_AUTH") self.streamer = None - def load_spotify(self): - spotify_file = open(self.spotify_file, mode='r') - self.spotify = json.load(spotify_file) + def get_streamer(self, name): + streamer = None + conn = sqlite3.connect("db.sqlite3") + c = conn.cursor() + c.execute( + "SELECT access_token, token_type, expires_in, refresh_token, scope, user_id from strolchibot_spotify where streamer = ?", + (name,)) + if s := c.fetchone(): + streamer = { + "access_token": s[0], + "token_type": s[1], + "expires_in": s[2], + "refresh_token": s[3], + "scope": s[4], + "user_id": s[5], + } - def save_spotify(self): - spotify_file = open(self.spotify_file, mode='w') - json.dump(self.spotify, spotify_file) + conn.close() + return streamer + + def update_streamer(self, streamer, name): + conn = sqlite3.connect("db.sqlite3") + c = conn.cursor() + c.execute( + "UPDATE strolchibot_spotify set access_token = ? where streamer = ?", + (streamer["access_token"], name,)) + conn.commit() + conn.close() @commands.command(name="song") async def cmd_song(self, ctx): @@ -39,44 +57,48 @@ class SpotifyCog(commands.Cog): "Ich würde ja gerne sagen, welcher kultige Song gerade läuft. Leider hat mir noch keiner gesagt, wer eigentlich gerade streamt strolchWut") def get_song(self): - response = requests.get("https://api.spotify.com/v1/me/player", headers={ - 'Authorization': f'Bearer {self.spotify[self.streamer]["access_token"]}' - }) - - if response.status_code == 204: - return None + streamer = self.get_streamer(self.streamer) + if streamer: + response = requests.get("https://api.spotify.com/v1/me/player", headers={ + 'Authorization': f'Bearer {streamer["access_token"]}' + }) - player = response.json() + if response.status_code == 204: + return None - if response.status_code == 401 and player.get("error") and player.get("error").get( - "message") == "The access token expired": - self.refresh_token() - return self.get_song() - elif response.status_code == 200 and player.get("is_playing"): player = response.json() - item = player['item'] - artists = ", ".join([artist['name'] for artist in item['artists']]) - name = item['name'] - album = item['album']['name'] - url = item['external_urls']['spotify'] - return {"name": name, "artists": artists, "album": album, "url": url} - else: - return None + if response.status_code == 401: + self.refresh_token() + return self.get_song() + elif response.status_code == 200 and player.get("is_playing"): + player = response.json() + item = player['item'] + artists = ", ".join([artist['name'] for artist in item['artists']]) + name = item['name'] + album = item['album']['name'] + url = item['external_urls']['spotify'] + + return {"name": name, "artists": artists, "album": album, "url": url} + + return None def refresh_token(self): - data = { - "grant_type": "refresh_token", - "refresh_token": f'{self.spotify[self.streamer]["refresh_token"]}' - } - headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': f'Basic {self.basic_auth}' - } - response = requests.post("https://accounts.spotify.com/api/token", data=data, headers=headers) - credentials = response.json() - self.spotify[self.streamer]["access_token"] = credentials["access_token"] - self.save_spotify() + streamer = self.get_streamer(self.streamer) + + if streamer: + data = { + "grant_type": "refresh_token", + "refresh_token": f'{streamer["refresh_token"]}' + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {self.basic_auth}' + } + response = requests.post("https://accounts.spotify.com/api/token", data=data, headers=headers) + credentials = response.json() + streamer["access_token"] = credentials["access_token"] + self.update_streamer(streamer, self.streamer) @commands.command(name="streamer") async def cmd_streamer(self, ctx, streamer): -- GitLab