Skip to content

Commit

Permalink
serve widgets from backend, cache in frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
teticio committed Aug 24, 2021
1 parent 995e66e commit 54344c9
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 56 deletions.
26 changes: 23 additions & 3 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import re
import base64
import urllib
import aiohttp
from . import models
Expand All @@ -9,6 +8,7 @@
from .deejai import DeejAI
from typing import Optional
from sqlalchemy import desc
from base64 import b64encode
from starlette.types import Scope
from sqlalchemy.orm import Session
from starlette.responses import Response
Expand Down Expand Up @@ -79,7 +79,7 @@ async def spotify_callback(code: str, state: Optional[str] = '/'):
headers = {
'Authorization':
'Basic ' +
base64.b64encode(f'{credentials.client_id}:{credentials.client_secret}'
b64encode(f'{credentials.client_id}:{credentials.client_secret}'
.encode('utf-8')).decode('utf-8')
}
async with aiohttp.ClientSession() as session:
Expand Down Expand Up @@ -108,7 +108,7 @@ async def spotify_refresh_token(refresh_token: str):
headers = {
'Authorization':
'Basic ' +
base64.b64encode(f'{credentials.client_id}:{credentials.client_secret}'
b64encode(f'{credentials.client_id}:{credentials.client_secret}'
.encode('utf-8')).decode('utf-8')
}
async with aiohttp.ClientSession() as session:
Expand All @@ -125,6 +125,26 @@ async def spotify_refresh_token(refresh_token: str):
return json


@app.get("/api/v1/widget")
async def widget(track_id: str):
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/{track_id}',
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))
return b64encode(text.encode('ascii'))


@app.get("/api/v1/search")
async def search_tracks(string: str, max_items: int):
ids = await deejai.search(string, max_items)
Expand Down
1 change: 1 addition & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// TODO
//
// frontend:
// robots.txt etc
// unit tests
// banner image and ico file
// fix warnings for unique key
Expand Down
9 changes: 8 additions & 1 deletion src/components/Banner.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ export default function Banner({ loggedIn = false, onSelect = f => f }) {
return (
<>
<VerticalSpacer px={120} />
<Navbar className="banner shadow-lg" fixed="top" bg="dark" variant="dark" expand="no" expanded={expanded}>
<Navbar
className="banner shadow-lg"
fixed="top"
bg="dark"
variant="dark"
expand="no"
expanded={expanded}
>
<Container>
<Navbar.Brand href="#">
<div className="row align-items-center">
Expand Down
8 changes: 6 additions & 2 deletions src/components/Playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ export default function Playlist({ track_ids = [], waypoints = [] }) {
return (
<>
{track_ids.map((track_id, i) => (
<div className="d-flex flex-row justify-content-center align-items-center">
<Track key={i} track_id={track_id} highlight={ waypoints.indexOf(track_id) >= 0 } />
<div className="d-flex flex-row justify-content-center align-items-center align-middle">
<Track
key={i}
track_id={track_id}
highlight={waypoints.indexOf(track_id) >= 0}
/>
</div>
))}
</>
Expand Down
12 changes: 5 additions & 7 deletions src/components/RemovableTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ export default function RemovableTrack({ track_id, uuid, onRemove = f => f }) {
>
</Track>
<HorizontalSpacer px={10} />
{loaded ?
<FaTimes
size="25"
className="link"
onClick={() => onRemove(uuid)}
/> : <></>
}
<FaTimes
size="25"
className="link"
onClick={() => onRemove(uuid)}
/>
</>
);
}
80 changes: 47 additions & 33 deletions src/components/Track.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,65 @@
import { useState } from 'react';
import { Suspense } from 'react';
import Spinner from 'react-bootstrap/Spinner';
import './Track.css';

function SpotifyTrackWidget({ track_id, highlight, loaded, onLoad = f => f }) {
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;
}
};
}

function SpotifyTrackWidget({ track_id, highlight, resource }) {
const iframe = (resource) ? resource.read() : null;

return (
<iframe
className={(loaded && highlight) ? "highlight" : ""}
className={highlight ? "highlight" : ""}
title={track_id}
src={"https://open.spotify.com/embed/track/" + track_id}
width={loaded ? "100%" : "0%"}
//src={`https://open.spotify.com/embed/track/${track_id}`}
srcdoc={Buffer.from(iframe.data, 'base64')}
width="100%"
height="80"
frameBorder="0"
allowtransparency="true"
allow="encrypted-media"
onLoad={() => onLoad()}
/>
);
}

export default function Track({ track_id, highlight = false, onLoad = f => f }) {
const [loaded, setLoaded] = useState(false);
export default function Track({ track_id, highlight = false }) {
if (typeof Track.cache == 'undefined') {
Track.cache = {};
}

const resource = (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 (
<>
{!loaded ?
<>
<Spinner animation="border" />
<SpotifyTrackWidget
track_id={track_id}
highlight={highlight}
loaded={loaded}
onLoad={() => {
setLoaded(true);
onLoad();
}}
/>
</> :
<SpotifyTrackWidget
track_id={track_id}
highlight={highlight}
loaded={loaded}
onLoad={() => {
setLoaded(true);
onLoad();
}}
/>
}
</>
<Suspense fallback={
<div
className="d-flex justify-content-center align-items-center align-middle"
style={{ height: 80, width: "100%" }}
>
<Spinner animation="border" />
</div>
} >
<SpotifyTrackWidget
track_id={track_id}
highlight={highlight}
resource={resource}
/>
</Suspense>
);
}
16 changes: 9 additions & 7 deletions src/components/TrackSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export default function TrackSelector({ spotify = null, onSelect = f => f, onSea
const id = setInterval(() => {
if (spotify && spotify.loggedIn()) {
spotify.autoRefresh(() => spotify.getMyCurrentPlayingTrack())
.then((response) => {
setCurrentTrack(response.item ? {
'track':
`${response.item.artists[0].name} - ${response.item.name}`,
'url': response.item.preview_url
} : null);
}).catch(); // ignore error
.then(response => {
if (response) {
setCurrentTrack(response.item ? {
'track':
`${response.item.artists[0].name} - ${response.item.name}`,
'url': response.item.preview_url
} : null);
}
}).catch(error => console.error('Error:', error));
} else {
setCurrentTrack(null);
}
Expand Down
22 changes: 19 additions & 3 deletions src/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
import { StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';

clientsClaim();

Expand Down Expand Up @@ -66,9 +66,25 @@ registerRoute(
);

registerRoute(
({ url }) => url.pathname.endsWith('/api/v1/search') || url.pathname.endsWith('/api/v1/search_similar'),
({ url }) =>
url.pathname.endsWith('/api/v1/search') ||
url.pathname.endsWith('/api/v1/search_similar') ||
url.pathname.endsWith('/api/v1/widget'),
new StaleWhileRevalidate({
cacheName: 'searches',
cacheName: 'api',
plugins: [
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 10 * 60 }),
],
})
);

registerRoute(
({ url }) =>
url.pathname.endsWith('/api/v1/latest_playlists') ||
url.pathname.endsWith('/api/v1/top_playlists') ||
url.pathname.endsWith('/api/v1/search_playlists'),
new NetworkFirst({
cacheName: 'api',
plugins: [
new ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 10 * 60 }),
],
Expand Down

0 comments on commit 54344c9

Please sign in to comment.