Added update jukebox

This commit is contained in:
Ben Arc
2021-05-03 23:22:40 +01:00
parent 245a819f19
commit be234c349f
6 changed files with 260 additions and 155 deletions

View File

@@ -20,8 +20,8 @@ async def create_jukebox(
juke_id = urlsafe_short_hash() juke_id = urlsafe_short_hash()
result = await db.execute( 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) INSERT INTO jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
juke_id, juke_id,
@@ -35,6 +35,7 @@ async def create_jukebox(
sp_device, sp_device,
sp_playlists, sp_playlists,
int(price), int(price),
0,
), ),
) )
jukebox = await get_jukebox(juke_id) jukebox = await get_jukebox(juke_id)
@@ -42,17 +43,17 @@ async def create_jukebox(
return jukebox return jukebox
async def update_jukebox(id: str, **kwargs) -> Optional[Jukebox]: async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute( await db.execute(
f"UPDATE jukebox SET {q} WHERE id = ?", (*kwargs.values(), id) f"UPDATE jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id)
) )
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None return Jukebox(**row) if row else None
async def get_jukebox(id: str) -> Optional[Jukebox]: async def get_jukebox(juke_id: str) -> Optional[Jukebox]:
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None return Jukebox(**row) if row else None
@@ -60,18 +61,20 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]:
row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (user,)) row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (user,))
return Jukebox(**row) if row else None return Jukebox(**row) if row else None
async def get_jukeboxs(user: str) -> List[Jukebox]: async def get_jukeboxs(user: str) -> List[Jukebox]:
rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,)) rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,))
for row in rows: for row in rows:
if not row.sp_playlists: if row.sp_playlists == "":
await delete_jukebox(row.id) await delete_jukebox(row.id)
rows.remove(row) rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,))
return [Jukebox.from_row(row) for row in rows] return [Jukebox.from_row(row) for row in rows]
async def delete_jukebox(id: str):
async def delete_jukebox(juke_id: str):
await db.execute( await db.execute(
""" """
DELETE FROM jukebox WHERE id = ? DELETE FROM jukebox WHERE id = ?
""", """,
(id), (juke_id),
) )

View File

@@ -15,7 +15,8 @@ async def m001_initial(db):
sp_refresh_token TEXT, sp_refresh_token TEXT,
sp_device TEXT, sp_device TEXT,
sp_playlists TEXT, sp_playlists TEXT,
price INTEGER price INTEGER,
profit INTEGER
); );
""" """
) )

View File

@@ -19,6 +19,7 @@ class Jukebox(NamedTuple):
sp_device: str sp_device: str
sp_playlists: str sp_playlists: str
price: int price: int
profit: int
@classmethod @classmethod
def from_row(cls, row: Row) -> "Jukebox": def from_row(cls, row: Row) -> "Jukebox":

View File

@@ -4,13 +4,13 @@ Vue.component(VueQrcode.name, VueQrcode)
var mapJukebox = obj => { var mapJukebox = obj => {
obj._data = _.clone(obj) obj._data = _.clone(obj)
obj.sp_id = obj.id
obj.device = obj.sp_device.split("-")[0] obj.device = obj.sp_device.split('-')[0]
playlists = obj.sp_playlists.split(",") playlists = obj.sp_playlists.split(',')
var i; var i
playlistsar = [] playlistsar = []
for (i = 0; i < playlists.length; i++) { for (i = 0; i < playlists.length; i++) {
playlistsar.push(playlists[i].split("-")[0]) playlistsar.push(playlists[i].split('-')[0])
} }
obj.playlist = playlistsar.join() obj.playlist = playlistsar.join()
return obj return obj
@@ -47,6 +47,12 @@ new Vue({
label: 'Price', label: 'Price',
field: 'price' field: 'price'
}, },
{
name: 'profit',
align: 'left',
label: 'Profit',
field: 'profit'
}
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10
@@ -60,8 +66,8 @@ new Vue({
playlists: [], playlists: [],
JukeboxLinks: [], JukeboxLinks: [],
step: 1, step: 1,
locationcbPath: "", locationcbPath: '',
locationcb: "", locationcb: '',
jukeboxDialog: { jukeboxDialog: {
show: false, show: false,
data: {} data: {}
@@ -69,9 +75,7 @@ new Vue({
spotifyDialog: false spotifyDialog: false
} }
}, },
computed: { computed: {},
},
methods: { methods: {
getJukeboxes() { getJukeboxes() {
self = this self = this
@@ -86,8 +90,15 @@ new Vue({
}, },
deleteJukebox(juke_id) { deleteJukebox(juke_id) {
self = this self = this
LNbits.utils
.confirmDialog('Are you sure you want to delete this Jukebox?')
.onOk(function () {
LNbits.api LNbits.api
.request('DELETE', '/jukebox/api/v1/jukebox/' + juke_id, self.g.user.wallets[0].adminkey) .request(
'DELETE',
'/jukebox/api/v1/jukebox/' + juke_id,
self.g.user.wallets[0].adminkey
)
.then(function (response) { .then(function (response) {
self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) { self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) {
return obj.id === juke_id return obj.id === juke_id
@@ -97,22 +108,34 @@ new Vue({
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(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.show = true
}, },
closeFormDialog() { closeFormDialog() {
this.jukeboxDialog.data = {} this.jukeboxDialog.data = {}
this.jukeboxDialog.show = false this.jukeboxDialog.show = false
this.step = 1 this.step = 1
}, },
submitSpotify() { submitSpotifyKeys() {
self = this self = this
self.jukeboxDialog.data.user = self.g.user.id self.jukeboxDialog.data.user = self.g.user.id
self.requestAuthorization()
this.$q.notify({ LNbits.api
spinner: true, .request(
message: 'Fetching token',
timeout: 4000
})
LNbits.api.request(
'POST', 'POST',
'/jukebox/api/v1/jukebox/', '/jukebox/api/v1/jukebox/',
self.g.user.wallets[0].adminkey, self.g.user.wallets[0].adminkey,
@@ -120,39 +143,64 @@ new Vue({
) )
.then(response => { .then(response => {
if (response.data) { if (response.data) {
var timerId = setInterval(function(){
if(!self.jukeboxDialog.data.sp_user){
clearInterval(timerId);
}
self.jukeboxDialog.data.sp_id = response.data.id self.jukeboxDialog.data.sp_id = response.data.id
LNbits.api
.request('GET', '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_id, self.g.user.wallets[0].inkey)
.then(response => {
if(response.data.sp_access_token){
self.jukeboxDialog.data.sp_access_token = response.data.sp_access_token
self.step = 3 self.step = 3
self.fetchAccessToken()
clearInterval(timerId)
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}, 3000)
} }
}) })
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
}, },
authAccess() {
self = this
self.requestAuthorization()
self.$q.notify({
spinner: true,
message: 'Fetching token',
timeout: 4000
})
self.getSpotifyTokens()
},
getSpotifyTokens() {
self = this
var timerId = setInterval(function () {
if (!self.jukeboxDialog.data.sp_user) {
clearInterval(timerId)
}
LNbits.api
.request(
'GET',
'/jukebox/api/v1/jukebox/spotify/' + 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()
self.step = 4
clearInterval(timerId)
}
}
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}, 5000)
},
requestAuthorization() { requestAuthorization() {
self = this self = this
var url = 'https://accounts.spotify.com/authorize' var url = 'https://accounts.spotify.com/authorize'
url += '?client_id=' + self.jukeboxDialog.data.sp_user url += '?client_id=' + self.jukeboxDialog.data.sp_user
url += '&response_type=code' url += '&response_type=code'
url += '&redirect_uri=' + encodeURI(self.locationcbPath) + self.jukeboxDialog.data.sp_user url +=
url += "&show_dialog=true" '&redirect_uri=' +
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' 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) window.open(url)
}, },
@@ -160,15 +208,12 @@ new Vue({
this.jukeboxDialog.show = true this.jukeboxDialog.show = true
this.jukeboxDialog.data = {} this.jukeboxDialog.data = {}
}, },
openUpdateDialog(itemId) {
this.jukeboxDialog.show = true
let item = this.jukebox.items.find(item => item.id === itemId)
this.jukeboxDialog.data = item
},
createJukebox() { createJukebox() {
self = this self = this
console.log(this.jukeboxDialog.data)
this.jukeboxDialog.data.sp_playlists = this.jukeboxDialog.data.sp_playlists.join() this.jukeboxDialog.data.sp_playlists = this.jukeboxDialog.data.sp_playlists.join()
LNbits.api.request( LNbits.api
.request(
'PUT', 'PUT',
'/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id,
self.g.user.wallets[0].adminkey, self.g.user.wallets[0].adminkey,
@@ -179,90 +224,118 @@ new Vue({
self.jukeboxDialog.show = false self.jukeboxDialog.show = false
}) })
}, },
playlistApi(method, url, body) { playlistApi(method, url, body) {
self = this self = this
let xhr = new XMLHttpRequest() let xhr = new XMLHttpRequest()
xhr.open(method, url, true) xhr.open(method, url, true)
xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Authorization', 'Bearer ' + this.jukeboxDialog.data.sp_access_token) xhr.setRequestHeader(
'Authorization',
'Bearer ' + this.jukeboxDialog.data.sp_access_token
)
xhr.send(body) xhr.send(body)
xhr.onload = function () { xhr.onload = function () {
let responseObj = JSON.parse(xhr.response) let responseObj = JSON.parse(xhr.response)
self.jukeboxDialog.data.playlists = null
self.playlists = [] self.playlists = []
var i; self.jukeboxDialog.data.playlists = []
var i
for (i = 0; i < responseObj.items.length; i++) { for (i = 0; i < responseObj.items.length; i++) {
self.playlists.push(responseObj.items[i].name + "-" + responseObj.items[i].id) self.playlists.push(
responseObj.items[i].name + '-' + responseObj.items[i].id
)
} }
} }
}, },
refreshPlaylists() { refreshPlaylists() {
self = this self = this
self.playlistApi( "GET", "https://api.spotify.com/v1/me/playlists", null) self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null)
}, },
deviceApi(method, url, body) { deviceApi(method, url, body) {
self = this self = this
let xhr = new XMLHttpRequest() let xhr = new XMLHttpRequest()
xhr.open(method, url, true) xhr.open(method, url, true)
xhr.setRequestHeader('Content-Type', 'application/json') xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('Authorization', 'Bearer ' + this.jukeboxDialog.data.sp_access_token) xhr.setRequestHeader(
'Authorization',
'Bearer ' + this.jukeboxDialog.data.sp_access_token
)
xhr.send(body) xhr.send(body)
xhr.onload = function () { xhr.onload = function () {
let responseObj = JSON.parse(xhr.response) let responseObj = JSON.parse(xhr.response)
self.jukeboxDialog.data.devices = []
self.devices = [] self.devices = []
var i; var i
for (i = 0; i < responseObj.devices.length; i++) { for (i = 0; i < responseObj.devices.length; i++) {
self.devices.push(responseObj.devices[i].name + "-" + responseObj.devices[i].id) self.devices.push(
responseObj.devices[i].name + '-' + responseObj.devices[i].id
)
} }
} }
}, },
refreshDevices() { refreshDevices() {
self = this self = this
self.deviceApi( "GET", "https://api.spotify.com/v1/me/player/devices", null) self.deviceApi(
'GET',
'https://api.spotify.com/v1/me/player/devices',
null
)
}, },
fetchAccessToken( ){ fetchAccessToken(code) {
self = this self = this
let body = "grant_type=authorization_code" let body = 'grant_type=authorization_code'
body += "&code=" + self.jukeboxDialog.data.sp_access_token body += '&code=' + code
body += '&redirect_uri=' + encodeURI(self.locationcbPath) + self.jukeboxDialog.data.sp_user body +=
'&redirect_uri=' +
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
this.callAuthorizationApi(body) this.callAuthorizationApi(body)
}, },
refreshAccessToken() { refreshAccessToken() {
self = this self = this
let body = "grant_type=refresh_token" let body = 'grant_type=refresh_token'
body += "&refresh_token=" + self.jukeboxDialog.data.sp_refresh_token body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token
body += "&client_id=" + self.jukeboxDialog.data.sp_user body += '&client_id=' + self.jukeboxDialog.data.sp_user
this.callAuthorizationApi(body) this.callAuthorizationApi(body)
}, },
callAuthorizationApi(body) { callAuthorizationApi(body) {
self = this self = this
let xhr = new XMLHttpRequest() let xhr = new XMLHttpRequest()
xhr.open("POST", "https://accounts.spotify.com/api/token", true) xhr.open('POST', 'https://accounts.spotify.com/api/token', true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.setRequestHeader('Authorization', 'Basic ' + btoa(this.jukeboxDialog.data.sp_user + ":" + this.jukeboxDialog.data.sp_secret)) xhr.setRequestHeader(
'Authorization',
'Basic ' +
btoa(
this.jukeboxDialog.data.sp_user +
':' +
this.jukeboxDialog.data.sp_secret
)
)
xhr.send(body) xhr.send(body)
xhr.onload = function () { xhr.onload = function () {
let responseObj = JSON.parse(xhr.response) let responseObj = JSON.parse(xhr.response)
if (responseObj.access_token) {
self.jukeboxDialog.data.sp_access_token = responseObj.access_token self.jukeboxDialog.data.sp_access_token = responseObj.access_token
self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token
self.refreshPlaylists() console.log(responseObj)
self.refreshDevices() }
}
} }
}, },
},
created() { created() {
var getJukeboxes = this.getJukeboxes var getJukeboxes = this.getJukeboxes
getJukeboxes() getJukeboxes()
this.selectedWallet = this.g.user.wallets[0] this.selectedWallet = this.g.user.wallets[0]
this.locationcbPath = String([ this.locationcbPath = String(
[
window.location.protocol, window.location.protocol,
'//', '//',
window.location.host, window.location.host,
'/jukebox/api/v1/jukebox/spotify/cb/' '/jukebox/api/v1/jukebox/spotify/cb/'
].join('')) ].join('')
)
this.locationcb = this.locationcbPath this.locationcb = this.locationcbPath
} }
}) })

View File

@@ -58,6 +58,14 @@
</q-btn> </q-btn>
</q-td> </q-td>
<q-td auto-width> <q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="updateJukebox(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn <q-btn
flat flat
dense dense
@@ -169,10 +177,7 @@
target="_blank" target="_blank"
href="https://developer.spotify.com/dashboard/applications" href="https://developer.spotify.com/dashboard/applications"
>here</a >here</a
>. <br /> >.
In the app go to edit-settings, set the redirect URI to this link
(replacing the CLIENT-ID with your own) {% raw %}{{ locationcb
}}CLIENT-ID{% endraw %}
<q-input <q-input
filled filled
class="q-pb-md q-pt-md" class="q-pb-md q-pt-md"
@@ -202,11 +207,43 @@
<q-btn <q-btn
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched" v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
color="green-7" color="green-7"
@click="submitSpotify" @click="submitSpotifyKeys"
>Get token</q-btn >Submit keys</q-btn
> >
<q-btn v-else color="green-7" disable color="green-7" <q-btn v-else color="green-7" disable color="green-7"
>Get token</q-btn >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">
In the app go to edit-settings, set the redirect URI to this link
<br /><small
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
%}</small
>
<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>
<div class="col-8"> <div class="col-8">
@@ -223,11 +260,11 @@
</q-step> </q-step>
<q-step <q-step
:name="3" :name="4"
title="Select playlists" title="Select playlists"
icon="queue_music" icon="queue_music"
active-color="green-8" active-color="green-8"
:done="step > 3" :done="step > 4"
> >
<q-select <q-select
class="q-pb-md q-pt-md" class="q-pb-md q-pt-md"

View File

@@ -22,12 +22,7 @@ async def api_get_jukeboxs():
try: try:
return ( return (
jsonify( jsonify(
[ [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)]
{
**jukebox._asdict()
}
for jukebox in await get_jukeboxs(g.wallet.user)
]
), ),
HTTPStatus.OK, HTTPStatus.OK,
) )
@@ -38,23 +33,23 @@ async def api_get_jukeboxs():
##################SPOTIFY AUTH##################### ##################SPOTIFY AUTH#####################
@jukebox_ext.route("/api/v1/jukebox/spotify/cb/<sp_user>", methods=["GET"]) @jukebox_ext.route("/api/v1/jukebox/spotify/cb/<juke_id>", methods=["GET"])
async def api_check_credentials_callbac(sp_user): async def api_check_credentials_callbac(juke_id):
print(request.args)
sp_code = "" sp_code = ""
sp_access_token = "" sp_access_token = ""
sp_refresh_token = "" sp_refresh_token = ""
print(request.args) jukebox = await get_jukebox(juke_id)
jukebox = await get_jukebox_by_user(sp_user)
if request.args.get("code"): if request.args.get("code"):
sp_code = request.args.get("code") sp_code = request.args.get("code")
jukebox = await update_jukebox( jukebox = await update_jukebox(
sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_access_token=sp_code juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code
) )
if request.args.get("access_token"): if request.args.get("access_token"):
sp_access_token = request.args.get("access_token") sp_access_token = request.args.get("access_token")
sp_refresh_token = request.args.get("refresh_token") sp_refresh_token = request.args.get("refresh_token")
jukebox = await update_jukebox( jukebox = await update_jukebox(
sp_user=sp_user, juke_id=juke_id,
sp_secret=jukebox.sp_secret, sp_secret=jukebox.sp_secret,
sp_access_token=sp_access_token, sp_access_token=sp_access_token,
sp_refresh_token=sp_refresh_token, sp_refresh_token=sp_refresh_token,
@@ -70,7 +65,7 @@ async def api_check_credentials_check(sp_id):
@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"]) @jukebox_ext.route("/api/v1/jukebox/", methods=["POST"])
@jukebox_ext.route("/api/v1/jukebox/<item_id>", methods=["PUT"]) @jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["PUT"])
@api_check_wallet_key("admin") @api_check_wallet_key("admin")
@api_validate_post_request( @api_validate_post_request(
schema={ schema={
@@ -86,9 +81,9 @@ async def api_check_credentials_check(sp_id):
"price": {"type": "string", "required": True}, "price": {"type": "string", "required": True},
} }
) )
async def api_create_update_jukebox(item_id=None): async def api_create_update_jukebox(juke_id=None):
if item_id: if juke_id:
jukebox = await update_jukebox(item_id, **g.data) jukebox = await update_jukebox(juke_id=juke_id, **g.data)
else: else:
jukebox = await create_jukebox(**g.data) jukebox = await create_jukebox(**g.data)
return jsonify(jukebox._asdict()), HTTPStatus.CREATED return jsonify(jukebox._asdict()), HTTPStatus.CREATED
@@ -101,12 +96,7 @@ async def api_delete_item(juke_id):
try: try:
return ( return (
jsonify( jsonify(
[ [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)]
{
**jukebox._asdict()
}
for jukebox in await get_jukeboxs(g.wallet.user)
]
), ),
HTTPStatus.OK, HTTPStatus.OK,
) )