minimum viable product
This commit is contained in:
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=}")
|
Reference in New Issue
Block a user