From 8f7167f93e89420da8ef6a6c206d7d9b92c7b615 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 28 Dec 2020 22:00:12 +0100 Subject: [PATCH] fix compatability with pyjwt update --- music_assistant/constants.py | 2 +- music_assistant/helpers/migration.py | 8 +++++++ music_assistant/managers/config.py | 35 +++++++++++++++++++++++++--- music_assistant/managers/database.py | 10 ++++++++ music_assistant/managers/streams.py | 3 +++ music_assistant/translations.json | 2 ++ music_assistant/web/server.py | 32 +++++++++++++------------ 7 files changed, 73 insertions(+), 19 deletions(-) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 65a41de3..e0e2ac82 100755 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -1,6 +1,6 @@ """All constants for Music Assistant.""" -__version__ = "0.0.81" +__version__ = "0.0.82" REQUIRED_PYTHON_VER = "3.7" # configuration keys/attributes diff --git a/music_assistant/helpers/migration.py b/music_assistant/helpers/migration.py index f8f3206b..14b212e8 100644 --- a/music_assistant/helpers/migration.py +++ b/music_assistant/helpers/migration.py @@ -188,6 +188,14 @@ async def async_create_db_tables(db_file): UNIQUE(item_id, provider));""" ) + await db_conn.execute( + """CREATE TABLE IF NOT EXISTS playlog( + item_id INTEGER NOT NULL, + provider TEXT NOT NULL, + timestamp REAL, + UNIQUE(item_id, provider));""" + ) + await db_conn.execute( """CREATE TABLE IF NOT EXISTS thumbs( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/music_assistant/managers/config.py b/music_assistant/managers/config.py index 04b7d4c8..a93befb9 100755 --- a/music_assistant/managers/config.py +++ b/music_assistant/managers/config.py @@ -1,6 +1,7 @@ """All classes and helpers for the Configuration.""" import copy +import datetime import json import logging import os @@ -398,14 +399,38 @@ class SecuritySettings(ConfigBaseItem): def add_app_token(self, token_info: dict): """Add token to config.""" client_id = token_info["client_id"] - self[CONF_KEY_SECURITY_APP_TOKENS][client_id] = token_info + self.conf_mgr.stored_config[CONF_KEY_SECURITY][CONF_KEY_SECURITY_APP_TOKENS][ + client_id + ] = token_info + self.conf_mgr.save() + + def set_last_login(self, client_id: str): + """Set last login to client.""" + if ( + client_id + not in self.conf_mgr.stored_config[CONF_KEY_SECURITY][ + CONF_KEY_SECURITY_APP_TOKENS + ] + ): + return + self.conf_mgr.stored_config[CONF_KEY_SECURITY][CONF_KEY_SECURITY_APP_TOKENS][ + client_id + ]["last_login"] = datetime.datetime.utcnow().timestamp() + self.conf_mgr.save() def revoke_app_token(self, client_id): """Revoke a token registered for an app.""" - self[CONF_KEY_SECURITY_APP_TOKENS].pop(client_id) + return_info = self.conf_mgr.stored_config[CONF_KEY_SECURITY][ + CONF_KEY_SECURITY_APP_TOKENS + ].pop(client_id) + self.conf_mgr.save() + self.conf_mgr.mass.signal_event( + EVENT_CONFIG_CHANGED, (CONF_KEY_SECURITY, CONF_KEY_SECURITY_APP_TOKENS) + ) + return return_info def is_token_revoked(self, token_info: dict): - """Return bool is token is revoked.""" + """Return bool if token is revoked.""" if not token_info.get("app_id"): # short lived token does not have app_id and is not stored so can't be revoked return False @@ -435,6 +460,10 @@ class SecuritySettings(ConfigBaseItem): label=token_info["app_id"], description="App connected to MusicAssistant API", store_hashed=False, + value={ + "expires": token_info.get("exp"), + "last_login": token_info.get("last_login"), + }, ) for client_id, token_info in self.conf_mgr.stored_config[ CONF_KEY_SECURITY diff --git a/music_assistant/managers/database.py b/music_assistant/managers/database.py index 0fb1ac64..d50179f1 100755 --- a/music_assistant/managers/database.py +++ b/music_assistant/managers/database.py @@ -2,6 +2,7 @@ # pylint: disable=too-many-lines import logging import os +from datetime import datetime from typing import List, Optional, Union import aiosqlite @@ -849,6 +850,15 @@ class DatabaseManager: return result[0] return None + async def async_mark_item_played(self, item_id: str, provider: str): + """Mark item as played in playlog.""" + timestamp = datetime.utcnow().timestamp() + async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: + sql_query = """INSERT or REPLACE INTO playlog + (item_id, provider, timestamp) VALUES(?,?,?);""" + await db_conn.execute(sql_query, (item_id, provider, timestamp)) + await db_conn.commit() + async def async_get_thumbnail_id(self, url, size): """Get/create id for thumbnail.""" async with aiosqlite.connect(self._dbfile, timeout=120) as db_conn: diff --git a/music_assistant/managers/streams.py b/music_assistant/managers/streams.py index 2b17bde5..df649df6 100755 --- a/music_assistant/managers/streams.py +++ b/music_assistant/managers/streams.py @@ -397,6 +397,9 @@ class StreamManager: streamdetails.provider, streamdetails.item_id, ) + await self.mass.database.async_mark_item_played( + streamdetails.item_id, streamdetails.provider + ) # send analyze job to background worker # TODO: feed audio chunks to analyzer while streaming diff --git a/music_assistant/translations.json b/music_assistant/translations.json index 6fdf5813..282baa57 100644 --- a/music_assistant/translations.json +++ b/music_assistant/translations.json @@ -14,6 +14,7 @@ "crossfade_duration": "Enable crossfade", "group_delay": "Correction of groupdelay", "security": "Security", + "app_tokens": "App tokens", "power_control": "Power Control", "volume_control": "Volume Control", @@ -43,6 +44,7 @@ "desc_player_name": "Stel een aangepaste naam in voor deze speler.", "crossfade_duration": "Crossfade inschakelen", "security": "Beveiliging", + "app_tokens": "App tokens", "group_delay": "Correctie van groepsvertraging", "power_control": "Power Control", "volume_control": "Volume Control", diff --git a/music_assistant/web/server.py b/music_assistant/web/server.py index 4e6ee504..c67bf24e 100755 --- a/music_assistant/web/server.py +++ b/music_assistant/web/server.py @@ -20,8 +20,6 @@ import ujson from aiohttp import WSMsgType, web from aiohttp.web import WebSocketResponse from music_assistant.constants import ( - CONF_KEY_SECURITY, - CONF_KEY_SECURITY_APP_TOKENS, CONF_KEY_SECURITY_LOGIN, CONF_PASSWORD, CONF_USERNAME, @@ -187,6 +185,11 @@ class WebServer: return web.json_response(self.discovery_info) return self.discovery_info + @api_route("revoke_token") + async def async_revoke_token(self, client_id: str): + """Revoke token for client.""" + return self.mass.config.security.revoke_app_token(client_id) + @api_route("get_token", False) async def async_get_token( self, username: str, password: str, app_id: str = "" @@ -206,19 +209,17 @@ class WebServer: "app_id": app_id, } if app_id: - token_info["exp"] = datetime.datetime.utcnow() + datetime.timedelta( - days=365 * 10 - ) + token_info["enabled"] = True + token_info["exp"] = ( + datetime.datetime.utcnow() + datetime.timedelta(days=365 * 10) + ).timestamp() else: - token_info["exp"] = datetime.datetime.utcnow() + datetime.timedelta( - hours=8 - ) - token = jwt.encode(token_info, self.jwt_key).decode() + token_info["exp"] = ( + datetime.datetime.utcnow() + datetime.timedelta(hours=8) + ).timestamp() + token = jwt.encode(token_info, self.jwt_key, algorithm="HS256") if app_id: - self.mass.config.stored_config[CONF_KEY_SECURITY][ - CONF_KEY_SECURITY_APP_TOKENS - ][client_id] = token_info - self.mass.config.save() + self.mass.config.security.add_app_token(token_info) token_info["token"] = token return token_info raise AuthenticationError("Invalid credentials") @@ -317,7 +318,7 @@ class WebServer: except Exception as exc: # pylint:disable=broad-except # log the error only await self.__async_send_json(ws_client, error=str(exc), **json_msg) - LOGGER.debug("Error with WS client", exc_info=exc) + LOGGER.error("Error with WS client", exc_info=exc) # websocket disconnected request.app["clients"].remove(ws_client) @@ -370,10 +371,11 @@ class WebServer: async def __async_handle_auth(self, ws_client: WebSocketResponse, token: str): """Handle authentication with JWT token.""" - token_info = jwt.decode(token, self.mass.web.jwt_key) + token_info = jwt.decode(token, self.mass.web.jwt_key, algorithms=["HS256"]) if self.mass.config.security.is_token_revoked(token_info): raise AuthenticationError("Token is revoked") ws_client.authenticated = True + self.mass.config.security.set_last_login(token_info["client_id"]) # TODO: store token/app_id on ws_client obj and periodiclaly check if token is expired or revoked await self.__async_send_json(ws_client, result="auth", data=token_info) -- 2.34.1