From c0b18d78cc7ddb4a17ceceb595dee0699d9df507 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 27 Apr 2021 10:07:17 +0100 Subject: [PATCH 01/83] =?UTF-8?q?Initial=20commit=20=F0=9F=8E=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lnbits/extensions/jukebox/README.md | 5 + lnbits/extensions/jukebox/__init__.py | 14 ++ lnbits/extensions/jukebox/config.json | 6 + lnbits/extensions/jukebox/crud.py | 34 +++ lnbits/extensions/jukebox/migrations.py | 16 ++ lnbits/extensions/jukebox/models.py | 18 ++ lnbits/extensions/jukebox/static/js/index.js | 216 ++++++++++++++++++ .../jukebox/templates/jukebox/_api_docs.html | 146 ++++++++++++ .../jukebox/templates/jukebox/index.html | 211 +++++++++++++++++ .../jukebox/templates/jukebox/jukebox.html | 25 ++ lnbits/extensions/jukebox/views.py | 25 ++ lnbits/extensions/jukebox/views_api.py | 54 +++++ 12 files changed, 770 insertions(+) create mode 100644 lnbits/extensions/jukebox/README.md create mode 100644 lnbits/extensions/jukebox/__init__.py create mode 100644 lnbits/extensions/jukebox/config.json create mode 100644 lnbits/extensions/jukebox/crud.py create mode 100644 lnbits/extensions/jukebox/migrations.py create mode 100644 lnbits/extensions/jukebox/models.py create mode 100644 lnbits/extensions/jukebox/static/js/index.js create mode 100644 lnbits/extensions/jukebox/templates/jukebox/_api_docs.html create mode 100644 lnbits/extensions/jukebox/templates/jukebox/index.html create mode 100644 lnbits/extensions/jukebox/templates/jukebox/jukebox.html create mode 100644 lnbits/extensions/jukebox/views.py create mode 100644 lnbits/extensions/jukebox/views_api.py diff --git a/lnbits/extensions/jukebox/README.md b/lnbits/extensions/jukebox/README.md new file mode 100644 index 00000000..b92e7ea6 --- /dev/null +++ b/lnbits/extensions/jukebox/README.md @@ -0,0 +1,5 @@ +# Jukebox + +To use this extension you need a Spotify client ID and client secret. You get these by creating an app in the Spotify developers dashboard here https://developer.spotify.com/dashboard/applications + +Select the playlists you want people to be able to pay for, share the frontend page, profit :) diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py new file mode 100644 index 00000000..0e02b92e --- /dev/null +++ b/lnbits/extensions/jukebox/__init__.py @@ -0,0 +1,14 @@ +from quart import Blueprint + +from lnbits.db import Database + +db = Database("ext_jukebox") + +jukebox_ext: Blueprint = Blueprint( + "jukebox", __name__, static_folder="static", template_folder="templates" +) + + +from .views_api import * # noqa +from .views import * # noqa +from .lnurl import * # noqa diff --git a/lnbits/extensions/jukebox/config.json b/lnbits/extensions/jukebox/config.json new file mode 100644 index 00000000..04c69cc1 --- /dev/null +++ b/lnbits/extensions/jukebox/config.json @@ -0,0 +1,6 @@ +{ + "name": "Jukebox", + "short_description": "Spotify jukebox middleware", + "icon": "audiotrack", + "contributors": ["benarc"] +} diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py new file mode 100644 index 00000000..c0efe405 --- /dev/null +++ b/lnbits/extensions/jukebox/crud.py @@ -0,0 +1,34 @@ +from typing import List, Optional + +from . import db +from .wordlists import animals +from .models import Shop, Item + + +async def create_update_jukebox(wallet_id: str) -> int: + juke_id = urlsafe_short_hash() + result = await db.execute( + """ + INSERT INTO jukebox (id, wallet, user, secret, token, playlists) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (juke_id, wallet_id, "", "", "", ""), + ) + return result._result_proxy.lastrowid + + +async def get_jukebox(id: str) -> Optional[Jukebox]: + row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) + return Shop(**dict(row)) if row else None + +async def get_jukeboxs(id: str) -> Optional[Jukebox]: + row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) + return Shop(**dict(row)) if row else None + +async def delete_jukebox(shop: int, item_id: int): + await db.execute( + """ + DELETE FROM jukebox WHERE id = ? + """, + (shop, item_id), + ) diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py new file mode 100644 index 00000000..55229348 --- /dev/null +++ b/lnbits/extensions/jukebox/migrations.py @@ -0,0 +1,16 @@ +async def m001_initial(db): + """ + Initial jukebox table. + """ + await db.execute( + """ + CREATE TABLE jukebox ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + wallet TEXT NOT NULL, + user TEXT NOT NULL, + secret TEXT NOT NULL, + token TEXT NOT NULL, + playlists TEXT NOT NULL + ); + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py new file mode 100644 index 00000000..8286bc89 --- /dev/null +++ b/lnbits/extensions/jukebox/models.py @@ -0,0 +1,18 @@ +import json +import base64 +import hashlib +from collections import OrderedDict +from quart import url_for +from typing import NamedTuple, Optional, List, Dict + +class Jukebox(NamedTuple): + id: int + wallet: str + user: str + secret: str + token: str + playlists: str + + @classmethod + def from_row(cls, row: Row) -> "Charges": + return cls(**dict(row)) \ No newline at end of file diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js new file mode 100644 index 00000000..699f505b --- /dev/null +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -0,0 +1,216 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +const pica = window.pica() + +const defaultItemData = { + unit: 'sat' +} + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data() { + return { + selectedWallet: null, + confirmationMethod: 'wordlist', + wordlistTainted: false, + jukebox: { + method: null, + wordlist: [], + items: [] + }, + itemDialog: { + show: false, + data: {...defaultItemData}, + units: ['sat'] + } + } + }, + computed: { + printItems() { + return this.jukebox.items.filter(({enabled}) => enabled) + } + }, + methods: { + openNewDialog() { + this.itemDialog.show = true + this.itemDialog.data = {...defaultItemData} + }, + openUpdateDialog(itemId) { + this.itemDialog.show = true + let item = this.jukebox.items.find(item => item.id === itemId) + this.itemDialog.data = item + }, + imageAdded(file) { + let blobURL = URL.createObjectURL(file) + let image = new Image() + image.src = blobURL + image.onload = async () => { + let canvas = document.createElement('canvas') + canvas.setAttribute('width', 100) + canvas.setAttribute('height', 100) + await pica.resize(image, canvas, { + quality: 0, + alpha: true, + unsharpAmount: 95, + unsharpRadius: 0.9, + unsharpThreshold: 70 + }) + this.itemDialog.data.image = canvas.toDataURL() + this.itemDialog = {...this.itemDialog} + } + }, + imageCleared() { + this.itemDialog.data.image = null + this.itemDialog = {...this.itemDialog} + }, + disabledAddItemButton() { + return ( + !this.itemDialog.data.name || + this.itemDialog.data.name.length === 0 || + !this.itemDialog.data.price || + !this.itemDialog.data.description || + !this.itemDialog.data.unit || + this.itemDialog.data.unit.length === 0 + ) + }, + changedWallet(wallet) { + this.selectedWallet = wallet + this.loadShop() + }, + loadShop() { + LNbits.api + .request('GET', '/jukebox/api/v1/jukebox', this.selectedWallet.inkey) + .then(response => { + this.jukebox = response.data + this.confirmationMethod = response.data.method + this.wordlistTainted = false + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + async setMethod() { + try { + await LNbits.api.request( + 'PUT', + '/jukebox/api/v1/jukebox/method', + this.selectedWallet.inkey, + {method: this.confirmationMethod, wordlist: this.jukebox.wordlist} + ) + } catch (err) { + LNbits.utils.notifyApiError(err) + return + } + + this.$q.notify({ + message: + `Method set to ${this.confirmationMethod}.` + + (this.confirmationMethod === 'wordlist' ? ' Counter reset.' : ''), + timeout: 700 + }) + this.loadShop() + }, + async sendItem() { + let {id, name, image, description, price, unit} = this.itemDialog.data + const data = { + name, + description, + image, + price, + unit + } + + try { + if (id) { + await LNbits.api.request( + 'PUT', + '/jukebox/api/v1/jukebox/items/' + id, + this.selectedWallet.inkey, + data + ) + } else { + await LNbits.api.request( + 'POST', + '/jukebox/api/v1/jukebox/items', + this.selectedWallet.inkey, + data + ) + this.$q.notify({ + message: `Item '${this.itemDialog.data.name}' added.`, + timeout: 700 + }) + } + } catch (err) { + LNbits.utils.notifyApiError(err) + return + } + + this.loadShop() + this.itemDialog.show = false + this.itemDialog.data = {...defaultItemData} + }, + toggleItem(itemId) { + let item = this.jukebox.items.find(item => item.id === itemId) + item.enabled = !item.enabled + + LNbits.api + .request( + 'PUT', + '/jukebox/api/v1/jukebox/items/' + itemId, + this.selectedWallet.inkey, + item + ) + .then(response => { + this.$q.notify({ + message: `Item ${item.enabled ? 'enabled' : 'disabled'}.`, + timeout: 700 + }) + this.jukebox.items = this.jukebox.items + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + deleteItem(itemId) { + LNbits.utils + .confirmDialog('Are you sure you want to delete this item?') + .onOk(() => { + LNbits.api + .request( + 'DELETE', + '/jukebox/api/v1/jukebox/items/' + itemId, + this.selectedWallet.inkey + ) + .then(response => { + this.$q.notify({ + message: `Item deleted.`, + timeout: 700 + }) + this.jukebox.items.splice( + this.jukebox.items.findIndex(item => item.id === itemId), + 1 + ) + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }) + } + }, + created() { + this.selectedWallet = this.g.user.wallets[0] + this.loadShop() + + LNbits.api + .request('GET', '/jukebox/api/v1/currencies') + .then(response => { + this.itemDialog = {...this.itemDialog, units: ['sat', ...response.data]} + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + } +}) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html new file mode 100644 index 00000000..7d15aa8f --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html @@ -0,0 +1,146 @@ + + + +
    +
  1. Register items.
  2. +
  3. + Print QR codes and paste them on your store, your menu, somewhere, + somehow. +
  4. +
  5. + Clients scan the QR codes and get information about the items plus the + price on their phones directly (they must have internet) +
  6. +
  7. + Once they decide to pay, they'll get an invoice on their phones + automatically +
  8. +
  9. + When the payment is confirmed, a confirmation code will be issued for + them. +
  10. +
+

+ The confirmation codes are words from a predefined sequential word list. + Each new payment bumps the words sequence by 1. So you can check the + confirmation codes manually by just looking at them. +

+

+ For example, if your wordlist is + [apple, banana, coconut] the first purchase will be + apple, the second banana and so on. When it + gets to the end it starts from the beginning again. +

+

Powered by LNURL-pay.

+
+
+
+ + + + + + POST +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
Returns 201 OK
+
Curl example
+ curl -X GET {{ request.url_root }}/jukebox/api/v1/jukebox/items -H + "Content-Type: application/json" -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" -d '{"name": <string>, + "description": <string>, "image": <data-uri string>, + "price": <integer>, "unit": <"sat" or "USD">}' + +
+
+
+ + + + GET +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ {"id": <integer>, "wallet": <string>, "wordlist": + <string>, "items": [{"id": <integer>, "name": + <string>, "description": <string>, "image": + <string>, "enabled": <boolean>, "price": <integer>, + "unit": <string>, "lnurl": <string>}, ...]}< +
Curl example
+ curl -X GET {{ request.url_root }}/jukebox/api/v1/jukebox -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+ + + + PUT +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
Returns 200 OK
+
Curl example
+ curl -X GET {{ request.url_root + }}/jukebox/api/v1/jukebox/items/<item_id> -H "Content-Type: + application/json" -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -d + '{"name": <string>, "description": <string>, "image": + <data-uri string>, "price": <integer>, "unit": <"sat" + or "USD">}' + +
+
+
+ + + + DELETE +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
Returns 200 OK
+
Curl example
+ curl -X GET {{ request.url_root + }}/jukebox/api/v1/jukebox/items/<item_id> -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
+
diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html new file mode 100644 index 00000000..48a83d51 --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -0,0 +1,211 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + +
+
+
Items
+
+
+ Add jukebox +
+
+ {% raw %} + + + + + {% endraw %} +
+
+
+ +
+ + +
LNbits jukebox extension
+
+ + + {% include "jukebox/_api_docs.html" %} + +
+
+ + + + +
+
Adding a new item
+ + + + + +
+ Copy LNURL +
+ + + + + + + + + + +
+
+ + {% raw %}{{ itemDialog.data.id ? 'Update' : 'Add' }}{% endraw %} + Item + +
+
+ Cancel +
+
+
+
+
+
+
+{% 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 new file mode 100644 index 00000000..fff12b4c --- /dev/null +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -0,0 +1,25 @@ +{% extends "print.html" %} {% block page %} {% raw %} +
+
+
{{ item.name }}
+ +
{{ item.price }}
+
+
+{% endraw %} {% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py new file mode 100644 index 00000000..d434f831 --- /dev/null +++ b/lnbits/extensions/jukebox/views.py @@ -0,0 +1,25 @@ +import time +from datetime import datetime +from quart import g, render_template, request +from http import HTTPStatus + +from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.core.models import Payment +from lnbits.core.crud import get_standalone_payment + +from . import jukebox_ext +from .crud import get_jukebox + + +@jukebox_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("jukebox/index.html", user=g.user) + + +@jukebox_ext.route("/") +async def print_qr_codes(juke_id): + jukebox = await get_jukebox(juke_id) + + return await render_template("jukebox/jukebox.html", jukebox=jukebox) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py new file mode 100644 index 00000000..5433ddde --- /dev/null +++ b/lnbits/extensions/jukebox/views_api.py @@ -0,0 +1,54 @@ +from quart import g, jsonify +from http import HTTPStatus +from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore + +from lnbits.decorators import api_check_wallet_key, api_validate_post_request + +from . import jukebox_ext +from .crud import ( + create_update_jukebox, + get_jukebox, + get_jukeboxs, + delete_jukebox, +) +from .models import Jukebox + + +@jukebox_ext.route("/api/v1/jukebox", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_get_jukeboxs(): + jukebox = await get_jukeboxs(g.wallet.id) + return ( + jsonify( + { + jukebox._asdict() + } + ), + HTTPStatus.OK, + ) + +#websocket get spotify crap + +@jukebox_ext.route("/api/v1/jukebox/items", methods=["POST"]) +@jukebox_ext.route("/api/v1/jukebox/items/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + + schema={ + "wallet": {"type": "string", "empty": False}, + "user": {"type": "string", "empty": False}, + "secret": {"type": "string", "required": False}, + "token": {"type": "string", "required": True}, + "playlists": {"type": "string", "required": True}, + } +) +async def api_create_update_jukebox(item_id=None): + jukebox = await create_update_jukebox(g.wallet.id, **g.data) + return jsonify(jukebox._asdict()), HTTPStatus.CREATED + + +@jukebox_ext.route("/api/v1/jukebox/", methods=["DELETE"]) +@api_check_wallet_key("admin") +async def api_delete_item(juke_id): + shop = await delete_jukebox(juke_id) + return "", HTTPStatus.NO_CONTENT \ No newline at end of file From 3f8890def786bbe5377a2821bd4532c35383538e Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Tue, 27 Apr 2021 10:13:04 +0100 Subject: [PATCH 02/83] =?UTF-8?q?Fixed=20few=20bugs=20=F0=9F=8E=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lnbits/extensions/jukebox/__init__.py | 1 - lnbits/extensions/jukebox/crud.py | 3 +-- lnbits/extensions/jukebox/models.py | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index 0e02b92e..785c9c57 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -11,4 +11,3 @@ jukebox_ext: Blueprint = Blueprint( from .views_api import * # noqa from .views import * # noqa -from .lnurl import * # noqa diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index c0efe405..98d48dc3 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -1,8 +1,7 @@ from typing import List, Optional from . import db -from .wordlists import animals -from .models import Shop, Item +from .models import Jukebox async def create_update_jukebox(wallet_id: str) -> int: diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 8286bc89..020a699c 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -4,6 +4,7 @@ import hashlib from collections import OrderedDict from quart import url_for from typing import NamedTuple, Optional, List, Dict +from sqlite3 import Row class Jukebox(NamedTuple): id: int From 88895f80be27d9eed208ebb8c03122772dca0072 Mon Sep 17 00:00:00 2001 From: supertestnet <58400631+supertestnet@users.noreply.github.com> Date: Tue, 27 Apr 2021 10:22:14 -0400 Subject: [PATCH 03/83] note on virtualenv installation (#185) --- docs/guide/installation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 4f1bb853..3c979a3a 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -13,6 +13,7 @@ Download this repo and install the dependencies: ```sh git clone https://github.com/lnbits/lnbits.git cd lnbits/ +# on debian based installations like ubuntu, you may have to intall global python packages. Try sudo apt install python3-venv -y python3 -m venv venv ./venv/bin/pip install -r requirements.txt cp .env.example .env From b6f015b561f40ba281b7f594b4d207376722a295 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 27 Apr 2021 15:52:44 -0300 Subject: [PATCH 04/83] lnurlp: account for invalid amount in querystring. --- lnbits/extensions/lnurlp/lnurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index 79076564..5d8bcf08 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -54,7 +54,7 @@ async def api_lnurl_callback(link_id): min = link.min * 1000 max = link.max * 1000 - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < min: return ( jsonify( From dc10a0f52b880f60ee1ae8a03e979c0ae3ac0f32 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 28 Apr 2021 12:04:47 +0100 Subject: [PATCH 05/83] Connects to spotify --- lnbits/extensions/jukebox/__init__.py | 1 - lnbits/extensions/jukebox/config.json | 2 +- lnbits/extensions/jukebox/crud.py | 55 +++- lnbits/extensions/jukebox/migrations.py | 17 +- lnbits/extensions/jukebox/models.py | 18 +- lnbits/extensions/jukebox/static/js/index.js | 304 +++++++----------- .../jukebox/templates/jukebox/_api_docs.html | 42 +-- .../jukebox/templates/jukebox/index.html | 251 +++++++++------ lnbits/extensions/jukebox/views_api.py | 58 ++-- 9 files changed, 395 insertions(+), 353 deletions(-) diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py index 785c9c57..b6ec402f 100644 --- a/lnbits/extensions/jukebox/__init__.py +++ b/lnbits/extensions/jukebox/__init__.py @@ -8,6 +8,5 @@ jukebox_ext: Blueprint = Blueprint( "jukebox", __name__, static_folder="static", template_folder="templates" ) - from .views_api import * # noqa from .views import * # noqa diff --git a/lnbits/extensions/jukebox/config.json b/lnbits/extensions/jukebox/config.json index 04c69cc1..def7c7a3 100644 --- a/lnbits/extensions/jukebox/config.json +++ b/lnbits/extensions/jukebox/config.json @@ -1,5 +1,5 @@ { - "name": "Jukebox", + "name": "SpotifyJukebox", "short_description": "Spotify jukebox middleware", "icon": "audiotrack", "contributors": ["benarc"] diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index 98d48dc3..99d1c4cd 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -2,32 +2,69 @@ from typing import List, Optional from . import db from .models import Jukebox +from lnbits.helpers import urlsafe_short_hash -async def create_update_jukebox(wallet_id: str) -> int: +async def create_jukebox( + wallet: str, + title: str, + price: int, + sp_user: str, + sp_secret: str, + sp_token: Optional[str] = "", + sp_device: Optional[str] = "", + sp_playlists: Optional[str] = "", +) -> Jukebox: juke_id = urlsafe_short_hash() result = await db.execute( """ - INSERT INTO jukebox (id, wallet, user, secret, token, playlists) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO jukebox (id, title, wallet, sp_user, sp_secret, sp_token, sp_device, sp_playlists, price) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, - (juke_id, wallet_id, "", "", "", ""), + ( + juke_id, + title, + wallet, + sp_user, + sp_secret, + sp_token, + sp_device, + sp_playlists, + int(price), + ), ) - return result._result_proxy.lastrowid + jukebox = await get_jukebox(juke_id) + assert jukebox, "Newly created Jukebox couldn't be retrieved" + return jukebox + +async def update_jukebox(sp_user: str, **kwargs) -> Optional[Jukebox]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE jukebox SET {q} WHERE sp_user = ?", (*kwargs.values(), sp_user) + ) + row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (sp_user,)) + return Jukebox(**row) if row else None async def get_jukebox(id: str) -> Optional[Jukebox]: row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) - return Shop(**dict(row)) if row else None + return Jukebox(**row) if row else None + + +async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: + row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (user,)) + return Jukebox(**row) if row else None + async def get_jukeboxs(id: str) -> Optional[Jukebox]: - row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) - return Shop(**dict(row)) if row else None + rows = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) + return [Jukebox(**row) for row in rows] + async def delete_jukebox(shop: int, item_id: int): await db.execute( """ DELETE FROM jukebox WHERE id = ? """, - (shop, item_id), + (Jukebox, item_id), ) diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py index 55229348..365f13e5 100644 --- a/lnbits/extensions/jukebox/migrations.py +++ b/lnbits/extensions/jukebox/migrations.py @@ -5,12 +5,15 @@ async def m001_initial(db): await db.execute( """ CREATE TABLE jukebox ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - wallet TEXT NOT NULL, - user TEXT NOT NULL, - secret TEXT NOT NULL, - token TEXT NOT NULL, - playlists TEXT NOT NULL + id TEXT PRIMARY KEY, + title TEXT, + wallet TEXT, + sp_user TEXT NOT NULL, + sp_secret TEXT NOT NULL, + sp_token TEXT, + sp_device TEXT, + sp_playlists TEXT, + price INTEGER ); """ - ) \ No newline at end of file + ) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 020a699c..9f3ec629 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -6,14 +6,18 @@ from quart import url_for from typing import NamedTuple, Optional, List, Dict from sqlite3 import Row + class Jukebox(NamedTuple): - id: int + id: str + title: str wallet: str - user: str - secret: str - token: str - playlists: str + sp_user: str + sp_secret: str + sp_token: str + sp_device: str + sp_playlists: str + price: int @classmethod - def from_row(cls, row: Row) -> "Charges": - return cls(**dict(row)) \ No newline at end of file + def from_row(cls, row: Row) -> "Jukebox": + return cls(**dict(row)) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 699f505b..07498053 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -4,28 +4,25 @@ Vue.component(VueQrcode.name, VueQrcode) const pica = window.pica() -const defaultItemData = { - unit: 'sat' -} + new Vue({ el: '#vue', mixins: [windowMixin], data() { return { - selectedWallet: null, - confirmationMethod: 'wordlist', - wordlistTainted: false, - jukebox: { - method: null, - wordlist: [], - items: [] - }, - itemDialog: { + isPwd: true, + tokenFetched: true, + device: [], + jukebox: {}, + playlists: [], + step: 1, + locationcbPath: "", + jukeboxDialog: { show: false, - data: {...defaultItemData}, - units: ['sat'] - } + data: {} + }, + spotifyDialog: false } }, computed: { @@ -34,183 +31,130 @@ new Vue({ } }, methods: { - openNewDialog() { - this.itemDialog.show = true - this.itemDialog.data = {...defaultItemData} + closeFormDialog() { + this.jukeboxDialog.data = {} + this.jukeboxDialog.show = false + this.step = 1 }, - openUpdateDialog(itemId) { - this.itemDialog.show = true - let item = this.jukebox.items.find(item => item.id === itemId) - this.itemDialog.data = item - }, - imageAdded(file) { - let blobURL = URL.createObjectURL(file) - let image = new Image() - image.src = blobURL - image.onload = async () => { - let canvas = document.createElement('canvas') - canvas.setAttribute('width', 100) - canvas.setAttribute('height', 100) - await pica.resize(image, canvas, { - quality: 0, - alpha: true, - unsharpAmount: 95, - unsharpRadius: 0.9, - unsharpThreshold: 70 + submitSpotify() { + + self = this + console.log(self.jukeboxDialog.data) + self.requestAuthorization() + this.$q.notify({ + spinner: true, + message: 'Fetching token', + timeout: 4000 }) - this.itemDialog.data.image = canvas.toDataURL() - this.itemDialog = {...this.itemDialog} - } - }, - imageCleared() { - this.itemDialog.data.image = null - this.itemDialog = {...this.itemDialog} - }, - disabledAddItemButton() { - return ( - !this.itemDialog.data.name || - this.itemDialog.data.name.length === 0 || - !this.itemDialog.data.price || - !this.itemDialog.data.description || - !this.itemDialog.data.unit || - this.itemDialog.data.unit.length === 0 - ) - }, - changedWallet(wallet) { - this.selectedWallet = wallet - this.loadShop() - }, - loadShop() { - LNbits.api - .request('GET', '/jukebox/api/v1/jukebox', this.selectedWallet.inkey) - .then(response => { - this.jukebox = response.data - this.confirmationMethod = response.data.method - this.wordlistTainted = false - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - async setMethod() { - try { - await LNbits.api.request( - 'PUT', - '/jukebox/api/v1/jukebox/method', - this.selectedWallet.inkey, - {method: this.confirmationMethod, wordlist: this.jukebox.wordlist} - ) - } catch (err) { - LNbits.utils.notifyApiError(err) - return - } - - this.$q.notify({ - message: - `Method set to ${this.confirmationMethod}.` + - (this.confirmationMethod === 'wordlist' ? ' Counter reset.' : ''), - timeout: 700 - }) - this.loadShop() - }, - async sendItem() { - let {id, name, image, description, price, unit} = this.itemDialog.data - const data = { - name, - description, - image, - price, - unit - } - - try { - if (id) { - await LNbits.api.request( - 'PUT', - '/jukebox/api/v1/jukebox/items/' + id, - this.selectedWallet.inkey, - data - ) - } else { - await LNbits.api.request( - 'POST', - '/jukebox/api/v1/jukebox/items', - this.selectedWallet.inkey, - data - ) - this.$q.notify({ - message: `Item '${this.itemDialog.data.name}' added.`, - timeout: 700 - }) - } - } catch (err) { - LNbits.utils.notifyApiError(err) - return - } - - this.loadShop() - this.itemDialog.show = false - this.itemDialog.data = {...defaultItemData} - }, - toggleItem(itemId) { - let item = this.jukebox.items.find(item => item.id === itemId) - item.enabled = !item.enabled - - LNbits.api - .request( - 'PUT', - '/jukebox/api/v1/jukebox/items/' + itemId, - this.selectedWallet.inkey, - item + LNbits.api.request( + 'POST', + '/jukebox/api/v1/jukebox/', + self.g.user.wallets[0].adminkey, + self.jukeboxDialog.data ) .then(response => { - this.$q.notify({ - message: `Item ${item.enabled ? 'enabled' : 'disabled'}.`, - timeout: 700 - }) - this.jukebox.items = this.jukebox.items - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - deleteItem(itemId) { - LNbits.utils - .confirmDialog('Are you sure you want to delete this item?') - .onOk(() => { - LNbits.api - .request( - 'DELETE', - '/jukebox/api/v1/jukebox/items/' + itemId, - this.selectedWallet.inkey - ) + if(response.data){ + var timerId = setInterval(function(){ + if(!self.jukeboxDialog.data.sp_user){ + clearInterval(timerId); + } + LNbits.api + .request('GET', '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_user, self.g.user.wallets[0].inkey) .then(response => { - this.$q.notify({ - message: `Item deleted.`, - timeout: 700 + if(response.data.sp_token){ + console.log(response.data.sp_token) + + self.step = 3 + clearInterval(timerId); + self.refreshPlaylists() + self.$q.notify({ + message: 'Success! App is now linked!', + timeout: 3000 }) - this.jukebox.items.splice( - this.jukebox.items.findIndex(item => item.id === itemId), - 1 - ) + //set devices, playlists + } }) .catch(err => { - LNbits.utils.notifyApiError(err) + LNbits.utils.notifyApiError(err) }) + }, 3000) + } }) - } + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + requestAuthorization(){ + self = this + let url = 'https://accounts.spotify.com/authorize' + url += '?scope=user-modify-playback-state%20user-read-playback-position' + url += '%20user-library-read%20streaming%20user-read-playback-state' + url += '%20user-read-recently-played%20playlist-read-private&response_type=code' + url += '&redirect_uri=' + encodeURIComponent(self.locationcbPath) + self.jukeboxDialog.data.sp_user + url += '&client_id=' + self.jukeboxDialog.data.sp_user + url += '&show_dialog=true' + console.log(url) + window.open(url) + }, + openNewDialog() { + this.jukeboxDialog.show = true + this.jukeboxDialog.data = {} + }, + openUpdateDialog(itemId) { + this.jukeboxDialog.show = true + let item = this.jukebox.items.find(item => item.id === itemId) + this.jukeboxDialog.data = item + }, + + callApi(method, url, body, callback){ + let xhr = new XMLHttpRequest() + xhr.open(method, url, true) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.setRequestHeader('Authorization', 'Bearer ' + self.jukeboxDialog.data.sp_token) + xhr.send(body) + xhr.onload = callback + }, + refreshPlaylists(){ + console.log("sdfvasdv") + callApi( "GET", "https://api.spotify.com/v1/me/playlists", null, handlePlaylistsResponse ) + }, + handlePlaylistsResponse(){ + console.log("data") + if ( this.status == 200 ){ + var data = JSON.parse(this.responseText) + console.log(data) + } + else if ( this.status == 401 ){ + refreshAccessToken() + } + else { + console.log(this.responseText) + alert(this.responseText) + } + }, + refreshAccessToken(){ + refresh_token = localStorage.getItem("refresh_token") + let body = "grant_type=refresh_token" + body += "&refresh_token=" + self.jukeboxDialog.data.sp_token + body += "&client_id=" + self.jukeboxDialog.data.sp_user + callAuthorizationApi(body) + }, + callAuthorizationApi(body){ + let xhr = new XMLHttpRequest() + xhr.open("POST", TOKEN, true) + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') + xhr.setRequestHeader('Authorization', 'Basic ' + btoa(self.jukeboxDialog.data.sp_user + ":" + self.jukeboxDialog.data.sp_secret)) + xhr.send(body) + xhr.onload = handleAuthorizationResponse + }, }, created() { this.selectedWallet = this.g.user.wallets[0] - this.loadShop() - - LNbits.api - .request('GET', '/jukebox/api/v1/currencies') - .then(response => { - this.itemDialog = {...this.itemDialog, units: ['sat', ...response.data]} - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) + this.locationcbPath = String([ + window.location.protocol, + '//', + window.location.host, + '/jukebox/api/v1/jukebox/spotify/cb/' + ].join('')) } }) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html index 7d15aa8f..f705aead 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html +++ b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html @@ -1,42 +1,20 @@ -
    -
  1. Register items.
  2. -
  3. - Print QR codes and paste them on your store, your menu, somewhere, - somehow. -
  4. -
  5. - Clients scan the QR codes and get information about the items plus the - price on their phones directly (they must have internet) -
  6. -
  7. - Once they decide to pay, they'll get an invoice on their phones - automatically -
  8. -
  9. - When the payment is confirmed, a confirmation code will be issued for - them. -
  10. -
-

- The confirmation codes are words from a predefined sequential word list. - Each new payment bumps the words sequence by 1. So you can check the - confirmation codes manually by just looking at them. -

-

- For example, if your wordlist is - [apple, banana, coconut] the first purchase will be - apple, the second banana and so on. When it - gets to the end it starts from the beginning again. -

-

Powered by LNURL-pay.

+ 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.
diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index 48a83d51..ab056931 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -4,15 +4,15 @@
+ Add Spotify Jukebox
-
-
Items
-
-
- Add jukebox -
+
Items
{% raw %}
- - - -
-
Adding a new item
- - - - - -
- Copy LNURL -
- + + + + - - - - - - - -
-
+ + +
+
Continue - {% raw %}{{ itemDialog.data.id ? 'Update' : 'Add' }}{% endraw %} - Item - + 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 developers dashboard + here.
+ In the app go to edit-settings, set the redirect URI to this link + (replacing the CLIENT-ID with your own) {% raw %}{{ locationcbPath + }}CLIENT-ID{% endraw %} + + + + + + +
+
+ Get token + Get token +
+
+ Cancel +
+
+ +
+
+ + + + +
+
+ Create Jukebox +
+
+ Cancel +
+
+
+
diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 5433ddde..fa719427 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -1,13 +1,15 @@ -from quart import g, jsonify +from quart import g, jsonify, request from http import HTTPStatus from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from lnbits.decorators import api_check_wallet_key, api_validate_post_request - +import httpx from . import jukebox_ext from .crud import ( - create_update_jukebox, + create_jukebox, + update_jukebox, get_jukebox, + get_jukebox_by_user, get_jukeboxs, delete_jukebox, ) @@ -17,33 +19,45 @@ from .models import Jukebox @jukebox_ext.route("/api/v1/jukebox", methods=["GET"]) @api_check_wallet_key("invoice") async def api_get_jukeboxs(): - jukebox = await get_jukeboxs(g.wallet.id) - return ( - jsonify( - { - jukebox._asdict() - } - ), - HTTPStatus.OK, + jsonify([jukebox._asdict() for jukebox in await get_jukeboxs(g.wallet.id)]), + + +##################SPOTIFY AUTH##################### + + +@jukebox_ext.route("/api/v1/jukebox/spotify/cb//", methods=["GET"]) +async def api_check_credentials_callbac(sp_user): + jukebox = await get_jukebox_by_user(sp_user) + jukebox = await update_jukebox( + sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_token=request.args.get('code') ) + return "

Success!

You can close this window

" -#websocket get spotify crap +@jukebox_ext.route("/api/v1/jukebox/spotify/", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_check_credentials_check(sp_user): + jukebox = await get_jukebox_by_user(sp_user) + return jsonify(jukebox._asdict()), HTTPStatus.CREATED -@jukebox_ext.route("/api/v1/jukebox/items", methods=["POST"]) -@jukebox_ext.route("/api/v1/jukebox/items/", methods=["PUT"]) + +@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"]) +@jukebox_ext.route("/api/v1/jukebox/", methods=["PUT"]) @api_check_wallet_key("admin") @api_validate_post_request( - schema={ - "wallet": {"type": "string", "empty": False}, - "user": {"type": "string", "empty": False}, - "secret": {"type": "string", "required": False}, - "token": {"type": "string", "required": True}, - "playlists": {"type": "string", "required": True}, + "title": {"type": "string", "empty": False, "required": True}, + "wallet": {"type": "string", "empty": False, "required": True}, + "sp_user": {"type": "string", "empty": False, "required": True}, + "sp_secret": {"type": "string", "required": True}, + "sp_token": {"type": "string", "required": False}, + "sp_device": {"type": "string", "required": False}, + "sp_playlists": {"type": "string", "required": False}, + "price": {"type": "string", "required": True}, } ) async def api_create_update_jukebox(item_id=None): - jukebox = await create_update_jukebox(g.wallet.id, **g.data) + print(g.data) + jukebox = await create_jukebox(**g.data) return jsonify(jukebox._asdict()), HTTPStatus.CREATED @@ -51,4 +65,4 @@ async def api_create_update_jukebox(item_id=None): @api_check_wallet_key("admin") async def api_delete_item(juke_id): shop = await delete_jukebox(juke_id) - return "", HTTPStatus.NO_CONTENT \ No newline at end of file + return "", HTTPStatus.NO_CONTENT From b594281f83acd0c52c9295e3e683d337ff1f274f Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Thu, 29 Apr 2021 00:40:58 +0100 Subject: [PATCH 06/83] Can connect to spotify ccount --- lnbits/extensions/jukebox/crud.py | 10 +- lnbits/extensions/jukebox/migrations.py | 3 +- lnbits/extensions/jukebox/models.py | 3 +- lnbits/extensions/jukebox/static/js/index.js | 137 ++++++++++++------ .../jukebox/templates/jukebox/index.html | 9 +- lnbits/extensions/jukebox/views.py | 4 +- lnbits/extensions/jukebox/views_api.py | 32 ++-- 7 files changed, 137 insertions(+), 61 deletions(-) diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index 99d1c4cd..da27e74d 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -11,15 +11,16 @@ async def create_jukebox( price: int, sp_user: str, sp_secret: str, - sp_token: Optional[str] = "", + sp_access_token: Optional[str] = "", + sp_refresh_token: Optional[str] = "", sp_device: Optional[str] = "", sp_playlists: Optional[str] = "", ) -> Jukebox: juke_id = urlsafe_short_hash() result = await db.execute( """ - INSERT INTO jukebox (id, title, wallet, sp_user, sp_secret, sp_token, sp_device, sp_playlists, price) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO jukebox (id, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( juke_id, @@ -27,7 +28,8 @@ async def create_jukebox( wallet, sp_user, sp_secret, - sp_token, + sp_access_token, + sp_refresh_token, sp_device, sp_playlists, int(price), diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py index 365f13e5..86f675fe 100644 --- a/lnbits/extensions/jukebox/migrations.py +++ b/lnbits/extensions/jukebox/migrations.py @@ -10,7 +10,8 @@ async def m001_initial(db): wallet TEXT, sp_user TEXT NOT NULL, sp_secret TEXT NOT NULL, - sp_token TEXT, + sp_access_token TEXT, + sp_refresh_token TEXT, sp_device TEXT, sp_playlists TEXT, price INTEGER diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 9f3ec629..dddebbc5 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -13,7 +13,8 @@ class Jukebox(NamedTuple): wallet: str sp_user: str sp_secret: str - sp_token: str + sp_access_token: str + sp_refresh_token: str sp_device: str sp_playlists: str price: int diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 07498053..fa5fd390 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -13,11 +13,12 @@ new Vue({ return { isPwd: true, tokenFetched: true, - device: [], + devices: [], jukebox: {}, playlists: [], step: 1, locationcbPath: "", + locationcb: "", jukeboxDialog: { show: false, data: {} @@ -55,22 +56,26 @@ new Vue({ .then(response => { if(response.data){ var timerId = setInterval(function(){ + console.log(response.data) if(!self.jukeboxDialog.data.sp_user){ clearInterval(timerId); } + self.jukeboxDialog.data.sp_id = response.data.id LNbits.api - .request('GET', '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_user, self.g.user.wallets[0].inkey) + .request('GET', '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_id, self.g.user.wallets[0].inkey) .then(response => { - if(response.data.sp_token){ - console.log(response.data.sp_token) - + if(response.data.sp_access_token){ + self.jukeboxDialog.data.sp_access_token = response.data.sp_access_token self.step = 3 - clearInterval(timerId); - self.refreshPlaylists() - self.$q.notify({ - message: 'Success! App is now linked!', - timeout: 3000 - }) + self.fetchAccessToken() + + clearInterval(timerId) + + // self.refreshPlaylists(response.data.sp_token) +// self.$q.notify({ +// message: 'Success! App is now linked!', +// timeout: 3000 +// }) //set devices, playlists } }) @@ -86,13 +91,12 @@ new Vue({ }, requestAuthorization(){ self = this - let url = 'https://accounts.spotify.com/authorize' - url += '?scope=user-modify-playback-state%20user-read-playback-position' - url += '%20user-library-read%20streaming%20user-read-playback-state' - url += '%20user-read-recently-played%20playlist-read-private&response_type=code' - url += '&redirect_uri=' + encodeURIComponent(self.locationcbPath) + self.jukeboxDialog.data.sp_user - url += '&client_id=' + self.jukeboxDialog.data.sp_user - url += '&show_dialog=true' + 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_user) + 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' console.log(url) window.open(url) }, @@ -105,47 +109,96 @@ new Vue({ let item = this.jukebox.items.find(item => item.id === itemId) this.jukeboxDialog.data = item }, + createJukebox(){ + self = this - callApi(method, url, body, callback){ + LNbits.api.request( + 'PUT', + '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, + self.g.user.wallets[0].adminkey, + self.jukeboxDialog.data + ) + .then(response => { + console.log(response.data) + + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + + 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 ' + self.jukeboxDialog.data.sp_token) + xhr.setRequestHeader('Authorization', 'Bearer ' + this.jukeboxDialog.data.sp_access_token) xhr.send(body) - xhr.onload = callback + xhr.onload = function() { + let responseObj = JSON.parse(xhr.response) + console.log(responseObj.items) + var i; + for (i = 0; i < responseObj.items.length; i++) { + self.playlists.push(responseObj.items[i].name + "-" + responseObj.items[i].id) + } + } }, refreshPlaylists(){ - console.log("sdfvasdv") - callApi( "GET", "https://api.spotify.com/v1/me/playlists", null, handlePlaylistsResponse ) + self = this + self.playlistApi( "GET", "https://api.spotify.com/v1/me/playlists", null) }, - handlePlaylistsResponse(){ - console.log("data") - if ( this.status == 200 ){ - var data = JSON.parse(this.responseText) - console.log(data) - } - else if ( this.status == 401 ){ - refreshAccessToken() - } - else { - console.log(this.responseText) - alert(this.responseText) + 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() { + let responseObj = xhr.response + alert(responseObj) + var i; + for (i = 0; i < responseObj.items.length; i++) { + self.devices.push(responseObj.items[i].name + "-" + responseObj.items[i].id) + } } }, + refreshDevices(){ + self = this + self.deviceApi( "GET", "https://api.spotify.com/v1/me/player/devices", null) + }, + fetchAccessToken( ){ + self = this + let body = "grant_type=authorization_code" + body += "&code=" + self.jukeboxDialog.data.sp_access_token + body += '&redirect_uri=' + encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_user) + this.callAuthorizationApi(body) + }, refreshAccessToken(){ - refresh_token = localStorage.getItem("refresh_token") + self = this let body = "grant_type=refresh_token" - body += "&refresh_token=" + self.jukeboxDialog.data.sp_token + body += "&refresh_token=" + self.jukeboxDialog.data.sp_refresh_token body += "&client_id=" + self.jukeboxDialog.data.sp_user - callAuthorizationApi(body) + this.callAuthorizationApi(body) }, callAuthorizationApi(body){ + self = this let xhr = new XMLHttpRequest() - xhr.open("POST", TOKEN, true) + 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.setRequestHeader('Authorization', 'Basic ' + btoa(this.jukeboxDialog.data.sp_user + ":" + this.jukeboxDialog.data.sp_secret)) xhr.send(body) - xhr.onload = handleAuthorizationResponse + console.log(('Authorization', 'Basic ' + btoa(this.jukeboxDialog.data.sp_user + ":" + this.jukeboxDialog.data.sp_secret))) + xhr.onload = function() { + let responseObj = JSON.parse(xhr.response) + alert(responseObj.access_token) + alert(responseObj.refresh_token) + self.jukeboxDialog.data.sp_access_token = responseObj.access_token + self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token + console.log(self.jukeboxDialog.data) + self.refreshPlaylists() + self.refreshDevices() + } }, }, created() { @@ -156,5 +209,7 @@ new Vue({ window.location.host, '/jukebox/api/v1/jukebox/spotify/cb/' ].join('')) + console.log(this.locationcbPath) + this.locationcb = this.locationcbPath } }) diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index ab056931..07e06ebd 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -174,7 +174,7 @@ >here.
In the app go to edit-settings, set the redirect URI to this link - (replacing the CLIENT-ID with your own) {% raw %}{{ locationcbPath + (replacing the CLIENT-ID with your own) {% raw %}{{ locationcb }}CLIENT-ID{% endraw %}
- Create Jukebox + Create Jukebox
/", methods=["GET"]) +@jukebox_ext.route("/api/v1/jukebox/spotify/cb/", methods=["GET"]) async def api_check_credentials_callbac(sp_user): + sp_code = "" + sp_access_token = "" + sp_refresh_token = "" + print(request.args) jukebox = await get_jukebox_by_user(sp_user) - jukebox = await update_jukebox( - sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_token=request.args.get('code') - ) + if request.args.get('code'): + sp_code = request.args.get('code') + jukebox = await update_jukebox( + sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_access_token=sp_code + ) + if request.args.get('access_token'): + sp_access_token = request.args.get('access_token') + sp_refresh_token = request.args.get('refresh_token') + jukebox = await update_jukebox( + sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_access_token=sp_access_token, sp_refresh_token=sp_refresh_token + ) return "

Success!

You can close this window

" -@jukebox_ext.route("/api/v1/jukebox/spotify/", methods=["GET"]) +@jukebox_ext.route("/api/v1/jukebox/spotify/", methods=["GET"]) @api_check_wallet_key("invoice") -async def api_check_credentials_check(sp_user): - jukebox = await get_jukebox_by_user(sp_user) +async def api_check_credentials_check(sp_id): + jukebox = await get_jukebox(sp_id) return jsonify(jukebox._asdict()), HTTPStatus.CREATED @@ -49,14 +61,16 @@ async def api_check_credentials_check(sp_user): "wallet": {"type": "string", "empty": False, "required": True}, "sp_user": {"type": "string", "empty": False, "required": True}, "sp_secret": {"type": "string", "required": True}, - "sp_token": {"type": "string", "required": False}, + "sp_access_token": {"type": "string", "required": False}, + "sp_refresh_token": {"type": "string", "required": False}, "sp_device": {"type": "string", "required": False}, "sp_playlists": {"type": "string", "required": False}, "price": {"type": "string", "required": True}, } ) async def api_create_update_jukebox(item_id=None): - print(g.data) + if item_id: + jukebox = await update_jukebox(**g.data) jukebox = await create_jukebox(**g.data) return jsonify(jukebox._asdict()), HTTPStatus.CREATED From 38c2270abfa36e1c6909321e73639f05419851bc Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Thu, 29 Apr 2021 08:47:58 +0100 Subject: [PATCH 07/83] black + added devices --- lnbits/extensions/jukebox/crud.py | 1 + lnbits/extensions/jukebox/static/js/index.js | 17 ++++++++++------- lnbits/extensions/jukebox/views.py | 3 ++- lnbits/extensions/jukebox/views_api.py | 16 ++++++++++------ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index da27e74d..3cdaf9dc 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -39,6 +39,7 @@ async def create_jukebox( assert jukebox, "Newly created Jukebox couldn't be retrieved" return jukebox + async def update_jukebox(sp_user: str, **kwargs) -> Optional[Jukebox]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index fa5fd390..bae0a0e7 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -94,7 +94,7 @@ new Vue({ 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_user) + url += '&redirect_uri=' + encodeURI(self.locationcbPath) + self.jukeboxDialog.data.sp_user 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' console.log(url) @@ -136,7 +136,7 @@ new Vue({ xhr.send(body) xhr.onload = function() { let responseObj = JSON.parse(xhr.response) - console.log(responseObj.items) + self.playlists = [] var i; for (i = 0; i < responseObj.items.length; i++) { self.playlists.push(responseObj.items[i].name + "-" + responseObj.items[i].id) @@ -155,12 +155,15 @@ new Vue({ xhr.setRequestHeader('Authorization', 'Bearer ' + this.jukeboxDialog.data.sp_access_token) xhr.send(body) xhr.onload = function() { - let responseObj = xhr.response - alert(responseObj) + let responseObj = JSON.parse(xhr.response) + console.log(responseObj.devices[0]) + self.devices = [] var i; - for (i = 0; i < responseObj.items.length; i++) { - self.devices.push(responseObj.items[i].name + "-" + responseObj.items[i].id) + for (i = 0; i < responseObj.devices.length; i++) { + self.devices.push(responseObj.devices[i].name + "-" + responseObj.devices[i].id) + console.log(responseObj.devices[i].name) } + } }, refreshDevices(){ @@ -171,7 +174,7 @@ new Vue({ self = this let body = "grant_type=authorization_code" body += "&code=" + self.jukeboxDialog.data.sp_access_token - body += '&redirect_uri=' + encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_user) + body += '&redirect_uri=' + encodeURI(self.locationcbPath) + self.jukeboxDialog.data.sp_user this.callAuthorizationApi(body) }, refreshAccessToken(){ diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index bfac9c87..c3bf621a 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -11,6 +11,7 @@ from . import jukebox_ext from .crud import get_jukebox from urllib.parse import unquote + @jukebox_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() @@ -22,4 +23,4 @@ async def index(): async def print_qr_codes(juke_id): jukebox = await get_jukebox(juke_id) - return await render_template("jukebox/jukebox.html", jukebox=jukebox) \ No newline at end of file + return await render_template("jukebox/jukebox.html", jukebox=jukebox) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 3e1eb6b3..d69032c8 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -32,19 +32,23 @@ async def api_check_credentials_callbac(sp_user): sp_refresh_token = "" print(request.args) jukebox = await get_jukebox_by_user(sp_user) - if request.args.get('code'): - sp_code = request.args.get('code') + if request.args.get("code"): + sp_code = request.args.get("code") jukebox = await update_jukebox( sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_access_token=sp_code ) - if request.args.get('access_token'): - sp_access_token = request.args.get('access_token') - sp_refresh_token = request.args.get('refresh_token') + if request.args.get("access_token"): + sp_access_token = request.args.get("access_token") + sp_refresh_token = request.args.get("refresh_token") jukebox = await update_jukebox( - sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_access_token=sp_access_token, sp_refresh_token=sp_refresh_token + sp_user=sp_user, + sp_secret=jukebox.sp_secret, + sp_access_token=sp_access_token, + sp_refresh_token=sp_refresh_token, ) return "

Success!

You can close this window

" + @jukebox_ext.route("/api/v1/jukebox/spotify/", methods=["GET"]) @api_check_wallet_key("invoice") async def api_check_credentials_check(sp_id): From 245a819f199fc8a33ecb44e578d4c9ee6018aab4 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Thu, 29 Apr 2021 22:38:57 +0100 Subject: [PATCH 08/83] Table working --- lnbits/extensions/jukebox/crud.py | 28 +++-- lnbits/extensions/jukebox/migrations.py | 1 + lnbits/extensions/jukebox/models.py | 1 + lnbits/extensions/jukebox/static/js/index.js | 114 +++++++++++++----- .../jukebox/templates/jukebox/index.html | 89 +++++++------- lnbits/extensions/jukebox/views_api.py | 38 +++++- 6 files changed, 178 insertions(+), 93 deletions(-) diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index 3cdaf9dc..01159c8e 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -6,6 +6,7 @@ from lnbits.helpers import urlsafe_short_hash async def create_jukebox( + user: str, wallet: str, title: str, price: int, @@ -19,11 +20,12 @@ async def create_jukebox( juke_id = urlsafe_short_hash() result = await db.execute( """ - INSERT INTO jukebox (id, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( juke_id, + user, title, wallet, sp_user, @@ -40,12 +42,12 @@ async def create_jukebox( return jukebox -async def update_jukebox(sp_user: str, **kwargs) -> Optional[Jukebox]: +async def update_jukebox(id: str, **kwargs) -> Optional[Jukebox]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( - f"UPDATE jukebox SET {q} WHERE sp_user = ?", (*kwargs.values(), sp_user) + f"UPDATE jukebox SET {q} WHERE id = ?", (*kwargs.values(), id) ) - row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (sp_user,)) + row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) return Jukebox(**row) if row else None @@ -58,16 +60,18 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (user,)) return Jukebox(**row) if row else None +async def get_jukeboxs(user: str) -> List[Jukebox]: + rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,)) + for row in rows: + if not row.sp_playlists: + await delete_jukebox(row.id) + rows.remove(row) + return [Jukebox.from_row(row) for row in rows] -async def get_jukeboxs(id: str) -> Optional[Jukebox]: - rows = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) - return [Jukebox(**row) for row in rows] - - -async def delete_jukebox(shop: int, item_id: int): +async def delete_jukebox(id: str): await db.execute( """ DELETE FROM jukebox WHERE id = ? """, - (Jukebox, item_id), + (id), ) diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py index 86f675fe..2f4e7afd 100644 --- a/lnbits/extensions/jukebox/migrations.py +++ b/lnbits/extensions/jukebox/migrations.py @@ -6,6 +6,7 @@ async def m001_initial(db): """ CREATE TABLE jukebox ( id TEXT PRIMARY KEY, + user TEXT, title TEXT, wallet TEXT, sp_user TEXT NOT NULL, diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index dddebbc5..f66219b1 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -9,6 +9,7 @@ from sqlite3 import Row class Jukebox(NamedTuple): id: str + user: str title: str wallet: str sp_user: str diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index bae0a0e7..3ec79f87 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -2,20 +2,63 @@ Vue.component(VueQrcode.name, VueQrcode) -const pica = window.pica() - - +var mapJukebox = obj => { + obj._data = _.clone(obj) + + obj.device = obj.sp_device.split("-")[0] + playlists = obj.sp_playlists.split(",") + var i; + playlistsar = [] + for (i = 0; i < playlists.length; i++) { + playlistsar.push(playlists[i].split("-")[0]) + } + obj.playlist = playlistsar.join() + return obj +} new Vue({ el: '#vue', mixins: [windowMixin], data() { return { + JukeboxTable: { + columns: [ + { + name: 'title', + align: 'left', + label: 'Title', + field: 'title' + }, + { + name: 'device', + align: 'left', + label: 'Device', + field: 'device' + }, + { + name: 'playlist', + align: 'left', + label: 'Playlist', + field: 'playlist' + }, + { + name: 'price', + align: 'left', + label: 'Price', + field: 'price' + }, + ], + pagination: { + rowsPerPage: 10 + } + }, isPwd: true, tokenFetched: true, devices: [], + filter: '', jukebox: {}, playlists: [], + JukeboxLinks: [], step: 1, locationcbPath: "", locationcb: "", @@ -27,20 +70,42 @@ new Vue({ } }, computed: { - printItems() { - return this.jukebox.items.filter(({enabled}) => enabled) - } + }, methods: { + getJukeboxes(){ + self = this + LNbits.api + .request('GET', '/jukebox/api/v1/jukebox', self.g.user.wallets[0].inkey) + .then(function (response) { + self.JukeboxLinks = response.data.map(mapJukebox) + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + deleteJukebox(juke_id){ + self = this + 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) + }) + }, closeFormDialog() { this.jukeboxDialog.data = {} this.jukeboxDialog.show = false this.step = 1 }, submitSpotify() { - self = this - console.log(self.jukeboxDialog.data) + self.jukeboxDialog.data.user = self.g.user.id self.requestAuthorization() this.$q.notify({ spinner: true, @@ -56,7 +121,6 @@ new Vue({ .then(response => { if(response.data){ var timerId = setInterval(function(){ - console.log(response.data) if(!self.jukeboxDialog.data.sp_user){ clearInterval(timerId); } @@ -68,15 +132,7 @@ new Vue({ self.jukeboxDialog.data.sp_access_token = response.data.sp_access_token self.step = 3 self.fetchAccessToken() - clearInterval(timerId) - - // self.refreshPlaylists(response.data.sp_token) -// self.$q.notify({ -// message: 'Success! App is now linked!', -// timeout: 3000 -// }) - //set devices, playlists } }) .catch(err => { @@ -97,7 +153,7 @@ new Vue({ url += '&redirect_uri=' + encodeURI(self.locationcbPath) + self.jukeboxDialog.data.sp_user 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' - console.log(url) + window.open(url) }, openNewDialog() { @@ -111,19 +167,16 @@ new Vue({ }, createJukebox(){ self = this - + this.jukeboxDialog.data.sp_playlists = this.jukeboxDialog.data.sp_playlists.join() LNbits.api.request( 'PUT', '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, self.g.user.wallets[0].adminkey, self.jukeboxDialog.data ) - .then(response => { - console.log(response.data) - - }) - .catch(err => { - LNbits.utils.notifyApiError(err) + .then(function (response) { + self.JukeboxLinks.push(mapJukebox(response.data)) + self.jukeboxDialog.show = false }) }, @@ -156,12 +209,11 @@ new Vue({ xhr.send(body) xhr.onload = function() { let responseObj = JSON.parse(xhr.response) - console.log(responseObj.devices[0]) self.devices = [] var i; for (i = 0; i < responseObj.devices.length; i++) { self.devices.push(responseObj.devices[i].name + "-" + responseObj.devices[i].id) - console.log(responseObj.devices[i].name) + } } @@ -191,20 +243,19 @@ new Vue({ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') xhr.setRequestHeader('Authorization', 'Basic ' + btoa(this.jukeboxDialog.data.sp_user + ":" + this.jukeboxDialog.data.sp_secret)) xhr.send(body) - console.log(('Authorization', 'Basic ' + btoa(this.jukeboxDialog.data.sp_user + ":" + this.jukeboxDialog.data.sp_secret))) xhr.onload = function() { let responseObj = JSON.parse(xhr.response) - alert(responseObj.access_token) - alert(responseObj.refresh_token) self.jukeboxDialog.data.sp_access_token = responseObj.access_token self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token - console.log(self.jukeboxDialog.data) self.refreshPlaylists() self.refreshDevices() } }, }, created() { + + var getJukeboxes = this.getJukeboxes + getJukeboxes() this.selectedWallet = this.g.user.wallets[0] this.locationcbPath = String([ window.location.protocol, @@ -212,7 +263,6 @@ new Vue({ window.location.host, '/jukebox/api/v1/jukebox/spotify/cb/' ].join('')) - console.log(this.locationcbPath) this.locationcb = this.locationcbPath } }) diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index 07e06ebd..c865794b 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -11,30 +11,36 @@ @click="openNewDialog()" >Add Spotify Jukebox
-
-
Items
-
+ {% raw %} + + @@ -253,9 +250,13 @@ >
- Create Jukebox + Create Jukebox
", methods=["DELETE"]) @api_check_wallet_key("admin") async def api_delete_item(juke_id): - shop = await delete_jukebox(juke_id) - return "", HTTPStatus.NO_CONTENT + await delete_jukebox(juke_id) + try: + return ( + jsonify( + [ + { + **jukebox._asdict() + } + for jukebox in await get_jukeboxs(g.wallet.user) + ] + ), + HTTPStatus.OK, + ) + except: + return "", HTTPStatus.NO_CONTENT From 9b793c9f3abebe260b26ea4d14ecc18226b2cb8f Mon Sep 17 00:00:00 2001 From: Dave Scotese Date: Sun, 2 May 2021 19:21:27 -0700 Subject: [PATCH 09/83] Nearly undetectable typo fix. (#188) Regularily => regularly --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61fa7965..bc700bfd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an * Fallback wallet for the LNURL scheme * Instant wallet for LN demonstrations -LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily. +LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly. See [lnbits.org](https://lnbits.org) for more detailed documentation. From be234c349f0ce7b519943437f2ec1c9fdb1589ae Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Mon, 3 May 2021 23:22:40 +0100 Subject: [PATCH 10/83] Added update jukebox --- lnbits/extensions/jukebox/crud.py | 25 +- lnbits/extensions/jukebox/migrations.py | 3 +- lnbits/extensions/jukebox/models.py | 1 + lnbits/extensions/jukebox/static/js/index.js | 297 +++++++++++------- .../jukebox/templates/jukebox/index.html | 55 +++- lnbits/extensions/jukebox/views_api.py | 34 +- 6 files changed, 260 insertions(+), 155 deletions(-) diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index 01159c8e..fc7ddca3 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -20,8 +20,8 @@ async def create_jukebox( juke_id = urlsafe_short_hash() result = await db.execute( """ - INSERT INTO jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( juke_id, @@ -35,6 +35,7 @@ async def create_jukebox( sp_device, sp_playlists, int(price), + 0, ), ) jukebox = await get_jukebox(juke_id) @@ -42,17 +43,17 @@ async def create_jukebox( return jukebox -async def update_jukebox(id: str, **kwargs) -> Optional[Jukebox]: +async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( - f"UPDATE jukebox SET {q} WHERE id = ?", (*kwargs.values(), id) + f"UPDATE jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id) ) - row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) + row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None -async def get_jukebox(id: str) -> Optional[Jukebox]: - row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (id,)) +async def get_jukebox(juke_id: str) -> Optional[Jukebox]: + row = await db.fetchone("SELECT * FROM jukebox WHERE id = ?", (juke_id,)) return Jukebox(**row) if row else None @@ -60,18 +61,20 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: row = await db.fetchone("SELECT * FROM jukebox WHERE sp_user = ?", (user,)) return Jukebox(**row) if row else None + async def get_jukeboxs(user: str) -> List[Jukebox]: rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,)) for row in rows: - if not row.sp_playlists: + if row.sp_playlists == "": await delete_jukebox(row.id) - rows.remove(row) + rows = await db.fetchall("SELECT * FROM jukebox WHERE user = ?", (user,)) return [Jukebox.from_row(row) for row in rows] -async def delete_jukebox(id: str): + +async def delete_jukebox(juke_id: str): await db.execute( """ DELETE FROM jukebox WHERE id = ? """, - (id), + (juke_id), ) diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py index 2f4e7afd..9ca09a4a 100644 --- a/lnbits/extensions/jukebox/migrations.py +++ b/lnbits/extensions/jukebox/migrations.py @@ -15,7 +15,8 @@ async def m001_initial(db): sp_refresh_token TEXT, sp_device TEXT, sp_playlists TEXT, - price INTEGER + price INTEGER, + profit INTEGER ); """ ) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index f66219b1..f869de4f 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -19,6 +19,7 @@ class Jukebox(NamedTuple): sp_device: str sp_playlists: str price: int + profit: int @classmethod def from_row(cls, row: Row) -> "Jukebox": diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 3ec79f87..fde88137 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -4,13 +4,13 @@ Vue.component(VueQrcode.name, VueQrcode) var mapJukebox = obj => { obj._data = _.clone(obj) - - obj.device = obj.sp_device.split("-")[0] - playlists = obj.sp_playlists.split(",") - var i; + obj.sp_id = obj.id + obj.device = obj.sp_device.split('-')[0] + playlists = obj.sp_playlists.split(',') + var i playlistsar = [] for (i = 0; i < playlists.length; i++) { - playlistsar.push(playlists[i].split("-")[0]) + playlistsar.push(playlists[i].split('-')[0]) } obj.playlist = playlistsar.join() return obj @@ -47,6 +47,12 @@ new Vue({ label: 'Price', field: 'price' }, + { + name: 'profit', + align: 'left', + label: 'Profit', + field: 'profit' + } ], pagination: { rowsPerPage: 10 @@ -60,8 +66,8 @@ new Vue({ playlists: [], JukeboxLinks: [], step: 1, - locationcbPath: "", - locationcb: "", + locationcbPath: '', + locationcb: '', jukeboxDialog: { show: false, data: {} @@ -69,90 +75,132 @@ new Vue({ spotifyDialog: false } }, - computed: { - - }, + computed: {}, methods: { - getJukeboxes(){ + getJukeboxes() { self = this LNbits.api - .request('GET', '/jukebox/api/v1/jukebox', self.g.user.wallets[0].inkey) - .then(function (response) { - self.JukeboxLinks = response.data.map(mapJukebox) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - deleteJukebox(juke_id){ - self = this - 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 + .request('GET', '/jukebox/api/v1/jukebox', self.g.user.wallets[0].inkey) + .then(function (response) { + self.JukeboxLinks = response.data.map(mapJukebox) }) - }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + deleteJukebox(juke_id) { + self = this + LNbits.utils + .confirmDialog('Are you sure you want to delete this Jukebox?') + .onOk(function () { + LNbits.api + .request( + 'DELETE', + '/jukebox/api/v1/jukebox/' + juke_id, + self.g.user.wallets[0].adminkey + ) + .then(function (response) { + self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) { + return obj.id === juke_id + }) + }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }) + }, + updateJukebox: function (linkId) { + self = this + var link = _.findWhere(self.JukeboxLinks, {id: linkId}) + self.jukeboxDialog.data = _.clone(link._data) + console.log(this.jukeboxDialog.data.sp_access_token) + + self.refreshDevices() + self.refreshPlaylists() + + self.step = 4 + self.jukeboxDialog.data.sp_device = [] + self.jukeboxDialog.data.sp_playlists = [] + self.jukeboxDialog.data.sp_id = self.jukeboxDialog.data.id + self.jukeboxDialog.show = true }, closeFormDialog() { this.jukeboxDialog.data = {} this.jukeboxDialog.show = false this.step = 1 }, - submitSpotify() { + submitSpotifyKeys() { self = this self.jukeboxDialog.data.user = self.g.user.id - self.requestAuthorization() - this.$q.notify({ - spinner: true, - message: 'Fetching token', - timeout: 4000 - }) - LNbits.api.request( + + LNbits.api + .request( 'POST', '/jukebox/api/v1/jukebox/', self.g.user.wallets[0].adminkey, self.jukeboxDialog.data ) .then(response => { - if(response.data){ - var timerId = setInterval(function(){ - if(!self.jukeboxDialog.data.sp_user){ - clearInterval(timerId); - } + if (response.data) { self.jukeboxDialog.data.sp_id = response.data.id - LNbits.api - .request('GET', '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_id, self.g.user.wallets[0].inkey) - .then(response => { - if(response.data.sp_access_token){ - self.jukeboxDialog.data.sp_access_token = response.data.sp_access_token - self.step = 3 - self.fetchAccessToken() - clearInterval(timerId) - } - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, 3000) - } + self.step = 3 + } }) .catch(err => { - LNbits.utils.notifyApiError(err) + LNbits.utils.notifyApiError(err) }) }, - requestAuthorization(){ + authAccess() { + self = this + self.requestAuthorization() + self.$q.notify({ + spinner: true, + message: 'Fetching token', + timeout: 4000 + }) + self.getSpotifyTokens() + }, + getSpotifyTokens() { + self = this + + var timerId = setInterval(function () { + if (!self.jukeboxDialog.data.sp_user) { + clearInterval(timerId) + } + LNbits.api + .request( + 'GET', + '/jukebox/api/v1/jukebox/spotify/' + self.jukeboxDialog.data.sp_id, + self.g.user.wallets[0].inkey + ) + .then(response => { + if (response.data.sp_access_token) { + self.fetchAccessToken(response.data.sp_access_token) + if (self.jukeboxDialog.data.sp_access_token) { + self.refreshPlaylists() + self.refreshDevices() + self.step = 4 + clearInterval(timerId) + } + } + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, 5000) + }, + requestAuthorization() { 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_user - 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' + 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) }, @@ -160,15 +208,12 @@ new Vue({ this.jukeboxDialog.show = true this.jukeboxDialog.data = {} }, - openUpdateDialog(itemId) { - this.jukeboxDialog.show = true - let item = this.jukebox.items.find(item => item.id === itemId) - this.jukeboxDialog.data = item - }, - createJukebox(){ + createJukebox() { self = this + console.log(this.jukeboxDialog.data) this.jukeboxDialog.data.sp_playlists = this.jukeboxDialog.data.sp_playlists.join() - LNbits.api.request( + LNbits.api + .request( 'PUT', '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, self.g.user.wallets[0].adminkey, @@ -179,90 +224,118 @@ new Vue({ self.jukeboxDialog.show = false }) }, - - playlistApi(method, url, body){ + 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.setRequestHeader( + 'Authorization', + 'Bearer ' + this.jukeboxDialog.data.sp_access_token + ) xhr.send(body) - xhr.onload = function() { + xhr.onload = function () { let responseObj = JSON.parse(xhr.response) + self.jukeboxDialog.data.playlists = null self.playlists = [] - var i; + 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) + self.playlists.push( + responseObj.items[i].name + '-' + responseObj.items[i].id + ) } } }, - refreshPlaylists(){ + refreshPlaylists() { self = this - self.playlistApi( "GET", "https://api.spotify.com/v1/me/playlists", null) + self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null) }, - deviceApi(method, url, body){ + deviceApi(method, url, body) { self = this 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.setRequestHeader( + 'Authorization', + 'Bearer ' + this.jukeboxDialog.data.sp_access_token + ) xhr.send(body) - xhr.onload = function() { + xhr.onload = function () { let responseObj = JSON.parse(xhr.response) - self.devices = [] - var i; - for (i = 0; i < responseObj.devices.length; i++) { - self.devices.push(responseObj.devices[i].name + "-" + responseObj.devices[i].id) + 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(){ + refreshDevices() { self = this - self.deviceApi( "GET", "https://api.spotify.com/v1/me/player/devices", null) + self.deviceApi( + 'GET', + 'https://api.spotify.com/v1/me/player/devices', + null + ) }, - fetchAccessToken( ){ + fetchAccessToken(code) { self = this - let body = "grant_type=authorization_code" - body += "&code=" + self.jukeboxDialog.data.sp_access_token - body += '&redirect_uri=' + encodeURI(self.locationcbPath) + self.jukeboxDialog.data.sp_user + let body = 'grant_type=authorization_code' + body += '&code=' + code + body += + '&redirect_uri=' + + encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id) + this.callAuthorizationApi(body) }, - refreshAccessToken(){ + 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 + let body = 'grant_type=refresh_token' + body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token + body += '&client_id=' + self.jukeboxDialog.data.sp_user this.callAuthorizationApi(body) }, - callAuthorizationApi(body){ + callAuthorizationApi(body) { self = this let xhr = new XMLHttpRequest() - xhr.open("POST", "https://accounts.spotify.com/api/token", true) + xhr.open('POST', 'https://accounts.spotify.com/api/token', true) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') - xhr.setRequestHeader('Authorization', 'Basic ' + btoa(this.jukeboxDialog.data.sp_user + ":" + this.jukeboxDialog.data.sp_secret)) + xhr.setRequestHeader( + 'Authorization', + 'Basic ' + + btoa( + this.jukeboxDialog.data.sp_user + + ':' + + this.jukeboxDialog.data.sp_secret + ) + ) xhr.send(body) - xhr.onload = function() { + xhr.onload = function () { let responseObj = JSON.parse(xhr.response) - self.jukeboxDialog.data.sp_access_token = responseObj.access_token - self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token - self.refreshPlaylists() - self.refreshDevices() + if (responseObj.access_token) { + self.jukeboxDialog.data.sp_access_token = responseObj.access_token + self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token + console.log(responseObj) + } } - }, + } }, 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.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/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index c865794b..1b0a5949 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -58,6 +58,14 @@ + here.
- In the app go to edit-settings, set the redirect URI to this link - (replacing the CLIENT-ID with your own) {% raw %}{{ locationcb - }}CLIENT-ID{% endraw %} + >. Get token
Submit keys Get tokenSubmit keys +
+
+ Cancel +
+
+ +
+ + + + In the app go to edit-settings, set the redirect URI to this link +
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw + %} + +
+
+ Authorise access + Authorise access
@@ -223,11 +260,11 @@ ", methods=["GET"]) -async def api_check_credentials_callbac(sp_user): +@jukebox_ext.route("/api/v1/jukebox/spotify/cb/", methods=["GET"]) +async def api_check_credentials_callbac(juke_id): + print(request.args) sp_code = "" sp_access_token = "" sp_refresh_token = "" - print(request.args) - jukebox = await get_jukebox_by_user(sp_user) + jukebox = await get_jukebox(juke_id) if request.args.get("code"): sp_code = request.args.get("code") jukebox = await update_jukebox( - sp_user=sp_user, sp_secret=jukebox.sp_secret, sp_access_token=sp_code + juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code ) if request.args.get("access_token"): sp_access_token = request.args.get("access_token") sp_refresh_token = request.args.get("refresh_token") jukebox = await update_jukebox( - sp_user=sp_user, + juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_access_token, sp_refresh_token=sp_refresh_token, @@ -70,7 +65,7 @@ async def api_check_credentials_check(sp_id): @jukebox_ext.route("/api/v1/jukebox/", methods=["POST"]) -@jukebox_ext.route("/api/v1/jukebox/", methods=["PUT"]) +@jukebox_ext.route("/api/v1/jukebox/", methods=["PUT"]) @api_check_wallet_key("admin") @api_validate_post_request( schema={ @@ -86,9 +81,9 @@ async def api_check_credentials_check(sp_id): "price": {"type": "string", "required": True}, } ) -async def api_create_update_jukebox(item_id=None): - if item_id: - jukebox = await update_jukebox(item_id, **g.data) +async def api_create_update_jukebox(juke_id=None): + if juke_id: + jukebox = await update_jukebox(juke_id=juke_id, **g.data) else: jukebox = await create_jukebox(**g.data) return jsonify(jukebox._asdict()), HTTPStatus.CREATED @@ -101,12 +96,7 @@ async def api_delete_item(juke_id): try: return ( jsonify( - [ - { - **jukebox._asdict() - } - for jukebox in await get_jukeboxs(g.wallet.user) - ] + [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] ), HTTPStatus.OK, ) From bfdff79dd5c8bcb529d458a18b11e51fff92ae2e Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 5 May 2021 15:58:33 +0100 Subject: [PATCH 11/83] Added shareable qr, broke jukebox creation --- lnbits/extensions/jukebox/config.json | 2 +- lnbits/extensions/jukebox/static/js/index.js | 42 +++++++++++++++---- .../extensions/jukebox/static/js/jukebox.js | 19 +++++++++ .../jukebox/templates/jukebox/index.html | 38 ++++++++++++++--- lnbits/extensions/jukebox/views_api.py | 12 ++++++ 5 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 lnbits/extensions/jukebox/static/js/jukebox.js diff --git a/lnbits/extensions/jukebox/config.json b/lnbits/extensions/jukebox/config.json index def7c7a3..91134bc2 100644 --- a/lnbits/extensions/jukebox/config.json +++ b/lnbits/extensions/jukebox/config.json @@ -1,6 +1,6 @@ { "name": "SpotifyJukebox", "short_description": "Spotify jukebox middleware", - "icon": "audiotrack", + "icon": "radio", "contributors": ["benarc"] } diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index fde88137..ae50af92 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -72,11 +72,24 @@ new Vue({ show: false, data: {} }, - spotifyDialog: false + spotifyDialog: false, + qrCodeDialog: { + show: false, + data: null + } } }, computed: {}, methods: { + openQrCodeDialog: function (linkId) { + var link = _.findWhere(this.JukeboxLinks, {id: linkId}) + + this.qrCodeDialog.data = _.clone(link) + console.log(this.qrCodeDialog.data) + this.qrCodeDialog.data.url = + window.location.protocol + '//' + window.location.host + this.qrCodeDialog.show = true + }, getJukeboxes() { self = this LNbits.api @@ -123,6 +136,7 @@ new Vue({ 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() { @@ -210,18 +224,24 @@ new Vue({ }, createJukebox() { self = this - console.log(this.jukeboxDialog.data) - this.jukeboxDialog.data.sp_playlists = this.jukeboxDialog.data.sp_playlists.join() + self.jukeboxDialog.data.sp_playlists = self.jukeboxDialog.data.sp_playlists.join() + self.updateDB() + self.jukeboxDialog.show = false + }, + updateDB(){ + self = this + console.log(self.jukeboxDialog.data) LNbits.api .request( 'PUT', - '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, + '/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id, self.g.user.wallets[0].adminkey, self.jukeboxDialog.data ) .then(function (response) { - self.JukeboxLinks.push(mapJukebox(response.data)) - self.jukeboxDialog.show = false + console.log(response.data) + self.getJukeboxes() + //self.JukeboxLinks.push(mapJukebox(response.data)) }) }, playlistApi(method, url, body) { @@ -235,6 +255,10 @@ new Vue({ ) 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 = [] @@ -262,6 +286,10 @@ new Vue({ ) 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 = [] @@ -319,7 +347,7 @@ new Vue({ if (responseObj.access_token) { self.jukeboxDialog.data.sp_access_token = responseObj.access_token self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token - console.log(responseObj) + self.updateDB() } } } diff --git a/lnbits/extensions/jukebox/static/js/jukebox.js b/lnbits/extensions/jukebox/static/js/jukebox.js new file mode 100644 index 00000000..b6e26f13 --- /dev/null +++ b/lnbits/extensions/jukebox/static/js/jukebox.js @@ -0,0 +1,19 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data() { + return { + } + }, + computed: {}, + methods: { + + }, + created() { + + } +}) diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html index 1b0a5949..e9e60ace 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ b/lnbits/extensions/jukebox/templates/jukebox/index.html @@ -48,13 +48,11 @@ unelevated dense size="xs" - icon="link" + icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" - type="a" - :href="props.row.displayUrl" - target="_blank" + @click="openQrCodeDialog(props.row.sp_id)" > - Jukebox link + Jukebox QR @@ -308,6 +306,36 @@ + + + +
Shareable Jukebox QR
+ + + +
+ Copy jukebox link + Open jukebox + Close +
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }} diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 358358dd..df93883d 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -102,3 +102,15 @@ async def api_delete_item(juke_id): ) except: return "", HTTPStatus.NO_CONTENT + + + +################JUKEBOX ENDPOINTS################## + + +@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) +async def api_get_jukebox_songs(sp_id): + jukebox = await get_jukebox(sp_id) + print(jukebox.playlists.split(",")[0].split("-")[1]) + + return jsonify(jukebox._asdict()), HTTPStatus.CREATED \ No newline at end of file From a698ba7a26e9125bcd2147876fb91bf30d64e787 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 5 May 2021 19:20:53 +0100 Subject: [PATCH 12/83] Adding endpoints for jukebox --- lnbits/extensions/jukebox/static/js/index.js | 9 ++- lnbits/extensions/jukebox/views_api.py | 69 +++++++++++++++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index ae50af92..3d3c1f34 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -227,6 +227,7 @@ new Vue({ self.jukeboxDialog.data.sp_playlists = self.jukeboxDialog.data.sp_playlists.join() self.updateDB() self.jukeboxDialog.show = false + self.getJukeboxes() }, updateDB(){ self = this @@ -234,14 +235,16 @@ new Vue({ LNbits.api .request( 'PUT', - '/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id, + '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, self.g.user.wallets[0].adminkey, self.jukeboxDialog.data ) .then(function (response) { console.log(response.data) - self.getJukeboxes() - //self.JukeboxLinks.push(mapJukebox(response.data)) + if(this.jukeboxDialog.data.devices){ + self.getJukeboxes() + } + self.JukeboxLinks.push(mapJukebox(response.data)) }) }, playlistApi(method, url, body) { diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index df93883d..2a13edb3 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -1,6 +1,8 @@ from quart import g, jsonify, request from http import HTTPStatus from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore +from base64 import urlsafe_b64encode +import base64 from lnbits.decorators import api_check_wallet_key, api_validate_post_request import httpx @@ -104,13 +106,74 @@ async def api_delete_item(juke_id): return "", HTTPStatus.NO_CONTENT - ################JUKEBOX ENDPOINTS################## @jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) async def api_get_jukebox_songs(sp_id): jukebox = await get_jukebox(sp_id) - print(jukebox.playlists.split(",")[0].split("-")[1]) + print(jukebox.sp_playlists.split(",")[0].split("-")[1]) + async with httpx.AsyncClient() as client: + try: + r = await client.get( + "https://api.spotify.com/v1/playlists/" + + jukebox.sp_playlists.split(",")[0].split("-")[1] + + "/tracks", + timeout=40, + headers={"Authorization": "Bearer " + jukebox.sp_access_token}, + ) + if r.json()["error"]["status"] == 401: + token = await api_get_token(sp_id) + if token['error'] == 'invalid_client': + print("invalid") + return "" + else: + return await api_get_jukebox_songs(sp_id) + print(r.json()["items"]) + resp = r.json()["items"][0] + print("id: " + resp["track"]["id"]) + print("name: " + resp["track"]["name"]) + print("album: " + resp["track"]["album"]["name"]) + print("artist: " + resp["track"]["artists"][0]["name"]) + print("image: " + resp["track"]["album"]["images"][0]) + except AssertionError: + something = None + return jsonify(jukebox._asdict()), HTTPStatus.CREATED - return jsonify(jukebox._asdict()), HTTPStatus.CREATED \ No newline at end of file + +@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) +async def api_get_token(sp_id): + jukebox = await get_jukebox(sp_id) + print(jukebox.sp_playlists.split(",")[0].split("-")[1]) + 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={ + "Authorization": "Bearer " + + base64.b64encode( + (jukebox.sp_user + ":" + jukebox.sp_refresh_token).encode( + "utf-8" + ) + ).decode("ascii"), + "Content-Type": "application/x-www-form-urlencoded", + }, + ) + print(r) + print(r.json()) + if r.json()['error'] == 'invalid_client': + return r.json() + #await update_jukebox( + # juke_id=sp_id, + # sp_access_token=r.json()["access_token"], + # sp_refresh_token=r.json()["refresh_token"], + #) + except AssertionError: + something = None + return jsonify(jukebox._asdict()), HTTPStatus.CREATED From 396de3cfa32d828432e3ede532b95c6f37105991 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 5 May 2021 23:03:22 +0100 Subject: [PATCH 13/83] Added jukebox page --- lnbits/extensions/jukebox/static/js/index.js | 5 + .../jukebox/templates/jukebox/jukebox.html | 102 ++++++++++++++++-- lnbits/extensions/jukebox/views.py | 15 ++- lnbits/extensions/jukebox/views_api.py | 77 ++++++------- 4 files changed, 150 insertions(+), 49 deletions(-) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 3d3c1f34..ae185201 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -332,6 +332,11 @@ new Vue({ }, callAuthorizationApi(body) { self = this + console.log(btoa( + this.jukeboxDialog.data.sp_user + + ':' + + this.jukeboxDialog.data.sp_secret + )) let xhr = new XMLHttpRequest() xhr.open('POST', 'https://accounts.spotify.com/api/token', true) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html index fff12b4c..c373f3fe 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -1,24 +1,104 @@ -{% extends "print.html" %} {% block page %} {% raw %} -
-
-
{{ item.name }}
- -
{{ item.price }}
+{% extends "public.html" %} {% block page %} {% raw %} +
+
+ + +

Currently playing

+
+
+ +
+
+ {{ currentPlaylist[0].name }}
+ {{ currentPlaylist[0].artist }} +
+
+
+
+ + + +

Pick a song

+ +
+ + + + + + +
+
+
+ + +

Queued

+
+ + + + + + + {{ song.name }} ({{ song.artist }}) + + +
+
{% endraw %} {% endblock %} {% block scripts %} + + diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index c3bf621a..830480de 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -10,6 +10,9 @@ from lnbits.core.crud import get_standalone_payment from . import jukebox_ext from .crud import get_jukebox from urllib.parse import unquote +from .views_api import ( + api_get_jukebox_songs, +) @jukebox_ext.route("/") @@ -22,5 +25,15 @@ async def index(): @jukebox_ext.route("/") async def print_qr_codes(juke_id): jukebox = await get_jukebox(juke_id) + if not jukebox: + return "error" + firstPlaylist = await api_get_jukebox_songs( + juke_id, jukebox.sp_playlists.split(",")[0].split("-")[1] + ) + print(firstPlaylist) - return await render_template("jukebox/jukebox.html", jukebox=jukebox) + return await render_template( + "jukebox/jukebox.html", + playlists=jukebox.sp_playlists.split(","), + firstPlaylist=firstPlaylist, + ) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 2a13edb3..e1174d8a 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -109,42 +109,49 @@ async def api_delete_item(juke_id): ################JUKEBOX ENDPOINTS################## -@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) -async def api_get_jukebox_songs(sp_id): +@jukebox_ext.route("/api/v1/jukebox/jb//", methods=["GET"]) +async def api_get_jukebox_songs(sp_id, sp_playlist): jukebox = await get_jukebox(sp_id) - print(jukebox.sp_playlists.split(",")[0].split("-")[1]) + tracks = [] async with httpx.AsyncClient() as client: try: r = await client.get( - "https://api.spotify.com/v1/playlists/" - + jukebox.sp_playlists.split(",")[0].split("-")[1] - + "/tracks", + "https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks", timeout=40, headers={"Authorization": "Bearer " + jukebox.sp_access_token}, ) - if r.json()["error"]["status"] == 401: - token = await api_get_token(sp_id) - if token['error'] == 'invalid_client': - print("invalid") - return "" - else: - return await api_get_jukebox_songs(sp_id) - print(r.json()["items"]) - resp = r.json()["items"][0] - print("id: " + resp["track"]["id"]) - print("name: " + resp["track"]["name"]) - print("album: " + resp["track"]["album"]["name"]) - print("artist: " + resp["track"]["artists"][0]["name"]) - print("image: " + resp["track"]["album"]["images"][0]) + if "items" not in r.json(): + if r.json()["error"]["status"] == 401: + token = await api_get_token(sp_id) + if token == False: + print("invalid") + return False + else: + return await api_get_jukebox_songs(sp_id, sp_playlist) + for item in r.json()["items"]: + tracks.append( + { + "id": item["track"]["id"], + "name": item["track"]["name"], + "album": item["track"]["album"]["name"], + "artist": item["track"]["artists"][0]["name"], + "image": item["track"]["album"]["images"][0]["url"], + } + ) except AssertionError: something = None - return jsonify(jukebox._asdict()), HTTPStatus.CREATED + print(jsonify(tracks)) + return tracks, HTTPStatus.OK -@jukebox_ext.route("/api/v1/jukebox/jb/", methods=["GET"]) async def api_get_token(sp_id): jukebox = await get_jukebox(sp_id) - print(jukebox.sp_playlists.split(",")[0].split("-")[1]) + print( + "Authorization: Bearer " + + base64.b64encode( + str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii") + ).decode("ascii") + ) async with httpx.AsyncClient() as client: try: r = await client.post( @@ -156,24 +163,20 @@ async def api_get_token(sp_id): "client_id": jukebox.sp_user, }, headers={ - "Authorization": "Bearer " + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Basic " + base64.b64encode( - (jukebox.sp_user + ":" + jukebox.sp_refresh_token).encode( - "utf-8" - ) + str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii") ).decode("ascii"), "Content-Type": "application/x-www-form-urlencoded", }, ) - print(r) - print(r.json()) - if r.json()['error'] == 'invalid_client': - return r.json() - #await update_jukebox( - # juke_id=sp_id, - # sp_access_token=r.json()["access_token"], - # sp_refresh_token=r.json()["refresh_token"], - #) + if "access_token" not in r.json(): + return False + else: + await update_jukebox( + juke_id=sp_id, sp_access_token=r.json()["access_token"] + ) except AssertionError: something = None - return jsonify(jukebox._asdict()), HTTPStatus.CREATED + return True From d5f5c9473f6d44d906b581bcd951eeafb276db6f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 5 May 2021 09:43:52 -0300 Subject: [PATCH 14/83] guard against amount=NaN in two other lnurlw contexts. --- lnbits/extensions/livestream/lnurl.py | 2 +- lnbits/extensions/offlineshop/lnurl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/livestream/lnurl.py b/lnbits/extensions/livestream/lnurl.py index 1e021f85..3b9e7e31 100644 --- a/lnbits/extensions/livestream/lnurl.py +++ b/lnbits/extensions/livestream/lnurl.py @@ -61,7 +61,7 @@ async def lnurl_callback(track_id): if not track: return jsonify({"status": "ERROR", "reason": "Couldn't find track."}) - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < track.min_sendable: return ( diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index 13944a29..d99e4cea 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -49,7 +49,7 @@ async def lnurl_callback(item_id): min = price * 995 max = price * 1010 - amount_received = int(request.args.get("amount")) + amount_received = int(request.args.get("amount") or 0) if amount_received < min: return jsonify( LnurlErrorResponse( From 2a4457afecc3bbe890cdd069c04d572ef9270591 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Thu, 6 May 2021 10:02:18 +0100 Subject: [PATCH 15/83] Fixed a few bugs, started invoice endpoint --- lnbits/extensions/jukebox/static/js/index.js | 67 +++++++++++++++----- lnbits/extensions/jukebox/views_api.py | 13 +++- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index ae185201..30ce3617 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -170,8 +170,8 @@ new Vue({ self.requestAuthorization() self.$q.notify({ spinner: true, - message: 'Fetching token', - timeout: 4000 + message: 'Processing', + timeout: 10000 }) self.getSpotifyTokens() }, @@ -194,8 +194,33 @@ new Vue({ if (self.jukeboxDialog.data.sp_access_token) { self.refreshPlaylists() self.refreshDevices() - self.step = 4 - clearInterval(timerId) + 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, and it has playlists', + 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) + } } } }) @@ -229,7 +254,7 @@ new Vue({ self.jukeboxDialog.show = false self.getJukeboxes() }, - updateDB(){ + updateDB() { self = this console.log(self.jukeboxDialog.data) LNbits.api @@ -241,10 +266,10 @@ new Vue({ ) .then(function (response) { console.log(response.data) - if(this.jukeboxDialog.data.devices){ + if (self.jukeboxDialog.data.sp_playlists && self.jukeboxDialog.data.sp_devices) { self.getJukeboxes() + // self.JukeboxLinks.push(mapJukebox(response.data)) } - self.JukeboxLinks.push(mapJukebox(response.data)) }) }, playlistApi(method, url, body) { @@ -258,9 +283,13 @@ new Vue({ ) xhr.send(body) xhr.onload = function () { - if(xhr.status == 401){ + if (xhr.status == 401) { self.refreshAccessToken() - self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null) + self.playlistApi( + 'GET', + 'https://api.spotify.com/v1/me/playlists', + null + ) } let responseObj = JSON.parse(xhr.response) self.jukeboxDialog.data.playlists = null @@ -289,9 +318,13 @@ new Vue({ ) xhr.send(body) xhr.onload = function () { - if(xhr.status == 401){ + if (xhr.status == 401) { self.refreshAccessToken() - self.deviceApi('GET', 'https://api.spotify.com/v1/me/player/devices', null) + self.deviceApi( + 'GET', + 'https://api.spotify.com/v1/me/player/devices', + null + ) } let responseObj = JSON.parse(xhr.response) self.jukeboxDialog.data.devices = [] @@ -332,11 +365,13 @@ new Vue({ }, callAuthorizationApi(body) { self = this - console.log(btoa( - this.jukeboxDialog.data.sp_user + - ':' + - this.jukeboxDialog.data.sp_secret - )) + console.log( + btoa( + this.jukeboxDialog.data.sp_user + + ':' + + this.jukeboxDialog.data.sp_secret + ) + ) let xhr = new XMLHttpRequest() xhr.open('POST', 'https://accounts.spotify.com/api/token', true) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index e1174d8a..6b6f4d92 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -16,7 +16,7 @@ from .crud import ( delete_jukebox, ) from .models import Jukebox - +from lnbits.core.services import create_invoice @jukebox_ext.route("/api/v1/jukebox", methods=["GET"]) @api_check_wallet_key("invoice") @@ -180,3 +180,14 @@ async def api_get_token(sp_id): except AssertionError: something = None return True + + +######GET INVOICE + + +@jukebox_ext.route("/api/v1/jukebox/jb///", methods=["GET"]) +async def api_get_jukebox_invoice(sp_id, sp_song): + jukebox = await get_jukebox(sp_id) + invoice = await create_invoice(wallet_id=jukebox.wallet,amount=jukebox.amount,memo="Jukebox " + jukebox.name) + + return invoice, HTTPStatus.OK From 2b21a4f47900597c6162e7b1d89d04af1a59f358 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 6 May 2021 12:41:44 -0300 Subject: [PATCH 16/83] g.nursery -> current_app.nursery --- lnbits/app.py | 4 ---- lnbits/core/views/api.py | 6 +++--- lnbits/core/views/generic.py | 5 +++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index cd700f5c..c5654f8e 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -114,10 +114,6 @@ def register_filters(app: QuartTrio): def register_request_hooks(app: QuartTrio): """Open the core db for each request so everything happens in a big transaction""" - @app.before_request - async def before_request(): - g.nursery = app.nursery - @app.after_request async def set_secure_headers(response): secure_headers.quart(response) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 89330ab3..2547435e 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -3,7 +3,7 @@ import json import lnurl # type: ignore import httpx from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult -from quart import g, jsonify, make_response, url_for +from quart import g, current_app, jsonify, make_response, url_for from http import HTTPStatus from binascii import unhexlify from typing import Dict, Union @@ -310,8 +310,8 @@ async def api_payments_sse(): await send_event.send(("keepalive", "")) await trio.sleep(25) - g.nursery.start_soon(payment_received) - g.nursery.start_soon(repeat_keepalive) + current_app.nursery.start_soon(payment_received) + current_app.nursery.start_soon(repeat_keepalive) async def send_events(): try: diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index e17d71b3..f48b054f 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -2,6 +2,7 @@ from os import path from http import HTTPStatus from quart import ( g, + current_app, abort, jsonify, request, @@ -154,7 +155,7 @@ async def lnurl_full_withdraw_callback(): async def pay(): await pay_invoice(wallet_id=wallet.id, payment_request=pr) - g.nursery.start_soon(pay) + current_app.nursery.start_soon(pay) balance_notify = request.args.get("balanceNotify") if balance_notify: @@ -197,7 +198,7 @@ async def lnurlwallet(): user = await get_user(account.id, conn=conn) wallet = await create_wallet(user_id=user.id, conn=conn) - g.nursery.start_soon( + current_app.nursery.start_soon( redeem_lnurl_withdraw, wallet.id, request.args.get("lightning"), From 8cff11bf721be3139be00d1cc1323609c9d21995 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 6 May 2021 23:22:02 -0300 Subject: [PATCH 17/83] global quart errorhandler. --- lnbits/app.py | 24 +++++++++++++++++++++--- lnbits/tasks.py | 7 ------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lnbits/app.py b/lnbits/app.py index c5654f8e..6137fdaf 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -1,6 +1,7 @@ import sys -import importlib import warnings +import importlib +import traceback from quart import g from quart_trio import QuartTrio @@ -23,7 +24,6 @@ from .tasks import ( invoice_listener, internal_invoice_listener, webhook_handler, - grab_app_for_later, ) from .settings import WALLET @@ -48,7 +48,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio: register_commands(app) register_request_hooks(app) register_async_tasks(app) - grab_app_for_later(app) + register_exception_handlers(app) return app @@ -135,3 +135,21 @@ def register_async_tasks(app): @app.after_serving async def stop_listeners(): pass + + +def register_exception_handlers(app): + @app.errorhandler(Exception) + async def basic_error(err): + etype, value, tb = sys.exc_info() + traceback.print_exception(etype, err, tb) + exc = traceback.format_exc() + return ( + "\n\n".join( + [ + "LNbits internal error!", + exc, + "If you believe this shouldn't be an error please bring it up on https://t.me/lnbits", + ] + ), + 500, + ) diff --git a/lnbits/tasks.py b/lnbits/tasks.py index 0e2ff98d..756b1142 100644 --- a/lnbits/tasks.py +++ b/lnbits/tasks.py @@ -13,13 +13,6 @@ from lnbits.core.crud import ( ) from lnbits.core.services import redeem_lnurl_withdraw -main_app: Optional[QuartTrio] = None - - -def grab_app_for_later(app: QuartTrio): - global main_app - main_app = app - deferred_async: List[Callable] = [] From e726d752d87491e99a12db4bfd4ef77b163d542e Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Mon, 10 May 2021 20:29:26 +0100 Subject: [PATCH 18/83] Damn, spotify json broken :( --- .../jukebox/templates/jukebox/jukebox.html | 6 +++--- lnbits/extensions/jukebox/views_api.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html index c373f3fe..25af9f43 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -25,7 +25,7 @@

Pick a song

@@ -85,8 +85,9 @@ mixins: [windowMixin], data() { return { - currentPlaylist: JSON.parse('{{ firstPlaylist[0] | tojson }}'), + currentPlaylist: JSON.parse('{{ firstPlaylist | tojson }}'), playlists: JSON.parse('{{ playlists | tojson }}'), + playlist: '', heavyList: [], queued: [] } @@ -98,7 +99,6 @@ this.queued[1] = this.currentPlaylist[5] this.queued[2] = this.currentPlaylist[6] this.queued[3] = this.currentPlaylist[7] - console.log(this.currentPlaylist) } }) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 6b6f4d92..6a0b3f51 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -108,6 +108,7 @@ async def api_delete_item(juke_id): ################JUKEBOX ENDPOINTS################## +######GET ACCESS TOKEN###### @jukebox_ext.route("/api/v1/jukebox/jb//", methods=["GET"]) async def api_get_jukebox_songs(sp_id, sp_playlist): @@ -128,19 +129,19 @@ async def api_get_jukebox_songs(sp_id, sp_playlist): return False else: return await api_get_jukebox_songs(sp_id, sp_playlist) + return r, HTTPStatus.OK for item in r.json()["items"]: tracks.append( { - "id": item["track"]["id"], - "name": item["track"]["name"], - "album": item["track"]["album"]["name"], - "artist": item["track"]["artists"][0]["name"], - "image": item["track"]["album"]["images"][0]["url"], + "id": str(item["track"]["id"]), + "name": str(item["track"]["name"]), + "album": str(item["track"]["album"]["name"]), + "artist": str(item["track"]["artists"][0]["name"]), + "image": str(item["track"]["album"]["images"][0]["url"]), } ) except AssertionError: something = None - print(jsonify(tracks)) return tracks, HTTPStatus.OK @@ -188,6 +189,7 @@ async def api_get_token(sp_id): @jukebox_ext.route("/api/v1/jukebox/jb///", methods=["GET"]) async def api_get_jukebox_invoice(sp_id, sp_song): jukebox = await get_jukebox(sp_id) - invoice = await create_invoice(wallet_id=jukebox.wallet,amount=jukebox.amount,memo="Jukebox " + jukebox.name) - + invoice = await create_invoice(wallet_id=jukebox.wallet,amount=jukebox.price,memo=jukebox.title) + + ####new table needed to store payment hashes return invoice, HTTPStatus.OK From f43c227b95daeeacf4dae2751f9fb2be48dea4e5 Mon Sep 17 00:00:00 2001 From: Ben Arc Date: Wed, 12 May 2021 15:47:21 +0100 Subject: [PATCH 19/83] Creates invoice --- lnbits/extensions/jukebox/static/js/index.js | 5 +- .../jukebox/templates/jukebox/jukebox.html | 144 ++++++++++++++++-- lnbits/extensions/jukebox/views.py | 17 +-- lnbits/extensions/jukebox/views_api.py | 64 ++++++-- 4 files changed, 195 insertions(+), 35 deletions(-) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js index 30ce3617..9967612d 100644 --- a/lnbits/extensions/jukebox/static/js/index.js +++ b/lnbits/extensions/jukebox/static/js/index.js @@ -177,8 +177,9 @@ new Vue({ }, getSpotifyTokens() { self = this - + var counter = 0 var timerId = setInterval(function () { + counter++ if (!self.jukeboxDialog.data.sp_user) { clearInterval(timerId) } @@ -217,7 +218,7 @@ new Vue({ clearInterval(timerId) self.closeFormDialog() } - else{ + else { self.step = 4 clearInterval(timerId) } diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html index 25af9f43..9aac2a24 100644 --- a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html +++ b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html @@ -28,6 +28,7 @@ v-model="playlist" :options="playlists" label="playlists" + @input="selectPlaylist()" > @@ -38,7 +39,13 @@ separator >