Removed jukebox
This commit is contained in:
parent
7a57690bd7
commit
e1d4582345
17 changed files with 0 additions and 2057 deletions
|
|
@ -1,36 +0,0 @@
|
||||||
# Jukebox
|
|
||||||
|
|
||||||
## An actual Jukebox where users pay sats to play their favourite music from your playlists
|
|
||||||
|
|
||||||
**Note:** To use this extension you need a Premium Spotify subscription.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Click on "ADD SPOTIFY JUKEBOX"\
|
|
||||||

|
|
||||||
2. Follow the steps required on the form\
|
|
||||||
|
|
||||||
- give your jukebox a name
|
|
||||||
- select a wallet to receive payment
|
|
||||||
- define the price a user must pay to select a song\
|
|
||||||

|
|
||||||
- follow the steps to get your Spotify App and get the client ID and secret key\
|
|
||||||

|
|
||||||
- paste the codes in the form\
|
|
||||||

|
|
||||||
- copy the _Redirect URL_ presented on the form\
|
|
||||||

|
|
||||||
- on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt
|
|
||||||

|
|
||||||
- back on LNbits, click "AUTORIZE ACCESS" and "Agree" on the page that will open
|
|
||||||
- choose on which device the LNbits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...)
|
|
||||||
- and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\
|
|
||||||

|
|
||||||
|
|
||||||
3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\
|
|
||||||

|
|
||||||
4. The users will see the Jukebox page and choose a song from the selected playlist\
|
|
||||||

|
|
||||||
5. After selecting a song they'd like to hear next a dialog will show presenting the music\
|
|
||||||

|
|
||||||
6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
|
||||||
|
|
||||||
db = Database("ext_jukebox")
|
|
||||||
|
|
||||||
jukebox_static_files = [
|
|
||||||
{
|
|
||||||
"path": "/jukebox/static",
|
|
||||||
"app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]),
|
|
||||||
"name": "jukebox_static",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
jukebox_ext: APIRouter = APIRouter(prefix="/jukebox", tags=["jukebox"])
|
|
||||||
|
|
||||||
|
|
||||||
def jukebox_renderer():
|
|
||||||
return template_renderer(["lnbits/extensions/jukebox/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
|
||||||
from .views import * # noqa: F401,F403
|
|
||||||
from .views_api import * # noqa: F401,F403
|
|
||||||
|
|
||||||
|
|
||||||
def jukebox_start():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Spotify Jukebox",
|
|
||||||
"short_description": "Spotify jukebox middleware",
|
|
||||||
"tile": "/jukebox/static/image/jukebox.png",
|
|
||||||
"contributors": ["benarc"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment
|
|
||||||
|
|
||||||
|
|
||||||
async def create_jukebox(data: CreateJukeLinkData) -> Jukebox:
|
|
||||||
juke_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
juke_id,
|
|
||||||
data.user,
|
|
||||||
data.title,
|
|
||||||
data.wallet,
|
|
||||||
data.sp_user,
|
|
||||||
data.sp_secret,
|
|
||||||
data.sp_access_token,
|
|
||||||
data.sp_refresh_token,
|
|
||||||
data.sp_device,
|
|
||||||
data.sp_playlists,
|
|
||||||
data.price,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
assert jukebox, "Newly created Jukebox couldn't be retrieved"
|
|
||||||
return jukebox
|
|
||||||
|
|
||||||
|
|
||||||
async def update_jukebox(
|
|
||||||
data: Union[CreateJukeLinkData, Jukebox], juke_id: str = ""
|
|
||||||
) -> Optional[Jukebox]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
|
||||||
items = [f"{field[1]}" for field in data]
|
|
||||||
items.append(juke_id)
|
|
||||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
|
||||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items,))
|
|
||||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
|
||||||
return Jukebox(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_jukebox(juke_id: str) -> Optional[Jukebox]:
|
|
||||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
|
||||||
return Jukebox(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_jukebox_by_user(user: str) -> Optional[Jukebox]:
|
|
||||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE sp_user = ?", (user,))
|
|
||||||
return Jukebox(**row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_jukeboxs(user: str) -> List[Jukebox]:
|
|
||||||
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
|
|
||||||
for row in rows:
|
|
||||||
if row.sp_playlists is None:
|
|
||||||
await delete_jukebox(row.id)
|
|
||||||
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
|
|
||||||
|
|
||||||
return [Jukebox(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_jukebox(juke_id: str):
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
DELETE FROM jukebox.jukebox WHERE id = ?
|
|
||||||
""",
|
|
||||||
(juke_id,),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#####################################PAYMENTS
|
|
||||||
|
|
||||||
|
|
||||||
async def create_jukebox_payment(data: CreateJukeboxPayment) -> CreateJukeboxPayment:
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(data.payment_hash, data.juke_id, data.song_id, False),
|
|
||||||
)
|
|
||||||
jukebox_payment = await get_jukebox_payment(data.payment_hash)
|
|
||||||
assert jukebox_payment, "Newly created Jukebox Payment couldn't be retrieved"
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def update_jukebox_payment(
|
|
||||||
payment_hash: str, **kwargs
|
|
||||||
) -> Optional[JukeboxPayment]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
|
||||||
await db.execute(
|
|
||||||
f"UPDATE jukebox.jukebox_payment SET {q} WHERE payment_hash = ?",
|
|
||||||
(*kwargs.values(), payment_hash),
|
|
||||||
)
|
|
||||||
return await get_jukebox_payment(payment_hash)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_jukebox_payment(payment_hash: str) -> Optional[JukeboxPayment]:
|
|
||||||
row = await db.fetchone(
|
|
||||||
"SELECT * FROM jukebox.jukebox_payment WHERE payment_hash = ?", (payment_hash,)
|
|
||||||
)
|
|
||||||
return JukeboxPayment(**row) if row else None
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
async def m001_initial(db):
|
|
||||||
"""
|
|
||||||
Initial jukebox table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE jukebox.jukebox (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
"user" TEXT,
|
|
||||||
title TEXT,
|
|
||||||
wallet TEXT,
|
|
||||||
inkey TEXT,
|
|
||||||
sp_user TEXT NOT NULL,
|
|
||||||
sp_secret TEXT NOT NULL,
|
|
||||||
sp_access_token TEXT,
|
|
||||||
sp_refresh_token TEXT,
|
|
||||||
sp_device TEXT,
|
|
||||||
sp_playlists TEXT,
|
|
||||||
price INTEGER,
|
|
||||||
profit INTEGER
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_initial(db):
|
|
||||||
"""
|
|
||||||
Initial jukebox_payment table.
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE jukebox.jukebox_payment (
|
|
||||||
payment_hash TEXT PRIMARY KEY,
|
|
||||||
juke_id TEXT,
|
|
||||||
song_id TEXT,
|
|
||||||
paid BOOL
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Query
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class CreateJukeLinkData(BaseModel):
|
|
||||||
user: str = Query(None)
|
|
||||||
title: str = Query(None)
|
|
||||||
wallet: str = Query(None)
|
|
||||||
sp_user: str = Query(None)
|
|
||||||
sp_secret: str = Query(None)
|
|
||||||
sp_access_token: str = Query(None)
|
|
||||||
sp_refresh_token: str = Query(None)
|
|
||||||
sp_device: str = Query(None)
|
|
||||||
sp_playlists: str = Query(None)
|
|
||||||
price: str = Query(None)
|
|
||||||
|
|
||||||
|
|
||||||
class Jukebox(BaseModel):
|
|
||||||
id: str
|
|
||||||
user: str
|
|
||||||
title: str
|
|
||||||
wallet: str
|
|
||||||
inkey: Optional[str]
|
|
||||||
sp_user: str
|
|
||||||
sp_secret: str
|
|
||||||
sp_access_token: Optional[str]
|
|
||||||
sp_refresh_token: Optional[str]
|
|
||||||
sp_device: Optional[str]
|
|
||||||
sp_playlists: Optional[str]
|
|
||||||
price: int
|
|
||||||
profit: int
|
|
||||||
|
|
||||||
|
|
||||||
class JukeboxPayment(BaseModel):
|
|
||||||
payment_hash: str
|
|
||||||
juke_id: str
|
|
||||||
song_id: str
|
|
||||||
paid: bool
|
|
||||||
|
|
||||||
|
|
||||||
class CreateJukeboxPayment(BaseModel):
|
|
||||||
invoice: str = Query(None)
|
|
||||||
payment_hash: str = Query(None)
|
|
||||||
juke_id: str = Query(None)
|
|
||||||
song_id: str = Query(None)
|
|
||||||
paid: bool = Query(False)
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -1,413 +0,0 @@
|
||||||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
|
||||||
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
var mapJukebox = obj => {
|
|
||||||
if (obj.sp_device) {
|
|
||||||
obj._data = _.clone(obj)
|
|
||||||
|
|
||||||
obj.sp_id = obj._data.id
|
|
||||||
obj.device = obj._data.sp_device.split('-')[0]
|
|
||||||
playlists = obj._data.sp_playlists.split(',')
|
|
||||||
var i
|
|
||||||
playlistsar = []
|
|
||||||
for (i = 0; i < playlists.length; i++) {
|
|
||||||
playlistsar.push(playlists[i].split('-')[0])
|
|
||||||
}
|
|
||||||
obj.playlist = playlistsar.join()
|
|
||||||
console.log(obj)
|
|
||||||
return obj
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
JukeboxTable: {
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Title',
|
|
||||||
field: 'title'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'device',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Device',
|
|
||||||
field: 'device'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'playlist',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Playlist',
|
|
||||||
field: 'playlist'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'price',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Price',
|
|
||||||
field: 'price'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isPwd: true,
|
|
||||||
tokenFetched: true,
|
|
||||||
devices: [],
|
|
||||||
filter: '',
|
|
||||||
jukebox: {},
|
|
||||||
playlists: [],
|
|
||||||
JukeboxLinks: [],
|
|
||||||
step: 1,
|
|
||||||
locationcbPath: '',
|
|
||||||
locationcb: '',
|
|
||||||
jukeboxDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {}
|
|
||||||
},
|
|
||||||
spotifyDialog: false,
|
|
||||||
qrCodeDialog: {
|
|
||||||
show: false,
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {
|
|
||||||
openQrCodeDialog: function (linkId) {
|
|
||||||
var link = _.findWhere(this.JukeboxLinks, {id: linkId})
|
|
||||||
|
|
||||||
this.qrCodeDialog.data = _.clone(link)
|
|
||||||
|
|
||||||
this.qrCodeDialog.data.url =
|
|
||||||
window.location.protocol + '//' + window.location.host
|
|
||||||
this.qrCodeDialog.show = true
|
|
||||||
},
|
|
||||||
getJukeboxes() {
|
|
||||||
self = this
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox',
|
|
||||||
self.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.JukeboxLinks = response.data.map(function (obj) {
|
|
||||||
return mapJukebox(obj)
|
|
||||||
})
|
|
||||||
console.log(self.JukeboxLinks)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteJukebox(juke_id) {
|
|
||||||
self = this
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this Jukebox?')
|
|
||||||
.onOk(function () {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
'/jukebox/api/v1/jukebox/' + juke_id,
|
|
||||||
self.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) {
|
|
||||||
return obj.id === juke_id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateJukebox: function (linkId) {
|
|
||||||
self = this
|
|
||||||
var link = _.findWhere(self.JukeboxLinks, {id: linkId})
|
|
||||||
self.jukeboxDialog.data = _.clone(link._data)
|
|
||||||
|
|
||||||
self.refreshDevices()
|
|
||||||
self.refreshPlaylists()
|
|
||||||
|
|
||||||
self.step = 4
|
|
||||||
self.jukeboxDialog.data.sp_device = []
|
|
||||||
self.jukeboxDialog.data.sp_playlists = []
|
|
||||||
self.jukeboxDialog.data.sp_id = self.jukeboxDialog.data.id
|
|
||||||
self.jukeboxDialog.data.price = String(self.jukeboxDialog.data.price)
|
|
||||||
self.jukeboxDialog.show = true
|
|
||||||
},
|
|
||||||
closeFormDialog() {
|
|
||||||
this.jukeboxDialog.data = {}
|
|
||||||
this.jukeboxDialog.show = false
|
|
||||||
this.step = 1
|
|
||||||
},
|
|
||||||
submitSpotifyKeys() {
|
|
||||||
self = this
|
|
||||||
self.jukeboxDialog.data.user = self.g.user.id
|
|
||||||
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'POST',
|
|
||||||
'/jukebox/api/v1/jukebox/',
|
|
||||||
self.g.user.wallets[0].adminkey,
|
|
||||||
self.jukeboxDialog.data
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
if (response.data) {
|
|
||||||
self.jukeboxDialog.data.sp_id = response.data.id
|
|
||||||
self.step = 3
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
authAccess() {
|
|
||||||
self = this
|
|
||||||
self.requestAuthorization()
|
|
||||||
self.getSpotifyTokens()
|
|
||||||
self.$q.notify({
|
|
||||||
spinner: true,
|
|
||||||
message: 'Processing',
|
|
||||||
timeout: 10000
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getSpotifyTokens() {
|
|
||||||
self = this
|
|
||||||
var counter = 0
|
|
||||||
var timerId = setInterval(function () {
|
|
||||||
counter++
|
|
||||||
if (!self.jukeboxDialog.data.sp_user) {
|
|
||||||
clearInterval(timerId)
|
|
||||||
}
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id,
|
|
||||||
self.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(response => {
|
|
||||||
if (response.data.sp_access_token) {
|
|
||||||
self.fetchAccessToken(response.data.sp_access_token)
|
|
||||||
if (self.jukeboxDialog.data.sp_access_token) {
|
|
||||||
self.refreshPlaylists()
|
|
||||||
self.refreshDevices()
|
|
||||||
setTimeout(function () {
|
|
||||||
if (self.devices.length < 1 || self.playlists.length < 1) {
|
|
||||||
self.$q.notify({
|
|
||||||
spinner: true,
|
|
||||||
color: 'red',
|
|
||||||
message:
|
|
||||||
'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something',
|
|
||||||
timeout: 10000
|
|
||||||
})
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'DELETE',
|
|
||||||
'/jukebox/api/v1/jukebox/' + response.data.id,
|
|
||||||
self.g.user.wallets[0].adminkey
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.getJukeboxes()
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
clearInterval(timerId)
|
|
||||||
self.closeFormDialog()
|
|
||||||
} else {
|
|
||||||
self.step = 4
|
|
||||||
clearInterval(timerId)
|
|
||||||
}
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
},
|
|
||||||
requestAuthorization() {
|
|
||||||
self = this
|
|
||||||
var url = 'https://accounts.spotify.com/authorize'
|
|
||||||
url += '?client_id=' + self.jukeboxDialog.data.sp_user
|
|
||||||
url += '&response_type=code'
|
|
||||||
url +=
|
|
||||||
'&redirect_uri=' +
|
|
||||||
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
|
|
||||||
url += '&show_dialog=true'
|
|
||||||
url +=
|
|
||||||
'&scope=user-read-private user-read-email user-modify-playback-state user-read-playback-position user-library-read streaming user-read-playback-state user-read-recently-played playlist-read-private'
|
|
||||||
|
|
||||||
window.open(url)
|
|
||||||
},
|
|
||||||
openNewDialog() {
|
|
||||||
this.jukeboxDialog.show = true
|
|
||||||
this.jukeboxDialog.data = {}
|
|
||||||
},
|
|
||||||
createJukebox() {
|
|
||||||
self = this
|
|
||||||
self.jukeboxDialog.data.sp_playlists =
|
|
||||||
self.jukeboxDialog.data.sp_playlists.join()
|
|
||||||
self.updateDB()
|
|
||||||
self.jukeboxDialog.show = false
|
|
||||||
self.getJukeboxes()
|
|
||||||
},
|
|
||||||
updateDB() {
|
|
||||||
self = this
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'PUT',
|
|
||||||
'/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id,
|
|
||||||
self.g.user.wallets[0].adminkey,
|
|
||||||
self.jukeboxDialog.data
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
if (
|
|
||||||
self.jukeboxDialog.data.sp_playlists &&
|
|
||||||
self.jukeboxDialog.data.sp_devices
|
|
||||||
) {
|
|
||||||
self.getJukeboxes()
|
|
||||||
// self.JukeboxLinks.push(mapJukebox(response.data))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
playlistApi(method, url, body) {
|
|
||||||
self = this
|
|
||||||
let xhr = new XMLHttpRequest()
|
|
||||||
xhr.open(method, url, true)
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'Authorization',
|
|
||||||
'Bearer ' + this.jukeboxDialog.data.sp_access_token
|
|
||||||
)
|
|
||||||
xhr.send(body)
|
|
||||||
xhr.onload = function () {
|
|
||||||
if (xhr.status == 401) {
|
|
||||||
self.refreshAccessToken()
|
|
||||||
self.playlistApi(
|
|
||||||
'GET',
|
|
||||||
'https://api.spotify.com/v1/me/playlists',
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let responseObj = JSON.parse(xhr.response)
|
|
||||||
self.jukeboxDialog.data.playlists = null
|
|
||||||
self.playlists = []
|
|
||||||
self.jukeboxDialog.data.playlists = []
|
|
||||||
var i
|
|
||||||
for (i = 0; i < responseObj.items.length; i++) {
|
|
||||||
self.playlists.push(
|
|
||||||
responseObj.items[i].name + '-' + responseObj.items[i].id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refreshPlaylists() {
|
|
||||||
self = this
|
|
||||||
self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null)
|
|
||||||
},
|
|
||||||
deviceApi(method, url, body) {
|
|
||||||
self = this
|
|
||||||
let xhr = new XMLHttpRequest()
|
|
||||||
xhr.open(method, url, true)
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'Authorization',
|
|
||||||
'Bearer ' + this.jukeboxDialog.data.sp_access_token
|
|
||||||
)
|
|
||||||
xhr.send(body)
|
|
||||||
xhr.onload = function () {
|
|
||||||
if (xhr.status == 401) {
|
|
||||||
self.refreshAccessToken()
|
|
||||||
self.deviceApi(
|
|
||||||
'GET',
|
|
||||||
'https://api.spotify.com/v1/me/player/devices',
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let responseObj = JSON.parse(xhr.response)
|
|
||||||
self.jukeboxDialog.data.devices = []
|
|
||||||
|
|
||||||
self.devices = []
|
|
||||||
var i
|
|
||||||
for (i = 0; i < responseObj.devices.length; i++) {
|
|
||||||
self.devices.push(
|
|
||||||
responseObj.devices[i].name + '-' + responseObj.devices[i].id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refreshDevices() {
|
|
||||||
self = this
|
|
||||||
self.deviceApi(
|
|
||||||
'GET',
|
|
||||||
'https://api.spotify.com/v1/me/player/devices',
|
|
||||||
null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
fetchAccessToken(code) {
|
|
||||||
self = this
|
|
||||||
let body = 'grant_type=authorization_code'
|
|
||||||
body += '&code=' + code
|
|
||||||
body +=
|
|
||||||
'&redirect_uri=' +
|
|
||||||
encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id)
|
|
||||||
|
|
||||||
self.callAuthorizationApi(body)
|
|
||||||
},
|
|
||||||
refreshAccessToken() {
|
|
||||||
self = this
|
|
||||||
let body = 'grant_type=refresh_token'
|
|
||||||
body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token
|
|
||||||
body += '&client_id=' + self.jukeboxDialog.data.sp_user
|
|
||||||
self.callAuthorizationApi(body)
|
|
||||||
},
|
|
||||||
callAuthorizationApi(body) {
|
|
||||||
self = this
|
|
||||||
let xhr = new XMLHttpRequest()
|
|
||||||
xhr.open('POST', 'https://accounts.spotify.com/api/token', true)
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
|
|
||||||
xhr.setRequestHeader(
|
|
||||||
'Authorization',
|
|
||||||
'Basic ' +
|
|
||||||
btoa(
|
|
||||||
self.jukeboxDialog.data.sp_user +
|
|
||||||
':' +
|
|
||||||
self.jukeboxDialog.data.sp_secret
|
|
||||||
)
|
|
||||||
)
|
|
||||||
xhr.send(body)
|
|
||||||
xhr.onload = function () {
|
|
||||||
let responseObj = JSON.parse(xhr.response)
|
|
||||||
if (responseObj.access_token) {
|
|
||||||
self.jukeboxDialog.data.sp_access_token = responseObj.access_token
|
|
||||||
self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token
|
|
||||||
self.updateDB()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
var getJukeboxes = this.getJukeboxes
|
|
||||||
getJukeboxes()
|
|
||||||
this.selectedWallet = this.g.user.wallets[0]
|
|
||||||
this.locationcbPath = String(
|
|
||||||
[
|
|
||||||
window.location.protocol,
|
|
||||||
'//',
|
|
||||||
window.location.host,
|
|
||||||
'/jukebox/api/v1/jukebox/spotify/cb/'
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
this.locationcb = this.locationcbPath
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 215 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 241 KiB |
|
|
@ -1,24 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
|
||||||
|
|
||||||
from .crud import update_jukebox_payment
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
|
||||||
invoice_queue = asyncio.Queue()
|
|
||||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
|
||||||
|
|
||||||
while True:
|
|
||||||
payment = await invoice_queue.get()
|
|
||||||
await on_invoice_paid(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
|
||||||
if payment.extra.get("tag") != "jukebox":
|
|
||||||
# not a jukebox invoice
|
|
||||||
return
|
|
||||||
|
|
||||||
await update_jukebox_payment(payment.payment_hash, paid=True)
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
<q-card-section>
|
|
||||||
To use this extension you need a Spotify client ID and client secret. You get
|
|
||||||
these by creating an app in the Spotify developers dashboard
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
style="color: #43a047"
|
|
||||||
href="https://developer.spotify.com/dashboard/applications"
|
|
||||||
>here
|
|
||||||
</a>
|
|
||||||
<br /><br />Select the playlists you want people to be able to pay for, share
|
|
||||||
the frontend page, profit :) <br /><br />
|
|
||||||
Made by,
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
style="color: #43a047"
|
|
||||||
href="https://twitter.com/arcbtc"
|
|
||||||
>benarc</a
|
|
||||||
>. Inspired by,
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
style="color: #43a047"
|
|
||||||
href="https://twitter.com/pirosb3/status/1056263089128161280"
|
|
||||||
>pirosb3</a
|
|
||||||
>.
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
group="extras"
|
|
||||||
icon="swap_vertical_circle"
|
|
||||||
label="API info"
|
|
||||||
:content-inset-level="0.5"
|
|
||||||
>
|
|
||||||
<q-btn flat label="Swagger API" type="a" href="../docs#/jukebox"></q-btn>
|
|
||||||
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="List jukeboxes">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code><span class="text-blue">GET</span> /jukebox/api/v1/jukebox</code>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <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.base_url }}jukebox/api/v1/jukebox -H
|
|
||||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item group="api" dense expand-separator label="Get jukebox">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-blue">GET</span>
|
|
||||||
/jukebox/api/v1/jukebox/<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.base_url
|
|
||||||
}}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
group="api"
|
|
||||||
dense
|
|
||||||
expand-separator
|
|
||||||
label="Create/update track"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<code
|
|
||||||
><span class="text-green">POST/PUT</span>
|
|
||||||
/jukebox/api/v1/jukebox/</code
|
|
||||||
>
|
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
|
||||||
<code>{"X-Api-Key": <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.base_url }}jukebox/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:
|
|
||||||
{{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.base_url
|
|
||||||
}}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
|
||||||
user.wallets[0].adminkey }}"
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item></q-expansion-item
|
|
||||||
>
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
|
||||||
<q-card class="q-pa-lg">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<center>
|
|
||||||
<h3 class="q-my-none">Jukebox error</h3>
|
|
||||||
<br />
|
|
||||||
<q-icon
|
|
||||||
name="warning"
|
|
||||||
class="text-grey"
|
|
||||||
style="font-size: 20rem"
|
|
||||||
></q-icon>
|
|
||||||
|
|
||||||
<h5 class="q-my-none">
|
|
||||||
Ask the host to turn on the device and launch spotify
|
|
||||||
</h5>
|
|
||||||
<br />
|
|
||||||
</center>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data: function () {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,388 +0,0 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
||||||
%} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
color="primary"
|
|
||||||
class="q-ma-lg"
|
|
||||||
@click="openNewDialog()"
|
|
||||||
>Add Spotify Jukebox</q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
{% raw %}
|
|
||||||
|
|
||||||
<q-table
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
:data="JukeboxLinks"
|
|
||||||
row-key="id"
|
|
||||||
:columns="JukeboxTable.columns"
|
|
||||||
:pagination.sync="JukeboxTable.pagination"
|
|
||||||
:filter="filter"
|
|
||||||
>
|
|
||||||
<template v-slot:header="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
|
|
||||||
<q-th
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
auto-width
|
|
||||||
>
|
|
||||||
<div v-if="col.name == 'id'"></div>
|
|
||||||
<div v-else>{{ col.label }}</div>
|
|
||||||
</q-th>
|
|
||||||
<q-th auto-width></q-th>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:body="props">
|
|
||||||
<q-tr :props="props">
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
unelevated
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
icon="launch"
|
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
|
||||||
@click="openQrCodeDialog(props.row.sp_id)"
|
|
||||||
>
|
|
||||||
<q-tooltip> Jukebox QR </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td auto-width>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="updateJukebox(props.row.id)"
|
|
||||||
icon="edit"
|
|
||||||
color="light-blue"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
@click="deleteJukebox(props.row.id)"
|
|
||||||
icon="cancel"
|
|
||||||
color="pink"
|
|
||||||
>
|
|
||||||
<q-tooltip> Delete Jukebox </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</q-td>
|
|
||||||
<q-td
|
|
||||||
v-for="col in props.cols"
|
|
||||||
:key="col.name"
|
|
||||||
:props="props"
|
|
||||||
auto-width
|
|
||||||
>
|
|
||||||
<div v-if="col.name == 'id'"></div>
|
|
||||||
<div v-else>{{ col.value }}</div>
|
|
||||||
</q-td>
|
|
||||||
</q-tr>
|
|
||||||
</template>
|
|
||||||
</q-table>
|
|
||||||
{% endraw %}
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<h6 class="text-subtitle1 q-my-none">
|
|
||||||
{{SITE_TITLE}} jukebox extension
|
|
||||||
</h6>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-list> {% include "jukebox/_api_docs.html" %} </q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="jukeboxDialog.show" position="top" @hide="closeFormDialog">
|
|
||||||
<q-card class="q-pa-md q-pt-lg q-mt-md" style="width: 100%">
|
|
||||||
<q-stepper
|
|
||||||
v-model="step"
|
|
||||||
active-color="primary"
|
|
||||||
inactive-color="secondary"
|
|
||||||
vertical
|
|
||||||
animated
|
|
||||||
>
|
|
||||||
<q-step
|
|
||||||
:name="1"
|
|
||||||
title="1. Pick Wallet and Price"
|
|
||||||
icon="account_balance_wallet"
|
|
||||||
:done="step > 1"
|
|
||||||
>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-pt-md"
|
|
||||||
dense
|
|
||||||
v-model.trim="jukeboxDialog.data.title"
|
|
||||||
label="Jukebox name"
|
|
||||||
></q-input>
|
|
||||||
<q-select
|
|
||||||
class="q-pb-md q-pt-md"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="jukeboxDialog.data.wallet"
|
|
||||||
:options="g.user.walletOptions"
|
|
||||||
label="Wallet to use"
|
|
||||||
></q-select>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
v-model.trim="jukeboxDialog.data.price"
|
|
||||||
type="number"
|
|
||||||
max="1440"
|
|
||||||
label="Price per track"
|
|
||||||
class="q-pb-lg"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-4">
|
|
||||||
<q-btn
|
|
||||||
v-if="jukeboxDialog.data.title != null && jukeboxDialog.data.price != null && jukeboxDialog.data.wallet != null"
|
|
||||||
color="primary"
|
|
||||||
@click="step = 2"
|
|
||||||
>Continue</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-else color="primary" disable>Continue</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
@click="closeFormDialog"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="2"
|
|
||||||
title="2. Add API keys"
|
|
||||||
icon="vpn_key"
|
|
||||||
:done="step > 2"
|
|
||||||
>
|
|
||||||
<img src="/jukebox/static/spotapi.gif" />
|
|
||||||
To use this extension you need a Spotify client ID and client secret.
|
|
||||||
You get these by creating an app in the Spotify Developer Dashboard
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<q-btn
|
|
||||||
type="a"
|
|
||||||
target="_blank"
|
|
||||||
color="primary"
|
|
||||||
href="https://developer.spotify.com/dashboard/applications"
|
|
||||||
>Open the Spotify Developer Dashboard</q-btn
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
class="q-pb-md q-pt-md"
|
|
||||||
dense
|
|
||||||
v-model.trim="jukeboxDialog.data.sp_user"
|
|
||||||
label="Client ID"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
v-model="jukeboxDialog.data.sp_secret"
|
|
||||||
filled
|
|
||||||
:type="isPwd ? 'password' : 'text'"
|
|
||||||
label="Client secret"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<q-icon
|
|
||||||
:name="isPwd ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="isPwd = !isPwd"
|
|
||||||
>
|
|
||||||
</q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-4">
|
|
||||||
<q-btn
|
|
||||||
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
|
|
||||||
color="primary"
|
|
||||||
@click="submitSpotifyKeys"
|
|
||||||
>Submit keys</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-else color="primary" disable color="primary"
|
|
||||||
>Submit keys</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
@click="closeFormDialog"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="3"
|
|
||||||
title="3. Add Redirect URI"
|
|
||||||
icon="link"
|
|
||||||
:done="step > 3"
|
|
||||||
>
|
|
||||||
<img src="/jukebox/static/spotapi1.gif" />
|
|
||||||
<p>
|
|
||||||
In the app go to edit-settings, set the redirect URI to this link
|
|
||||||
</p>
|
|
||||||
<q-card
|
|
||||||
class="cursor-pointer word-break"
|
|
||||||
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
|
|
||||||
>
|
|
||||||
<q-card-section style="word-break: break-all">
|
|
||||||
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
|
|
||||||
%}
|
|
||||||
</q-card-section>
|
|
||||||
<q-tooltip> Click to copy URL </q-tooltip>
|
|
||||||
</q-card>
|
|
||||||
<br />
|
|
||||||
<q-btn
|
|
||||||
type="a"
|
|
||||||
target="_blank"
|
|
||||||
color="primary"
|
|
||||||
href="https://developer.spotify.com/dashboard/applications"
|
|
||||||
>Open the Spotify Application Settings</q-btn
|
|
||||||
>
|
|
||||||
<br /><br />
|
|
||||||
<p>
|
|
||||||
After adding the redirect URI, click the "Authorise access" button
|
|
||||||
below.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-4">
|
|
||||||
<q-btn
|
|
||||||
v-if="jukeboxDialog.data.sp_secret != null && jukeboxDialog.data.sp_user != null && tokenFetched"
|
|
||||||
color="primary"
|
|
||||||
@click="authAccess"
|
|
||||||
>Authorise access</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-else color="primary" disable color="primary"
|
|
||||||
>Authorise access</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
@click="closeFormDialog"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
</q-step>
|
|
||||||
|
|
||||||
<q-step
|
|
||||||
:name="4"
|
|
||||||
title="4. Select Device and Playlists"
|
|
||||||
icon="queue_music"
|
|
||||||
active-color="primary"
|
|
||||||
:done="step > 4"
|
|
||||||
>
|
|
||||||
<q-select
|
|
||||||
class="q-pb-md q-pt-md"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
emit-value
|
|
||||||
v-model="jukeboxDialog.data.sp_device"
|
|
||||||
:options="devices"
|
|
||||||
label="Device jukebox will play to"
|
|
||||||
></q-select>
|
|
||||||
<q-select
|
|
||||||
class="q-pb-md"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
multiple
|
|
||||||
emit-value
|
|
||||||
v-model="jukeboxDialog.data.sp_playlists"
|
|
||||||
:options="playlists"
|
|
||||||
label="Playlists available to the jukebox"
|
|
||||||
></q-select>
|
|
||||||
<div class="row q-mt-md">
|
|
||||||
<div class="col-5">
|
|
||||||
<q-btn
|
|
||||||
v-if="jukeboxDialog.data.sp_device != null && jukeboxDialog.data.sp_playlists != null"
|
|
||||||
color="primary"
|
|
||||||
@click="createJukebox"
|
|
||||||
>Create Jukebox</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-else color="primary" disable>Create Jukebox</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col-7">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
class="float-right"
|
|
||||||
@click="closeFormDialog"
|
|
||||||
>Cancel</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-step>
|
|
||||||
</q-stepper>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
|
|
||||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
|
||||||
<center>
|
|
||||||
<h5 class="q-my-none">Shareable Jukebox QR</h5>
|
|
||||||
</center>
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
|
||||||
<qrcode
|
|
||||||
:value="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id, 'Link copied to clipboard!')"
|
|
||||||
>
|
|
||||||
Copy jukebox link</q-btn
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
type="a"
|
|
||||||
:href="qrCodeDialog.data.url + '/jukebox/' + qrCodeDialog.data.id"
|
|
||||||
target="_blank"
|
|
||||||
>Open jukebox</q-btn
|
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
|
||||||
<script src="/jukebox/static/js/index.js"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row q-col-gutter-md justify-center">
|
|
||||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
|
||||||
<q-card class="q-pa-lg">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<p style="font-size: 22px">Currently playing</p>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-4">
|
|
||||||
<img style="width: 100px" :src="currentPlay.image" />
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
{% raw %}
|
|
||||||
<strong style="font-size: 20px">{{ currentPlay.name }}</strong
|
|
||||||
><br />
|
|
||||||
<strong style="font-size: 15px">{{ currentPlay.artist }}</strong>
|
|
||||||
</div>
|
|
||||||
{% endraw %}
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card class="q-mt-lg">
|
|
||||||
<q-card-section>
|
|
||||||
<p style="font-size: 22px">Pick a song</p>
|
|
||||||
<q-select
|
|
||||||
outlined
|
|
||||||
v-model="playlist"
|
|
||||||
:options="playlists"
|
|
||||||
label="playlists"
|
|
||||||
@input="selectPlaylist()"
|
|
||||||
>
|
|
||||||
</q-select>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-virtual-scroll
|
|
||||||
style="max-height: 300px"
|
|
||||||
:items="currentPlaylist"
|
|
||||||
separator
|
|
||||||
>
|
|
||||||
<template v-slot="{ item, index }">
|
|
||||||
<q-item
|
|
||||||
:key="index"
|
|
||||||
dense
|
|
||||||
clickable
|
|
||||||
v-ripple
|
|
||||||
@click="payForSong(item.id, item.name, item.artist, item.image)"
|
|
||||||
>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>
|
|
||||||
{% raw %} {{ item.name }} - ({{ item.artist }}){% endraw %}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
</q-virtual-scroll>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-dialog v-model="receive.dialogues.first" position="top">
|
|
||||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-4">
|
|
||||||
<img style="width: 100px" :src="receive.image" />
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
{% raw %}
|
|
||||||
<strong style="font-size: 20px">{{ receive.name }}</strong><br />
|
|
||||||
<strong style="font-size: 15px">{{ receive.artist }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<br />
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<q-btn outline color="grey" @click="getInvoice(receive.id)"
|
|
||||||
>Play for {% endraw %}{{ price }}sats
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
<q-dialog v-model="receive.dialogues.second" position="top">
|
|
||||||
<q-card class="q-pa-lg lnbits__dialog-card">
|
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
|
||||||
<qrcode
|
|
||||||
:value="'lightning:' + receive.paymentReq.toUpperCase()"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
|
||||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
|
||||||
>Copy invoice</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentPlaylist: [],
|
|
||||||
currentlyPlaying: {},
|
|
||||||
playlists: {},
|
|
||||||
playlist: '',
|
|
||||||
heavyList: [],
|
|
||||||
selectedWallet: {},
|
|
||||||
paid: false,
|
|
||||||
receive: {
|
|
||||||
dialogues: {
|
|
||||||
first: false,
|
|
||||||
second: false
|
|
||||||
},
|
|
||||||
paymentReq: '',
|
|
||||||
paymentHash: '',
|
|
||||||
name: '',
|
|
||||||
artist: '',
|
|
||||||
image: '',
|
|
||||||
id: '',
|
|
||||||
showQR: false,
|
|
||||||
data: null,
|
|
||||||
dismissMsg: null,
|
|
||||||
paymentChecker: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
currentPlay() {
|
|
||||||
return this.currentlyPlaying
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
payForSong(song_id, name, artist, image) {
|
|
||||||
self = this
|
|
||||||
self.receive.name = name
|
|
||||||
self.receive.artist = artist
|
|
||||||
self.receive.image = image
|
|
||||||
self.receive.id = song_id
|
|
||||||
self.receive.dialogues.first = true
|
|
||||||
},
|
|
||||||
getInvoice(song_id) {
|
|
||||||
self = this
|
|
||||||
var dialog = this.receive
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox/jb/invoice/' +
|
|
||||||
'{{ juke_id }}' +
|
|
||||||
'/' +
|
|
||||||
song_id
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
console.log(response.data)
|
|
||||||
self.receive.paymentReq = response.data.invoice
|
|
||||||
self.receive.paymentHash = response.data.payment_hash
|
|
||||||
self.receive.dialogues.second = true
|
|
||||||
dialog.data = response.data
|
|
||||||
|
|
||||||
dialog.dismissMsg = self.$q.notify({
|
|
||||||
timeout: 0,
|
|
||||||
message: 'Waiting for payment...'
|
|
||||||
})
|
|
||||||
dialog.paymentChecker = setInterval(function () {
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox/jb/checkinvoice/' +
|
|
||||||
self.receive.paymentHash +
|
|
||||||
'/{{ juke_id }}'
|
|
||||||
)
|
|
||||||
.then(function (res) {
|
|
||||||
console.log(res)
|
|
||||||
if (res.data.paid == true) {
|
|
||||||
clearInterval(dialog.paymentChecker)
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox/jb/invoicep/' +
|
|
||||||
self.receive.id +
|
|
||||||
'/{{ juke_id }}/' +
|
|
||||||
self.receive.paymentHash
|
|
||||||
)
|
|
||||||
.then(function (ress) {
|
|
||||||
console.log('ress')
|
|
||||||
console.log(ress)
|
|
||||||
console.log('ress')
|
|
||||||
if (ress.data.song_id == self.receive.id) {
|
|
||||||
clearInterval(dialog.paymentChecker)
|
|
||||||
dialog.dismissMsg()
|
|
||||||
self.receive.dialogues.second = false
|
|
||||||
|
|
||||||
self.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message:
|
|
||||||
'Success! "' +
|
|
||||||
self.receive.name +
|
|
||||||
'" will be played soon',
|
|
||||||
timeout: 3000
|
|
||||||
})
|
|
||||||
self.receive.dialogues.first = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
self.getCurrent()
|
|
||||||
}, 500)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
self.$q.notify({
|
|
||||||
color: 'warning',
|
|
||||||
html: true,
|
|
||||||
message:
|
|
||||||
'<center>Device is not connected! <br/> Ask the host to turn on their device and have Spotify open</center>',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getCurrent() {
|
|
||||||
LNbits.api
|
|
||||||
.request('GET', '/jukebox/api/v1/jukebox/jb/currently/{{juke_id}}')
|
|
||||||
.then(function (res) {
|
|
||||||
if (res.data.id) {
|
|
||||||
self.currentlyPlaying = res.data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPlaylist() {
|
|
||||||
self = this
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox/jb/playlist/' +
|
|
||||||
'{{ juke_id }}' +
|
|
||||||
'/' +
|
|
||||||
self.playlist.split(',')[0].split('-')[1]
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.currentPlaylist = response.data
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
currentSong() {}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.getCurrent()
|
|
||||||
this.playlists = JSON.parse('{{ playlists | tojson }}')
|
|
||||||
this.selectedWallet.inkey = '{{ inkey }}'
|
|
||||||
self = this
|
|
||||||
LNbits.api
|
|
||||||
.request(
|
|
||||||
'GET',
|
|
||||||
'/jukebox/api/v1/jukebox/jb/playlist/' +
|
|
||||||
'{{ juke_id }}' +
|
|
||||||
'/' +
|
|
||||||
self.playlists[0].split(',')[0].split('-')[1]
|
|
||||||
)
|
|
||||||
.then(function (response) {
|
|
||||||
self.currentPlaylist = response.data
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
from lnbits.decorators import check_user_exists
|
|
||||||
|
|
||||||
from . import jukebox_ext, jukebox_renderer
|
|
||||||
from .crud import get_jukebox
|
|
||||||
from .views_api import api_get_jukebox_device_check
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/", response_class=HTMLResponse)
|
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
return jukebox_renderer().TemplateResponse(
|
|
||||||
"jukebox/index.html", {"request": request, "user": user.dict()}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/{juke_id}", response_class=HTMLResponse)
|
|
||||||
async def connect_to_jukebox(request: Request, juke_id):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist."
|
|
||||||
)
|
|
||||||
devices = await api_get_jukebox_device_check(juke_id)
|
|
||||||
deviceConnected = False
|
|
||||||
assert jukebox.sp_device
|
|
||||||
assert jukebox.sp_playlists
|
|
||||||
for device in devices["devices"]:
|
|
||||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
|
||||||
deviceConnected = True
|
|
||||||
if deviceConnected:
|
|
||||||
return jukebox_renderer().TemplateResponse(
|
|
||||||
"jukebox/jukebox.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"playlists": jukebox.sp_playlists.split(","),
|
|
||||||
"juke_id": juke_id,
|
|
||||||
"price": jukebox.price,
|
|
||||||
"inkey": jukebox.inkey,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return jukebox_renderer().TemplateResponse(
|
|
||||||
"jukebox/error.html",
|
|
||||||
{"request": request, "jukebox": jukebox.dict()},
|
|
||||||
)
|
|
||||||
|
|
@ -1,454 +0,0 @@
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi import Depends, Query
|
|
||||||
from starlette.exceptions import HTTPException
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
|
||||||
from lnbits.core.views.api import api_payment
|
|
||||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
|
||||||
|
|
||||||
from . import jukebox_ext
|
|
||||||
from .crud import (
|
|
||||||
create_jukebox,
|
|
||||||
create_jukebox_payment,
|
|
||||||
delete_jukebox,
|
|
||||||
get_jukebox,
|
|
||||||
get_jukebox_payment,
|
|
||||||
get_jukeboxs,
|
|
||||||
update_jukebox,
|
|
||||||
update_jukebox_payment,
|
|
||||||
)
|
|
||||||
from .models import CreateJukeboxPayment, CreateJukeLinkData
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox")
|
|
||||||
async def api_get_jukeboxs(
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
):
|
|
||||||
wallet_user = wallet.wallet.user
|
|
||||||
|
|
||||||
try:
|
|
||||||
jukeboxs = [jukebox.dict() for jukebox in await get_jukeboxs(wallet_user)]
|
|
||||||
return jukeboxs
|
|
||||||
|
|
||||||
except:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukeboxes")
|
|
||||||
|
|
||||||
|
|
||||||
##################SPOTIFY AUTH#####################
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/spotify/cb/{juke_id}", response_class=HTMLResponse)
|
|
||||||
async def api_check_credentials_callbac(
|
|
||||||
juke_id: str = Query(None),
|
|
||||||
code: str = Query(None),
|
|
||||||
access_token: str = Query(None),
|
|
||||||
refresh_token: str = Query(None),
|
|
||||||
):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN)
|
|
||||||
if code:
|
|
||||||
jukebox.sp_access_token = code
|
|
||||||
await update_jukebox(jukebox, juke_id=juke_id)
|
|
||||||
if access_token:
|
|
||||||
jukebox.sp_access_token = access_token
|
|
||||||
jukebox.sp_refresh_token = refresh_token
|
|
||||||
await update_jukebox(jukebox, juke_id=juke_id)
|
|
||||||
return "<h1>Success!</h1><h2>You can close this window</h2>"
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)])
|
|
||||||
async def api_check_credentials_check(juke_id: str = Query(None)):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
return jukebox
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.post(
|
|
||||||
"/api/v1/jukebox",
|
|
||||||
status_code=HTTPStatus.CREATED,
|
|
||||||
dependencies=[Depends(require_admin_key)],
|
|
||||||
)
|
|
||||||
@jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK)
|
|
||||||
async def api_create_update_jukebox(
|
|
||||||
data: CreateJukeLinkData, juke_id: str = Query(None)
|
|
||||||
):
|
|
||||||
if juke_id:
|
|
||||||
jukebox = await update_jukebox(data, juke_id=juke_id)
|
|
||||||
else:
|
|
||||||
jukebox = await create_jukebox(data)
|
|
||||||
return jukebox
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.delete(
|
|
||||||
"/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]
|
|
||||||
)
|
|
||||||
async def api_delete_item(
|
|
||||||
juke_id: str = Query(None),
|
|
||||||
):
|
|
||||||
await delete_jukebox(juke_id)
|
|
||||||
# try:
|
|
||||||
# return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)]
|
|
||||||
# except:
|
|
||||||
# raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox")
|
|
||||||
|
|
||||||
|
|
||||||
################JUKEBOX ENDPOINTS##################
|
|
||||||
|
|
||||||
######GET ACCESS TOKEN######
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/playlist/{juke_id}/{sp_playlist}")
|
|
||||||
async def api_get_jukebox_song(
|
|
||||||
juke_id: str = Query(None),
|
|
||||||
sp_playlist: str = Query(None),
|
|
||||||
retry: bool = Query(False),
|
|
||||||
):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
|
||||||
tracks = []
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
assert jukebox.sp_access_token
|
|
||||||
r = await client.get(
|
|
||||||
"https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks",
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
if "items" not in r.json():
|
|
||||||
if r.status_code == 401:
|
|
||||||
token = await api_get_token(juke_id)
|
|
||||||
if token is False:
|
|
||||||
return False
|
|
||||||
elif retry:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Failed to get auth",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await api_get_jukebox_song(
|
|
||||||
juke_id, sp_playlist, retry=True
|
|
||||||
)
|
|
||||||
return r
|
|
||||||
for item in r.json()["items"]:
|
|
||||||
tracks.append(
|
|
||||||
{
|
|
||||||
"id": item["track"]["id"],
|
|
||||||
"name": item["track"]["name"],
|
|
||||||
"album": item["track"]["album"]["name"],
|
|
||||||
"artist": item["track"]["artists"][0]["name"],
|
|
||||||
"image": item["track"]["album"]["images"][0]["url"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return [track for track in tracks]
|
|
||||||
|
|
||||||
|
|
||||||
async def api_get_token(juke_id):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
r = await client.post(
|
|
||||||
"https://accounts.spotify.com/api/token",
|
|
||||||
timeout=40,
|
|
||||||
params={
|
|
||||||
"grant_type": "refresh_token",
|
|
||||||
"refresh_token": jukebox.sp_refresh_token,
|
|
||||||
"client_id": jukebox.sp_user,
|
|
||||||
},
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
"Authorization": "Basic "
|
|
||||||
+ base64.b64encode(
|
|
||||||
str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii")
|
|
||||||
).decode("ascii"),
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if "access_token" not in r.json():
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
jukebox.sp_access_token = r.json()["access_token"]
|
|
||||||
await update_jukebox(jukebox, juke_id=juke_id)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
######CHECK DEVICE
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/{juke_id}")
|
|
||||||
async def api_get_jukebox_device_check(
|
|
||||||
juke_id: str = Query(None), retry: bool = Query(False)
|
|
||||||
):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes")
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
assert jukebox.sp_access_token
|
|
||||||
rDevice = await client.get(
|
|
||||||
"https://api.spotify.com/v1/me/player/devices",
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
if rDevice.status_code == 204 or rDevice.status_code == 200:
|
|
||||||
return json.loads(rDevice.text)
|
|
||||||
elif rDevice.status_code == 401 or rDevice.status_code == 403:
|
|
||||||
token = await api_get_token(juke_id)
|
|
||||||
if token is False:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="No devices connected"
|
|
||||||
)
|
|
||||||
elif retry:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await api_get_jukebox_device_check(juke_id, retry=True)
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="No device connected"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
######GET INVOICE STUFF
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}")
|
|
||||||
async def api_get_jukebox_invoice(juke_id, song_id):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
|
||||||
try:
|
|
||||||
|
|
||||||
assert jukebox.sp_device
|
|
||||||
devices = await api_get_jukebox_device_check(juke_id)
|
|
||||||
deviceConnected = False
|
|
||||||
for device in devices["devices"]:
|
|
||||||
if device["id"] == jukebox.sp_device.split("-")[1]:
|
|
||||||
deviceConnected = True
|
|
||||||
if not deviceConnected:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="No device connected"
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="No device connected"
|
|
||||||
)
|
|
||||||
|
|
||||||
invoice = await create_invoice(
|
|
||||||
wallet_id=jukebox.wallet,
|
|
||||||
amount=jukebox.price,
|
|
||||||
memo=jukebox.title,
|
|
||||||
extra={"tag": "jukebox"},
|
|
||||||
)
|
|
||||||
|
|
||||||
payment_hash = invoice[0]
|
|
||||||
data = CreateJukeboxPayment(
|
|
||||||
invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id
|
|
||||||
)
|
|
||||||
jukebox_payment = await create_jukebox_payment(data)
|
|
||||||
return jukebox_payment
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}")
|
|
||||||
async def api_get_jukebox_invoice_check(
|
|
||||||
pay_hash: str = Query(None), juke_id: str = Query(None)
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
await get_jukebox(juke_id)
|
|
||||||
except:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
|
||||||
try:
|
|
||||||
status = await api_payment(pay_hash)
|
|
||||||
if status["paid"]:
|
|
||||||
await update_jukebox_payment(pay_hash, paid=True)
|
|
||||||
return {"paid": True}
|
|
||||||
except:
|
|
||||||
return {"paid": False}
|
|
||||||
|
|
||||||
return {"paid": False}
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/invoicep/{song_id}/{juke_id}/{pay_hash}")
|
|
||||||
async def api_get_jukebox_invoice_paid(
|
|
||||||
song_id: str = Query(None),
|
|
||||||
juke_id: str = Query(None),
|
|
||||||
pay_hash: str = Query(None),
|
|
||||||
retry: bool = Query(False),
|
|
||||||
):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
|
||||||
await api_get_jukebox_invoice_check(pay_hash, juke_id)
|
|
||||||
jukebox_payment = await get_jukebox_payment(pay_hash)
|
|
||||||
if jukebox_payment and jukebox_payment.paid:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
assert jukebox.sp_access_token
|
|
||||||
r = await client.get(
|
|
||||||
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
rDevice = await client.get(
|
|
||||||
"https://api.spotify.com/v1/me/player",
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
isPlaying = False
|
|
||||||
if rDevice.status_code == 200:
|
|
||||||
isPlaying = rDevice.json()["is_playing"]
|
|
||||||
|
|
||||||
if r.status_code == 204 or isPlaying is False:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
uri = ["spotify:track:" + song_id]
|
|
||||||
assert jukebox.sp_device
|
|
||||||
r = await client.put(
|
|
||||||
"https://api.spotify.com/v1/me/player/play?device_id="
|
|
||||||
+ jukebox.sp_device.split("-")[1],
|
|
||||||
json={"uris": uri},
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
if r.status_code == 204:
|
|
||||||
return jukebox_payment
|
|
||||||
elif r.status_code == 401 or r.status_code == 403:
|
|
||||||
token = await api_get_token(juke_id)
|
|
||||||
if token is False:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Invoice not paid",
|
|
||||||
)
|
|
||||||
elif retry:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Failed to get auth",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return api_get_jukebox_invoice_paid(
|
|
||||||
song_id, juke_id, pay_hash, retry=True
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
|
|
||||||
)
|
|
||||||
elif r.status_code == 200:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
assert jukebox.sp_access_token
|
|
||||||
assert jukebox.sp_device
|
|
||||||
r = await client.post(
|
|
||||||
"https://api.spotify.com/v1/me/player/queue?uri=spotify%3Atrack%3A"
|
|
||||||
+ song_id
|
|
||||||
+ "&device_id="
|
|
||||||
+ jukebox.sp_device.split("-")[1],
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
if r.status_code == 204:
|
|
||||||
return jukebox_payment
|
|
||||||
|
|
||||||
elif r.status_code == 401 or r.status_code == 403:
|
|
||||||
token = await api_get_token(juke_id)
|
|
||||||
if token is False:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Invoice not paid",
|
|
||||||
)
|
|
||||||
elif retry:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
|
||||||
detail="Failed to get auth",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await api_get_jukebox_invoice_paid(
|
|
||||||
song_id, juke_id, pay_hash
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.OK, detail="Invoice not paid"
|
|
||||||
)
|
|
||||||
elif r.status_code == 401 or r.status_code == 403:
|
|
||||||
token = await api_get_token(juke_id)
|
|
||||||
if token is False:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.OK, detail="Invoice not paid"
|
|
||||||
)
|
|
||||||
elif retry:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await api_get_jukebox_invoice_paid(
|
|
||||||
song_id, juke_id, pay_hash
|
|
||||||
)
|
|
||||||
raise HTTPException(status_code=HTTPStatus.OK, detail="Invoice not paid")
|
|
||||||
|
|
||||||
|
|
||||||
############################GET TRACKS
|
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.get("/api/v1/jukebox/jb/currently/{juke_id}")
|
|
||||||
async def api_get_jukebox_currently(
|
|
||||||
retry: bool = Query(False), juke_id: str = Query(None)
|
|
||||||
):
|
|
||||||
jukebox = await get_jukebox(juke_id)
|
|
||||||
if not jukebox:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox")
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
assert jukebox.sp_access_token
|
|
||||||
r = await client.get(
|
|
||||||
"https://api.spotify.com/v1/me/player/currently-playing?market=ES",
|
|
||||||
timeout=40,
|
|
||||||
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
|
||||||
)
|
|
||||||
if r.status_code == 204:
|
|
||||||
raise HTTPException(status_code=HTTPStatus.OK, detail="Nothing")
|
|
||||||
elif r.status_code == 200:
|
|
||||||
try:
|
|
||||||
response = r.json()
|
|
||||||
|
|
||||||
track = {
|
|
||||||
"id": response["item"]["id"],
|
|
||||||
"name": response["item"]["name"],
|
|
||||||
"album": response["item"]["album"]["name"],
|
|
||||||
"artist": response["item"]["artists"][0]["name"],
|
|
||||||
"image": response["item"]["album"]["images"][0]["url"],
|
|
||||||
}
|
|
||||||
return track
|
|
||||||
except:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif r.status_code == 401:
|
|
||||||
token = await api_get_token(juke_id)
|
|
||||||
if token is False:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
|
|
||||||
)
|
|
||||||
elif retry:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return await api_get_jukebox_currently(retry=True, juke_id=juke_id)
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong"
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail="Something went wrong, or no song is playing yet",
|
|
||||||
)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue