Skip to content

Commit

Permalink
Small refactor of Chapters and PodcastEpisodes
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Jan 5, 2025
1 parent b88d391 commit 477c473
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 225 deletions.
58 changes: 2 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,7 +131,6 @@ 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),
},
Expand Down Expand Up @@ -186,7 +167,6 @@ 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
),
Expand All @@ -199,40 +179,6 @@ async def _update_library_item(
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
27 changes: 22 additions & 5 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,7 +104,7 @@ 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(
Expand All @@ -111,6 +117,17 @@ async def episodes(
)
return await self._get_provider_podcast_episodes(item_id, provider_instance_id_or_domain)

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,
item_id: str,
Expand Down Expand Up @@ -194,7 +211,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 +221,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 +230,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
34 changes: 20 additions & 14 deletions music_assistant/controllers/music.py
Original file line number Diff line number Diff line change
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 @@ -648,7 +653,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 +866,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 +972,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 +1051,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 +1214,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 Down Expand Up @@ -1351,7 +1358,6 @@ 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,
Expand Down
Loading

0 comments on commit 477c473

Please sign in to comment.