minimum viable product
This commit is contained in:
commit
bb4bae77a8
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
todo
|
||||||
|
env
|
||||||
|
__pycache__
|
2
common/__init__.py
Normal file
2
common/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .db import Db
|
||||||
|
from .update_playlists import PlaylistUpdater
|
117
common/db.py
Normal file
117
common/db.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import redis
|
||||||
|
import uuid
|
||||||
|
import requests as rq
|
||||||
|
import base64
|
||||||
|
|
||||||
|
class Key:
|
||||||
|
@staticmethod
|
||||||
|
def users():
|
||||||
|
return f"users"
|
||||||
|
@staticmethod
|
||||||
|
def spotify_token(spotify_id):
|
||||||
|
return f"user:{spotify_id}:token"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def spotify_token_refresh(spotify_id):
|
||||||
|
return f"user:{spotify_id}:token_refresh"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def client_secret(spotify_id):
|
||||||
|
return f"user:{spotify_id}:client_secret"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def email(spotify_id):
|
||||||
|
return f"user:{spotify_id}:email"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def user_playlists(spotify_id):
|
||||||
|
return f"user:{spotify_id}:playlists"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def playlist_count(playlist_id):
|
||||||
|
return f"playlist:{playlist_id}:count"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def playlist_counttype(playlist_id):
|
||||||
|
return f"playlist:{playlist_id}:counttype"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def playlist_snapshot(playlist_id):
|
||||||
|
return f"playlist:{playlist_id}:snapshot"
|
||||||
|
|
||||||
|
|
||||||
|
class Db:
|
||||||
|
def __init__(self, host, port, db, client_id, client_secret):
|
||||||
|
self.rdb = redis.Redis(host=host, port=port, db=db, decode_responses=True)
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
|
||||||
|
def get_all_users(self):
|
||||||
|
return self.rdb.smembers(Key.users())
|
||||||
|
|
||||||
|
# user secret is forgotten after a year by default, requiring them to log in again
|
||||||
|
def add_user(self, client_secret, spotify_token, spotify_id, token_expires, refresh_token, client_secret_expires=31557600):
|
||||||
|
self.rdb.setex(Key.client_secret(spotify_id), client_secret_expires, client_secret)
|
||||||
|
|
||||||
|
self.rdb.setex(Key.spotify_token(spotify_id), token_expires, spotify_token)
|
||||||
|
self.rdb.set(Key.spotify_token_refresh(spotify_id), refresh_token)
|
||||||
|
self.rdb.sadd(Key.users(), spotify_id)
|
||||||
|
|
||||||
|
def user_add_playlist(self, spotify_id, playlist_id, count, counttype):
|
||||||
|
self.rdb.sadd(Key.user_playlists(spotify_id), playlist_id)
|
||||||
|
self.rdb.set(Key.playlist_count(playlist_id), count)
|
||||||
|
self.rdb.set(Key.playlist_counttype(playlist_id), counttype)
|
||||||
|
|
||||||
|
def user_del_playlist(self, spotify_id, playlist_id):
|
||||||
|
self.rdb.srem(Key.user_playlists(spotify_id), playlist_id)
|
||||||
|
self.rdb.delete(Key.playlist_count(playlist_id))
|
||||||
|
self.rdb.delete(Key.playlist_counttype(playlist_id))
|
||||||
|
|
||||||
|
|
||||||
|
def del_user(self, spotify_id):
|
||||||
|
self.rdb.delete(Key.spotify_token(spotify_id))
|
||||||
|
self.rdb.delete(Key.email(spotify_id))
|
||||||
|
self.rdb.srem(Key.users(), spotify_id)
|
||||||
|
|
||||||
|
playlists = self.rdb.smembers(Key.user_playlists(spotify_id))
|
||||||
|
for pid in playlists:
|
||||||
|
self.rdb.delete(Key.playlist_count(pid))
|
||||||
|
self.rdb.delete(Key.playlist_counttype)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_auth_token(self, spotify_id):
|
||||||
|
auth_token = self.rdb.get(Key.spotify_token(spotify_id))
|
||||||
|
if auth_token:
|
||||||
|
return auth_token
|
||||||
|
|
||||||
|
refresh_token = self.rdb.get(Key.spotify_token_refresh(spotify_id))
|
||||||
|
if not refresh_token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"{refresh_token=}")
|
||||||
|
authstring = base64.b64encode(bytes(f"{self.client_id}:{self.client_secret}", 'utf-8')).decode('utf-8')
|
||||||
|
res = rq.post('https://accounts.spotify.com/api/token', headers = { 'Authorization': f'Basic {authstring}', 'Content-Type': 'application/x-www-form-urlencoded' }, data = {
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': refresh_token,
|
||||||
|
})
|
||||||
|
print(res.status_code)
|
||||||
|
print(res.content)
|
||||||
|
res = res.json()
|
||||||
|
self.rdb.setex(Key.spotify_token(spotify_id), res['expires_in'], res['access_token'])
|
||||||
|
|
||||||
|
return res['access_token']
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_client_secret(self, spotify_id):
|
||||||
|
return self.rdb.get(Key.client_secret(spotify_id))
|
||||||
|
|
||||||
|
def get_user_playlists(self, spotify_id):
|
||||||
|
return self.rdb.smembers(Key.user_playlists(spotify_id))
|
||||||
|
|
||||||
|
def get_user_playlist_count(self, playlist_id):
|
||||||
|
return int(self.rdb.get(Key.playlist_count(playlist_id)))
|
||||||
|
|
||||||
|
def get_user_playlist_counttype(self, playlist_id):
|
||||||
|
return self.rdb.get(Key.playlist_counttype(playlist_id))
|
111
common/update_playlists.py
Normal file
111
common/update_playlists.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import requests as rq
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
COUNTTYPE_DAYS = 'days'
|
||||||
|
COUNTTYPE_TRACKS = 'tracks'
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistUpdater:
|
||||||
|
def __init__(self, db, auth_token, spotify_api_endpoint='https://api.spotify.com/v1'):
|
||||||
|
self._db = db
|
||||||
|
self._tracks = []
|
||||||
|
self._auth_token = auth_token
|
||||||
|
self._spotify_api_endpoint = spotify_api_endpoint
|
||||||
|
|
||||||
|
self._update_tracks()
|
||||||
|
|
||||||
|
def _update_tracks(self):
|
||||||
|
self._tracks = []
|
||||||
|
headers = { 'Authorization': f"Bearer {self._auth_token}" }
|
||||||
|
rnext = f"{self._spotify_api_endpoint}/me/tracks?limit=50"
|
||||||
|
while rnext is not None:
|
||||||
|
r = rq.get(rnext, headers = headers).json()
|
||||||
|
self._tracks += r['items']
|
||||||
|
rnext = r['next']
|
||||||
|
|
||||||
|
|
||||||
|
def update_playlist(self, playlist_id):
|
||||||
|
pltracks = []
|
||||||
|
newtracklist = []
|
||||||
|
count = self._db.get_user_playlist_count(playlist_id)
|
||||||
|
counttype = self._db.get_user_playlist_counttype(playlist_id)
|
||||||
|
headers = { 'Authorization': f"Bearer {self._auth_token}" }
|
||||||
|
added_at_dict = {}
|
||||||
|
|
||||||
|
r = rq.get(f"{self._spotify_api_endpoint}/playlists/{playlist_id}?fields=tracks,snapshot_id", headers = headers).json()
|
||||||
|
snapshot_id = r['snapshot_id']
|
||||||
|
pltracks = [t['track']['id'] for t in r['tracks']['items']]
|
||||||
|
rnext = r['tracks']['next']
|
||||||
|
while rnext is not None:
|
||||||
|
r = rq.get(rnext, headers = headers).json()
|
||||||
|
for track in r['items']:
|
||||||
|
pltracks.append(track['track']['id'])
|
||||||
|
rnext = r['next']
|
||||||
|
|
||||||
|
for i, track in enumerate(self._tracks):
|
||||||
|
# for some reason, python datetime doesn't support utc zulu timezone
|
||||||
|
added_at = datetime.fromisoformat(track['added_at'].replace('Z', '+00:00')).timestamp()
|
||||||
|
added_at_dict[track['track']['id']] = added_at
|
||||||
|
|
||||||
|
if counttype == COUNTTYPE_DAYS:
|
||||||
|
if added_at < (datetime.now() - timedelta(days=count)).timestamp():
|
||||||
|
break
|
||||||
|
|
||||||
|
if counttype == COUNTTYPE_TRACKS and i >= count:
|
||||||
|
break
|
||||||
|
|
||||||
|
newtracklist.append(track['track']['id'])
|
||||||
|
|
||||||
|
|
||||||
|
pltracks = set(pltracks)
|
||||||
|
newtracklist = set(newtracklist)
|
||||||
|
|
||||||
|
toremove = list(pltracks - newtracklist)
|
||||||
|
toadd = sorted(list(newtracklist - pltracks), key = lambda t: added_at_dict[t])
|
||||||
|
|
||||||
|
print(f"{pltracks=}")
|
||||||
|
print(f"{newtracklist=}")
|
||||||
|
print(f"{toremove=}")
|
||||||
|
print(f"{toadd=}")
|
||||||
|
print(f"{count=}")
|
||||||
|
print(f"{counttype=}")
|
||||||
|
print(f"{COUNTTYPE_TRACKS=}")
|
||||||
|
print(f"{counttype==COUNTTYPE_TRACKS=}")
|
||||||
|
|
||||||
|
while toremove:
|
||||||
|
tracks = []
|
||||||
|
for i in range(100):
|
||||||
|
if not toremove:
|
||||||
|
break
|
||||||
|
tracks.append({
|
||||||
|
'uri': f"spotify:track:{toremove.pop()}"
|
||||||
|
})
|
||||||
|
|
||||||
|
r = rq.delete(
|
||||||
|
f"{self._spotify_api_endpoint}/playlists/{playlist_id}/tracks",
|
||||||
|
headers = headers,
|
||||||
|
json = {
|
||||||
|
'tracks': tracks,
|
||||||
|
'snapshot_id': snapshot_id
|
||||||
|
}
|
||||||
|
).json()
|
||||||
|
print(f"{json.dumps(r, indent=2)=}")
|
||||||
|
|
||||||
|
while toadd:
|
||||||
|
tracks = []
|
||||||
|
for i in range(100):
|
||||||
|
if not toadd:
|
||||||
|
break
|
||||||
|
tracks.append(f"spotify:track:{toadd.pop()}")
|
||||||
|
|
||||||
|
r = rq.post(
|
||||||
|
f"{self._spotify_api_endpoint}/playlists/{playlist_id}/tracks",
|
||||||
|
headers = headers,
|
||||||
|
json = {
|
||||||
|
'uris': tracks,
|
||||||
|
'position': 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"{r.text=}")
|
53
populater/src/app.py
Executable file
53
populater/src/app.py
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
from common import Db, PlaylistUpdater
|
||||||
|
|
||||||
|
|
||||||
|
INTERVAL = int(os.environ.get('INTERVAL', '0')) or 3600
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def get_args():
|
||||||
|
""" Get command line arguments """
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
""" Entry point for script """
|
||||||
|
users = db.get_all_users()
|
||||||
|
print(f"{users=}")
|
||||||
|
for user_id in users:
|
||||||
|
playlists = db.get_user_playlists(user_id)
|
||||||
|
if len(playlists) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
auth_token = db.get_user_auth_token(user_id)
|
||||||
|
updater = PlaylistUpdater(db, auth_token)
|
||||||
|
|
||||||
|
for playlist_id in playlists:
|
||||||
|
updater.update_playlist(playlist_id)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"{__name__}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
while True:
|
||||||
|
print("executing...")
|
||||||
|
main(get_args())
|
||||||
|
print(f"done. sleeping for {INTERVAL} seconds...")
|
||||||
|
time.sleep(INTERVAL)
|
1
populater/src/common
Symbolic link
1
populater/src/common
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../common/
|
14
requirements.txt
Normal file
14
requirements.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
async-timeout==4.0.2
|
||||||
|
certifi==2022.12.7
|
||||||
|
charset-normalizer==2.1.1
|
||||||
|
click==8.1.3
|
||||||
|
Flask==2.2.2
|
||||||
|
idna==3.4
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
Jinja2==3.1.2
|
||||||
|
MarkupSafe==2.1.1
|
||||||
|
redis==4.4.0
|
||||||
|
requests==2.28.1
|
||||||
|
six==1.16.0
|
||||||
|
urllib3==1.26.13
|
||||||
|
Werkzeug==2.2.2
|
188
web/src/app.py
Normal file
188
web/src/app.py
Normal 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
1
web/src/common
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../common/
|
22
web/src/templates/base.html
Normal file
22
web/src/templates/base.html
Normal 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>
|
||||||
|
|
15
web/src/templates/delete.html
Normal file
15
web/src/templates/delete.html
Normal 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 %}
|
29
web/src/templates/index_authorised.html
Normal file
29
web/src/templates/index_authorised.html
Normal 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 %}
|
7
web/src/templates/index_unauthorised.html
Normal file
7
web/src/templates/index_unauthorised.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user