minimum viable product

This commit is contained in:
2022-12-16 15:17:07 +00:00
commit bb4bae77a8
13 changed files with 564 additions and 0 deletions

188
web/src/app.py Normal file
View File

@@ -0,0 +1,188 @@
import os
from flask import Flask, render_template, request, make_response, redirect, url_for
import requests as rq
import urllib.parse
import base64
from datetime import date, datetime
import time
import json
from common import Db, PlaylistUpdater
import uuid
app = Flask(__name__)
USER_SECRET_EXPIRATION = 31557600
URL_PREFIX = os.environ.get('URL_PREFIX', '/')
#app.config.update(APPLICATION_ROOT=URL_PREFIX)
CLIENT_ID = os.environ.get('CLIENT_ID', None)
if CLIENT_ID is None:
raise ValueError("CLIENT_ID cannot be None, set using environment variable")
CLIENT_SECRET = os.environ.get('CLIENT_SECRET', None)
if CLIENT_SECRET is None:
raise ValueError("CLIENT_SECRET cannot be None, set using environment variable")
db = Db('localhost', 6379, 0, CLIENT_ID, CLIENT_SECRET)
SPOTIFY_AUTHORIZE_ENDPOINT = os.environ.get('SPOTIFY_AUTHORIZE_ENDPOINT', 'https://accounts.spotify.com/authorize?')
SPOTIFY_TOKEN_ENDPOINT = os.environ.get('SPOTIFY_AUTHORIZE_ENDPOINT', 'https://accounts.spotify.com/api/token')
SPOTIFY_API_ENDPOINT = os.environ.get('SPOTIFY_API_ENDPOINT', 'https://api.spotify.com/v1')
SCOPES = ' '.join([
'playlist-read-private',
'playlist-read-collaborative',
'playlist-modify-private',
'playlist-modify-public',
'user-read-email',
'user-library-read',
'user-library-modify'
])
def isauth(cookies):
user_id = cookies.get('spotify_id')
user_secret = cookies.get('user_secret')
auth_token = db.get_user_auth_token(user_id)
return user_secret == db.get_user_client_secret(user_id)
@app.route("/", methods=['GET', 'POST'])
def index():
user_id = request.cookies.get('spotify_id')
user_secret = request.cookies.get('user_secret')
auth_token = db.get_user_auth_token(user_id)
if (not auth_token) or (user_secret != db.get_user_client_secret(user_id)):
resp = make_response(render_template(
"index_unauthorised.html",
loginurl = SPOTIFY_AUTHORIZE_ENDPOINT + urllib.parse.urlencode({
'client_id': CLIENT_ID,
'scope': SCOPES,
'redirect_uri': 'http://localhost:5000' + url_for('cb'),
'response_type': 'code'
})
))
resp.set_cookie(
'user_secret',
str(uuid.uuid4()),
secure = True,
expires = time.time() + USER_SECRET_EXPIRATION
)
return resp
if request.method=='GET':
message = request.args.get('message')
return render_template("index_authorised.html", message = json.loads(message) if message else "" )
playlist_name = request.form.get('pname')
count = request.form.get('count')
public = request.form.get('public') == 'public'
counttype = request.form.get('counttype')
create_rq = rq.post(f"{SPOTIFY_API_ENDPOINT}/users/{user_id}/playlists",
headers = { 'Authorization': f"Bearer {auth_token}" },
json = {
'name': playlist_name,
'public': public,
'collaborative': False,
'description': f"{count}{counttype}"
})
resp = create_rq.json()
if create_rq.status_code != 201:
return resp
playlist_id = resp.get('id')
db.user_add_playlist(user_id, playlist_id, count, counttype)
PlaylistUpdater(db, auth_token).update_playlist(playlist_id)
return render_template("index_authorised.html", message={
'type': 'info',
'text': f"created playlist {playlist_name}. it will be populated shortly"
})
@app.route('/authcb')
def cb():
user_secret = request.cookies.get('user_secret')
if not user_secret:
return 'client secret is not existing or something: {user_secret=}'
error = request.args.get('error')
if error:
return 'error'
spotify_code = request.args.get('code')
authstring = base64.b64encode(bytes(f"{CLIENT_ID}:{CLIENT_SECRET}", 'utf-8')).decode('utf-8')
data = {
'code': spotify_code,
'grant_type': 'authorization_code',
'redirect_uri': 'http://localhost:5000' + url_for('cb'),
}
headers = {
'Authorization': f'Basic {authstring}',
'Content-Type': 'application/x-www-form-urlencoded'
}
token_rq = rq.post(f"{SPOTIFY_TOKEN_ENDPOINT}", data = data, headers = headers)
if token_rq.status_code != rq.codes.ok:
resp = make_response(json.dumps({
'status_code': token_rq.status_code,
'resp:': token_rq.json(),
'data': data
}))
resp.headers.set('Content-Type', 'application/json')
return resp
resp = token_rq.json()
spotify_token = resp['access_token']
token_expires = resp['expires_in']
refresh_token = resp['refresh_token']
me_rq = rq.get(f"{SPOTIFY_API_ENDPOINT}/me", headers = { 'Authorization': f"Bearer {spotify_token}" })
resp = me_rq.json()
if me_rq.status_code != 200:
return resp
spotify_id = resp['id']
db.add_user(user_secret, spotify_token, spotify_id, token_expires, refresh_token, client_secret_expires=USER_SECRET_EXPIRATION)
resp = make_response(redirect(url_for('index', message=json.dumps({
'type': 'info',
'text': "successfully logged in with spotify"
}))))
resp.set_cookie('spotify_id', spotify_id)
return resp
@app.route('/logout')
def logout():
resp = make_response(redirect(url_for('index')))
resp.set_cookie('user_secret', '')
return resp
@app.route('/delete')
def delete():
if not isauth(request.cookies):
return redirect(url_for('index'))
user_id = request.cookies.get('spotify_id')
playlist_id = request.args.get('playlist')
if not playlist_id:
return render_template("delete.html", message={'type':'', 'text': ''}, playlist_ids=db.get_user_playlists(user_id))
db.user_del_playlist(user_id, playlist_id)
return render_template("delete.html", message={
'type': 'info',
'text': f'successfully deleted playlist {playlist_id=}'
}, playlist_ids=db.get_user_playlists(user_id))

1
web/src/common Symbolic link
View File

@@ -0,0 +1 @@
../../common/

View File

@@ -0,0 +1,22 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@import url("https://styles.alv.cx/colors/gruvbox.css");
@import url("https://styles.alv.cx/base.css");
@import url("https://styles.alv.cx/modules/darkmode.css");
.messagetypeinfo { background-color: var(--blue); }
.messagetypeerror { background-color: var(--red); }
.messagetypewarning { background-color: var(--yellow); }
#message { padding: 1em; }
</style>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<p class="messagetype{{message['type']}}"id="message">{{message['text']}}</p>
{% block body %}
{% endblock %}
<p> built with ❤ by <a href="https://alv.cx">alv</a></p>
</body>

View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}rolling_liked | delete a playlist{% endblock %}
{% block body %}
<h1> delete a playlist </h1>
<ul>
{% for pid in playlist_ids %}
<li><a href="?playlist={{pid}}">{{pid}}</a></li>
{% endfor %}
</ul>
<a href="..">home</a>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block title %}rolling_liked{% endblock %}
{% block body %}
<h1> rolling_liked </h1>
<form action="./" method="post">
create a
<select id="public" name="public">
<option value="public">public</option>
<option selected value="private">private</option>
</select> playlist called <br><br>
<input type="text" id="pname" name="pname" placeholder="playlist_name"><br><br>
which always has the most recent<br><br>
<input type="number" id="count" name="count" value="25">
<select id="counttype" name="counttype">
<option value="days">days</option>
<option value="tracks">tracks</option>
</select><br><br>
from my spotify liked songs playlist
<br><br>
<input type="submit" value="create playlist">
<br>
<br>
<br>
<br>
<a href="./delete">delete a playlist</a>
<a href="./logout">logout</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block title %}rolling_liked{% endblock %}
{% block body %}
<h1> rolling_liked </h1>
<h2> <a href="{{loginurl}}"> login with spotify </a> </h2>
{% endblock %}