formatting head
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SpotifyConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'spotify'
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-18 22:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SpotifyToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user', models.CharField(max_length=50, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('refresh_token', models.CharField(max_length=150)),
|
||||
('access_token', models.CharField(max_length=150)),
|
||||
('expires_in', models.DateTimeField()),
|
||||
('token_type', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SpotifyToken(models.Model):
|
||||
user = models.CharField(max_length=50, unique=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
refresh_token = models.CharField(max_length=150)
|
||||
access_token = models.CharField(max_length=150)
|
||||
expires_in = models.DateTimeField()
|
||||
token_type = models.CharField(max_length=50)
|
||||
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
from .models import SpotifyToken
|
||||
|
||||
|
||||
class TokenSerializer(serializers.ModelSerializer):
|
||||
class Meta: # pyright: ignore
|
||||
model = SpotifyToken
|
||||
fields = (
|
||||
'id',
|
||||
'user',
|
||||
'access_token',
|
||||
'token_type',
|
||||
'expires_in',
|
||||
'refresh_token',
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
from .views import AuthURL, CurrentSong, PauseSong, PlaySong, SpotifyList, spotify_callback, IsAuthenticated
|
||||
|
||||
urlpatterns = [
|
||||
path('get-auth-url', AuthURL.as_view()),
|
||||
path('redirect', spotify_callback),
|
||||
path('is-auth', IsAuthenticated.as_view()),
|
||||
path('current-song', CurrentSong.as_view()),
|
||||
path('tokens', SpotifyList.as_view()),
|
||||
path('play', PlaySong.as_view()),
|
||||
path('pause', PauseSong.as_view()),
|
||||
]
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
from django.utils import timezone
|
||||
from .models import SpotifyToken
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
from requests import post, put, get
|
||||
|
||||
load_dotenv()
|
||||
|
||||
CLIENT_ID = os.getenv('CLIENT_ID')
|
||||
CLIENT_SECRET = os.getenv('CLIENT_SECRET')
|
||||
|
||||
BASE_URL = 'https://api.spotify.com/v1/me/'
|
||||
|
||||
|
||||
def get_user_token(session_id):
|
||||
user_tokens = SpotifyToken.objects.filter(user=session_id)
|
||||
# DEBUG
|
||||
print('## get_user_token()##')
|
||||
print('user_tokens:', user_tokens.first())
|
||||
print('session_id', session_id)
|
||||
|
||||
if user_tokens.exists():
|
||||
return user_tokens[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def update_or_create_user_tokens(session_id, access_token, token_type, expires_in, refresh_token):
|
||||
# default expires in = 3600 from spotify
|
||||
tokens = get_user_token(session_id)
|
||||
expires_in = timezone.now() + timedelta(seconds=expires_in)
|
||||
|
||||
# update if exist
|
||||
if tokens:
|
||||
tokens.access_token = access_token
|
||||
tokens.expires_in = expires_in
|
||||
tokens.refresh_token = refresh_token
|
||||
tokens.token_type = token_type
|
||||
tokens.save(update_fields=['access_token', 'expires_in', 'refresh_token', 'token_type'])
|
||||
|
||||
else: # create on db
|
||||
tokens = SpotifyToken(
|
||||
user=session_id,
|
||||
access_token=access_token,
|
||||
token_type=token_type,
|
||||
expires_in=expires_in,
|
||||
refresh_token=refresh_token,
|
||||
)
|
||||
tokens.save()
|
||||
|
||||
|
||||
def is_spotify_authenticated(session_id):
|
||||
tokens = get_user_token(session_id)
|
||||
|
||||
if tokens:
|
||||
expiry = tokens.expires_in
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def refresh_spotify_token(session_id):
|
||||
refresh_token = get_user_token(session_id).refresh_token
|
||||
|
||||
_response = post(
|
||||
'https://accounts.spotify.com/api/token',
|
||||
data={
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token,
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
},
|
||||
).json()
|
||||
|
||||
# dont need these just in case
|
||||
access_token = _response.get('access_token')
|
||||
token_type = _response.get('token_type')
|
||||
expires_in = _response.get('expires_in')
|
||||
|
||||
update_or_create_user_tokens(session_id, access_token, token_type, expires_in, refresh_token)
|
||||
|
||||
|
||||
def spotify_api_request(session_id, endpoint, post_=False, put_=False):
|
||||
tokens = get_user_token(session_id)
|
||||
|
||||
# endpoint = 'player/currently-playing'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + tokens.access_token,
|
||||
}
|
||||
if post_:
|
||||
response = post(BASE_URL + endpoint, headers=headers)
|
||||
try:
|
||||
return response.json()
|
||||
except:
|
||||
return {'Error': 'Issue with POST request'}
|
||||
|
||||
if put_:
|
||||
response = put(BASE_URL + endpoint, headers=headers)
|
||||
try:
|
||||
return response.json()
|
||||
except:
|
||||
return {'Error': 'Issue with PUT request'}
|
||||
|
||||
# GET request (default)
|
||||
spotify_response = get(BASE_URL + endpoint, headers=headers)
|
||||
|
||||
try:
|
||||
return spotify_response.json()
|
||||
except:
|
||||
return {'Error': 'Issue with GET request', 'status_code': spotify_response.status_code}
|
||||
|
||||
|
||||
def play_song(session_id):
|
||||
return spotify_api_request(session_id, 'player/play', put_=True)
|
||||
|
||||
|
||||
def pause_song(session_id):
|
||||
return spotify_api_request(session_id, 'player/pause', put_=True)
|
||||
@@ -0,0 +1,200 @@
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
from dotenv import load_dotenv
|
||||
from requests import Request, post
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status, generics
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import redirect
|
||||
from .util import is_spotify_authenticated, update_or_create_user_tokens, spotify_api_request, play_song, pause_song
|
||||
from api.models import Room
|
||||
from .models import SpotifyToken
|
||||
from .serializers import TokenSerializer
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if os.getenv('Docker'):
|
||||
REACT_PORT = os.getenv('REACT_PORT')
|
||||
API_PORT = os.getenv('API_PORT')
|
||||
WEBSITE = 'https://gxnet.cc'
|
||||
REDIRECT_URI = 'https://gxnet.cc/spotify/redirect'
|
||||
else:
|
||||
REACT_PORT = 5173
|
||||
API_PORT = 8000
|
||||
REDIRECT_URI = f'http://127.0.0.1:{API_PORT}/spotify/redirect'
|
||||
|
||||
CLIENT_ID = os.getenv('CLIENT_ID')
|
||||
CLIENT_SECRET = os.getenv('CLIENT_SECRET')
|
||||
|
||||
|
||||
class AuthURL(APIView):
|
||||
def get(self, request):
|
||||
if not request.session.exists(request.session.session_key):
|
||||
request.session.create()
|
||||
orig_sid = request.session.session_key
|
||||
|
||||
room_code = request.GET.get('state')
|
||||
state_obj = {"room": room_code, "sid": orig_sid}
|
||||
state = base64.urlsafe_b64encode(json.dumps(state_obj).encode()).decode()
|
||||
|
||||
scopes = 'user-read-playback-state user-modify-playback-state user-read-currently-playing'
|
||||
url = (
|
||||
Request(
|
||||
'GET',
|
||||
'https://accounts.spotify.com/authorize',
|
||||
params={
|
||||
'scope': scopes,
|
||||
'response_type': 'code',
|
||||
'client_id': CLIENT_ID,
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'state': state,
|
||||
},
|
||||
)
|
||||
.prepare()
|
||||
.url
|
||||
)
|
||||
|
||||
# sanity debug
|
||||
print('🎧client_id:', CLIENT_ID)
|
||||
print('🎧 orig_sid:', orig_sid)
|
||||
print(' 🎧url:', url)
|
||||
|
||||
return Response({'url': url}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# https://developer-assets.spotifycdn.com/images/documentation/web-api/auth-code-flow.png
|
||||
def spotify_callback(request):
|
||||
code = request.GET.get('code')
|
||||
raw_state = request.GET.get("state") or ""
|
||||
state = json.loads(base64.urlsafe_b64decode(raw_state).decode())
|
||||
error = request.GET.get('error')
|
||||
|
||||
room_code = state.get("room") # <- plain string
|
||||
orig_sid = state.get("sid")
|
||||
|
||||
response = post(
|
||||
'https://accounts.spotify.com/api/token',
|
||||
data={
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': REDIRECT_URI,
|
||||
'client_id': CLIENT_ID,
|
||||
'client_secret': CLIENT_SECRET,
|
||||
},
|
||||
).json()
|
||||
|
||||
access_token = response.get('access_token')
|
||||
token_type = response.get('token_type')
|
||||
refresh_token = response.get('refresh_token')
|
||||
expires_in = response.get('expires_in')
|
||||
error = response.get('error')
|
||||
|
||||
if not request.session.exists(request.session.session_key):
|
||||
request.session.create()
|
||||
cur_sid = request.session.session_key
|
||||
|
||||
# if session changed, rebind room host
|
||||
room = Room.objects.filter(code=room_code, host=orig_sid).first()
|
||||
if room and cur_sid != orig_sid:
|
||||
room.host = cur_sid
|
||||
room.save(update_fields=["host"])
|
||||
|
||||
update_or_create_user_tokens(request.session.session_key, access_token, token_type, expires_in, refresh_token)
|
||||
|
||||
if room_code:
|
||||
request.session['room_code'] = room_code
|
||||
|
||||
print('🎧### On spotify Callback() ## 🎧')
|
||||
print("Host:", request.get_host())
|
||||
print("Cookies:", request.COOKIES)
|
||||
print("Session key:", request.session.session_key)
|
||||
print('room_code REDIRECT:', room_code)
|
||||
|
||||
if WEBSITE:
|
||||
target = f'{WEBSITE}/room/{room_code}?code={room_code}&auth=done'
|
||||
else:
|
||||
target = f'http://127.0.0.1:{REACT_PORT}/room/{room_code}?code={room_code}&auth=done'
|
||||
|
||||
return redirect(target)
|
||||
|
||||
|
||||
class IsAuthenticated(APIView):
|
||||
def get(self, request):
|
||||
is_auth = is_spotify_authenticated(self.request.session.session_key)
|
||||
return Response({'status': is_auth, 'message': '🎧'})
|
||||
|
||||
|
||||
class CurrentSong(APIView):
|
||||
def get(self, request):
|
||||
room_code = self.request.session.get('room_code')
|
||||
print('DEBUG ; room_code:', room_code)
|
||||
|
||||
room = Room.objects.filter(code=room_code).first()
|
||||
if room:
|
||||
host = room.host
|
||||
|
||||
else:
|
||||
return Response({'message': 'not a room'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
endpoint = 'player/currently-playing'
|
||||
response = spotify_api_request(host, endpoint)
|
||||
|
||||
if 'error' in response or 'item' not in response:
|
||||
return Response({'error': 'error response from spotify'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
else:
|
||||
item = response.get('item')
|
||||
duration = item.get('duration_ms')
|
||||
progress = response.get('progress_ms')
|
||||
album_cover = item.get('album').get('images')[0].get('url')
|
||||
is_playing = response.get('is_playing')
|
||||
song_id = item.get('id')
|
||||
|
||||
artist_string = ""
|
||||
|
||||
for i, artist in enumerate(item.get('artists')):
|
||||
if i > 0:
|
||||
artist_string += ", "
|
||||
name = artist.get('name')
|
||||
artist_string += name
|
||||
|
||||
song = {
|
||||
'title': item.get('name'),
|
||||
'artist': artist_string,
|
||||
'duration': duration,
|
||||
'time': progress,
|
||||
'image_url': album_cover,
|
||||
'is_playing': is_playing,
|
||||
'votes': 0,
|
||||
'id': song_id,
|
||||
}
|
||||
|
||||
return Response(song, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class SpotifyList(generics.ListAPIView):
|
||||
queryset = SpotifyToken.objects.all()
|
||||
serializer_class = TokenSerializer
|
||||
|
||||
|
||||
class PauseSong(APIView):
|
||||
def put(self, request):
|
||||
room_code = self.request.session.get('room_code')
|
||||
room = Room.objects.filter(code=room_code)[0]
|
||||
|
||||
if self.request.session.session_key == room.host or room.guest_can_pause:
|
||||
upstream_response = pause_song(room.host)
|
||||
return Response(upstream_response, status=status.HTTP_200_OK)
|
||||
return Response({'Not allowed': 'you are not the host'}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
class PlaySong(APIView):
|
||||
def put(self, request):
|
||||
room_code = self.request.session.get('room_code')
|
||||
room = Room.objects.filter(code=room_code)[0]
|
||||
|
||||
if self.request.session.session_key == room.host or room.guest_can_pause:
|
||||
upstream_response = play_song(room.host)
|
||||
return Response(upstream_response, status=status.HTTP_200_OK)
|
||||
return Response({'Not allowed': 'you are not the host'}, status=status.HTTP_403_FORBIDDEN)
|
||||
Reference in New Issue
Block a user