fix compatability with pyjwt update
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 28 Dec 2020 21:00:12 +0000 (22:00 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 28 Dec 2020 21:00:12 +0000 (22:00 +0100)
music_assistant/constants.py
music_assistant/helpers/migration.py
music_assistant/managers/config.py
music_assistant/managers/database.py
music_assistant/managers/streams.py
music_assistant/translations.json
music_assistant/web/server.py

index 65a41de371312662d7a93721635d25f86cc2db22..e0e2ac82223bec5e4974463041bacbb97205a608 100755 (executable)
@@ -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
index f8f3206b2d7048469e5cbd1c7c8d48149019b114..14b212e80531a9507eabb428e38bbf5c9e53b351 100644 (file)
@@ -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,
index 04b7d4c81a29902d537c64248426b569618ffa98..a93befb91d618e18b0e2ffa24bad87af39381913 100755 (executable)
@@ -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
index 0fb1ac6461d1e325403369cff32f1dafec9c7f0b..d50179f1197a89a759cb5e7a124c70aa7a34fda5 100755 (executable)
@@ -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:
index 2b17bde55bf1b8c45137b8d753558f9ced125b63..df649df6dc3121caf29d060d99f6d3336e1a686c 100755 (executable)
@@ -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
index 6fdf5813dff893f9f099aecd33da49dfc8c4d501..282baa5712f96ef8aafbebf997ba9c8860d4b747 100644 (file)
@@ -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",
index 4e6ee504c01e7a7c870973826f51513e6ad9ea1e..c67bf24e7bc88218d40353deda9ad4169032e901 100755 (executable)
@@ -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)