diff --git a/lnbits/extensions/jukebox/README.md b/lnbits/extensions/jukebox/README.md deleted file mode 100644 index a5aa897a..00000000 --- a/lnbits/extensions/jukebox/README.md +++ /dev/null @@ -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"\ - ![add jukebox](https://i.imgur.com/NdVoKXd.png) -2. Follow the steps required on the form\ - - - give your jukebox a name - - select a wallet to receive payment - - define the price a user must pay to select a song\ - ![pick wallet price](https://i.imgur.com/4bJ8mb9.png) - - follow the steps to get your Spotify App and get the client ID and secret key\ - ![spotify keys](https://i.imgur.com/w2EzFtB.png) - - paste the codes in the form\ - ![api keys](https://i.imgur.com/6b9xauo.png) - - copy the _Redirect URL_ presented on the form\ - ![redirect url](https://i.imgur.com/GMzl0lG.png) - - on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt - ![spotify app setting](https://i.imgur.com/vb0x4Tl.png) - - back on LNbits, click "AUTORIZE ACCESS" and "Agree" on the page that will open - - choose on which device the LNbits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...) - - and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\ - ![select playlists](https://i.imgur.com/g4dbtED.png) - -3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\ - ![shareable jukebox](https://i.imgur.com/EAh9PI0.png) -4. The users will see the Jukebox page and choose a song from the selected playlist\ - ![select song](https://i.imgur.com/YYjeQAs.png) -5. After selecting a song they'd like to hear next a dialog will show presenting the music\ - ![play for sats](https://i.imgur.com/eEHl3o8.png) -6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py deleted file mode 100644 index 6307c923..00000000 --- a/lnbits/extensions/jukebox/__init__.py +++ /dev/null @@ -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)) diff --git a/lnbits/extensions/jukebox/config.json b/lnbits/extensions/jukebox/config.json deleted file mode 100644 index 283ab82b..00000000 --- a/lnbits/extensions/jukebox/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Spotify Jukebox", - "short_description": "Spotify jukebox middleware", - "tile": "/jukebox/static/image/jukebox.png", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py deleted file mode 100644 index f046727b..00000000 --- a/lnbits/extensions/jukebox/crud.py +++ /dev/null @@ -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 diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py deleted file mode 100644 index a0a3bd28..00000000 --- a/lnbits/extensions/jukebox/migrations.py +++ /dev/null @@ -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 - ); - """ - ) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py deleted file mode 100644 index 507504f4..00000000 --- a/lnbits/extensions/jukebox/models.py +++ /dev/null @@ -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) diff --git a/lnbits/extensions/jukebox/static/image/jukebox.png b/lnbits/extensions/jukebox/static/image/jukebox.png deleted file mode 100644 index bf78edf9..00000000 Binary files a/lnbits/extensions/jukebox/static/image/jukebox.png and /dev/null differ diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js deleted file mode 100644 index 1241b80e..00000000 --- a/lnbits/extensions/jukebox/static/js/index.js +++ /dev/null @@ -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 - } -}) diff --git a/lnbits/extensions/jukebox/static/spotapi.gif b/lnbits/extensions/jukebox/static/spotapi.gif deleted file mode 100644 index 023efc9a..00000000 Binary files a/lnbits/extensions/jukebox/static/spotapi.gif and /dev/null differ diff --git a/lnbits/extensions/jukebox/static/spotapi1.gif b/lnbits/extensions/jukebox/static/spotapi1.gif deleted file mode 100644 index 478032c5..00000000 Binary files a/lnbits/extensions/jukebox/static/spotapi1.gif and /dev/null differ diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py deleted file mode 100644 index 37489edb..00000000 --- a/lnbits/extensions/jukebox/tasks.py +++ /dev/null @@ -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) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html deleted file mode 100644 index 637e1453..00000000 --- a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html +++ /dev/null @@ -1,135 +0,0 @@ - - To use this extension you need a Spotify client ID and client secret. You get - these by creating an app in the Spotify developers dashboard - here - -

Select the playlists you want people to be able to pay for, share - the frontend page, profit :)

- Made by, - benarc. Inspired by, - pirosb3. -
- - - - - - - - GET /jukebox/api/v1/jukebox -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<jukebox_object>, ...] -
Curl example
- curl -X GET {{ request.base_url }}jukebox/api/v1/jukebox -H - "X-Api-Key: {{ user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /jukebox/api/v1/jukebox/<juke_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- <jukebox_object> -
Curl example
- curl -X GET {{ request.base_url - }}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
- - - - POST/PUT - /jukebox/api/v1/jukebox/ -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- <jukbox_object> -
Curl example
- 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 }}" - -
-
-
- - - - DELETE - /jukebox/api/v1/jukebox/<juke_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- <jukebox_object> -
Curl example
- curl -X DELETE {{ request.base_url - }}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{ - user.wallets[0].adminkey }}" - -
-
-
diff --git a/lnbits/extensions/jukebox/templates/jukebox/error.html b/lnbits/extensions/jukebox/templates/jukebox/error.html deleted file mode 100644 index f6f7fd58..00000000 --- a/lnbits/extensions/jukebox/templates/jukebox/error.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
-

Jukebox error

-
- - -
- Ask the host to turn on the device and launch spotify -
-
-
-
-
-
- - {% endblock %} {% block scripts %} - - - - {% endblock %} -
diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html deleted file mode 100644 index a67767fb..00000000 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ /dev/null @@ -1,388 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - Add Spotify Jukebox - - {% raw %} - - - - - - - {% endraw %} - - -
- -
- - -
- {{SITE_TITLE}} jukebox extension -
-
- - - {% include "jukebox/_api_docs.html" %} - -
-
- - - - - - - - - -
-
- Continue - Continue -
-
- Cancel -
-
- -
-
- - - - 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 -
-
- Open the Spotify Developer Dashboard - - - - - - - - -
-
- Submit keys - Submit keys -
-
- Cancel -
-
- -
-
- - - -

- In the app go to edit-settings, set the redirect URI to this link -

- - - {% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw - %} - - Click to copy URL - -
- Open the Spotify Application Settings -

-

- After adding the redirect URI, click the "Authorise access" button - below. -

- -
-
- Authorise access - Authorise access -
-
- Cancel -
-
- -
-
- - - - -
-
- Create Jukebox - Create Jukebox -
-
- Cancel -
-
-
-
-
-
- - - -
-
Shareable Jukebox QR
-
- - - -
- - Copy jukebox link - Open jukebox - Close -
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - -{% endblock %} diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html deleted file mode 100644 index 15e551e3..00000000 --- a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html +++ /dev/null @@ -1,281 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -

Currently playing

-
-
- -
-
- {% raw %} - {{ currentPlay.name }}
- {{ currentPlay.artist }} -
- {% endraw %} -
-
-
- - - -

Pick a song

- - -
- - - - - - -
- - - - -
-
- -
-
- {% raw %} - {{ receive.name }}
- {{ receive.artist }} -
-
-
-
-
- Play for {% endraw %}{{ price }}sats - -
-
-
- - - - - -
- Copy invoice -
-
-
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py deleted file mode 100644 index e1c55449..00000000 --- a/lnbits/extensions/jukebox/views.py +++ /dev/null @@ -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()}, - ) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py deleted file mode 100644 index 8e0bdd27..00000000 --- a/lnbits/extensions/jukebox/views_api.py +++ /dev/null @@ -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 "

Success!

You can close this window

" - - -@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", - )