Fix fully_played should return boolean
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 20:18:34 +0000 (21:18 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 20:18:34 +0000 (21:18 +0100)
music_assistant/controllers/media/audiobooks.py
music_assistant/controllers/media/base.py
music_assistant/controllers/media/podcasts.py
music_assistant/controllers/music.py
music_assistant/helpers/util.py

index ad2cc4765ebda33a3a79ac0b5cd5a5192e625765..65b463f2f06ee11d40a5c83f4bd86d6153df9bbd 100644 (file)
@@ -18,6 +18,7 @@ from music_assistant.helpers.compare import (
 from music_assistant.helpers.database import UNSET
 from music_assistant.helpers.datetime import utc_timestamp
 from music_assistant.helpers.json import serialize_to_json
+from music_assistant.helpers.util import parse_optional_bool
 from music_assistant.models.music_provider import MusicProvider
 
 if TYPE_CHECKING:
@@ -316,7 +317,7 @@ class AudiobooksController(MediaControllerBase[Audiobook]):
         # abort if nothing changed
         if (
             cur_entry
-            and cur_entry["fully_played"] == media_item.fully_played
+            and parse_optional_bool(cur_entry["fully_played"]) == media_item.fully_played
             and abs((cur_entry["seconds_played"] or 0) - seconds_played) <= 2
         ):
             return
index f07b3cb92f5d565fd6500ca23976157bee883ca6..2954a45afbe905753b0e5a158469702778cbb92e 100644 (file)
@@ -34,7 +34,7 @@ from music_assistant.controllers.webserver.helpers.auth_middleware import get_cu
 from music_assistant.helpers.compare import compare_media_item, create_safe_string
 from music_assistant.helpers.database import UNSET
 from music_assistant.helpers.json import json_loads, serialize_to_json
-from music_assistant.helpers.util import guard_single_request
+from music_assistant.helpers.util import guard_single_request, parse_optional_bool
 
 if TYPE_CHECKING:
     from collections.abc import AsyncGenerator, Mapping
@@ -1095,6 +1095,10 @@ class MediaControllerBase[ItemCls: "MediaItemType"](metaclass=ABCMeta):
                 continue
             db_row_dict[key] = json_loads(raw_value)
 
+        # parse "fully_played" as bool if present in the row
+        if "fully_played" in db_row_dict:
+            db_row_dict["fully_played"] = parse_optional_bool(db_row_dict["fully_played"])
+
         # copy track_album --> album
         if track_album := db_row_dict.get("track_album"):
             db_row_dict["album"] = track_album
index 1d7778174d47cd47419516e2f035abb4bbf6a51c..4c89c6932ea8cd76ad3a8687bed584fddff78f65 100644 (file)
@@ -238,7 +238,7 @@ class PodcastsController(MediaControllerBase[Podcast]):
             if resume_info_db_row["seconds_played"]:
                 episode.resume_position_ms = int(resume_info_db_row["seconds_played"] * 1000)
             if resume_info_db_row["fully_played"] is not None:
-                episode.fully_played = resume_info_db_row["fully_played"]
+                episode.fully_played = bool(resume_info_db_row["fully_played"])
 
         # grab the episodes from the provider
         # note that we do not cache any of this because its
index 16fc5180f51c62c5c4240f81fff163ba835e8dbf..03e833a96b555f4bf328edd87639cad76ab80d13 100644 (file)
@@ -75,7 +75,7 @@ from music_assistant.helpers.datetime import utc_timestamp
 from music_assistant.helpers.json import json_dumps, json_loads, serialize_to_json
 from music_assistant.helpers.tags import split_artists
 from music_assistant.helpers.uri import parse_uri
-from music_assistant.helpers.util import TaskManager, parse_title_and_version
+from music_assistant.helpers.util import TaskManager, parse_optional_bool, parse_title_and_version
 from music_assistant.models.core_controller import CoreController
 from music_assistant.models.music_provider import MusicProvider
 from music_assistant.models.smart_fades import SmartFadesAnalysis, SmartFadesAnalysisFragment
@@ -1446,7 +1446,7 @@ class MusicController(CoreController):
             params["userid"] = userid
         if db_entry := await self.database.get_row(DB_TABLE_PLAYLOG, params):
             ma_position_ms = db_entry["seconds_played"] * 1000 if db_entry["seconds_played"] else 0
-            ma_fully_played = db_entry["fully_played"]
+            ma_fully_played = parse_optional_bool(db_entry["fully_played"])
 
         # Return the higher position to ensure users never lose progress
         if ma_position_ms >= provider_position_ms:
index 2b8f5424b762258f9366c072f25005f27bb03ff1..3e849f685cee28501ed1c92ffdb86a5b89078b47 100644 (file)
@@ -672,6 +672,23 @@ async def detect_charset(data: bytes, fallback: str = "utf-8") -> str:
     return fallback
 
 
+def parse_optional_bool(value: Any) -> bool | None:
+    """Parse an optional boolean value from various input types."""
+    if value is None:
+        return None
+    if isinstance(value, bool):
+        return value
+    if isinstance(value, str):
+        value_lower = value.strip().lower()
+        if value_lower in ("true", "1", "yes", "on"):
+            return True
+        if value_lower in ("false", "0", "no", "off"):
+            return False
+    if isinstance(value, (int, float)):
+        return bool(value)
+    return None
+
+
 def merge_dict(
     base_dict: dict[Any, Any],
     new_dict: dict[Any, Any],