Ignore playlists which are stored with album tracks (#2017)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 10 Mar 2025 13:19:37 +0000 (14:19 +0100)
committerGitHub <noreply@github.com>
Mon, 10 Mar 2025 13:19:37 +0000 (14:19 +0100)
music_assistant/providers/filesystem_local/__init__.py
music_assistant/providers/filesystem_local/constants.py
music_assistant/providers/filesystem_smb/__init__.py

index e96c39f5601bfb1a1217e627990fed61525da63b..d4c8a8c18ef4e159c0795bc38dc080bd94b8f17f 100644 (file)
@@ -75,6 +75,7 @@ from .constants import (
     AUDIOBOOK_EXTENSIONS,
     CONF_ENTRY_CONTENT_TYPE,
     CONF_ENTRY_CONTENT_TYPE_READ_ONLY,
+    CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS,
     CONF_ENTRY_MISSING_ALBUM_ARTIST,
     CONF_ENTRY_PATH,
     IMAGE_EXTENSIONS,
@@ -136,8 +137,14 @@ async def get_config_entries(
             CONF_ENTRY_CONTENT_TYPE,
             CONF_ENTRY_PATH,
             CONF_ENTRY_MISSING_ALBUM_ARTIST,
+            CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS,
         )
-    return (CONF_ENTRY_PATH, CONF_ENTRY_CONTENT_TYPE_READ_ONLY, CONF_ENTRY_MISSING_ALBUM_ARTIST)
+    return (
+        CONF_ENTRY_PATH,
+        CONF_ENTRY_CONTENT_TYPE_READ_ONLY,
+        CONF_ENTRY_MISSING_ALBUM_ARTIST,
+        CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS,
+    )
 
 
 class LocalFileSystemProvider(MusicProvider):
@@ -354,12 +361,9 @@ class LocalFileSystemProvider(MusicProvider):
             self.sync_running = True
             try:
                 for item in listdir(self.base_path):
-                    cur_filenames.add(item.relative_path)
-                    # continue if the item did not change (checksum still the same)
                     prev_checksum = file_checksums.get(item.relative_path)
-                    if item.checksum == prev_checksum:
-                        continue
-                    self._process_item(item, prev_checksum)
+                    if self._process_item(item, prev_checksum):
+                        cur_filenames.add(item.relative_path)
             finally:
                 self.sync_running = False
 
@@ -378,10 +382,27 @@ class LocalFileSystemProvider(MusicProvider):
         # process orphaned albums and artists
         await self._process_orphaned_albums_and_artists()
 
-    def _process_item(self, item: FileSystemItem, prev_checksum: str | None) -> None:
+    def _process_item(self, item: FileSystemItem, prev_checksum: str | None) -> bool:
         """Process a single item. NOT async friendly."""
         try:
             self.logger.debug("Processing: %s", item.relative_path)
+
+            # ignore playlists that are in album directories
+            # we need to run this check early because the setting may have changed
+            if (
+                item.ext in PLAYLIST_EXTENSIONS
+                and self.media_content_type == "music"
+                and self.config.get_value(CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS.key)
+            ):
+                # we assume this in a bit of a dumb way by just checking if the playlist
+                # is more than 1 level deep in the directory structure
+                if len(item.relative_path.split("/")) > 2:
+                    return False
+
+            # return early if the item did not change (checksum still the same)
+            if item.checksum == prev_checksum:
+                return True
+
             if item.ext in TRACK_EXTENSIONS and self.media_content_type == "music":
                 # handle track item
                 tags = parse_tags(item.absolute_path, item.file_size)
@@ -396,7 +417,7 @@ class LocalFileSystemProvider(MusicProvider):
                     )
 
                 asyncio.run_coroutine_threadsafe(process_track(), self.mass.loop).result()
-                return
+                return True
 
             if item.ext in AUDIOBOOK_EXTENSIONS and self.media_content_type == "audiobooks":
                 # handle audiobook item
@@ -415,7 +436,7 @@ class LocalFileSystemProvider(MusicProvider):
                     )
 
                 asyncio.run_coroutine_threadsafe(process_audiobook(), self.mass.loop).result()
-                return
+                return True
 
             if item.ext in PODCAST_EPISODE_EXTENSIONS and self.media_content_type == "podcasts":
                 # handle podcast(episode) item
@@ -432,9 +453,10 @@ class LocalFileSystemProvider(MusicProvider):
                     )
 
                 asyncio.run_coroutine_threadsafe(process_episode(), self.mass.loop).result()
-                return
+                return True
 
             if item.ext in PLAYLIST_EXTENSIONS and self.media_content_type == "music":
+                # handle playlist item
 
                 async def process_playlist() -> None:
                     playlist = await self.get_playlist(item.relative_path)
@@ -446,7 +468,7 @@ class LocalFileSystemProvider(MusicProvider):
                     )
 
                 asyncio.run_coroutine_threadsafe(process_playlist(), self.mass.loop).result()
-                return
+                return True
 
         except Exception as err:
             # we don't want the whole sync to crash on one file so we catch all exceptions here
@@ -456,6 +478,7 @@ class LocalFileSystemProvider(MusicProvider):
                 str(err),
                 exc_info=err if self.logger.isEnabledFor(logging.DEBUG) else None,
             )
+        return False
 
     async def _process_orphaned_albums_and_artists(self) -> None:
         """Process deletion of orphaned albums and artists."""
index cb536700f3a6c7eb60b9d21c0ddf844e8eac0d0d..9e79427dafbecd8cb31dd576f928a71b66664994 100644 (file)
@@ -53,6 +53,18 @@ CONF_ENTRY_CONTENT_TYPE_READ_ONLY = ConfigEntry.from_dict(
     }
 )
 
+CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS = ConfigEntry(
+    key="ignore_album_playlists",
+    type=ConfigEntryType.BOOLEAN,
+    label="Ignore playlists with album tracks within album folders",
+    description="A digital album often comes with a playlist file (.m3u) "
+    "that contains the tracks of the album. Adding all these playlists to the library, "
+    "is not very practical so it's better to just ignore them.\n\n"
+    "If this option is enabled, any playlists will be ignored which are more than "
+    "1 level deep in the folder structure. E.g. /music/artistname/albumname/playlist.m3u",
+    default_value=True,
+    required=False,
+)
 
 TRACK_EXTENSIONS = {
     "aac",
index 3d762e39b1ac8de9b8cbf6d2ac20da4998e5682d..606582c0f7810583102df328514e49fdcbd0b54b 100644 (file)
@@ -17,6 +17,7 @@ from music_assistant.providers.filesystem_local import LocalFileSystemProvider,
 from music_assistant.providers.filesystem_local.constants import (
     CONF_ENTRY_CONTENT_TYPE,
     CONF_ENTRY_CONTENT_TYPE_READ_ONLY,
+    CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS,
     CONF_ENTRY_MISSING_ALBUM_ARTIST,
 )
 
@@ -121,6 +122,7 @@ async def get_config_entries(
             "want to pass to the mount command if needed for your particular setup.",
         ),
         CONF_ENTRY_MISSING_ALBUM_ARTIST,
+        CONF_ENTRY_IGNORE_ALBUM_PLAYLISTS,
     )
 
     if instance_id is None or values is None: