This commit is contained in:
matthias@matsewe.de
2024-05-21 12:20:25 +02:00
parent df764bd85d
commit 9401e0727b
15 changed files with 425 additions and 84 deletions

View File

@@ -5,6 +5,7 @@ from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from app.dependencies import engine
from app.sql_models import Base
from app.routers.songs import get_songs
Base.metadata.create_all(engine)
@@ -25,6 +26,24 @@ async def root(request: Request, session_id : str = ""):
request=request, name="landing.html"
)
else:
songs = await get_songs(session_id)
songs_by_category = {}
all_categories = set()
for song in songs:
if song.main_category not in songs_by_category:
songs_by_category[song.main_category] = []
songs_by_category[song.main_category].append(song)
all_categories.update(song.categories.keys())
return templates.TemplateResponse(
request=request, name="voting.html"
request=request, name="voting.html", context={
"songs_by_category": songs_by_category,
"all_categories": {c: i+1 for i, c in enumerate(all_categories)},
"session_id": session_id
}
)
#@app.get("/vote", response_class=HTMLResponse)
#async def vote(request: Request, session_id : str = ""):
# return templates.TemplateResponse(
# request=request, name="voting-old.html"
# )

View File

@@ -13,8 +13,10 @@ class Song(BaseModel):
id: int
og_artist: Optional[str]
aca_artist: Optional[str]
title: str
yt_url: Optional[str]
title: Optional[str]
url: Optional[str]
yt_id: Optional[str]
spfy_id: Optional[str]
thumbnail: Optional[str]
is_aca: bool
arng_url: Optional[str]

View File

@@ -1,11 +1,17 @@
from fastapi import APIRouter, Security
from app.sql_models import SqlSong
from app.dependencies import session
from app.dependencies import engine
from app.routers.user import get_current_user
import pandas as pd
import numpy as np
import re
import requests
import os
from app.sql_models import Base
router = APIRouter(
prefix="/admin",
@@ -22,7 +28,10 @@ def get_main_category(categories) -> int:
else:
return np.argmax(categories != None, axis=0)
def youtube_url_validation(url):
def get_youtube_id(url):
if url is None:
return None
youtube_regex = (
r'(https?://)?(www\.)?'
'(youtube|youtu|youtube-nocookie)\.(com|be)/'
@@ -30,23 +39,38 @@ def youtube_url_validation(url):
youtube_regex_match = re.match(youtube_regex, url)
if youtube_regex_match:
return youtube_regex_match
return youtube_regex_match.group(6)
return False
return None
def get_thumbnail(url):
m = youtube_url_validation(url)
if url is None:
return "/static/cover.jpg"
m = get_youtube_id(url)
if m:
thumbnail_url = "https://img.youtube.com/vi/" + m.group(6) + "/mqdefault.jpg"
thumbnail_url = "https://img.youtube.com/vi/" + m + "/mqdefault.jpg"
return thumbnail_url
elif "spotify" in url:
return re.findall(r'(https?://i.scdn.co/image[^"]+)', requests.get(url).text)[0]
else:
return "/static/cover.jpg"
@router.post("/process_file")
async def create_upload_file(link_share: str):
song_list = pd.read_excel(link_share)
def get_spotify_id(url):
if url is None:
return None
if "spotify" in url:
return url.split("/track/")[1]
else:
return None
@router.post("/load_list")
async def create_upload_file():
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
song_list = pd.read_excel(os.environ['LIST_URL'])
song_list = song_list.replace({np.nan: None})
song_list = song_list.replace({"n/a": None})
@@ -55,15 +79,25 @@ async def create_upload_file(link_share: str):
for row in song_list[1:].iterrows():
row = np.array(row[1])
yt_id = get_youtube_id(row[3])
spfy_id = get_spotify_id(row[3])
categories = {n: v for n, v in zip(
category_names, row[6:19] != None)}
if not np.any(list(categories.values())):
continue
s = SqlSong(og_artist=row[0],
aca_artist=row[1],
title=row[2],
yt_url=row[3],
url=row[3],
yt_id=yt_id,
spfy_id=spfy_id,
thumbnail=get_thumbnail(row[3]),
is_aca=row[4] == "ja",
arng_url=row[5],
categories={n: v for n, v in zip(
category_names, row[6:19] != None)},
categories=categories,
main_category=category_names[get_main_category(row[6:19])],
singable=row[19] != "nein"
)

View File

@@ -9,13 +9,24 @@ router = APIRouter(
responses={404: {"description": "Not found"}},
)
async def songify(s, votes):
return Song(**s.__dict__, vote=votes.get(s.id, None))
@router.get("/")
async def get_songs(user_id : str = "") -> list[Song]:
sqlsongs = session.query(SqlSong).filter(SqlSong.singable == True).all()
votes = session.query(SqlVote).filter(SqlVote.user_id == user_id).all()
votes = {v.song_id : v.vote for v in votes}
return [Song(**s.__dict__, vote=votes.get(s.id, None)) for s in sqlsongs] # type: ignore
songs = []
for s in sqlsongs:
try:
songs.append(Song(**s.__dict__, vote=votes.get(s.id, None)))
except:
print(s.__dict__)
pass
return songs
#return [Song(**s.__dict__, vote=votes.get(s.id, None)) for s in sqlsongs] # type: ignore
@router.post("/{song_id}/vote")
async def vote(song_id : str, user_id : str, vote : int):

View File

@@ -6,13 +6,26 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm, Se
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, ValidationError
import os
from app.secrets import SECRET_KEY, fake_users_db
#from app.secrets import SECRET_KEY, fake_users_db
# to get a string like this run:
# openssl rand -hex 32
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 31
SECRET_KEY = os.environ['SECRET_KEY']
fake_users_db = {
"admin": {
"username": "admin",
"email": "admin@example.com",
"hashed_password": os.environ["ADMIN_PWD"],
"disabled": False,
"scopes" : ["admin", "public"]
}
}
class Token(BaseModel):
@@ -28,7 +41,7 @@ class TokenData(BaseModel):
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
#full_name: str | None = None
disabled: bool | None = None
@@ -53,6 +66,7 @@ router = APIRouter(
def verify_password(plain_password, hashed_password):
print(get_password_hash(plain_password))
return pwd_context.verify(plain_password, hashed_password)

View File

@@ -9,7 +9,9 @@ class SqlSong(Base):
og_artist = Column(String)
aca_artist = Column(String)
title = Column(String)
yt_url = Column(String)
url = Column(String)
yt_id = Column(String)
spfy_id = Column(String)
thumbnail = Column(String)
is_aca = Column(Boolean)
arng_url = Column(String)

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="326.09866"

Before

Width:  |  Height:  |  Size: 789 B

After

Width:  |  Height:  |  Size: 708 B

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="466.66666"

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 448 B

21
static/open.svg Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="600.07782"
height="600.07782"
viewBox="0 0 18.002335 18.002335"
fill="none"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="m 17.002335,1 -8,8 m 8,-8 v 4.5 m 0,-4.5 h -4.5 m 3.5,8.5 v 4.3 c 0,1.1201 0,1.6802 -0.218,2.108 -0.1917,0.3763 -0.4977,0.6823 -0.874,0.874 -0.4278,0.218 -0.9879,0.218 -2.108,0.218 h -8.6 c -1.1201,0 -1.68016,0 -2.10798,-0.218 C 1.718025,16.5903 1.412065,16.2843 1.220325,15.908 1.002335,15.4802 1.002335,14.9201 1.002335,13.8 V 5.2 c 0,-1.1201 0,-1.68016 0.21799,-2.10798 0.19174,-0.37633 0.4977,-0.68229 0.87403,-0.87403 C 2.522175,2 3.082225,2 4.202335,2 h 4.3"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 902 B

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="259.76822"

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -2,17 +2,31 @@
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: sans-serif;
}
.vote-buttons,
.cover-container,
.categories {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
@media only screen and (min-resolution: 200dpi) {
body {
font-size: 3.2vmin;
}
}
.text {
padding: 0.3em;
margin-bottom: 0.5em;
display: inline-block;
}
.clear {
clear: both;
}
@@ -101,11 +115,12 @@
.button {
height: 1.5em;
width: 3em;
display: inline-block;
text-align: center;
vertical-align: middle;
font-size: 1.5em;
position: relative;
float: left;
clear: none;
}
.button img {
@@ -202,6 +217,8 @@
font-size: 0.7em;
}
h1 {
font-family: sans-serif;
padding: 0.1em;
@@ -224,20 +241,21 @@ h1 {
}
#spotify-player {
bottom: 0.2em;
background-color: blue;
bottom: 0.4em;
}
#close-player {
position: absolute;
bottom: 320px;
right: 0.5em;
background-color: rgba(255,255,255,0.9);
position: fixed;
bottom: calc(360px + 0.2em - 3em);
right: 0.7em;
background-color: rgba(255, 255, 255, 1);
width: 3em;
height: 3em;
border-radius: 1.5em;
display: none;
cursor: pointer;
}
#close-player img {
position: relative;
left: 50%;

54
static/stop.svg Normal file
View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="176.76801"
height="419.52487"
viewBox="0 0 5.3030402 12.585746"
fill="none"
version="1.1"
id="svg1"
sodipodi:docname="stop.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.916407"
inkscape:cx="104.36196"
inkscape:cy="177.15443"
inkscape:window-width="2560"
inkscape:window-height="1351"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<defs
id="defs1" />
<path
d="M 1.0001448,2.2242414 V 10.361541"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1"
style="stroke:#000000;stroke-opacity:1"
sodipodi:nodetypes="cc" />
<path
d="m 4.2993174,2.22424 v 8.1373"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1-3"
style="stroke:#000000;stroke-opacity:1"
sodipodi:nodetypes="cc" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -143,6 +143,7 @@ $(document).ready(function () {
"no_selected": (song.vote == -1) ? "selected" : "",
"neutral_selected": (song.vote == 0) ? "selected" : "",
"yes_selected": (song.vote == 1) ? "selected" : "",
"play_button": (song.yt_id || song.spfy_id) ? "play" : "open",
"categories": cats
})).join('')
@@ -175,22 +176,30 @@ window.onSpotifyIframeApiReady = (IFrameAPI) => {
IFrameAPI.createController(element, options, callback);
};
function stop() {
$("#song-" + is_playing + " .cover-container .overlay img").attr("src", "/static/play.svg");
function play(id) {
$("#yt-player").css("display", "none");
$("#spotify-player").css("display", "none");
$("#close-player").css("display", "none");
$("#yt-player").html("");
spotify_embed_controller.pause();
if (is_playing == id) {
is_playing = -1;
}
function play(id) {
if (is_playing == id) {
stop();
} else {
stop();
is_playing = id;
$("#song-" + id + " .cover-container .overlay img").attr("src", "/static/stop.svg");
song = all_songs[id];
yt_id = song.yt_url.split('v=')[1]
spotify_id = song.yt_url.split('/track/')[1]
yt_id = song.yt_id
spotify_id = song.spfy_id
if (yt_id) {
$("#yt-player").css("display", "flex");
@@ -205,15 +214,9 @@ function play(id) {
spotify_embed_controller.play();
}
else {
$("#yt-player").css("display", "none");
$("#spotify-player").css("display", "none");
$("#yt-player").html("");
spotify_embed_controller.pause();
window.open(song.yt_url, '_blank').focus();
stop();
$("#song-" + id + " .cover-container .overlay img").attr("src", "/static/open.svg");
window.open(song.url, '_blank').focus();
}
}
}
//<iframe style="border-radius:12px" src="https://open.spotify.com/embed/track/2DS7lDZNFM7safSGNm8vd4?utm_source=generator" width="100%" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
// https://open.spotify.com/intl-de/track/2DS7lDZNFM7safSGNm8vd4

54
templates/voting-old.html Normal file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title>Liederwahl</title>
<link rel="stylesheet" type="text/css" href="/static/colors.css">
<link rel="stylesheet" type="text/css" href="/static/site.css">
<script src="https://open.spotify.com/embed/iframe-api/v1" async></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="/static/voting_script.js"></script>
<script type="text/template" data-template="song">
<div class="song" id="song-${id}">
<div class="cover-container">
<img src="${cover_image}" class="cover">
<div class="overlay" onclick="play(${id})"><img src="/static/${play_button}.svg"></div>
</div>
<div class="song-title">${title}</div>
<div class="song-artist">${artist}</div>
<div class="categories" id="container">
${categories}<span style="--main-color: transparent;">&nbsp;</span>
</div>
<div class="vote-buttons">
<div class="button button-no ${no_selected}" onmousedown="vote(${id}, -1); return false;" onclick="return false;"><img src="/static/no.svg"></div><div class="button button-neutral ${neutral_selected}" onmousedown="vote(${id}, 0)"><img src="/static/neutral.svg"></div><div class="button button-yes ${yes_selected}" onmousedown="vote(${id}, 1)"><img src="/static/yes.svg"></div>
</div>
<div class="clear"></div>
</div>
</script>
</head>
<body>
<h1 style="--main-color: #888888; ">Hallo :)</h1>
<div class="text">Du kannst die Liederwahl jederzeit unterbrechen und zu einem späteren Zeitpunkt weitermachen.</div>
<div id="songs"></div>
<div id="spotify-player"><div id="spotify-embed"></div></div>
<div id="yt-player"></div>
<div id="close-player" onclick="stop(); return false;"><img src="/static/no.svg"></div>
<!--<div class="song">
<div class="cover-container">
<img src="{{ url_for('static', path='/cover.jpg') }}" class="cover">
<div class="overlay"><img src="{{ url_for('static', path='/play.svg') }}"></div>
</div>
<div class="song-data">VoicePlay: In The Air Tonight</div>
<div class="categories" id="container">
<span class="cat-1">Ballade</span><span class="cat-2">&lt; 90er Remake</span><span class="cat-3">Something else </span><span class="cat-4">Something else </span>
</div>
<div class="vote-buttons">
<div class="button button-no"><img src="{{ url_for('static', path='/no.svg') }}"></div><div class="button button-neutral"><img src="{{ url_for('static', path='/neutral.svg') }}"></div><div class="button button-yes selected"><img src="{{ url_for('static', path='/yes.svg') }}"></div>
</div>
<div class="clear"></div>
</div>-->
</body>
</html>

View File

@@ -1,52 +1,164 @@
<!DOCTYPE html>
<html>
<head>
<title>Liederwahl</title>
<link rel="apple-touch-icon" href="https://choriosity.de/assets/images/apple-touch-icon.png" type="image/png">
<link rel="alternate icon" href="https://choriosity.de/assets/images/favicon.png" type="image/png">
<link rel="shortcut icon" href="https://choriosity.de/assets/images/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" type="text/css" href="/static/colors.css">
<link rel="stylesheet" type="text/css" href="/static/site.css">
<script src="https://open.spotify.com/embed/iframe-api/v1" async></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="/static/voting_script.js"></script>
<script type="text/template" data-template="song">
<div class="song" id="song-${id}">
<div class="cover-container">
<img src="${cover_image}" class="cover">
<div class="overlay" onclick="play(${id})"><img src="/static/play.svg"></div>
</div>
<div class="song-title">${title}</div>
<div class="song-artist">${artist}</div>
<div class="categories" id="container">
${categories}<span style="--main-color: transparent;">&nbsp;</span>
</div>
<div class="vote-buttons">
<div class="button button-no ${no_selected}" onmousedown="vote(${id}, -1); return false;" onclick="return false;"><img src="/static/no.svg"></div><div class="button button-neutral ${neutral_selected}" onmousedown="vote(${id}, 0)"><img src="/static/neutral.svg"></div><div class="button button-yes ${yes_selected}" onmousedown="vote(${id}, 1)"><img src="/static/yes.svg"></div>
</div>
<div class="clear"></div>
</div>
</script>
<script type="text/javascript">
var spotify_embed_controller;
window.onSpotifyIframeApiReady = (IFrameAPI) => {
const element = document.getElementById('spotify-embed');
const options = {
width: '640',
height: '360'
};
const callback = (EmbedController) => {
spotify_embed_controller = EmbedController;
};
IFrameAPI.createController(element, options, callback);
};
var is_playing = -1;
function stop() {
$("#song-" + is_playing + " .cover-container .overlay img").attr("src", "/static/play.svg");
$("#yt-player").css("display", "none");
$("#spotify-player").css("display", "none");
$("#close-player").css("display", "none");
$("#yt-player").html("");
spotify_embed_controller.pause();
is_playing = -1;
}
function playYt(song_id, yt_id) {
if (is_playing == song_id) {
stop();
} else {
stop();
is_playing = song_id;
$("#song-" + song_id + " .cover-container .overlay img").attr("src", "/static/stop.svg");
$("#yt-player").css("display", "flex");
$("#close-player").css("display", "block");
iframe_code = '<iframe src="https://www.youtube.com/embed/' + yt_id + '?autoplay=1" title="" width="640" height="360" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen></iframe>';
$("#yt-player").html(iframe_code);
}
}
function playSpfy(song_id, spfy_id) {
if (is_playing == song_id) {
stop();
} else {
stop();
is_playing = song_id;
$("#song-" + song_id + " .cover-container .overlay img").attr("src", "/static/stop.svg");
$("#spotify-player").css("display", "flex");
$("#close-player").css("display", "block");
spotify_embed_controller.loadUri("spotify:track:" + spfy_id);
spotify_embed_controller.play();
}
}
function openUrl(song_id, url) {
stop();
window.open(url, '_blank').focus();
}
function vote(song_id, vote) {
var session_id = "{{ session_id }}";
no_button = $("#song-" + song_id).find(".button-no")
yes_button = $("#song-" + song_id).find(".button-yes")
neutral_button = $("#song-" + song_id).find(".button-neutral")
no_button.removeClass("selected")
yes_button.removeClass("selected")
neutral_button.removeClass("selected")
switch (vote) {
case 0:
neutral_button.addClass("selected")
break;
case 1:
yes_button.addClass("selected")
break;
case -1:
no_button.addClass("selected")
default:
break;
}
$.ajax({
url: "/songs/" + song_id + "/vote?" + $.param({ user_id: session_id, vote: vote }),
method: "POST"
})
}
</script>
</head>
<body>
<div id="songs"></div>
<div id="spotify-player"><div id="spotify-embed"></div></div>
<div id="yt-player"></div>
<div id="close-player"><img src="/static/no.svg"></div>
<!--<div class="song">
<div class="cover-container">
<img src="{{ url_for('static', path='/cover.jpg') }}" class="cover">
<div class="overlay"><img src="{{ url_for('static', path='/play.svg') }}"></div>
<h1 style="--main-color: #888888; ">Hallo :)</h1>
<div class="text">Du kannst die Liederwahl jederzeit unterbrechen und zu einem späteren Zeitpunkt weitermachen.
</div>
<div class="song-data">VoicePlay: In The Air Tonight</div>
<div class="categories" id="container">
<span class="cat-1">Ballade</span><span class="cat-2">&lt; 90er Remake</span><span class="cat-3">Something else </span><span class="cat-4">Something else </span>
<div id="songs">
{% for main_category, songs in songs_by_category.items() %}
<h1 class="cat-{{ all_categories[main_category] }}">{{ main_category }}</h1>
{% for song in songs -%}
<div class="song" id="song-{{ song.id }}">
<div class="cover-container">
<img src="{{ song.thumbnail }}" class="cover">
<div class="overlay"
onclick="{% if song.yt_id %}playYt({{ song.id }}, '{{ song.yt_id }}'){% else %}{% if song.spfy_id %}playSpfy({{ song.id }}, '{{ song.spfy_id }}'){% else %}openUrl({{ song.id }}, '{{ song.url }}'){% endif %}{% endif %}">
<img src="/static/{% if song.yt_id or song.spfy_id %}play{% else %}open{% endif %}.svg">
</div>
</div>
<div class="song-title">{{ song.title }}</div>
<div class="song-artist">{% if song.og_artist %}{{ song.og_artist }}{% if song.aca_artist and
song.aca_artist != song.og_artist %} / {{ song.aca_artist
}}{% endif %}{% else %}{{ song.aca_artist }}{% endif %}</div>
<div class="categories" id="container">{% for category_name, is_in_category in song.categories.items() %}{%
if is_in_category %}<span class="cat-{{ all_categories[category_name] }}">{{ category_name }}</span>{%
endif %}{% endfor %}<span style="--main-color: transparent;">&nbsp;</span>
</div>
<div class="vote-buttons">
<div class="button button-no"><img src="{{ url_for('static', path='/no.svg') }}"></div><div class="button button-neutral"><img src="{{ url_for('static', path='/neutral.svg') }}"></div><div class="button button-yes selected"><img src="{{ url_for('static', path='/yes.svg') }}"></div>
<div class="button button-no {% if song.vote == -1 %}selected{% endif %}"
onmousedown="vote({{ song.id }}, -1); return false;" onclick="return false;"><img
src="/static/no.svg">
</div>
<div class="button button-neutral {% if song.vote == 0 %}selected{% endif %}"
onmousedown="vote({{ song.id }}, 0); return false;" onclick="return false;"><img
src="/static/neutral.svg"></div>
<div class="button button-yes {% if song.vote == 1 %}selected{% endif %}"
onmousedown="vote({{ song.id }}, 1); return false;" onclick="return false;"><img
src="/static/yes.svg">
</div>
</div>
<div class="clear"></div>
</div>-->
</body>
</html>
</div>
{% endfor %}
{% endfor %}
</div>
<div id="spotify-player">
<div id="spotify-embed"></div>
</div>
<div id="yt-player"></div>
<div id="close-player" onclick="stop(); return false;"><img src="/static/no.svg"></div>
</body>
</html>