Skip to content

Commit

Permalink
Several small tweaks and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Jan 6, 2025
1 parent b88d391 commit e1c7e5b
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 252 deletions.
60 changes: 4 additions & 56 deletions music_assistant/controllers/media/audiobooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Any

from music_assistant_models.enums import MediaType, ProviderFeature
from music_assistant_models.errors import InvalidDataError
from music_assistant_models.media_items import Artist, Audiobook, Chapter, UniqueList
from music_assistant_models.media_items import Artist, Audiobook, UniqueList

from music_assistant.constants import DB_TABLE_AUDIOBOOKS, DB_TABLE_PLAYLOG
from music_assistant.constants import DB_TABLE_AUDIOBOOKS
from music_assistant.controllers.media.base import MediaControllerBase
from music_assistant.helpers.compare import (
compare_audiobook,
Expand Down Expand Up @@ -50,7 +49,6 @@ def __init__(self, *args, **kwargs) -> None:
FROM audiobooks""" # noqa: E501
# register (extra) api handlers
api_base = self.api_base
self.mass.register_api_command(f"music/{api_base}/audiobook_chapters", self.chapters)
self.mass.register_api_command(f"music/{api_base}/audiobook_versions", self.versions)

async def library_items(
Expand Down Expand Up @@ -94,22 +92,6 @@ async def library_items(
)
return result

async def chapters(
self,
item_id: str,
provider_instance_id_or_domain: str,
) -> UniqueList[Chapter]:
"""Return audiobook chapters for the given provider audiobook id."""
if library_audiobook := await self.get_library_item_by_prov_id(
item_id, provider_instance_id_or_domain
):
# return items from first/only provider
for provider_mapping in library_audiobook.provider_mappings:
return await self._get_provider_audiobook_chapters(
provider_mapping.item_id, provider_mapping.provider_instance
)
return await self._get_provider_audiobook_chapters(item_id, provider_instance_id_or_domain)

async def versions(
self,
item_id: str,
Expand Down Expand Up @@ -149,9 +131,9 @@ async def _add_library_item(self, item: Audiobook) -> int:
"metadata": serialize_to_json(item.metadata),
"external_ids": serialize_to_json(item.external_ids),
"publisher": item.publisher,
"total_chapters": item.total_chapters,
"authors": serialize_to_json(item.authors),
"narrators": serialize_to_json(item.narrators),
"duration": item.duration,
},
)
# update/set provider_mappings table
Expand Down Expand Up @@ -186,53 +168,19 @@ async def _update_library_item(
update.external_ids if overwrite else cur_item.external_ids
),
"publisher": cur_item.publisher or update.publisher,
"total_chapters": cur_item.total_chapters or update.total_chapters,
"authors": serialize_to_json(
update.authors if overwrite else cur_item.authors or update.authors
),
"narrators": serialize_to_json(
update.narrators if overwrite else cur_item.narrators or update.narrators
),
"duration": update.duration or update.duration,
},
)
# update/set provider_mappings table
await self._set_provider_mappings(db_id, provider_mappings, overwrite)
self.logger.debug("updated %s in database: (id %s)", update.name, db_id)

async def _get_provider_audiobook_chapters(
self, item_id: str, provider_instance_id_or_domain: str
) -> list[Chapter]:
"""Return audiobook chapters for the given provider audiobook id."""
prov: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain)
if prov is None:
return []
# grab the chapters from the provider
# note that we do not cache any of this because its
# always a rather small list and we want fresh resume info
items = await prov.get_audiobook_chapters(item_id)

async def set_resume_position(chapter: Chapter) -> None:
if chapter.fully_played is not None or chapter.resume_position_ms:
return
# TODO: inject resume position info here for providers that do not natively provide it
resume_info_db_row = await self.mass.music.database.get_row(
DB_TABLE_PLAYLOG,
{
"item_id": chapter.item_id,
"provider": prov.lookup_key,
"media_type": MediaType.CHAPTER,
},
)
if resume_info_db_row is None:
return
if resume_info_db_row["seconds_played"]:
chapter.resume_position_ms = int(resume_info_db_row["seconds_played"] * 1000)
if resume_info_db_row["fully_played"] is not None:
chapter.fully_played = resume_info_db_row["fully_played"]

await asyncio.gather(*[set_resume_position(chapter) for chapter in items])
return items

async def radio_mode_base_tracks(
self,
item_id: str,
Expand Down
35 changes: 28 additions & 7 deletions music_assistant/controllers/media/podcasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

from music_assistant_models.enums import MediaType, ProviderFeature
from music_assistant_models.errors import InvalidDataError
from music_assistant_models.media_items import Artist, Episode, Podcast, UniqueList
from music_assistant_models.media_items import (
Artist,
Podcast,
PodcastEpisode,
UniqueList,
)

from music_assistant.constants import DB_TABLE_PLAYLOG, DB_TABLE_PODCASTS
from music_assistant.controllers.media.base import MediaControllerBase
Expand Down Expand Up @@ -51,6 +56,7 @@ def __init__(self, *args, **kwargs) -> None:
# register (extra) api handlers
api_base = self.api_base
self.mass.register_api_command(f"music/{api_base}/podcast_episodes", self.episodes)
self.mass.register_api_command(f"music/{api_base}/podcast_episode", self.episode)
self.mass.register_api_command(f"music/{api_base}/podcast_versions", self.versions)

async def library_items(
Expand Down Expand Up @@ -98,18 +104,33 @@ async def episodes(
self,
item_id: str,
provider_instance_id_or_domain: str,
) -> UniqueList[Episode]:
) -> UniqueList[PodcastEpisode]:
"""Return podcast episodes for the given provider podcast id."""
# always check if we have a library item for this podcast
if library_podcast := await self.get_library_item_by_prov_id(
item_id, provider_instance_id_or_domain
):
# return items from first/only provider
for provider_mapping in library_podcast.provider_mappings:
return await self._get_provider_podcast_episodes(
episodes = await self._get_provider_podcast_episodes(
provider_mapping.item_id, provider_mapping.provider_instance
)
return await self._get_provider_podcast_episodes(item_id, provider_instance_id_or_domain)
return sorted(episodes, key=lambda x: x.position)
episodes = await self._get_provider_podcast_episodes(
item_id, provider_instance_id_or_domain
)
return sorted(episodes, key=lambda x: x.position)

async def episode(
self,
item_id: str,
provider_instance_id_or_domain: str,
) -> UniqueList[PodcastEpisode]:
"""Return single podcast episode by the given provider podcast id."""
prov: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain)
if not prov:
raise InvalidDataError("Provider not found")
return await prov.get_podcast_episode(item_id)

async def versions(
self,
Expand Down Expand Up @@ -194,7 +215,7 @@ async def _update_library_item(

async def _get_provider_podcast_episodes(
self, item_id: str, provider_instance_id_or_domain: str
) -> list[Episode]:
) -> list[PodcastEpisode]:
"""Return podcast episodes for the given provider podcast id."""
prov: MusicProvider = self.mass.get_provider(provider_instance_id_or_domain)
if prov is None:
Expand All @@ -204,7 +225,7 @@ async def _get_provider_podcast_episodes(
# always a rather small list and we want fresh resume info
items = await prov.get_podcast_episodes(item_id)

async def set_resume_position(episode: Episode) -> None:
async def set_resume_position(episode: PodcastEpisode) -> None:
if episode.fully_played is not None or episode.resume_position_ms:
return
# TODO: inject resume position info here for providers that do not natively provide it
Expand All @@ -213,7 +234,7 @@ async def set_resume_position(episode: Episode) -> None:
{
"item_id": episode.item_id,
"provider": prov.lookup_key,
"media_type": MediaType.EPISODE,
"media_type": MediaType.PODCAST_EPISODE,
},
)
if resume_info_db_row is None:
Expand Down
42 changes: 26 additions & 16 deletions music_assistant/controllers/music.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
CONF_SYNC_INTERVAL = "sync_interval"
CONF_DELETED_PROVIDERS = "deleted_providers"
CONF_ADD_LIBRARY_ON_PLAY = "add_library_on_play"
DB_SCHEMA_VERSION: Final[int] = 12
DB_SCHEMA_VERSION: Final[int] = 13


class MusicController(CoreController):
Expand Down Expand Up @@ -443,7 +443,12 @@ async def browse(self, path: str | None = None) -> list[MediaItemType]:
else:
back_path = f"{provider_instance}://" + "/".join(sub_path.split("/")[:-1])
prepend_items.append(
BrowseFolder(item_id="back", provider=provider_instance, path=back_path, name="..")
BrowseFolder(
item_id="back",
provider=provider_instance,
path=back_path,
name="..",
)
)
# limit -1 to account for the prepended items
prov_items = await prov.browse(path=path)
Expand Down Expand Up @@ -500,6 +505,9 @@ async def get_item(
if provider_instance_id_or_domain == "builtin":
# handle special case of 'builtin' MusicProvider which allows us to play regular url's
return await self.mass.get_provider("builtin").parse_item(item_id)
if media_type == MediaType.PODCAST_EPISODE:
# special case for podcast episodes
return await self.podcasts.episode(item_id, provider_instance_id_or_domain)
ctrl = self.get_controller(media_type)
return await ctrl.get(
item_id=item_id,
Expand Down Expand Up @@ -648,7 +656,9 @@ async def refresh_item(
continue
with suppress(MediaNotFoundError):
media_item = await ctrl.get_provider_item(
prov_mapping.item_id, prov_mapping.provider_instance, force_refresh=True
prov_mapping.item_id,
prov_mapping.provider_instance,
force_refresh=True,
)
provider = media_item.provider
item_id = media_item.item_id
Expand Down Expand Up @@ -859,13 +869,9 @@ def get_controller(
return self.playlists
if media_type == MediaType.AUDIOBOOK:
return self.audiobooks
if media_type == MediaType.CHAPTER:
return self.audiobooks
if media_type == MediaType.EPISODE:
return self.podcasts
if media_type == MediaType.PODCAST:
return self.podcasts
if media_type == MediaType.EPISODE:
if media_type == MediaType.PODCAST_EPISODE:
return self.podcasts
return None

Expand Down Expand Up @@ -969,7 +975,8 @@ async def cleanup_provider(self, provider_instance: str) -> None:

# cleanup media items from db matched to deleted provider
self.logger.info(
"Removing provider %s from library, this can take a a while...", provider_instance
"Removing provider %s from library, this can take a a while...",
provider_instance,
)
errors = 0
for ctrl in (
Expand Down Expand Up @@ -1047,7 +1054,13 @@ async def _cleanup_database(self) -> None:
DB_TABLE_PLAYLOG, f"timestamp < strftime('%s','now') - {3600 * 24 * 90}"
)
# db tables cleanup
for ctrl in (self.albums, self.artists, self.tracks, self.playlists, self.radio):
for ctrl in (
self.albums,
self.artists,
self.tracks,
self.playlists,
self.radio,
):
# Provider mappings where the db item is removed
query = (
f"item_id not in (SELECT item_id from {ctrl.db_table}) "
Expand Down Expand Up @@ -1204,10 +1217,7 @@ async def __migrate_database(self, prev_version: int) -> None:
await self.database.execute("DROP TABLE IF EXISTS track_loudness")

if prev_version <= 10:
# recreate db tables for audiobooks and podcasts due to some mistakes in early version
await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_AUDIOBOOKS}")
await self.database.execute(f"DROP TABLE IF EXISTS {DB_TABLE_PODCASTS}")
await self.__create_database_tables()
# add new columns to playlog table
try:
await self.database.execute(
f"ALTER TABLE {DB_TABLE_PLAYLOG} ADD COLUMN fully_played BOOLEAN"
Expand All @@ -1219,7 +1229,7 @@ async def __migrate_database(self, prev_version: int) -> None:
if "duplicate column" not in str(err):
raise

if prev_version <= 11:
if prev_version <= 12:
# Need to drop the NOT NULL requirement on podcasts.publisher and audiobooks.publisher
# However, because there is no ALTER COLUMN support in sqlite, we will need
# to create the tables again.
Expand Down Expand Up @@ -1351,10 +1361,10 @@ async def __create_database_tables(self) -> None:
[version] TEXT,
[favorite] BOOLEAN DEFAULT 0,
[publisher] TEXT,
[total_chapters] INTEGER,
[authors] json NOT NULL,
[narrators] json NOT NULL,
[metadata] json NOT NULL,
[duration] INTEGER,
[external_ids] json NOT NULL,
[play_count] INTEGER DEFAULT 0,
[last_played] INTEGER DEFAULT 0,
Expand Down
Loading

0 comments on commit e1c7e5b

Please sign in to comment.