mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-23 22:47:05 +01:00
commit
0d9f9508ac
16 changed files with 1791 additions and 0 deletions
5
lnbits/extensions/jukebox/README.md
Normal file
5
lnbits/extensions/jukebox/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Jukebox
|
||||
|
||||
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 here https://developer.spotify.com/dashboard/applications
|
||||
|
||||
Select the playlists you want people to be able to pay for, share the frontend page, profit :)
|
12
lnbits/extensions/jukebox/__init__.py
Normal file
12
lnbits/extensions/jukebox/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
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
|
6
lnbits/extensions/jukebox/config.json
Normal file
6
lnbits/extensions/jukebox/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "SpotifyJukebox",
|
||||
"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 (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 SET {q} WHERE id = ?", (*kwargs.values(), juke_id)
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM 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 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 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 WHERE user = ?", (user,))
|
||||
for row in rows:
|
||||
if row.sp_playlists == "":
|
||||
await delete_jukebox(row.id)
|
||||
rows = await db.fetchall("SELECT * FROM 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 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_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_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_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 (
|
||||
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_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))
|
422
lnbits/extensions/jukebox/static/js/index.js
Normal file
422
lnbits/extensions/jukebox/static/js/index.js
Normal file
|
@ -0,0 +1,422 @@
|
|||
/* 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'
|
||||
},
|
||||
{
|
||||
name: 'profit',
|
||||
align: 'left',
|
||||
label: 'Profit',
|
||||
field: 'profit'
|
||||
}
|
||||
],
|
||||
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].inkey
|
||||
)
|
||||
.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
|
||||
}
|
||||
})
|
19
lnbits/extensions/jukebox/static/js/jukebox.js
Normal file
19
lnbits/extensions/jukebox/static/js/jukebox.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* 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 |
101
lnbits/extensions/jukebox/templates/jukebox/_api_docs.html
Normal file
101
lnbits/extensions/jukebox/templates/jukebox/_api_docs.html
Normal file
|
@ -0,0 +1,101 @@
|
|||
<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>
|
35
lnbits/extensions/jukebox/templates/jukebox/error.html
Normal file
35
lnbits/extensions/jukebox/templates/jukebox/error.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% 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>
|
189
lnbits/extensions/jukebox/templates/jukebox/index.html
Normal file
189
lnbits/extensions/jukebox/templates/jukebox/index.html
Normal file
|
@ -0,0 +1,189 @@
|
|||
{% 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="green-7" 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">LNbits 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="green-7" inactive-color="green-10" 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="green-7" @click="step = 2">Continue</q-btn>
|
||||
<q-btn v-else color="green-7" disable>Continue</q-btn>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn color="green-7" 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="green-7" @click="submitSpotifyKeys">Submit keys</q-btn>
|
||||
<q-btn v-else color="green-7" disable color="green-7">Submit keys</q-btn>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn color="green-7" 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="green-7" 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="green-7" @click="authAccess">Authorise access</q-btn>
|
||||
<q-btn v-else color="green-7" disable color="green-7">Authorise access</q-btn>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-btn color="green-7" 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="green-8" :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="green-7" @click="createJukebox">Create Jukebox</q-btn>
|
||||
<q-btn v-else color="green-7" disable>Create Jukebox</q-btn>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<q-btn color="green-7" 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 %}
|
276
lnbits/extensions/jukebox/templates/jukebox/jukebox.html
Normal file
276
lnbits/extensions/jukebox/templates/jukebox/jukebox.html
Normal file
|
@ -0,0 +1,276 @@
|
|||
{% 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: {},
|
||||
cancelListener: () => { },
|
||||
playlists: {},
|
||||
playlist: '',
|
||||
heavyList: [],
|
||||
selectedWallet: {},
|
||||
paid: false,
|
||||
receive: {
|
||||
dialogues: {
|
||||
first: false,
|
||||
second: false
|
||||
},
|
||||
paymentReq: '',
|
||||
paymentHash: '',
|
||||
name: '',
|
||||
artist: '',
|
||||
image: '',
|
||||
id: '',
|
||||
showQR: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentPlay() {
|
||||
return this.currentlyPlaying
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelPayment: function () {
|
||||
this.paymentReq = null
|
||||
clearInterval(this.paymentDialog.checker)
|
||||
if (this.paymentDialog.dismissMsg) {
|
||||
this.paymentDialog.dismissMsg()
|
||||
}
|
||||
},
|
||||
closeReceiveDialog() { },
|
||||
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
|
||||
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
|
||||
|
||||
var paymentChecker = setInterval(function () {
|
||||
if (!self.paid) {
|
||||
self.checkInvoice(self.receive.paymentHash, '{{ juke_id }}')
|
||||
}
|
||||
if (self.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
self.paid = true
|
||||
self.receive.dialogues.first = false
|
||||
self.receive.dialogues.second = false
|
||||
self.$q.notify({
|
||||
message:
|
||||
'Processing',
|
||||
})
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/invoicep/' + song_id + '/{{ juke_id }}/' + self.receive.paymentHash)
|
||||
.then(function (response1) {
|
||||
|
||||
if (response1.data[2] == song_id) {
|
||||
setTimeout(function () { self.getCurrent() }, 500)
|
||||
self.$q.notify({
|
||||
color: 'green',
|
||||
message:
|
||||
'Success! "' + self.receive.name + '" will be played soon',
|
||||
timeout: 3000
|
||||
})
|
||||
|
||||
self.paid = false
|
||||
response1 = []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
LNbits.utils.notifyApiError(err)
|
||||
self.paid = false
|
||||
response1 = []
|
||||
})
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
})
|
||||
.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
|
||||
})
|
||||
})
|
||||
},
|
||||
checkInvoice(juke_id, paymentHash) {
|
||||
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/jukebox/api/v1/jukebox/jb/checkinvoice/' + juke_id + '/' + paymentHash,
|
||||
'filla'
|
||||
)
|
||||
.then(function (response) {
|
||||
|
||||
self.paid = response.data.paid
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
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 }}')
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// this.startPaymentNotifier()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
42
lnbits/extensions/jukebox/views.py
Normal file
42
lnbits/extensions/jukebox/views.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
import time
|
||||
from datetime import datetime
|
||||
from quart import g, render_template, request, jsonify, websocket
|
||||
from http import HTTPStatus
|
||||
import trio
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from lnbits.core.models import Payment
|
||||
|
||||
import json
|
||||
from . import jukebox_ext
|
||||
from .crud import get_jukebox
|
||||
from .views_api import api_get_jukebox_device_check
|
||||
|
||||
|
||||
@jukebox_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("jukebox/index.html", user=g.user)
|
||||
|
||||
|
||||
@jukebox_ext.route("/<juke_id>")
|
||||
async def connect_to_jukebox(juke_id):
|
||||
jukebox = await get_jukebox(juke_id)
|
||||
if not jukebox:
|
||||
return "error"
|
||||
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 await render_template(
|
||||
"jukebox/jukebox.html",
|
||||
playlists=jukebox.sp_playlists.split(","),
|
||||
juke_id=juke_id,
|
||||
price=jukebox.price,
|
||||
inkey=jukebox.inkey,
|
||||
)
|
||||
else:
|
||||
return await render_template("jukebox/error.html")
|
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,
|
||||
)
|
||||
from lnbits.core.services import create_invoice, check_invoice_status
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
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…
Add table
Reference in a new issue