Skip to content

Commit

Permalink
custom playlsit widget
Browse files Browse the repository at this point in the history
  • Loading branch information
teticio committed Oct 3, 2021
1 parent dfa2c91 commit 6011f0e
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 40 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ __pycache__
spotifytovec.p
tracktovec.p
spotify_tracks.p
spotify_urls.p
speccy_model
deejai.db
deejai-test.db
Expand Down
57 changes: 32 additions & 25 deletions backend/deejai.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
import numpy as np
from starlette.concurrency import run_in_threadpool

if 'HACKINTOSH' not in os.environ: # can't get tensorflow to work on Hackintosh due to missing AVX support
# can't get tensorflow to work on Hackintosh due to missing AVX support
if 'HACKINTOSH' not in os.environ:
import tensorflow as tf
from keras.models import load_model

Expand All @@ -41,18 +42,21 @@ def __init__(self):
]))
with open('spotify_tracks.p', 'rb') as file:
self.tracks = pickle.load(file)
with open('spotify_urls.p', 'rb') as file:
self.urls = pickle.load(file)
self.track_ids = list(mp3tovecs)
self.track_indices = dict(
map(lambda x: (x[1], x[0]), enumerate(mp3tovecs)))
self.mp3tovecs = np.array([[mp3tovecs[_], tracktovecs[_]]
for _ in mp3tovecs])
del mp3tovecs, tracktovecs
if 'HACKINTOSH' not in os.environ:
self.model = load_model('speccy_model',
custom_objects={
'cosine_proximity':
tf.compat.v1.keras.losses.cosine_proximity
})
self.model = load_model(
'speccy_model',
custom_objects={
'cosine_proximity':
tf.compat.v1.keras.losses.cosine_proximity
})

def get_tracks(self):
"""Get tracks.
Expand Down Expand Up @@ -106,13 +110,14 @@ async def playlist(self, track_ids, size, creativity, noise):
size=size,
noise=noise)

async def most_similar(self, # pylint: disable=too-many-arguments
mp3tovecs,
weights,
positive=iter(()),
negative=iter(()),
noise=0,
vecs=None):
async def most_similar(
self, # pylint: disable=too-many-arguments
mp3tovecs,
weights,
positive=iter(()),
negative=iter(()),
noise=0,
vecs=None):
"""Most similar IDs.
"""
mp3_vecs_i = np.array([
Expand All @@ -137,12 +142,13 @@ async def most_similar(self, # pylint: disable=too-many-arguments
del result[result.index(i)]
return result

async def most_similar_by_vec(self, # pylint: disable=too-many-arguments
mp3tovecs,
weights,
positives=iter(()),
negatives=iter(()),
noise=0):
async def most_similar_by_vec(
self, # pylint: disable=too-many-arguments
mp3tovecs,
weights,
positives=iter(()),
negatives=iter(()),
noise=0):
"""Most similar IDs by vector.
"""
mp3_vecs_i = np.array([
Expand Down Expand Up @@ -191,12 +197,13 @@ async def join_the_dots(self, weights, ids, size=5, noise=0):
playlist.append(end)
return playlist

async def make_playlist(self, # pylint: disable=too-many-arguments
weights,
playlist,
size=10,
lookback=3,
noise=0):
async def make_playlist(
self, # pylint: disable=too-many-arguments
weights,
playlist,
size=10,
lookback=3,
noise=0):
"""Generate playlist starting from seed track(s).
"""
playlist_tracks = [self.tracks[_] for _ in playlist]
Expand Down
85 changes: 78 additions & 7 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import os
import re
import json
import urllib
import logging
from typing import Optional
Expand All @@ -23,6 +24,8 @@

import aiohttp

from bs4 import BeautifulSoup

from . import models
from . import schemas
from . import credentials
Expand Down Expand Up @@ -162,18 +165,19 @@ async def spotify_callback(code: str, state: Optional[str] = '/'):
if response.status != 200:
raise HTTPException(status_code=response.status,
detail=response.reason)
json = await response.json()
_json = await response.json()
except aiohttp.ClientError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
body = {
'access_token': json['access_token'],
'refresh_token': json['refresh_token'],
'access_token': _json['access_token'],
'refresh_token': _json['refresh_token'],
'route': state
}
if state.startswith('deejai://'):
url = state + '?' + urllib.parse.urlencode(body)
else:
url = os.environ.get('APP_URL', '') + '#' + urllib.parse.urlencode(body)
url = os.environ.get('APP_URL',
'') + '#' + urllib.parse.urlencode(body)
return RedirectResponse(url=url)


Expand Down Expand Up @@ -202,10 +206,10 @@ async def spotify_refresh_token(refresh_token: str):
if response.status != 200:
raise HTTPException(status_code=response.status,
detail=response.reason)
json = await response.json()
_json = await response.json()
except aiohttp.ClientError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return json
return _json


@app.get('/api/v1/widget')
Expand Down Expand Up @@ -234,7 +238,74 @@ async def widget(track_id: str):
text = await response.text()
except aiohttp.ClientError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return b64encode(text.encode('ascii'))
soup = BeautifulSoup(text, 'html.parser')
tag = soup.find(id="resource")
track = json.loads(urllib.parse.unquote(tag.string))
track['preview_url'] = deejai.urls.get(track_id, '')
tag.string.replace_with(urllib.parse.quote(json.dumps(track)))
return b64encode(str(soup).encode('ascii'))


@app.post('/api/v1/playlist_widget')
async def make_playlist_widget(playlist_widget: schemas.PlaylistWidget):
"""Make a new Spotify playlist widget.
Args:
playlist_widget (schemas.PlaylistWidget): List of track IDs.
Returns:
str: Base 64 encoded HTML which can be embedded in an iframe.
"""

assert len(playlist_widget.track_ids) > 0
headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/92.0.4515.159 Safari/537.36'
}
async with aiohttp.ClientSession() as session:
try:
async with session.get(
f'https://open.spotify.com/embed/track/{playlist_widget.track_ids[0]}',
headers=headers) as response:
if response.status != 200:
raise HTTPException(status_code=response.status,
detail=response.reason)
text = await response.text()
except aiohttp.ClientError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
soup = BeautifulSoup(text, 'html.parser')
tag = soup.find(id="resource")
track = json.loads(urllib.parse.unquote(tag.string))
playlist = {
'images': track['album']['images'],
'tracks': {},
'type': 'playlist',
'uri': 'spotify:playlist:6itFIZoAKyetrbFr8BxSd2',
'dominantColor': track['dominantColor']
}
playlist['tracks']['items'] = []
for track_id in playlist_widget.track_ids:
title = deejai.tracks[track_id]
track = {
'is_local': True,
'is_playable': True,
'name': title[title.find(' - ') + 3:],
'preview_url': deejai.urls.get(track_id, ''),
'artists': [{
'name': title[:title.find(' - ')]
}],
'duration_ms': '30000',
'uri': f'spotify:track{track_id}'
}
playlist['tracks']['items'].append({'track': track})
playlist['name'] = playlist['tracks']['items'][0]['track']['name']
playlist['owner'] = {
'display_name':
playlist['tracks']['items'][0]['track']['artists'][0]['name']
}
tag.string.replace_with(urllib.parse.quote(json.dumps(playlist)))
return b64encode(str(soup).encode('ascii'))


@app.get('/api/v1/search')
Expand Down
5 changes: 5 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ class Playlist(BaseModel): # pylint: disable=too-few-public-methods
creativity: Optional[float] = 0.5
noise: Optional[float] = 0
uploads: Optional[int] = 0

class PlaylistWidget(BaseModel): # pylint: disable=too-few-public-methods
"""Schema for generating a new playlist widget.
"""
track_ids: list
2 changes: 1 addition & 1 deletion src/components/ErrorBoundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class ErrorBoundary extends Component {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<Text h3 style={{ textAlign: 'center' }}>Whoopsie daisy, it looks like something has gone wrong.</Text>
<Text h4 style={{ textAlign: 'center' }}>Whoopsie daisy, it looks like something has gone wrong.</Text>
);
}

Expand Down
76 changes: 76 additions & 0 deletions src/components/Playlist.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { PureComponent, Suspense, useState } from 'react';
import { WebView, View, Spinner } from './Platform';

function createResource(pending) {
let error, response;
pending.then(r => response = r).catch(e => error = e);
return {
read() {
if (error) throw error;
if (response) return response;
throw pending;
}
};
}

// Avoid unncessary re-renders
class SpotifyPlaylistWidget extends PureComponent {
render() {
const { resource, height } = this.props;
const iframe = (resource) ? resource.read() : null;

return (
<WebView
style={{
flex: 0,
height: height,
backgroundColor: 'transparent'
}}
androidLayerType='software'
source={{
html: iframe? Buffer.from(iframe.data, 'base64').toString() : ''
}}
/>
);
}
}

export default function Playlist({ track_ids = [], waypoints = [] }) {
const height = 80 + 50 * track_ids.length;
const [ resource, _ ] = useState(createResource(new Promise(resolves => {
fetch(`${process.env.REACT_APP_API_URL}/playlist_widget`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'track_ids': track_ids
})
})
.then(response => (response.status === 200) ? response.text() : '')
.then(data => resolves({ data: data }))
.catch(error => console.error('Error:', error));
})));

return (
<Suspense fallback={
<View
style={{
display: 'flex',
height: height,
width: '100%',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Spinner size='large' />
</View>
} >
<SpotifyPlaylistWidget
resource={resource}
height={height}
/>
</Suspense>
);
}

17 changes: 10 additions & 7 deletions src/components/Track.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { PureComponent, Suspense } from 'react';
import Spinner from 'react-bootstrap/Spinner';
import { PureComponent, Suspense, useState } from 'react';
import { View, Spinner } from './Platform';
import VisibilitySensor from 'react-visibility-sensor';
import './Track.css';

try {
require('./Track.css');
} catch (e) { }

function createResource(pending) {
let error, response;
Expand Down Expand Up @@ -50,18 +53,18 @@ export default function Track({
Track.cache = {};
}

const resource = (track_id in Track.cache) ? Track.cache[track_id] :
const [ resource, _ ] = useState((track_id in Track.cache) ? Track.cache[track_id] :
createResource(new Promise(resolves => {
fetch(`${process.env.REACT_APP_API_URL}/widget?track_id=${track_id}`)
.then(response => (response.status === 200) ? response.text() : '')
.then(data => resolves({ data: data }))
.catch(error => console.error('Error:', error));
}));
})));
Track.cache[track_id] = resource;

return (
<Suspense fallback={
<div
<View
style={{
display: 'flex',
height: 80,
Expand All @@ -71,7 +74,7 @@ export default function Track({
}}
>
<Spinner animation='border' />
</div>
</View>
} >
<SpotifyTrackWidget
track_id={track_id}
Expand Down

0 comments on commit 6011f0e

Please sign in to comment.