initial
This commit is contained in:
parent
c1a82dad9d
commit
7dd29b3eca
36
lnbits/extensions/jukebox/README.md
Normal file
36
lnbits/extensions/jukebox/README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# 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
|
17
lnbits/extensions/jukebox/__init__.py
Normal file
17
lnbits/extensions/jukebox/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from quart import Blueprint
|
||||
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_jukebox")
|
||||
|
||||
jukebox_ext: Blueprint = Blueprint(
|
||||
"jukebox", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .tasks import register_listeners
|
||||
|
||||
from lnbits.tasks import record_async
|
||||
|
||||
jukebox_ext.record(record_async(register_listeners))
|
6
lnbits/extensions/jukebox/config.json
Normal file
6
lnbits/extensions/jukebox/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Spotify Jukebox",
|
||||
"short_description": "Spotify jukebox middleware",
|
||||
"icon": "radio",
|
||||
"contributors": ["benarc"]
|
||||
}
|
122
lnbits/extensions/jukebox/crud.py
Normal file
122
lnbits/extensions/jukebox/crud.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from . import db
|
||||
from .models import Jukebox, JukeboxPayment
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
|
||||
async def create_jukebox(
|
||||
inkey: str,
|
||||
user: str,
|
||||
wallet: str,
|
||||
title: str,
|
||||
price: int,
|
||||
sp_user: str,
|
||||
sp_secret: str,
|
||||
sp_access_token: Optional[str] = "",
|
||||
sp_refresh_token: Optional[str] = "",
|
||||
sp_device: Optional[str] = "",
|
||||
sp_playlists: Optional[str] = "",
|
||||
) -> Jukebox:
|
||||
juke_id = urlsafe_short_hash()
|
||||
result = 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,
|
||||
user,
|
||||
title,
|
||||
wallet,
|
||||
sp_user,
|
||||
sp_secret,
|
||||
sp_access_token,
|
||||
sp_refresh_token,
|
||||
sp_device,
|
||||
sp_playlists,
|
||||
int(price),
|
||||
0,
|
||||
),
|
||||
)
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
assert jukebox, "Newly created Jukebox couldn't be retrieved"
|
||||
return jukebox
|
||||
|
||||
|
||||
async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id)
|
||||
)
|
||||
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 == "":
|
||||
await delete_jukebox(row.id)
|
||||
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,))
|
||||
return [Jukebox.from_row(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(
|
||||
song_id: str, payment_hash: str, juke_id: str
|
||||
) -> JukeboxPayment:
|
||||
result = await db.execute(
|
||||
"""
|
||||
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
payment_hash,
|
||||
juke_id,
|
||||
song_id,
|
||||
False,
|
||||
),
|
||||
)
|
||||
jukebox_payment = await get_jukebox_payment(payment_hash)
|
||||
assert jukebox_payment, "Newly created Jukebox Payment couldn't be retrieved"
|
||||
return jukebox_payment
|
||||
|
||||
|
||||
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
|
39
lnbits/extensions/jukebox/migrations.py
Normal file
39
lnbits/extensions/jukebox/migrations.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
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
|
||||
);
|
||||
"""
|
||||
)
|
33
lnbits/extensions/jukebox/models.py
Normal file
33
lnbits/extensions/jukebox/models.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from typing import NamedTuple
|
||||
from sqlite3 import Row
|
||||
|
||||
|
||||
class Jukebox(NamedTuple):
|
||||
id: str
|
||||
user: str
|
||||
title: str
|
||||
wallet: str
|
||||
inkey: str
|
||||
sp_user: str
|
||||
sp_secret: str
|
||||
sp_access_token: str
|
||||
sp_refresh_token: str
|
||||
sp_device: str
|
||||
sp_playlists: str
|
||||
price: int
|
||||
profit: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Jukebox":
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class JukeboxPayment(NamedTuple):
|
||||
payment_hash: str
|
||||
juke_id: str
|
||||
song_id: str
|
||||
paid: bool
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "JukeboxPayment":
|
||||
return cls(**dict(row))
|
420
lnbits/extensions/jukebox/static/js/index.js
Normal file
420
lnbits/extensions/jukebox/static/js/index.js
Normal file
|
@ -0,0 +1,420 @@
|
|||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
||||
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var mapJukebox = obj => {
|
||||
obj._data = _.clone(obj)
|
||||
obj.sp_id = obj.id
|
||||
obj.device = obj.sp_device.split('-')[0]
|
||||
playlists = obj.sp_playlists.split(',')
|
||||
var i
|
||||
playlistsar = []
|
||||
for (i = 0; i < playlists.length; i++) {
|
||||
playlistsar.push(playlists[i].split('-')[0])
|
||||
}
|
||||
obj.playlist = playlistsar.join()
|
||||
return obj
|
||||
}
|
||||
|
||||
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)
|
||||
console.log(this.qrCodeDialog.data)
|
||||
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(mapJukebox)
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
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)
|
||||
console.log(this.jukeboxDialog.data.sp_access_token)
|
||||
|
||||
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()
|
||||
console.log('this.devices')
|
||||
console.log(self.devices)
|
||||
console.log('this.devices')
|
||||
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
|
||||
console.log(self.jukeboxDialog.data)
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id,
|
||||
self.g.user.wallets[0].adminkey,
|
||||
self.jukeboxDialog.data
|
||||
)
|
||||
.then(function (response) {
|
||||
console.log(response.data)
|
||||
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
|
||||
)
|
||||
}
|
||||
console.log(self.playlists)
|
||||
}
|
||||
},
|
||||
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
|
||||
console.log(
|
||||
btoa(
|
||||
self.jukeboxDialog.data.sp_user +
|
||||
':' +
|
||||
self.jukeboxDialog.data.sp_secret
|
||||
)
|
||||
)
|
||||
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() {
|
||||
console.log(this.g.user.wallets[0])
|
||||
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
|
||||
}
|
||||
})
|
14
lnbits/extensions/jukebox/static/js/jukebox.js
Normal file
14
lnbits/extensions/jukebox/static/js/jukebox.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
||||
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
created() {}
|
||||
})
|
BIN
lnbits/extensions/jukebox/static/spotapi.gif
Normal file
BIN
lnbits/extensions/jukebox/static/spotapi.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 215 KiB |
BIN
lnbits/extensions/jukebox/static/spotapi1.gif
Normal file
BIN
lnbits/extensions/jukebox/static/spotapi1.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 241 KiB |
28
lnbits/extensions/jukebox/tasks.py
Normal file
28
lnbits/extensions/jukebox/tasks.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import json
|
||||
import trio # type: ignore
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.tasks import register_invoice_listener, internal_invoice_paid
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .crud import get_jukebox, update_jukebox_payment
|
||||
|
||||
|
||||
async def register_listeners():
|
||||
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
|
||||
register_invoice_listener(invoice_paid_chan_send)
|
||||
await wait_for_paid_invoices(invoice_paid_chan_recv)
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||
async for payment in invoice_paid_chan:
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "jukebox" != payment.extra.get("tag"):
|
||||
# not a jukebox invoice
|
||||
return
|
||||
await update_jukebox_payment(payment.payment_hash, paid=True)
|
125
lnbits/extensions/jukebox/templates/jukebox/_api_docs.html
Normal file
125
lnbits/extensions/jukebox/templates/jukebox/_api_docs.html
Normal file
|
@ -0,0 +1,125 @@
|
|||
<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
|
||||
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 style="color: #43a047" href="https://twitter.com/arcbtc">benarc</a>.
|
||||
Inspired by,
|
||||
<a
|
||||
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-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": <admin_key>}</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>[<jukebox_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{
|
||||
g.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/<juke_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</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><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H
|
||||
"X-Api-Key: {{ g.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": <admin_key>}</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><jukbox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user":
|
||||
<string, user_id>, "title": <string>,
|
||||
"wallet":<string>, "sp_user": <string,
|
||||
spotify_user_account>, "sp_secret": <string,
|
||||
spotify_user_secret>, "sp_access_token": <string,
|
||||
not_required>, "sp_refresh_token": <string, not_required>,
|
||||
"sp_device": <string, spotify_user_secret>, "sp_playlists":
|
||||
<string, not_required>, "price": <integer, not_required>}'
|
||||
-H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{g.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/<juke_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</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><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id>
|
||||
-H "X-Api-Key: {{ g.user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item></q-expansion-item
|
||||
>
|
37
lnbits/extensions/jukebox/templates/jukebox/error.html
Normal file
37
lnbits/extensions/jukebox/templates/jukebox/error.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% 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>
|
368
lnbits/extensions/jukebox/templates/jukebox/index.html
Normal file
368
lnbits/extensions/jukebox/templates/jukebox/index.html
Normal file
|
@ -0,0 +1,368 @@
|
|||
{% 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="Pick wallet, 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="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 developers dashboard
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: #43a047"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here</a
|
||||
>.
|
||||
<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="Add Redirect URI" icon="link" :done="step > 3">
|
||||
<img src="/jukebox/static/spotapi1.gif" />
|
||||
In the app go to edit-settings, set the redirect URI to this link
|
||||
<br />
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
unelevated
|
||||
color="primary"
|
||||
size="xs"
|
||||
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
|
||||
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
|
||||
%}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-btn>
|
||||
<br />
|
||||
Settings can be found
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: #43a047"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here</a
|
||||
>.
|
||||
|
||||
<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="Select 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 %}
|
277
lnbits/extensions/jukebox/templates/jukebox/jukebox.html
Normal file
277
lnbits/extensions/jukebox/templates/jukebox/jukebox.html
Normal file
|
@ -0,0 +1,277 @@
|
|||
{% extends "public.html" %} {% block page %} {% raw %}
|
||||
<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">
|
||||
<strong style="font-size: 20px">{{ currentPlay.name }}</strong
|
||||
><br />
|
||||
<strong style="font-size: 15px">{{ currentPlay.artist }}</strong>
|
||||
</div>
|
||||
</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>
|
||||
{{ item.name }} - ({{ item.artist }})
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-virtual-scroll>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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 }}{% raw %} 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"
|
||||
: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>
|
||||
{% endraw %} {% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<style></style>
|
||||
<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) {
|
||||
self.receive.paymentReq = response.data[0][1]
|
||||
self.receive.paymentHash = response.data[0][0]
|
||||
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)
|
||||
if (ress.data[2] == 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 %}
|
50
lnbits/extensions/jukebox/views.py
Normal file
50
lnbits/extensions/jukebox/views.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from lnbits.decorators import check_user_exists
|
||||
from . import jukebox_ext, jukebox_renderer
|
||||
from .crud import get_jukebox
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
from lnbits.core.models import User, Payment
|
||||
|
||||
|
||||
@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."
|
||||
)
|
||||
deviceCheck = await api_get_jukebox_device_check(juke_id)
|
||||
devices = json.loads(deviceCheck[0].text)
|
||||
deviceConnected = False
|
||||
for device in devices["devices"]:
|
||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
||||
deviceConnected = True
|
||||
if deviceConnected:
|
||||
return jukebox_renderer().TemplateResponse(
|
||||
"jukebox/display.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}
|
||||
)
|
490
lnbits/extensions/jukebox/views_api.py
Normal file
490
lnbits/extensions/jukebox/views_api.py
Normal file
|
@ -0,0 +1,490 @@
|
|||
from quart import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
import base64
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.services import create_invoice, check_invoice_status
|
||||
import json
|
||||
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
import httpx
|
||||
from . import jukebox_ext
|
||||
from .crud import (
|
||||
create_jukebox,
|
||||
update_jukebox,
|
||||
get_jukebox,
|
||||
get_jukeboxs,
|
||||
delete_jukebox,
|
||||
create_jukebox_payment,
|
||||
get_jukebox_payment,
|
||||
update_jukebox_payment,
|
||||
)
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox", methods=["GET"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_get_jukeboxs():
|
||||
try:
|
||||
return (
|
||||
jsonify(
|
||||
[{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except:
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
##################SPOTIFY AUTH#####################
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/spotify/cb/<juke_id>", methods=["GET"])
|
||||
async def api_check_credentials_callbac(juke_id):
|
||||
sp_code = ""
|
||||
sp_access_token = ""
|
||||
sp_refresh_token = ""
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
if request.args.get("code"):
|
||||
sp_code = request.args.get("code")
|
||||
jukebox = await update_jukebox(
|
||||
juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code
|
||||
)
|
||||
if request.args.get("access_token"):
|
||||
sp_access_token = request.args.get("access_token")
|
||||
sp_refresh_token = request.args.get("refresh_token")
|
||||
jukebox = await update_jukebox(
|
||||
juke_id=juke_id,
|
||||
sp_secret=jukebox.sp_secret,
|
||||
sp_access_token=sp_access_token,
|
||||
sp_refresh_token=sp_refresh_token,
|
||||
)
|
||||
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["GET"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_check_credentials_check(juke_id):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"])
|
||||
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"user": {"type": "string", "empty": False, "required": True},
|
||||
"title": {"type": "string", "empty": False, "required": True},
|
||||
"wallet": {"type": "string", "empty": False, "required": True},
|
||||
"sp_user": {"type": "string", "empty": False, "required": True},
|
||||
"sp_secret": {"type": "string", "required": True},
|
||||
"sp_access_token": {"type": "string", "required": False},
|
||||
"sp_refresh_token": {"type": "string", "required": False},
|
||||
"sp_device": {"type": "string", "required": False},
|
||||
"sp_playlists": {"type": "string", "required": False},
|
||||
"price": {"type": "string", "required": False},
|
||||
}
|
||||
)
|
||||
async def api_create_update_jukebox(juke_id=None):
|
||||
if juke_id:
|
||||
jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **g.data)
|
||||
else:
|
||||
jukebox = await create_jukebox(inkey=g.wallet.inkey, **g.data)
|
||||
|
||||
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_delete_item(juke_id):
|
||||
await delete_jukebox(juke_id)
|
||||
try:
|
||||
return (
|
||||
jsonify(
|
||||
[{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)]
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
except:
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
################JUKEBOX ENDPOINTS##################
|
||||
|
||||
######GET ACCESS TOKEN######
|
||||
|
||||
|
||||
@jukebox_ext.route(
|
||||
"/api/v1/jukebox/jb/playlist/<juke_id>/<sp_playlist>", methods=["GET"]
|
||||
)
|
||||
async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
tracks = []
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
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 == False:
|
||||
return False
|
||||
elif retry:
|
||||
return (
|
||||
jsonify({"error": "Failed to get auth"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return await api_get_jukebox_song(
|
||||
juke_id, sp_playlist, retry=True
|
||||
)
|
||||
return r, HTTPStatus.OK
|
||||
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 AssertionError:
|
||||
something = None
|
||||
return jsonify([track for track in tracks])
|
||||
|
||||
|
||||
async def api_get_token(juke_id):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
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:
|
||||
await update_jukebox(
|
||||
juke_id=juke_id, sp_access_token=r.json()["access_token"]
|
||||
)
|
||||
except AssertionError:
|
||||
something = None
|
||||
return True
|
||||
|
||||
|
||||
######CHECK DEVICE
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/jb/<juke_id>", methods=["GET"])
|
||||
async def api_get_jukebox_device_check(juke_id, retry=False):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
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 (
|
||||
rDevice,
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
elif rDevice.status_code == 401 or rDevice.status_code == 403:
|
||||
token = await api_get_token(juke_id)
|
||||
if token == False:
|
||||
return (
|
||||
jsonify({"error": "No device connected"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
elif retry:
|
||||
return (
|
||||
jsonify({"error": "Failed to get auth"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return api_get_jukebox_device_check(juke_id, retry=True)
|
||||
else:
|
||||
return (
|
||||
jsonify({"error": "No device connected"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
|
||||
######GET INVOICE STUFF
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/jb/invoice/<juke_id>/<song_id>", methods=["GET"])
|
||||
async def api_get_jukebox_invoice(juke_id, song_id):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
try:
|
||||
deviceCheck = await api_get_jukebox_device_check(juke_id)
|
||||
devices = json.loads(deviceCheck[0].text)
|
||||
deviceConnected = False
|
||||
for device in devices["devices"]:
|
||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
||||
deviceConnected = True
|
||||
if not deviceConnected:
|
||||
return (
|
||||
jsonify({"error": "No device connected"}),
|
||||
HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No device connected"}),
|
||||
HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
|
||||
invoice = await create_invoice(
|
||||
wallet_id=jukebox.wallet,
|
||||
amount=jukebox.price,
|
||||
memo=jukebox.title,
|
||||
extra={"tag": "jukebox"},
|
||||
)
|
||||
|
||||
jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id)
|
||||
|
||||
return jsonify(invoice, jukebox_payment)
|
||||
|
||||
|
||||
@jukebox_ext.route(
|
||||
"/api/v1/jukebox/jb/checkinvoice/<pay_hash>/<juke_id>", methods=["GET"]
|
||||
)
|
||||
async def api_get_jukebox_invoice_check(pay_hash, juke_id):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
try:
|
||||
status = await check_invoice_status(jukebox.wallet, pay_hash)
|
||||
is_paid = not status.pending
|
||||
except Exception as exc:
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
if is_paid:
|
||||
wallet = await get_wallet(jukebox.wallet)
|
||||
payment = await wallet.get_payment(pay_hash)
|
||||
await payment.set_pending(False)
|
||||
await update_jukebox_payment(pay_hash, paid=True)
|
||||
return jsonify({"paid": True}), HTTPStatus.OK
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
|
||||
@jukebox_ext.route(
|
||||
"/api/v1/jukebox/jb/invoicep/<song_id>/<juke_id>/<pay_hash>", methods=["GET"]
|
||||
)
|
||||
async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
await api_get_jukebox_invoice_check(pay_hash, juke_id)
|
||||
jukebox_payment = await get_jukebox_payment(pay_hash)
|
||||
if jukebox_payment.paid:
|
||||
async with httpx.AsyncClient() as client:
|
||||
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 == False:
|
||||
async with httpx.AsyncClient() as client:
|
||||
uri = ["spotify:track:" + song_id]
|
||||
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 jsonify(jukebox_payment), HTTPStatus.OK
|
||||
elif r.status_code == 401 or r.status_code == 403:
|
||||
token = await api_get_token(juke_id)
|
||||
if token == False:
|
||||
return (
|
||||
jsonify({"error": "Invoice not paid"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
elif retry:
|
||||
return (
|
||||
jsonify({"error": "Failed to get auth"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return api_get_jukebox_invoice_paid(
|
||||
song_id, juke_id, pay_hash, retry=True
|
||||
)
|
||||
else:
|
||||
return (
|
||||
jsonify({"error": "Invoice not paid"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
elif r.status_code == 200:
|
||||
async with httpx.AsyncClient() as client:
|
||||
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 jsonify(jukebox_payment), HTTPStatus.OK
|
||||
|
||||
elif r.status_code == 401 or r.status_code == 403:
|
||||
token = await api_get_token(juke_id)
|
||||
if token == False:
|
||||
return (
|
||||
jsonify({"error": "Invoice not paid"}),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
elif retry:
|
||||
return (
|
||||
jsonify({"error": "Failed to get auth"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return await api_get_jukebox_invoice_paid(
|
||||
song_id, juke_id, pay_hash
|
||||
)
|
||||
else:
|
||||
return (
|
||||
jsonify({"error": "Invoice not paid"}),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
elif r.status_code == 401 or r.status_code == 403:
|
||||
token = await api_get_token(juke_id)
|
||||
if token == False:
|
||||
return (
|
||||
jsonify({"error": "Invoice not paid"}),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
elif retry:
|
||||
return (
|
||||
jsonify({"error": "Failed to get auth"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return await api_get_jukebox_invoice_paid(
|
||||
song_id, juke_id, pay_hash
|
||||
)
|
||||
return jsonify({"error": "Invoice not paid"}), HTTPStatus.OK
|
||||
|
||||
|
||||
############################GET TRACKS
|
||||
|
||||
|
||||
@jukebox_ext.route("/api/v1/jukebox/jb/currently/<juke_id>", methods=["GET"])
|
||||
async def api_get_jukebox_currently(juke_id, retry=False):
|
||||
try:
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
except:
|
||||
return (
|
||||
jsonify({"error": "No Jukebox"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
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:
|
||||
return jsonify({"error": "Nothing"}), HTTPStatus.OK
|
||||
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 jsonify(track), HTTPStatus.OK
|
||||
except:
|
||||
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
|
||||
|
||||
elif r.status_code == 401:
|
||||
token = await api_get_token(juke_id)
|
||||
if token == False:
|
||||
return (
|
||||
jsonify({"error": "Invoice not paid"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
elif retry:
|
||||
return (
|
||||
jsonify({"error": "Failed to get auth"}),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
else:
|
||||
return await api_get_jukebox_currently(juke_id, retry=True)
|
||||
else:
|
||||
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
|
||||
except AssertionError:
|
||||
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
|
Loading…
Reference in New Issue
Block a user