Connects to spotify
This commit is contained in:
parent
3f8890def7
commit
dc10a0f52b
9 changed files with 395 additions and 353 deletions
|
|
@ -8,6 +8,5 @@ jukebox_ext: Blueprint = Blueprint(
|
||||||
"jukebox", __name__, static_folder="static", template_folder="templates"
|
"jukebox", __name__, static_folder="static", template_folder="templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "Jukebox",
|
"name": "SpotifyJukebox",
|
||||||
"short_description": "Spotify jukebox middleware",
|
"short_description": "Spotify jukebox middleware",
|
||||||
"icon": "audiotrack",
|
"icon": "audiotrack",
|
||||||
"contributors": ["benarc"]
|
"contributors": ["benarc"]
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,69 @@ from typing import List, Optional
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import Jukebox
|
from .models import Jukebox
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
|
||||||
async def create_update_jukebox(wallet_id: str) -> int:
|
async def create_jukebox(
|
||||||
|
wallet: str,
|
||||||
|
title: str,
|
||||||
|
price: int,
|
||||||
|
sp_user: str,
|
||||||
|
sp_secret: str,
|
||||||
|
sp_token: Optional[str] = "",
|
||||||
|
sp_device: Optional[str] = "",
|
||||||
|
sp_playlists: Optional[str] = "",
|
||||||
|
) -> Jukebox:
|
||||||
juke_id = urlsafe_short_hash()
|
juke_id = urlsafe_short_hash()
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO jukebox (id, wallet, user, secret, token, playlists)
|
INSERT INTO jukebox (id, title, wallet, sp_user, sp_secret, sp_token, sp_device, sp_playlists, price)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(juke_id, wallet_id, "", "", "", ""),
|
(
|
||||||
|
juke_id,
|
||||||
|
title,
|
||||||
|
wallet,
|
||||||
|
sp_user,
|
||||||
|
sp_secret,
|
||||||
|
sp_token,
|
||||||
|
sp_device,
|
||||||
|
sp_playlists,
|
||||||
|
int(price),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return result._result_proxy.lastrowid
|
jukebox = await get_jukebox(juke_id)
|
||||||
|
assert jukebox, "Newly created Jukebox couldn't be retrieved"
|
||||||
|
return jukebox
|
||||||
|
|
||||||
|
async def update_jukebox(sp_user: str, **kwargs) -> Optional[Jukebox]:
|
||||||
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
|
await db.execute(
|
||||||
|
f"UPDATE jukebox SET {q} WHERE sp_user = ?", (*kwargs.values(), sp_user)
|
||||||
|
)
|
||||||
|
row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (sp_user,))
|
||||||
|
return Jukebox(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_jukebox(id: str) -> Optional[Jukebox]:
|
async def get_jukebox(id: str) -> Optional[Jukebox]:
|
||||||
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,))
|
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,))
|
||||||
return Shop(**dict(row)) if row else None
|
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(id: str) -> Optional[Jukebox]:
|
async def get_jukeboxs(id: str) -> Optional[Jukebox]:
|
||||||
row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,))
|
rows = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,))
|
||||||
return Shop(**dict(row)) if row else None
|
return [Jukebox(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def delete_jukebox(shop: int, item_id: int):
|
async def delete_jukebox(shop: int, item_id: int):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
DELETE FROM jukebox WHERE id = ?
|
DELETE FROM jukebox WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(shop, item_id),
|
(Jukebox, item_id),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ async def m001_initial(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE jukebox (
|
CREATE TABLE jukebox (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id TEXT PRIMARY KEY,
|
||||||
wallet TEXT NOT NULL,
|
title TEXT,
|
||||||
user TEXT NOT NULL,
|
wallet TEXT,
|
||||||
secret TEXT NOT NULL,
|
sp_user TEXT NOT NULL,
|
||||||
token TEXT NOT NULL,
|
sp_secret TEXT NOT NULL,
|
||||||
playlists TEXT NOT NULL
|
sp_token TEXT,
|
||||||
|
sp_device TEXT,
|
||||||
|
sp_playlists TEXT,
|
||||||
|
price INTEGER
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
@ -6,14 +6,18 @@ from quart import url_for
|
||||||
from typing import NamedTuple, Optional, List, Dict
|
from typing import NamedTuple, Optional, List, Dict
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
|
|
||||||
|
|
||||||
class Jukebox(NamedTuple):
|
class Jukebox(NamedTuple):
|
||||||
id: int
|
id: str
|
||||||
|
title: str
|
||||||
wallet: str
|
wallet: str
|
||||||
user: str
|
sp_user: str
|
||||||
secret: str
|
sp_secret: str
|
||||||
token: str
|
sp_token: str
|
||||||
playlists: str
|
sp_device: str
|
||||||
|
sp_playlists: str
|
||||||
|
price: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Charges":
|
def from_row(cls, row: Row) -> "Jukebox":
|
||||||
return cls(**dict(row))
|
return cls(**dict(row))
|
||||||
|
|
@ -4,28 +4,25 @@ Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
const pica = window.pica()
|
const pica = window.pica()
|
||||||
|
|
||||||
const defaultItemData = {
|
|
||||||
unit: 'sat'
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedWallet: null,
|
isPwd: true,
|
||||||
confirmationMethod: 'wordlist',
|
tokenFetched: true,
|
||||||
wordlistTainted: false,
|
device: [],
|
||||||
jukebox: {
|
jukebox: {},
|
||||||
method: null,
|
playlists: [],
|
||||||
wordlist: [],
|
step: 1,
|
||||||
items: []
|
locationcbPath: "",
|
||||||
},
|
jukeboxDialog: {
|
||||||
itemDialog: {
|
|
||||||
show: false,
|
show: false,
|
||||||
data: {...defaultItemData},
|
data: {}
|
||||||
units: ['sat']
|
},
|
||||||
}
|
spotifyDialog: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -34,183 +31,130 @@ new Vue({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openNewDialog() {
|
closeFormDialog() {
|
||||||
this.itemDialog.show = true
|
this.jukeboxDialog.data = {}
|
||||||
this.itemDialog.data = {...defaultItemData}
|
this.jukeboxDialog.show = false
|
||||||
|
this.step = 1
|
||||||
},
|
},
|
||||||
openUpdateDialog(itemId) {
|
submitSpotify() {
|
||||||
this.itemDialog.show = true
|
|
||||||
let item = this.jukebox.items.find(item => item.id === itemId)
|
self = this
|
||||||
this.itemDialog.data = item
|
console.log(self.jukeboxDialog.data)
|
||||||
},
|
self.requestAuthorization()
|
||||||
imageAdded(file) {
|
this.$q.notify({
|
||||||
let blobURL = URL.createObjectURL(file)
|
spinner: true,
|
||||||
let image = new Image()
|
message: 'Fetching token',
|
||||||
image.src = blobURL
|
timeout: 4000
|
||||||
image.onload = async () => {
|
|
||||||
let canvas = document.createElement('canvas')
|
|
||||||
canvas.setAttribute('width', 100)
|
|
||||||
canvas.setAttribute('height', 100)
|
|
||||||
await pica.resize(image, canvas, {
|
|
||||||
quality: 0,
|
|
||||||
alpha: true,
|
|
||||||
unsharpAmount: 95,
|
|
||||||
unsharpRadius: 0.9,
|
|
||||||
unsharpThreshold: 70
|
|
||||||
})
|
})
|
||||||
this.itemDialog.data.image = canvas.toDataURL()
|
LNbits.api.request(
|
||||||
this.itemDialog = {...this.itemDialog}
|
'POST',
|
||||||
}
|
'/jukebox/api/v1/jukebox/',
|
||||||
},
|
self.g.user.wallets[0].adminkey,
|
||||||
imageCleared() {
|
self.jukeboxDialog.data
|
||||||
this.itemDialog.data.image = null
|
|
||||||
this.itemDialog = {...this.itemDialog}
|
|
||||||
},
|
|
||||||
disabledAddItemButton() {
|
|
||||||
return (
|
|
||||||
!this.itemDialog.data.name ||
|
|
||||||
this.itemDialog.data.name.length === 0 ||
|
|
||||||
!this.itemDialog.data.price ||
|
|
||||||
!this.itemDialog.data.description ||
|
|
||||||
!this.itemDialog.data.unit ||
|
|
||||||
this.itemDialog.data.unit.length === 0
|
|
||||||
)
|
|
||||||
},
|
|
||||||
changedWallet(wallet) {
|
|
||||||
this.selectedWallet = wallet
|
|
||||||
this.loadShop()
|
|
||||||
},
|
|
||||||
loadShop() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/jukebox/api/v1/jukebox', this.selectedWallet.inkey)
|
|
||||||
.then(response => {
|
|
||||||
this.jukebox = response.data
|
|
||||||
this.confirmationMethod = response.data.method
|
|
||||||
this.wordlistTainted = false
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async setMethod() {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
'/jukebox/api/v1/jukebox/method',
|
|
||||||
this.selectedWallet.inkey,
|
|
||||||
{method: this.confirmationMethod, wordlist: this.jukebox.wordlist}
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$q.notify({
|
|
||||||
message:
|
|
||||||
`Method set to ${this.confirmationMethod}.` +
|
|
||||||
(this.confirmationMethod === 'wordlist' ? ' Counter reset.' : ''),
|
|
||||||
timeout: 700
|
|
||||||
})
|
|
||||||
this.loadShop()
|
|
||||||
},
|
|
||||||
async sendItem() {
|
|
||||||
let {id, name, image, description, price, unit} = this.itemDialog.data
|
|
||||||
const data = {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
image,
|
|
||||||
price,
|
|
||||||
unit
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (id) {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'PUT',
|
|
||||||
'/jukebox/api/v1/jukebox/items/' + id,
|
|
||||||
this.selectedWallet.inkey,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
'/jukebox/api/v1/jukebox/items',
|
|
||||||
this.selectedWallet.inkey,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
message: `Item '${this.itemDialog.data.name}' added.`,
|
|
||||||
timeout: 700
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadShop()
|
|
||||||
this.itemDialog.show = false
|
|
||||||
this.itemDialog.data = {...defaultItemData}
|
|
||||||
},
|
|
||||||
toggleItem(itemId) {
|
|
||||||
let item = this.jukebox.items.find(item => item.id === itemId)
|
|
||||||
item.enabled = !item.enabled
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'PUT',
|
|
||||||
'/jukebox/api/v1/jukebox/items/' + itemId,
|
|
||||||
this.selectedWallet.inkey,
|
|
||||||
item
|
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$q.notify({
|
if(response.data){
|
||||||
message: `Item ${item.enabled ? 'enabled' : 'disabled'}.`,
|
var timerId = setInterval(function(){
|
||||||
timeout: 700
|
if(!self.jukeboxDialog.data.sp_user){
|
||||||
})
|
clearInterval(timerId);
|
||||||
this.jukebox.items = this.jukebox.items
|
}
|
||||||
})
|
LNbits.api
|
||||||
.catch(err => {
|
.request('GET', '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_user, self.g.user.wallets[0].inkey)
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteItem(itemId) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this item?')
|
|
||||||
.onOk(() => {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
'/jukebox/api/v1/jukebox/items/' + itemId,
|
|
||||||
this.selectedWallet.inkey
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$q.notify({
|
if(response.data.sp_token){
|
||||||
message: `Item deleted.`,
|
console.log(response.data.sp_token)
|
||||||
timeout: 700
|
|
||||||
|
self.step = 3
|
||||||
|
clearInterval(timerId);
|
||||||
|
self.refreshPlaylists()
|
||||||
|
self.$q.notify({
|
||||||
|
message: 'Success! App is now linked!',
|
||||||
|
timeout: 3000
|
||||||
})
|
})
|
||||||
this.jukebox.items.splice(
|
//set devices, playlists
|
||||||
this.jukebox.items.findIndex(item => item.id === itemId),
|
}
|
||||||
1
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
requestAuthorization(){
|
||||||
|
self = this
|
||||||
|
let url = 'https://accounts.spotify.com/authorize'
|
||||||
|
url += '?scope=user-modify-playback-state%20user-read-playback-position'
|
||||||
|
url += '%20user-library-read%20streaming%20user-read-playback-state'
|
||||||
|
url += '%20user-read-recently-played%20playlist-read-private&response_type=code'
|
||||||
|
url += '&redirect_uri=' + encodeURIComponent(self.locationcbPath) + self.jukeboxDialog.data.sp_user
|
||||||
|
url += '&client_id=' + self.jukeboxDialog.data.sp_user
|
||||||
|
url += '&show_dialog=true'
|
||||||
|
console.log(url)
|
||||||
|
window.open(url)
|
||||||
|
},
|
||||||
|
openNewDialog() {
|
||||||
|
this.jukeboxDialog.show = true
|
||||||
|
this.jukeboxDialog.data = {}
|
||||||
|
},
|
||||||
|
openUpdateDialog(itemId) {
|
||||||
|
this.jukeboxDialog.show = true
|
||||||
|
let item = this.jukebox.items.find(item => item.id === itemId)
|
||||||
|
this.jukeboxDialog.data = item
|
||||||
|
},
|
||||||
|
|
||||||
|
callApi(method, url, body, callback){
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open(method, url, true)
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||||
|
xhr.setRequestHeader('Authorization', 'Bearer ' + self.jukeboxDialog.data.sp_token)
|
||||||
|
xhr.send(body)
|
||||||
|
xhr.onload = callback
|
||||||
|
},
|
||||||
|
refreshPlaylists(){
|
||||||
|
console.log("sdfvasdv")
|
||||||
|
callApi( "GET", "https://api.spotify.com/v1/me/playlists", null, handlePlaylistsResponse )
|
||||||
|
},
|
||||||
|
handlePlaylistsResponse(){
|
||||||
|
console.log("data")
|
||||||
|
if ( this.status == 200 ){
|
||||||
|
var data = JSON.parse(this.responseText)
|
||||||
|
console.log(data)
|
||||||
|
}
|
||||||
|
else if ( this.status == 401 ){
|
||||||
|
refreshAccessToken()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(this.responseText)
|
||||||
|
alert(this.responseText)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshAccessToken(){
|
||||||
|
refresh_token = localStorage.getItem("refresh_token")
|
||||||
|
let body = "grant_type=refresh_token"
|
||||||
|
body += "&refresh_token=" + self.jukeboxDialog.data.sp_token
|
||||||
|
body += "&client_id=" + self.jukeboxDialog.data.sp_user
|
||||||
|
callAuthorizationApi(body)
|
||||||
|
},
|
||||||
|
callAuthorizationApi(body){
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("POST", 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 = handleAuthorizationResponse
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.selectedWallet = this.g.user.wallets[0]
|
this.selectedWallet = this.g.user.wallets[0]
|
||||||
this.loadShop()
|
this.locationcbPath = String([
|
||||||
|
window.location.protocol,
|
||||||
LNbits.api
|
'//',
|
||||||
.request('GET', '/jukebox/api/v1/currencies')
|
window.location.host,
|
||||||
.then(response => {
|
'/jukebox/api/v1/jukebox/spotify/cb/'
|
||||||
this.itemDialog = {...this.itemDialog, units: ['sat', ...response.data]}
|
].join(''))
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,20 @@
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
group="extras"
|
group="extras"
|
||||||
icon="swap_vertical_circle"
|
icon="swap_vertical_circle"
|
||||||
label="How to use"
|
label="About"
|
||||||
:content-inset-level="0.5"
|
:content-inset-level="0.5"
|
||||||
>
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<ol>
|
To use this extension you need a Spotify client ID and client secret. You
|
||||||
<li>Register items.</li>
|
get these by creating an app in the Spotify developers dashboard
|
||||||
<li>
|
<a href="https://developer.spotify.com/dashboard/applications">here </a>
|
||||||
Print QR codes and paste them on your store, your menu, somewhere,
|
<br /><br />Select the playlists you want people to be able to pay for,
|
||||||
somehow.
|
share the frontend page, profit :) <br /><br />
|
||||||
</li>
|
Made by, <a href="https://twitter.com/arcbtc">benarc</a>. Inspired by,
|
||||||
<li>
|
<a href="https://twitter.com/pirosb3/status/1056263089128161280"
|
||||||
Clients scan the QR codes and get information about the items plus the
|
>pirosb3</a
|
||||||
price on their phones directly (they must have internet)
|
>.
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Once they decide to pay, they'll get an invoice on their phones
|
|
||||||
automatically
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
When the payment is confirmed, a confirmation code will be issued for
|
|
||||||
them.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<p>
|
|
||||||
The confirmation codes are words from a predefined sequential word list.
|
|
||||||
Each new payment bumps the words sequence by 1. So you can check the
|
|
||||||
confirmation codes manually by just looking at them.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
For example, if your wordlist is
|
|
||||||
<code>[apple, banana, coconut]</code> the first purchase will be
|
|
||||||
<code>apple</code>, the second <code>banana</code> and so on. When it
|
|
||||||
gets to the end it starts from the beginning again.
|
|
||||||
</p>
|
|
||||||
<p>Powered by LNURL-pay.</p>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@
|
||||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="green-7"
|
||||||
|
class="q-ma-lg"
|
||||||
|
@click="openNewDialog()"
|
||||||
|
>Add Spotify Jukebox</q-btn
|
||||||
|
>
|
||||||
<div class="row items-center no-wrap q-mb-md">
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
<div class="col">
|
<h5 class="text-subtitle1 q-my-none">Items</h5>
|
||||||
<h5 class="text-subtitle1 q-my-none">Items</h5>
|
|
||||||
</div>
|
|
||||||
<div class="col q-ml-lg">
|
|
||||||
<q-btn unelevated color="deep-purple" @click="openNewDialog()"
|
|
||||||
>Add jukebox</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<q-table
|
<q-table
|
||||||
|
|
@ -101,107 +101,170 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-dialog v-model="itemDialog.show">
|
<q-dialog v-model="jukeboxDialog.show" position="top" @hide="closeFormDialog">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-md q-pt-lg q-mt-md" style="width: 100%">
|
||||||
<q-card-section>
|
<q-stepper
|
||||||
<h5
|
v-model="step"
|
||||||
class="q-ma-none"
|
active-color="green-7"
|
||||||
v-if="itemDialog.data.id"
|
inactive-color="green-10"
|
||||||
v-text="itemDialog.data.name"
|
vertical
|
||||||
></h5>
|
animated
|
||||||
<h5 class="q-ma-none q-mb-xl" v-else>Adding a new item</h5>
|
>
|
||||||
|
<q-step
|
||||||
<q-responsive v-if="itemDialog.data.id" :ratio="1">
|
:name="1"
|
||||||
<qrcode
|
title="Pick wallet, price"
|
||||||
:value="itemDialog.data.lnurl"
|
icon="account_balance_wallet"
|
||||||
:options="{width: 800}"
|
:done="step > 1"
|
||||||
class="rounded-borders"
|
>
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
|
|
||||||
<div v-if="itemDialog.data.id" class="row q-gutter-sm justify-center">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(itemDialog.data.lnurl, 'LNURL copied to clipboard!')"
|
|
||||||
class="q-mb-lg"
|
|
||||||
>Copy LNURL</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<q-form @submit="sendItem" class="q-gutter-md">
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
|
class="q-pt-md"
|
||||||
dense
|
dense
|
||||||
v-model.trim="itemDialog.data.name"
|
v-model.trim="jukeboxDialog.data.title"
|
||||||
type="text"
|
label="Jukebox name"
|
||||||
label="Item name"
|
|
||||||
></q-input>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="itemDialog.data.description"
|
|
||||||
type="text"
|
|
||||||
label="Brief description"
|
|
||||||
></q-input>
|
|
||||||
<q-file
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
capture="environment"
|
|
||||||
accept="image/jpeg, image/png"
|
|
||||||
:max-file-size="3*1024**2"
|
|
||||||
label="Small image (optional)"
|
|
||||||
clearable
|
|
||||||
@input="imageAdded"
|
|
||||||
@clear="imageCleared"
|
|
||||||
>
|
|
||||||
<template v-if="itemDialog.data.image" v-slot:before>
|
|
||||||
<img style="height: 1em" :src="itemDialog.data.image" />
|
|
||||||
</template>
|
|
||||||
<template v-if="itemDialog.data.image" v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="cancel"
|
|
||||||
@click.stop.prevent="imageCleared"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.number="itemDialog.data.price"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
:label="`Item price (${itemDialog.data.unit})`"
|
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-select
|
<q-select
|
||||||
|
class="q-pb-md q-pt-md"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="itemDialog.data.unit"
|
emit-value
|
||||||
type="text"
|
v-model="jukeboxDialog.data.wallet"
|
||||||
label="Unit"
|
:options="g.user.walletOptions"
|
||||||
:options="itemDialog.units"
|
label="Wallet to use"
|
||||||
></q-select>
|
></q-select>
|
||||||
|
<q-input
|
||||||
<div class="row q-mt-lg">
|
filled
|
||||||
<div class="col q-ml-lg">
|
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
|
<q-btn
|
||||||
unelevated
|
v-if="jukeboxDialog.data.title != null && jukeboxDialog.data.price != null && jukeboxDialog.data.wallet != null"
|
||||||
color="deep-purple"
|
color="green-7"
|
||||||
:disable="disabledAddItemButton()"
|
@click="step = 2"
|
||||||
type="submit"
|
>Continue</q-btn
|
||||||
>
|
>
|
||||||
{% raw %}{{ itemDialog.data.id ? 'Update' : 'Add' }}{% endraw %}
|
<q-btn v-else color="green-7" disable>Continue</q-btn>
|
||||||
Item
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col q-ml-lg">
|
<div class="col-8">
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
<q-btn
|
||||||
|
color="green-7"
|
||||||
|
class="float-right"
|
||||||
|
@click="closeFormDialog"
|
||||||
>Cancel</q-btn
|
>Cancel</q-btn
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
|
||||||
</q-card-section>
|
<br />
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step :name="2" title="Add api keys" icon="vpn_key" :done="step > 2">
|
||||||
|
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"
|
||||||
|
href="https://developer.spotify.com/dashboard/applications"
|
||||||
|
>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 %}{{ locationcbPath
|
||||||
|
}}CLIENT-ID{% endraw %}
|
||||||
|
<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="submitSpotify"
|
||||||
|
>Get token</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-else color="green-7" disable color="green-7"
|
||||||
|
>Get token</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="Select playlists"
|
||||||
|
icon="queue_music"
|
||||||
|
active-color="green-8"
|
||||||
|
:done="step > 3"
|
||||||
|
>
|
||||||
|
<q-select
|
||||||
|
class="q-pb-md q-pt-md"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="jukeboxDialog.data.sp_device"
|
||||||
|
:options="device"
|
||||||
|
label="Device jukebox will play to"
|
||||||
|
></q-select>
|
||||||
|
<q-select
|
||||||
|
class="q-pb-md"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
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 color="green-7" @click="step = 2">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-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
from quart import g, jsonify
|
from quart import g, jsonify, request
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
|
||||||
|
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
|
import httpx
|
||||||
from . import jukebox_ext
|
from . import jukebox_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_update_jukebox,
|
create_jukebox,
|
||||||
|
update_jukebox,
|
||||||
get_jukebox,
|
get_jukebox,
|
||||||
|
get_jukebox_by_user,
|
||||||
get_jukeboxs,
|
get_jukeboxs,
|
||||||
delete_jukebox,
|
delete_jukebox,
|
||||||
)
|
)
|
||||||
|
|
@ -17,33 +19,45 @@ from .models import Jukebox
|
||||||
@jukebox_ext.route("/api/v1/jukebox", methods=["GET"])
|
@jukebox_ext.route("/api/v1/jukebox", methods=["GET"])
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_get_jukeboxs():
|
async def api_get_jukeboxs():
|
||||||
jukebox = await get_jukeboxs(g.wallet.id)
|
jsonify([jukebox._asdict() for jukebox in await get_jukeboxs(g.wallet.id)]),
|
||||||
return (
|
|
||||||
jsonify(
|
|
||||||
{
|
##################SPOTIFY AUTH#####################
|
||||||
jukebox._asdict()
|
|
||||||
}
|
|
||||||
),
|
@jukebox_ext.route("/api/v1/jukebox/spotify/cb/<sp_user>/", methods=["GET"])
|
||||||
HTTPStatus.OK,
|
async def api_check_credentials_callbac(sp_user):
|
||||||
|
jukebox = await get_jukebox_by_user(sp_user)
|
||||||
|
jukebox = await update_jukebox(
|
||||||
|
sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_token=request.args.get('code')
|
||||||
)
|
)
|
||||||
|
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
||||||
|
|
||||||
#websocket get spotify crap
|
@jukebox_ext.route("/api/v1/jukebox/spotify/<sp_user>", methods=["GET"])
|
||||||
|
@api_check_wallet_key("invoice")
|
||||||
|
async def api_check_credentials_check(sp_user):
|
||||||
|
jukebox = await get_jukebox_by_user(sp_user)
|
||||||
|
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
||||||
|
|
||||||
@jukebox_ext.route("/api/v1/jukebox/items", methods=["POST"])
|
|
||||||
@jukebox_ext.route("/api/v1/jukebox/items/<item_id>", methods=["PUT"])
|
@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"])
|
||||||
|
@jukebox_ext.route("/api/v1/jukebox/<item_id>", methods=["PUT"])
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
@api_validate_post_request(
|
@api_validate_post_request(
|
||||||
|
|
||||||
schema={
|
schema={
|
||||||
"wallet": {"type": "string", "empty": False},
|
"title": {"type": "string", "empty": False, "required": True},
|
||||||
"user": {"type": "string", "empty": False},
|
"wallet": {"type": "string", "empty": False, "required": True},
|
||||||
"secret": {"type": "string", "required": False},
|
"sp_user": {"type": "string", "empty": False, "required": True},
|
||||||
"token": {"type": "string", "required": True},
|
"sp_secret": {"type": "string", "required": True},
|
||||||
"playlists": {"type": "string", "required": True},
|
"sp_token": {"type": "string", "required": False},
|
||||||
|
"sp_device": {"type": "string", "required": False},
|
||||||
|
"sp_playlists": {"type": "string", "required": False},
|
||||||
|
"price": {"type": "string", "required": True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
async def api_create_update_jukebox(item_id=None):
|
async def api_create_update_jukebox(item_id=None):
|
||||||
jukebox = await create_update_jukebox(g.wallet.id, **g.data)
|
print(g.data)
|
||||||
|
jukebox = await create_jukebox(**g.data)
|
||||||
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue