Removed jukebox

This commit is contained in:
ben 2023-02-14 14:07:53 +00:00
parent 7a57690bd7
commit e1d4582345
17 changed files with 0 additions and 2057 deletions

View File

@ -1,36 +0,0 @@
# Jukebox
## An actual Jukebox where users pay sats to play their favourite music from your playlists
**Note:** To use this extension you need a Premium Spotify subscription.
## Usage
1. Click on "ADD SPOTIFY JUKEBOX"\
![add jukebox](https://i.imgur.com/NdVoKXd.png)
2. Follow the steps required on the form\
- give your jukebox a name
- select a wallet to receive payment
- define the price a user must pay to select a song\
![pick wallet price](https://i.imgur.com/4bJ8mb9.png)
- follow the steps to get your Spotify App and get the client ID and secret key\
![spotify keys](https://i.imgur.com/w2EzFtB.png)
- paste the codes in the form\
![api keys](https://i.imgur.com/6b9xauo.png)
- copy the _Redirect URL_ presented on the form\
![redirect url](https://i.imgur.com/GMzl0lG.png)
- on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt
![spotify app setting](https://i.imgur.com/vb0x4Tl.png)
- back on LNbits, click "AUTORIZE ACCESS" and "Agree" on the page that will open
- choose on which device the LNbits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...)
- and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\
![select playlists](https://i.imgur.com/g4dbtED.png)
3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\
![shareable jukebox](https://i.imgur.com/EAh9PI0.png)
4. The users will see the Jukebox page and choose a song from the selected playlist\
![select song](https://i.imgur.com/YYjeQAs.png)
5. After selecting a song they'd like to hear next a dialog will show presenting the music\
![play for sats](https://i.imgur.com/eEHl3o8.png)
6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing

View File

@ -1,34 +0,0 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_jukebox")
jukebox_static_files = [
{
"path": "/jukebox/static",
"app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]),
"name": "jukebox_static",
}
]
jukebox_ext: APIRouter = APIRouter(prefix="/jukebox", tags=["jukebox"])
def jukebox_renderer():
return template_renderer(["lnbits/extensions/jukebox/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
def jukebox_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

@ -1,6 +0,0 @@
{
"name": "Spotify Jukebox",
"short_description": "Spotify jukebox middleware",
"tile": "/jukebox/static/image/jukebox.png",
"contributors": ["benarc"]
}

View File

@ -1,108 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment
async def create_jukebox(data: CreateJukeLinkData) -> Jukebox:
juke_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
juke_id,
data.user,
data.title,
data.wallet,
data.sp_user,
data.sp_secret,
data.sp_access_token,
data.sp_refresh_token,
data.sp_device,
data.sp_playlists,
data.price,
0,
),
)
jukebox = await get_jukebox(juke_id)
assert jukebox, "Newly created Jukebox couldn't be retrieved"
return jukebox
async def update_jukebox(
data: Union[CreateJukeLinkData, Jukebox], juke_id: str = ""
) -> Optional[Jukebox]:
q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data]
items.append(juke_id)
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items,))
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None
async def get_jukebox(juke_id: str) -> Optional[Jukebox]:
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None
async def get_jukebox_by_user(user: str) -> Optional[Jukebox]:
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE sp_user = ?", (user,))
return Jukebox(**row) if row else None
async def get_jukeboxs(user: str) -> List[Jukebox]:
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
for row in rows:
if row.sp_playlists is None:
await delete_jukebox(row.id)
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
return [Jukebox(**row) for row in rows]
async def delete_jukebox(juke_id: str):
await db.execute(
"""
DELETE FROM jukebox.jukebox WHERE id = ?
""",
(juke_id,),
)
#####################################PAYMENTS
async def create_jukebox_payment(data: CreateJukeboxPayment) -> CreateJukeboxPayment:
await db.execute(
"""
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
VALUES (?, ?, ?, ?)
""",
(data.payment_hash, data.juke_id, data.song_id, False),
)
jukebox_payment = await get_jukebox_payment(data.payment_hash)
assert jukebox_payment, "Newly created Jukebox Payment couldn't be retrieved"
return data
async def update_jukebox_payment(
payment_hash: str, **kwargs
) -> Optional[JukeboxPayment]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE jukebox.jukebox_payment SET {q} WHERE payment_hash = ?",
(*kwargs.values(), payment_hash),
)
return await get_jukebox_payment(payment_hash)
async def get_jukebox_payment(payment_hash: str) -> Optional[JukeboxPayment]:
row = await db.fetchone(
"SELECT * FROM jukebox.jukebox_payment WHERE payment_hash = ?", (payment_hash,)
)
return JukeboxPayment(**row) if row else None

View File

@ -1,39 +0,0 @@
async def m001_initial(db):
"""
Initial jukebox table.
"""
await db.execute(
"""
CREATE TABLE jukebox.jukebox (
id TEXT PRIMARY KEY,
"user" TEXT,
title TEXT,
wallet TEXT,
inkey TEXT,
sp_user TEXT NOT NULL,
sp_secret TEXT NOT NULL,
sp_access_token TEXT,
sp_refresh_token TEXT,
sp_device TEXT,
sp_playlists TEXT,
price INTEGER,
profit INTEGER
);
"""
)
async def m002_initial(db):
"""
Initial jukebox_payment table.
"""
await db.execute(
"""
CREATE TABLE jukebox.jukebox_payment (
payment_hash TEXT PRIMARY KEY,
juke_id TEXT,
song_id TEXT,
paid BOOL
);
"""
)

View File

@ -1,48 +0,0 @@
from typing import Optional
from fastapi import Query
from pydantic import BaseModel
class CreateJukeLinkData(BaseModel):
user: str = Query(None)
title: str = Query(None)
wallet: str = Query(None)
sp_user: str = Query(None)
sp_secret: str = Query(None)
sp_access_token: str = Query(None)
sp_refresh_token: str = Query(None)
sp_device: str = Query(None)
sp_playlists: str = Query(None)
price: str = Query(None)
class Jukebox(BaseModel):
id: str
user: str
title: str
wallet: str
inkey: Optional[str]
sp_user: str
sp_secret: str
sp_access_token: Optional[str]
sp_refresh_token: Optional[str]
sp_device: Optional[str]
sp_playlists: Optional[str]
price: int
profit: int
class JukeboxPayment(BaseModel):
payment_hash: str
juke_id: str
song_id: str
paid: bool
class CreateJukeboxPayment(BaseModel):
invoice: str = Query(None)
payment_hash: str = Query(None)
juke_id: str = Query(None)
song_id: str = Query(None)
paid: bool = Query(False)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,413 +0,0 @@
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
Vue.component(VueQrcode.name, VueQrcode)
var mapJukebox = obj => {
if (obj.sp_device) {
obj._data = _.clone(obj)
obj.sp_id = obj._data.id
obj.device = obj._data.sp_device.split('-')[0]
playlists = obj._data.sp_playlists.split(',')
var i
playlistsar = []
for (i = 0; i < playlists.length; i++) {
playlistsar.push(playlists[i].split('-')[0])
}
obj.playlist = playlistsar.join()
console.log(obj)
return obj
} else {
return
}
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
JukeboxTable: {
columns: [
{
name: 'title',
align: 'left',
label: 'Title',
field: 'title'
},
{
name: 'device',
align: 'left',
label: 'Device',
field: 'device'
},
{
name: 'playlist',
align: 'left',
label: 'Playlist',
field: 'playlist'
},
{
name: 'price',
align: 'left',
label: 'Price',
field: 'price'
}
],
pagination: {
rowsPerPage: 10
}
},
isPwd: true,
tokenFetched: true,
devices: [],
filter: '',
jukebox: {},
playlists: [],
JukeboxLinks: [],
step: 1,
locationcbPath: '',
locationcb: '',
jukeboxDialog: {
show: false,
data: {}
},
spotifyDialog: false,
qrCodeDialog: {
show: false,
data: null
}
}
},
computed: {},
methods: {
openQrCodeDialog: function (linkId) {
var link = _.findWhere(this.JukeboxLinks, {id: linkId})
this.qrCodeDialog.data = _.clone(link)
this.qrCodeDialog.data.url =
window.location.protocol + '//' + window.location.host
this.qrCodeDialog.show = true
},
getJukeboxes() {
self = this
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.JukeboxLinks = response.data.map(function (obj) {
return mapJukebox(obj)
})
console.log(self.JukeboxLinks)
})
},
deleteJukebox(juke_id) {
self = this
LNbits.utils
.confirmDialog('Are you sure you want to delete this Jukebox?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/jukebox/api/v1/jukebox/' + juke_id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) {
return obj.id === juke_id
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
})
},
updateJukebox: function (linkId) {
self = this
var link = _.findWhere(self.JukeboxLinks, {id: linkId})
self.jukeboxDialog.data = _.clone(link._data)
self.refreshDevices()
self.refreshPlaylists()
self.step = 4
self.jukeboxDialog.data.sp_device = []
self.jukeboxDialog.data.sp_playlists = []
self.jukeboxDialog.data.sp_id = self.jukeboxDialog.data.id
self.jukeboxDialog.data.price = String(self.jukeboxDialog.data.price)
self.jukeboxDialog.show = true
},
closeFormDialog() {
this.jukeboxDialog.data = {}
this.jukeboxDialog.show = false
this.step = 1
},
submitSpotifyKeys() {
self = this
self.jukeboxDialog.data.user = self.g.user.id
LNbits.api
.request(
'POST',
'/jukebox/api/v1/jukebox/',
self.g.user.wallets[0].adminkey,
self.jukeboxDialog.data
)
.then(response => {
if (response.data) {
self.jukeboxDialog.data.sp_id = response.data.id
self.step = 3
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
authAccess() {
self = this
self.requestAuthorization()
self.getSpotifyTokens()
self.$q.notify({
spinner: true,
message: 'Processing',
timeout: 10000
})
},
getSpotifyTokens() {
self = this
var counter = 0
var timerId = setInterval(function () {
counter++
if (!self.jukeboxDialog.data.sp_user) {
clearInterval(timerId)
}
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id,
self.g.user.wallets[0].adminkey
)
.then(response => {
if (response.data.sp_access_token) {
self.fetchAccessToken(response.data.sp_access_token)
if (self.jukeboxDialog.data.sp_access_token) {
self.refreshPlaylists()
self.refreshDevices()
setTimeout(function () {
if (self.devices.length < 1 || self.playlists.length < 1) {
self.$q.notify({
spinner: true,
color: 'red',
message:
'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something',
timeout: 10000
})
LNbits.api
.request(
'DELETE',
'/jukebox/api/v1/jukebox/' + response.data.id,
self.g.user.wallets[0].adminkey
)
.then(function (response) {
self.getJukeboxes()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
clearInterval(timerId)
self.closeFormDialog()
} else {
self.step = 4
clearInterval(timerId)
}
}, 2000)
}
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}, 3000)
},
requestAuthorization() {
self = this
var url = 'https://accounts.spotify.com/authorize'
url += '?client_id=' + self.jukeboxDialog.data.sp_user
url += '&response_type=code'
url +=
'&redirect_uri=' +
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
url += '&show_dialog=true'
url +=
'&scope=user-read-private user-read-email user-modify-playback-state user-read-playback-position user-library-read streaming user-read-playback-state user-read-recently-played playlist-read-private'
window.open(url)
},
openNewDialog() {
this.jukeboxDialog.show = true
this.jukeboxDialog.data = {}
},
createJukebox() {
self = this
self.jukeboxDialog.data.sp_playlists =
self.jukeboxDialog.data.sp_playlists.join()
self.updateDB()
self.jukeboxDialog.show = false
self.getJukeboxes()
},
updateDB() {
self = this
LNbits.api
.request(
'PUT',
'/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id,
self.g.user.wallets[0].adminkey,
self.jukeboxDialog.data
)
.then(function (response) {
if (
self.jukeboxDialog.data.sp_playlists &&
self.jukeboxDialog.data.sp_devices
) {
self.getJukeboxes()
// self.JukeboxLinks.push(mapJukebox(response.data))
}
})
},
playlistApi(method, url, body) {
self = this
let xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader(
'Authorization',
'Bearer ' + this.jukeboxDialog.data.sp_access_token
)
xhr.send(body)
xhr.onload = function () {
if (xhr.status == 401) {
self.refreshAccessToken()
self.playlistApi(
'GET',
'https://api.spotify.com/v1/me/playlists',
null
)
}
let responseObj = JSON.parse(xhr.response)
self.jukeboxDialog.data.playlists = null
self.playlists = []
self.jukeboxDialog.data.playlists = []
var i
for (i = 0; i < responseObj.items.length; i++) {
self.playlists.push(
responseObj.items[i].name + '-' + responseObj.items[i].id
)
}
}
},
refreshPlaylists() {
self = this
self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null)
},
deviceApi(method, url, body) {
self = this
let xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader(
'Authorization',
'Bearer ' + this.jukeboxDialog.data.sp_access_token
)
xhr.send(body)
xhr.onload = function () {
if (xhr.status == 401) {
self.refreshAccessToken()
self.deviceApi(
'GET',
'https://api.spotify.com/v1/me/player/devices',
null
)
}
let responseObj = JSON.parse(xhr.response)
self.jukeboxDialog.data.devices = []
self.devices = []
var i
for (i = 0; i < responseObj.devices.length; i++) {
self.devices.push(
responseObj.devices[i].name + '-' + responseObj.devices[i].id
)
}
}
},
refreshDevices() {
self = this
self.deviceApi(
'GET',
'https://api.spotify.com/v1/me/player/devices',
null
)
},
fetchAccessToken(code) {
self = this
let body = 'grant_type=authorization_code'
body += '&code=' + code
body +=
'&redirect_uri=' +
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
self.callAuthorizationApi(body)
},
refreshAccessToken() {
self = this
let body = 'grant_type=refresh_token'
body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token
body += '&client_id=' + self.jukeboxDialog.data.sp_user
self.callAuthorizationApi(body)
},
callAuthorizationApi(body) {
self = this
let xhr = new XMLHttpRequest()
xhr.open('POST', 'https://accounts.spotify.com/api/token', true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.setRequestHeader(
'Authorization',
'Basic ' +
btoa(
self.jukeboxDialog.data.sp_user +
':' +
self.jukeboxDialog.data.sp_secret
)
)
xhr.send(body)
xhr.onload = function () {
let responseObj = JSON.parse(xhr.response)
if (responseObj.access_token) {
self.jukeboxDialog.data.sp_access_token = responseObj.access_token
self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token
self.updateDB()
}
}
}
},
created() {
var getJukeboxes = this.getJukeboxes
getJukeboxes()
this.selectedWallet = this.g.user.wallets[0]
this.locationcbPath = String(
[
window.location.protocol,
'//',
window.location.host,
'/jukebox/api/v1/jukebox/spotify/cb/'
].join('')
)
this.locationcb = this.locationcbPath
}
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

View File

@ -1,24 +0,0 @@
import asyncio
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import update_jukebox_payment
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "jukebox":
# not a jukebox invoice
return
await update_jukebox_payment(payment.payment_hash, paid=True)

View File

@ -1,135 +0,0 @@
<q-card-section>
To use this extension you need a Spotify client ID and client secret. You get
these by creating an app in the Spotify developers dashboard
<a
class="text-secondary"
style="color: #43a047"
href="https://developer.spotify.com/dashboard/applications"
>here
</a>
<br /><br />Select the playlists you want people to be able to pay for, share
the frontend page, profit :) <br /><br />
Made by,
<a
class="text-secondary"
style="color: #43a047"
href="https://twitter.com/arcbtc"
>benarc</a
>. Inspired by,
<a
class="text-secondary"
style="color: #43a047"
href="https://twitter.com/pirosb3/status/1056263089128161280"
>pirosb3</a
>.
</q-card-section>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/jukebox"></q-btn>
<q-expansion-item group="api" dense expand-separator label="List jukeboxes">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /jukebox/api/v1/jukebox</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;jukebox_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}jukebox/api/v1/jukebox -H
"X-Api-Key: {{ user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Get jukebox">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/jukebox/api/v1/jukebox/&lt;juke_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url
}}jukebox/api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create/update track"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST/PUT</span>
/jukebox/api/v1/jukebox/</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>&lt;jukbox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}jukebox/api/v1/jukebox/ -d
'{"user": &lt;string, user_id&gt;, "title": &lt;string&gt;,
"wallet":&lt;string&gt;, "sp_user": &lt;string,
spotify_user_account&gt;, "sp_secret": &lt;string,
spotify_user_secret&gt;, "sp_access_token": &lt;string,
not_required&gt;, "sp_refresh_token": &lt;string, not_required&gt;,
"sp_device": &lt;string, spotify_user_secret&gt;, "sp_playlists":
&lt;string, not_required&gt;, "price": &lt;integer, not_required&gt;}'
-H "Content-type: application/json" -H "X-Api-Key:
{{user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Delete jukebox">
<q-card>
<q-card-section>
<code
><span class="text-red">DELETE</span>
/jukebox/api/v1/jukebox/&lt;juke_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>&lt;jukebox_object&gt;</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}jukebox/api/v1/jukebox/&lt;juke_id&gt; -H "X-Api-Key: {{
user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item></q-expansion-item
>

View File

@ -1,37 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<center>
<h3 class="q-my-none">Jukebox error</h3>
<br />
<q-icon
name="warning"
class="text-grey"
style="font-size: 20rem"
></q-icon>
<h5 class="q-my-none">
Ask the host to turn on the device and launch spotify
</h5>
<br />
</center>
</q-card-section>
</q-card>
</div>
{% endblock %} {% block scripts %}
<script>
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {}
}
})
</script>
{% endblock %}
</div>

View File

@ -1,388 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
unelevated
color="primary"
class="q-ma-lg"
@click="openNewDialog()"
>Add Spotify Jukebox</q-btn
>
{% raw %}
<q-table
flat
dense
:data="JukeboxLinks"
row-key="id"
:columns="JukeboxTable.columns"
:pagination.sync="JukeboxTable.pagination"
:filter="filter"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width></q-th>
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.sp_id)"
>
<q-tooltip> Jukebox QR </q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateJukebox(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteJukebox(props.row.id)"
icon="cancel"
color="pink"
>
<q-tooltip> Delete Jukebox </q-tooltip>
</q-btn>
</q-td>
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.value }}</div>
</q-td>
</q-tr>
</template>
</q-table>
{% endraw %}
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} jukebox extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "jukebox/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="jukeboxDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-md q-pt-lg q-mt-md" style="width: 100%">
<q-stepper
v-model="step"
active-color="primary"
inactive-color="secondary"
vertical
animated
>
<q-step
:name="1"
title="1. Pick Wallet and Price"
icon="account_balance_wallet"
:done="step > 1"
>
<q-input
filled
class="q-pt-md"
dense
v-model.trim="jukeboxDialog.data.title"
label="Jukebox name"
></q-input>
<q-select
class="q-pb-md q-pt-md"
filled
dense
emit-value
v-model="jukeboxDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet to use"
></q-select>
<q-input
filled
dense
v-model.trim="jukeboxDialog.data.price"
type="number"
max="1440"
label="Price per track"
class="q-pb-lg"
>
</q-input>
<div class="row">
<div class="col-4">
<q-btn
v-if="jukeboxDialog.data.title != null && jukeboxDialog.data.price != null && jukeboxDialog.data.wallet != null"
color="primary"
@click="step = 2"
>Continue</q-btn
>
<q-btn v-else color="primary" disable>Continue</q-btn>
</div>
<div class="col-8">
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
<br />
</q-step>
<q-step
:name="2"
title="2. Add API keys"
icon="vpn_key"
:done="step > 2"
>
<img src="/jukebox/static/spotapi.gif" />
To use this extension you need a Spotify client ID and client secret.
You get these by creating an app in the Spotify Developer Dashboard
<br />
<br />
<q-btn
type="a"
target="_blank"
color="primary"
href="https://developer.spotify.com/dashboard/applications"
>Open the Spotify Developer Dashboard</q-btn
>
<q-input
filled
class="q-pb-md q-pt-md"
dense
v-model.trim="jukeboxDialog.data.sp_user"
label="Client ID"
>
</q-input>
<q-input
dense
v-model="jukeboxDialog.data.sp_secret"
filled
:type="isPwd ? 'password' : 'text'"
label="Client secret"
>
<template #append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
>
</q-icon>
</template>
</q-input>
<div class="row q-mt-md">
<div class="col-4">
<q-btn
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="primary"
@click="submitSpotifyKeys"
>Submit keys</q-btn
>
<q-btn v-else color="primary" disable color="primary"
>Submit keys</q-btn
>
</div>
<div class="col-8">
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
<br />
</q-step>
<q-step
:name="3"
title="3. Add Redirect URI"
icon="link"
:done="step > 3"
>
<img src="/jukebox/static/spotapi1.gif" />
<p>
In the app go to edit-settings, set the redirect URI to this link
</p>
<q-card
class="cursor-pointer word-break"
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
>
<q-card-section style="word-break: break-all">
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}
</q-card-section>
<q-tooltip> Click to copy URL </q-tooltip>
</q-card>
<br />
<q-btn
type="a"
target="_blank"
color="primary"
href="https://developer.spotify.com/dashboard/applications"
>Open the Spotify Application Settings</q-btn
>
<br /><br />
<p>
After adding the redirect URI, click the "Authorise access" button
below.
</p>
<div class="row q-mt-md">
<div class="col-4">
<q-btn
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="primary"
@click="authAccess"
>Authorise access</q-btn
>
<q-btn v-else color="primary" disable color="primary"
>Authorise access</q-btn
>
</div>
<div class="col-8">
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
<br />
</q-step>
<q-step
:name="4"
title="4. Select Device and Playlists"
icon="queue_music"
active-color="primary"
:done="step > 4"
>
<q-select
class="q-pb-md q-pt-md"
filled
dense
emit-value
v-model="jukeboxDialog.data.sp_device"
:options="devices"
label="Device jukebox will play to"
></q-select>
<q-select
class="q-pb-md"
filled
dense
multiple
emit-value
v-model="jukeboxDialog.data.sp_playlists"
:options="playlists"
label="Playlists available to the jukebox"
></q-select>
<div class="row q-mt-md">
<div class="col-5">
<q-btn
v-if="jukeboxDialog.data.sp_device != null && jukeboxDialog.data.sp_playlists != null"
color="primary"
@click="createJukebox"
>Create Jukebox</q-btn
>
<q-btn v-else color="primary" disable>Create Jukebox</q-btn>
</div>
<div class="col-7">
<q-btn
color="primary"
class="float-right"
@click="closeFormDialog"
>Cancel</q-btn
>
</div>
</div>
</q-step>
</q-stepper>
</q-card>
</q-dialog>
<q-dialog v-model="qrCodeDialog.show" position="top">
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
<center>
<h5 class="q-my-none">Shareable Jukebox QR</h5>
</center>
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="row q-mt-lg q-gutter-sm">
<q-btn
outline
color="grey"
@click="copyText(qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id, 'Link copied to clipboard!')"
>
Copy jukebox link</q-btn
>
<q-btn
outline
color="grey"
type="a"
:href="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
target="_blank"
>Open jukebox</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
<script src="/jukebox/static/js/index.js"></script>
{% endblock %}

View File

@ -1,281 +0,0 @@
{% extends "public.html" %} {% block page %}
<div class="row q-col-gutter-md justify-center">
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
<q-card class="q-pa-lg">
<q-card-section class="q-pa-none">
<p style="font-size: 22px">Currently playing</p>
<div class="row">
<div class="col-4">
<img style="width: 100px" :src="currentPlay.image" />
</div>
<div class="col-8">
{% raw %}
<strong style="font-size: 20px">{{ currentPlay.name }}</strong
><br />
<strong style="font-size: 15px">{{ currentPlay.artist }}</strong>
</div>
{% endraw %}
</div>
</q-card-section>
</q-card>
<q-card class="q-mt-lg">
<q-card-section>
<p style="font-size: 22px">Pick a song</p>
<q-select
outlined
v-model="playlist"
:options="playlists"
label="playlists"
@input="selectPlaylist()"
>
</q-select>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-virtual-scroll
style="max-height: 300px"
:items="currentPlaylist"
separator
>
<template v-slot="{ item, index }">
<q-item
:key="index"
dense
clickable
v-ripple
@click="payForSong(item.id, item.name, item.artist, item.image)"
>
<q-item-section>
<q-item-label>
{% raw %} {{ item.name }} - ({{ item.artist }}){% endraw %}
</q-item-label>
</q-item-section>
</q-item>
</template>
</q-virtual-scroll>
</q-card-section>
</q-card>
<q-dialog v-model="receive.dialogues.first" position="top">
<q-card class="q-pa-lg lnbits__dialog-card">
<q-card-section class="q-pa-none">
<div class="row">
<div class="col-4">
<img style="width: 100px" :src="receive.image" />
</div>
<div class="col-8">
{% raw %}
<strong style="font-size: 20px">{{ receive.name }}</strong><br />
<strong style="font-size: 15px">{{ receive.artist }}</strong>
</div>
</div>
</q-card-section>
<br />
<div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="getInvoice(receive.id)"
>Play for {% endraw %}{{ price }}sats
</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="receive.dialogues.second" position="top">
<q-card class="q-pa-lg lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="'lightning:' + receive.paymentReq.toUpperCase()"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
</div>
</q-card>
</q-dialog>
</div>
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
currentPlaylist: [],
currentlyPlaying: {},
playlists: {},
playlist: '',
heavyList: [],
selectedWallet: {},
paid: false,
receive: {
dialogues: {
first: false,
second: false
},
paymentReq: '',
paymentHash: '',
name: '',
artist: '',
image: '',
id: '',
showQR: false,
data: null,
dismissMsg: null,
paymentChecker: null
}
}
},
computed: {
currentPlay() {
return this.currentlyPlaying
}
},
methods: {
payForSong(song_id, name, artist, image) {
self = this
self.receive.name = name
self.receive.artist = artist
self.receive.image = image
self.receive.id = song_id
self.receive.dialogues.first = true
},
getInvoice(song_id) {
self = this
var dialog = this.receive
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/jb/invoice/' +
'{{ juke_id }}' +
'/' +
song_id
)
.then(function (response) {
console.log(response.data)
self.receive.paymentReq = response.data.invoice
self.receive.paymentHash = response.data.payment_hash
self.receive.dialogues.second = true
dialog.data = response.data
dialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
dialog.paymentChecker = setInterval(function () {
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/jb/checkinvoice/' +
self.receive.paymentHash +
'/{{ juke_id }}'
)
.then(function (res) {
console.log(res)
if (res.data.paid == true) {
clearInterval(dialog.paymentChecker)
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/jb/invoicep/' +
self.receive.id +
'/{{ juke_id }}/' +
self.receive.paymentHash
)
.then(function (ress) {
console.log('ress')
console.log(ress)
console.log('ress')
if (ress.data.song_id == self.receive.id) {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
self.receive.dialogues.second = false
self.$q.notify({
type: 'positive',
message:
'Success! "' +
self.receive.name +
'" will be played soon',
timeout: 3000
})
self.receive.dialogues.first = false
}
})
}
})
}, 3000)
setTimeout(() => {
self.getCurrent()
}, 500)
})
.catch(err => {
self.$q.notify({
color: 'warning',
html: true,
message:
'<center>Device is not connected! <br/> Ask the host to turn on their device and have Spotify open</center>',
timeout: 5000
})
})
},
getCurrent() {
LNbits.api
.request('GET', '/jukebox/api/v1/jukebox/jb/currently/{{juke_id}}')
.then(function (res) {
if (res.data.id) {
self.currentlyPlaying = res.data
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
selectPlaylist() {
self = this
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/jb/playlist/' +
'{{ juke_id }}' +
'/' +
self.playlist.split(',')[0].split('-')[1]
)
.then(function (response) {
self.currentPlaylist = response.data
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
currentSong() {}
},
created() {
this.getCurrent()
this.playlists = JSON.parse('{{ playlists | tojson }}')
this.selectedWallet.inkey = '{{ inkey }}'
self = this
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/jb/playlist/' +
'{{ juke_id }}' +
'/' +
self.playlists[0].split(',')[0].split('-')[1]
)
.then(function (response) {
self.currentPlaylist = response.data
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
})
</script>
{% endblock %}

View File

@ -1,54 +0,0 @@
from http import HTTPStatus
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import jukebox_ext, jukebox_renderer
from .crud import get_jukebox
from .views_api import api_get_jukebox_device_check
templates = Jinja2Templates(directory="templates")
@jukebox_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return jukebox_renderer().TemplateResponse(
"jukebox/index.html", {"request": request, "user": user.dict()}
)
@jukebox_ext.get("/{juke_id}", response_class=HTMLResponse)
async def connect_to_jukebox(request: Request, juke_id):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist."
)
devices = await api_get_jukebox_device_check(juke_id)
deviceConnected = False
assert jukebox.sp_device
assert jukebox.sp_playlists
for device in devices["devices"]:
if device["id"] == jukebox.sp_device.split("-")[1]:
deviceConnected = True
if deviceConnected:
return jukebox_renderer().TemplateResponse(
"jukebox/jukebox.html",
{
"request": request,
"playlists": jukebox.sp_playlists.split(","),
"juke_id": juke_id,
"price": jukebox.price,
"inkey": jukebox.inkey,
},
)
else:
return jukebox_renderer().TemplateResponse(
"jukebox/error.html",
{"request": request, "jukebox": jukebox.dict()},
)

View File

@ -1,454 +0,0 @@
import base64
import json
from http import HTTPStatus
import httpx
from fastapi import Depends, Query
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, require_admin_key
from . import jukebox_ext
from .crud import (
create_jukebox,
create_jukebox_payment,
delete_jukebox,
get_jukebox,
get_jukebox_payment,
get_jukeboxs,
update_jukebox,
update_jukebox_payment,
)
from .models import CreateJukeboxPayment, CreateJukeLinkData
@jukebox_ext.get("/api/v1/jukebox")
async def api_get_jukeboxs(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
wallet_user = wallet.wallet.user
try:
jukeboxs = [jukebox.dict() for jukebox in await get_jukeboxs(wallet_user)]
return jukeboxs
except:
raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukeboxes")
##################SPOTIFY AUTH#####################
@jukebox_ext.get("/api/v1/jukebox/spotify/cb/{juke_id}", response_class=HTMLResponse)
async def api_check_credentials_callbac(
juke_id: str = Query(None),
code: str = Query(None),
access_token: str = Query(None),
refresh_token: str = Query(None),
):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN)
if code:
jukebox.sp_access_token = code
await update_jukebox(jukebox, juke_id=juke_id)
if access_token:
jukebox.sp_access_token = access_token
jukebox.sp_refresh_token = refresh_token
await update_jukebox(jukebox, juke_id=juke_id)
return "<h1>Success!</h1><h2>You can close this window</h2>"
@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)])
async def api_check_credentials_check(juke_id: str = Query(None)):
jukebox = await get_jukebox(juke_id)
return jukebox
@jukebox_ext.post(
"/api/v1/jukebox",
status_code=HTTPStatus.CREATED,
dependencies=[Depends(require_admin_key)],
)
@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK)
async def api_create_update_jukebox(
data: CreateJukeLinkData, juke_id: str = Query(None)
):
if juke_id:
jukebox = await update_jukebox(data, juke_id=juke_id)
else:
jukebox = await create_jukebox(data)
return jukebox
@jukebox_ext.delete(
"/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]
)
async def api_delete_item(
juke_id: str = Query(None),
):
await delete_jukebox(juke_id)
# try:
# return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
# except:
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
################JUKEBOX ENDPOINTS##################
######GET ACCESS TOKEN######
@jukebox_ext.get("/api/v1/jukebox/jb/playlist/{juke_id}/{sp_playlist}")
async def api_get_jukebox_song(
juke_id: str = Query(None),
sp_playlist: str = Query(None),
retry: bool = Query(False),
):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
tracks = []
async with httpx.AsyncClient() as client:
try:
assert jukebox.sp_access_token
r = await client.get(
"https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks",
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if "items" not in r.json():
if r.status_code == 401:
token = await api_get_token(juke_id)
if token is False:
return False
elif retry:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Failed to get auth",
)
else:
return await api_get_jukebox_song(
juke_id, sp_playlist, retry=True
)
return r
for item in r.json()["items"]:
tracks.append(
{
"id": item["track"]["id"],
"name": item["track"]["name"],
"album": item["track"]["album"]["name"],
"artist": item["track"]["artists"][0]["name"],
"image": item["track"]["album"]["images"][0]["url"],
}
)
except:
pass
return [track for track in tracks]
async def api_get_token(juke_id):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
async with httpx.AsyncClient() as client:
try:
r = await client.post(
"https://accounts.spotify.com/api/token",
timeout=40,
params={
"grant_type": "refresh_token",
"refresh_token": jukebox.sp_refresh_token,
"client_id": jukebox.sp_user,
},
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic "
+ base64.b64encode(
str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii")
).decode("ascii"),
"Content-Type": "application/x-www-form-urlencoded",
},
)
if "access_token" not in r.json():
return False
else:
jukebox.sp_access_token = r.json()["access_token"]
await update_jukebox(jukebox, juke_id=juke_id)
except:
pass
return True
######CHECK DEVICE
@jukebox_ext.get("/api/v1/jukebox/jb/{juke_id}")
async def api_get_jukebox_device_check(
juke_id: str = Query(None), retry: bool = Query(False)
):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
async with httpx.AsyncClient() as client:
assert jukebox.sp_access_token
rDevice = await client.get(
"https://api.spotify.com/v1/me/player/devices",
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if rDevice.status_code == 204 or rDevice.status_code == 200:
return json.loads(rDevice.text)
elif rDevice.status_code == 401 or rDevice.status_code == 403:
token = await api_get_token(juke_id)
if token is False:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="No devices connected"
)
elif retry:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
)
else:
return await api_get_jukebox_device_check(juke_id, retry=True)
else:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="No device connected"
)
######GET INVOICE STUFF
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}")
async def api_get_jukebox_invoice(juke_id, song_id):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
try:
assert jukebox.sp_device
devices = await api_get_jukebox_device_check(juke_id)
deviceConnected = False
for device in devices["devices"]:
if device["id"] == jukebox.sp_device.split("-")[1]:
deviceConnected = True
if not deviceConnected:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No device connected"
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No device connected"
)
invoice = await create_invoice(
wallet_id=jukebox.wallet,
amount=jukebox.price,
memo=jukebox.title,
extra={"tag": "jukebox"},
)
payment_hash = invoice[0]
data = CreateJukeboxPayment(
invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id
)
jukebox_payment = await create_jukebox_payment(data)
return jukebox_payment
@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}")
async def api_get_jukebox_invoice_check(
pay_hash: str = Query(None), juke_id: str = Query(None)
):
try:
await get_jukebox(juke_id)
except:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
try:
status = await api_payment(pay_hash)
if status["paid"]:
await update_jukebox_payment(pay_hash, paid=True)
return {"paid": True}
except:
return {"paid": False}
return {"paid": False}
@jukebox_ext.get("/api/v1/jukebox/jb/invoicep/{song_id}/{juke_id}/{pay_hash}")
async def api_get_jukebox_invoice_paid(
song_id: str = Query(None),
juke_id: str = Query(None),
pay_hash: str = Query(None),
retry: bool = Query(False),
):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
await api_get_jukebox_invoice_check(pay_hash, juke_id)
jukebox_payment = await get_jukebox_payment(pay_hash)
if jukebox_payment and jukebox_payment.paid:
async with httpx.AsyncClient() as client:
assert jukebox.sp_access_token
r = await client.get(
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
rDevice = await client.get(
"https://api.spotify.com/v1/me/player",
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
isPlaying = False
if rDevice.status_code == 200:
isPlaying = rDevice.json()["is_playing"]
if r.status_code == 204 or isPlaying is False:
async with httpx.AsyncClient() as client:
uri = ["spotify:track:" + song_id]
assert jukebox.sp_device
r = await client.put(
"https://api.spotify.com/v1/me/player/play?device_id="
+ jukebox.sp_device.split("-")[1],
json={"uris": uri},
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if r.status_code == 204:
return jukebox_payment
elif r.status_code == 401 or r.status_code == 403:
token = await api_get_token(juke_id)
if token is False:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Invoice not paid",
)
elif retry:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Failed to get auth",
)
else:
return api_get_jukebox_invoice_paid(
song_id, juke_id, pay_hash, retry=True
)
else:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
)
elif r.status_code == 200:
async with httpx.AsyncClient() as client:
assert jukebox.sp_access_token
assert jukebox.sp_device
r = await client.post(
"https://api.spotify.com/v1/me/player/queue?uri=spotify%3Atrack%3A"
+ song_id
+ "&device_id="
+ jukebox.sp_device.split("-")[1],
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if r.status_code == 204:
return jukebox_payment
elif r.status_code == 401 or r.status_code == 403:
token = await api_get_token(juke_id)
if token is False:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Invoice not paid",
)
elif retry:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Failed to get auth",
)
else:
return await api_get_jukebox_invoice_paid(
song_id, juke_id, pay_hash
)
else:
raise HTTPException(
status_code=HTTPStatus.OK, detail="Invoice not paid"
)
elif r.status_code == 401 or r.status_code == 403:
token = await api_get_token(juke_id)
if token is False:
raise HTTPException(
status_code=HTTPStatus.OK, detail="Invoice not paid"
)
elif retry:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
)
else:
return await api_get_jukebox_invoice_paid(
song_id, juke_id, pay_hash
)
raise HTTPException(status_code=HTTPStatus.OK, detail="Invoice not paid")
############################GET TRACKS
@jukebox_ext.get("/api/v1/jukebox/jb/currently/{juke_id}")
async def api_get_jukebox_currently(
retry: bool = Query(False), juke_id: str = Query(None)
):
jukebox = await get_jukebox(juke_id)
if not jukebox:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
async with httpx.AsyncClient() as client:
try:
assert jukebox.sp_access_token
r = await client.get(
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
timeout=40,
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if r.status_code == 204:
raise HTTPException(status_code=HTTPStatus.OK, detail="Nothing")
elif r.status_code == 200:
try:
response = r.json()
track = {
"id": response["item"]["id"],
"name": response["item"]["name"],
"album": response["item"]["album"]["name"],
"artist": response["item"]["artists"][0]["name"],
"image": response["item"]["album"]["images"][0]["url"],
}
return track
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong"
)
elif r.status_code == 401:
token = await api_get_token(juke_id)
if token is False:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
)
elif retry:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
)
else:
return await api_get_jukebox_currently(retry=True, juke_id=juke_id)
else:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong"
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Something went wrong, or no song is playing yet",
)