Fix audiobook related controller bugs (#2412)
authorOzGav <gavnosp@hotmail.com>
Mon, 22 Sep 2025 14:29:33 +0000 (00:29 +1000)
committerGitHub <noreply@github.com>
Mon, 22 Sep 2025 14:29:33 +0000 (16:29 +0200)
music_assistant/controllers/media/audiobooks.py
music_assistant/controllers/music.py

index 8ac8a8c424e5f3b564cf05765e4a309850090b49..417698b3ae71be1ea9dcb6176a14ea562c9b4e9d 100644 (file)
@@ -180,7 +180,7 @@ class AudiobooksController(MediaControllerBase[Audiobook]):
                 "narrators": serialize_to_json(
                     update.narrators if overwrite else cur_item.narrators or update.narrators
                 ),
-                "duration": update.duration or update.duration,
+                "duration": update.duration if overwrite else cur_item.duration or update.duration,
                 "search_name": create_safe_string(name, True, True),
                 "search_sort_name": create_safe_string(sort_name, True, True),
             },
index 0dd420d0258aaabd59738b74b66a3132e30d2a2f..95bcde056dc78fead97656c39429c951cfff3547 100644 (file)
@@ -69,6 +69,7 @@ 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.models.core_controller import CoreController
+from music_assistant.models.music_provider import MusicProvider
 
 from .media.albums import AlbumsController
 from .media.artists import ArtistsController
@@ -82,7 +83,6 @@ if TYPE_CHECKING:
     from music_assistant_models.config_entries import CoreConfig
     from music_assistant_models.media_items import Audiobook, PodcastEpisode
 
-    from music_assistant.models.music_provider import MusicProvider
 
 CONF_RESET_DB = "reset_db"
 DEFAULT_SYNC_INTERVAL = 12 * 60  # default sync interval in minutes
@@ -1076,15 +1076,28 @@ class MusicController(CoreController):
         Returns a boolean with the fully_played status
         and an integer with the resume position in ms.
         """
+        provider_fully_played = False
+        provider_position_ms = 0
+
+        # Try to get position from providers
         for prov_mapping in media_item.provider_mappings:
-            if not (music_prov := self.mass.get_provider(prov_mapping.provider_instance)):
+            if not (provider := self.mass.get_provider(prov_mapping.provider_instance)):
                 continue
-            with suppress(NotImplementedError):
-                return await music_prov.get_resume_position(
-                    prov_mapping.item_id, media_item.media_type
-                )
-        # no provider info found, fallback to library playlog
-        if db_entry := await self.mass.music.database.get_row(
+            # Type guard: ensure this is a MusicProvider with get_resume_position method
+            if isinstance(provider, MusicProvider):
+                with suppress(NotImplementedError):
+                    (
+                        provider_fully_played,
+                        provider_position_ms,
+                    ) = await provider.get_resume_position(
+                        prov_mapping.item_id, media_item.media_type
+                    )
+                    break  # Use first provider that returns data
+
+        # Get MA's internal position from playlog
+        ma_fully_played = False
+        ma_position_ms = 0
+        if db_entry := await self.database.get_row(
             DB_TABLE_PLAYLOG,
             {
                 "media_type": media_item.media_type.value,
@@ -1092,12 +1105,14 @@ class MusicController(CoreController):
                 "provider": media_item.provider,
             },
         ):
-            resume_position_ms = (
-                db_entry["seconds_played"] * 1000 if db_entry["seconds_played"] else 0
-            )
-            return (db_entry["fully_played"], resume_position_ms)
+            ma_position_ms = db_entry["seconds_played"] * 1000 if db_entry["seconds_played"] else 0
+            ma_fully_played = db_entry["fully_played"]
 
-        return (False, 0)
+        # Return the higher position to ensure users never lose progress
+        if ma_position_ms >= provider_position_ms:
+            return ma_fully_played, ma_position_ms
+        else:
+            return provider_fully_played, provider_position_ms
 
     def get_controller(
         self, media_type: MediaType