From: Marcel van der Veldt Date: Thu, 11 Apr 2024 18:42:57 +0000 (+0200) Subject: Add support to specify the Metadata language (#1217) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=06552dfdc847e43f2b05d8a4a9897384b0260ee9;p=music-assistant-server.git Add support to specify the Metadata language (#1217) --- diff --git a/music_assistant/constants.py b/music_assistant/constants.py index fde91282..14917e41 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -61,6 +61,7 @@ CONF_ANNOUNCE_VOLUME: Final[str] = "announce_volume" CONF_ANNOUNCE_VOLUME_MIN: Final[str] = "announce_volume_min" CONF_ANNOUNCE_VOLUME_MAX: Final[str] = "announce_volume_max" CONF_ICON: Final[str] = "icon" +CONF_LANGUAGE: Final[str] = "language" # config default values DEFAULT_HOST: Final[str] = "0.0.0.0" diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index 6f58f5f9..e75fa331 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -620,7 +620,18 @@ class ConfigController: # only allow setting raw values if main entry exists msg = f"Invalid provider_instance: {provider_instance}" raise KeyError(msg) - self.set(f"{CONF_PROVIDERS}/{provider_instance}/{key}", value) + self.set(f"{CONF_PROVIDERS}/{provider_instance}/values/{key}", value) + + def set_raw_core_config_value(self, core_module: str, key: str, value: ConfigValueType) -> None: + """ + Set (raw) single config(entry) value for a core controller. + + Note that this only stores the (raw) value without any validation or default. + """ + if not self.get(f"{CONF_CORE}/{core_module}"): + # create base object first if needed + self.set(f"{CONF_CORE}/{core_module}", CoreConfig({}, core_module).to_raw()) + self.set(f"{CONF_CORE}/{core_module}/values/{key}", value) def set_raw_player_config_value(self, player_id: str, key: str, value: ConfigValueType) -> None: """ diff --git a/music_assistant/server/controllers/metadata.py b/music_assistant/server/controllers/metadata.py index 23ff8b05..1a5675e1 100644 --- a/music_assistant/server/controllers/metadata.py +++ b/music_assistant/server/controllers/metadata.py @@ -8,13 +8,24 @@ import urllib.parse from base64 import b64encode from contextlib import suppress from time import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 import aiofiles from aiohttp import web -from music_assistant.common.models.enums import ImageType, MediaType, ProviderFeature, ProviderType +from music_assistant.common.models.config_entries import ( + ConfigEntry, + ConfigValueOption, + ConfigValueType, +) +from music_assistant.common.models.enums import ( + ConfigEntryType, + ImageType, + MediaType, + ProviderFeature, + ProviderType, +) from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError from music_assistant.common.models.media_items import ( Album, @@ -26,7 +37,8 @@ from music_assistant.common.models.media_items import ( Radio, Track, ) -from music_assistant.constants import VARIOUS_ARTISTS_ID_MBID, VARIOUS_ARTISTS_NAME +from music_assistant.constants import CONF_LANGUAGE, VARIOUS_ARTISTS_ID_MBID, VARIOUS_ARTISTS_NAME +from music_assistant.server.helpers.api import api_command from music_assistant.server.helpers.compare import compare_strings from music_assistant.server.helpers.images import create_collage, get_image_thumb from music_assistant.server.models.core_controller import CoreController @@ -36,6 +48,42 @@ if TYPE_CHECKING: from music_assistant.server.models.metadata_provider import MetadataProvider from music_assistant.server.providers.musicbrainz import MusicbrainzProvider +LOCALES = { + "af_ZA": "African", + "ar_AE": "Arabic (United Arab Emirates)", + "ar_EG": "Arabic (Egypt)", + "ar_SA": "Saudi Arabia", + "bg_BG": "Bulgarian", + "cs_CZ": "Czech", + "zh_CN": "Chinese", + "hr_HR": "Croatian", + "da_DK": "Danish", + "de_DE": "German", + "el_GR": "Greek", + "en_US": "English (US)", + "en_UK": "English (UK)", + "es_ES": "Spanish", + "et_EE": "Estonian", + "fi_FI": "Finnish", + "fr_FR": "French", + "hu_HU": "Hungarian", + "it_IT": "Italian", + "lt_LT": "Lithuanian", + "lv_LV": "Latvian", + "nl_NL": "Dutch", + "no_NO": "Norwegian", + "pl_PL": "Polish", + "pt_PT": "Portuguese", + "ro_RO": "Romanian", + "ru_RU": "Russian", + "sk_SK": "Slovak", + "sl_SI": "Slovenian", + "sv_SE": "Swedish", + "tr_TR": "Turkish", +} + +DEFAULT_LANGUAGE = "en_US" + class MetaDataController(CoreController): """Several helpers to search and store metadata for mediaitems.""" @@ -54,6 +102,26 @@ class MetaDataController(CoreController): ) self.manifest.icon = "book-information-variant" + async def get_config_entries( + self, + action: str | None = None, + values: dict[str, ConfigValueType] | None = None, + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + return ( + ConfigEntry( + key=CONF_LANGUAGE, + type=ConfigEntryType.STRING, + label="Preferred language", + required=False, + default_value=DEFAULT_LANGUAGE, + description="Preferred language for metadata.\n\n" + "Note that English will always be used as fallback when content " + "in your preferred language is not available.", + options=tuple(ConfigValueOption(value, key) for key, value in LOCALES.items()), + ), + ) + async def setup(self, config: CoreConfig) -> None: """Async initialize of module.""" self.mass.streams.register_dynamic_route("/imageproxy", self.handle_imageproxy) @@ -65,24 +133,51 @@ class MetaDataController(CoreController): @property def providers(self) -> list[MetadataProvider]: """Return all loaded/running MetadataProviders.""" - return self.mass.get_providers(ProviderType.METADATA) # type: ignore[return-value] + if TYPE_CHECKING: + return cast(list[MetadataProvider], self.mass.get_providers(ProviderType.METADATA)) + return self.mass.get_providers(ProviderType.METADATA) @property def preferred_language(self) -> str: - """Return preferred language for metadata as 2 letter country code (uppercase). + """Return preferred language for metadata (as 2 letter language code 'en').""" + return self.locale.split("_")[0] - Defaults to English (EN). - """ - return self._pref_lang or "EN" + @property + def locale(self) -> str: + """Return preferred language for metadata (as full locale code 'en_EN').""" + return self.mass.config.get_raw_core_config_value( + self.domain, CONF_LANGUAGE, DEFAULT_LANGUAGE + ) - @preferred_language.setter - def preferred_language(self, lang: str) -> None: - """Set preferred language to 2 letter country code. + @api_command("metadata/set_default_preferred_language") + def set_default_preferred_language(self, lang: str) -> None: + """ + Set the (default) preferred language. - Can only be set once. + Reasoning behind this is that the backend can not make a wise choice for the default, + so relies on some external source that knows better to set this info, like the frontend + or a streaming provider. + Can only be set once (by this call or the user). """ - if self._pref_lang is None: - self._pref_lang = lang.upper() + if self.mass.config.get_raw_core_config_value(self.domain, CONF_LANGUAGE): + return # already set + if lang in LOCALES: + self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, lang) + return + lang = lang.lower() + # try strict match first + for locale_code, lang_name in LOCALES.items(): + if lang in (locale_code.lower(), lang_name.lower()): + self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, locale_code) + return + # attempt loose match on either language or country code + for locale_code in tuple(LOCALES): + language_code, region_code = locale_code.lower().split("_", 1) + if lang in (language_code, region_code): + self.mass.config.set_raw_core_config_value(self.domain, CONF_LANGUAGE, locale_code) + return + # if we reach this point, we couldn't match the language + self.logger.warning("%s is not a valid language", lang) def start_scan(self) -> None: """Start background scan for missing metadata.""" diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index ad95fa98..20dd5f33 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -311,6 +311,8 @@ class MusicController(CoreController): # handle regular provider listing, always add back folder first if not prov or not sub_path: yield BrowseFolder(item_id="root", provider="library", path="root", name="..") + if not prov: + return else: back_path = f"{provider_instance}://" + "/".join(sub_path.split("/")[:-1]) yield BrowseFolder( diff --git a/music_assistant/server/controllers/webserver.py b/music_assistant/server/controllers/webserver.py index 7863aef6..c2fef846 100644 --- a/music_assistant/server/controllers/webserver.py +++ b/music_assistant/server/controllers/webserver.py @@ -216,6 +216,8 @@ class WebserverController(CoreController): async def _handle_ws_client(self, request: web.Request) -> web.WebSocketResponse: connection = WebsocketClientHandler(self, request) + if lang := request.headers.get("Accept-Language"): + self.mass.metadata.set_default_preferred_language(lang.split(",")[0]) try: self.clients.add(connection) return await connection.handle_client() diff --git a/music_assistant/server/helpers/webserver.py b/music_assistant/server/helpers/webserver.py index 9ea282bd..b5dbe467 100644 --- a/music_assistant/server/helpers/webserver.py +++ b/music_assistant/server/helpers/webserver.py @@ -113,7 +113,7 @@ class Webserver: key = f"{method}.{path}" self._dynamic_routes.pop(key) - async def serve_static(self, file_path: str, _request: web.Request) -> web.FileResponse: + async def serve_static(self, file_path: str, request: web.Request) -> web.FileResponse: """Serve file response.""" headers = {"Cache-Control": "no-cache"} return web.FileResponse(file_path, headers=headers) diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/server/providers/qobuz/__init__.py index 0e975730..0f0b3f96 100644 --- a/music_assistant/server/providers/qobuz/__init__.py +++ b/music_assistant/server/providers/qobuz/__init__.py @@ -669,7 +669,7 @@ class QobuzProvider(MusicProvider): self.logger.info( "Successfully logged in to Qobuz as %s", details["user"]["display_name"] ) - self.mass.metadata.preferred_language = details["user"]["country_code"] + self.mass.metadata.set_default_preferred_language(details["user"]["country_code"]) return details["user_auth_token"] return None @@ -698,6 +698,9 @@ class QobuzProvider(MusicProvider): # pylint: disable=too-many-branches url = f"http://www.qobuz.com/api.json/0.2/{endpoint}" headers = {"X-App-Id": app_var(0)} + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + headers["Accept-Language"] = f"{locale}, {language};q=0.9, *;q=0.5" if endpoint != "user/login": auth_token = await self._auth_token() if not auth_token: diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/server/providers/spotify/__init__.py index ee25a68e..7826ac36 100644 --- a/music_assistant/server/providers/spotify/__init__.py +++ b/music_assistant/server/providers/spotify/__init__.py @@ -609,7 +609,7 @@ class SpotifyProvider(MusicProvider): # return existing token if we have one in memory if ( self._auth_token - and asyncio.to_thread(os.path.isdir, self._cache_dir) + and await asyncio.to_thread(os.path.isdir, self._cache_dir) and (self._auth_token["expiresAt"] > int(time.time()) + 600) ): return self._auth_token @@ -639,7 +639,7 @@ class SpotifyProvider(MusicProvider): if tokeninfo and userinfo: self._auth_token = tokeninfo self._sp_user = userinfo - self.mass.metadata.preferred_language = userinfo["country"] + self.mass.metadata.set_default_preferred_language(userinfo["country"]) self.logger.info("Successfully logged in to Spotify as %s", userinfo["id"]) self._auth_token = tokeninfo return tokeninfo @@ -758,6 +758,9 @@ class SpotifyProvider(MusicProvider): if tokeninfo is None: tokeninfo = await self.login() headers = {"Authorization": f'Bearer {tokeninfo["accessToken"]}'} + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + headers["Accept-Language"] = f"{locale}, {language};q=0.9, *;q=0.5" async with ( self._throttler, self.mass.http_session.get( diff --git a/music_assistant/server/providers/theaudiodb/__init__.py b/music_assistant/server/providers/theaudiodb/__init__.py index 2b1f0c72..4197c155 100644 --- a/music_assistant/server/providers/theaudiodb/__init__.py +++ b/music_assistant/server/providers/theaudiodb/__init__.py @@ -191,7 +191,10 @@ class AudioDbMetadataProvider(MetadataProvider): if link := artist_obj.get(key): metadata.links.add(MediaItemLink(type=link_type, url=link)) # description/biography - if desc := artist_obj.get(f"strBiography{self.mass.metadata.preferred_language}"): + lang_code, lang_country = self.mass.metadata.locale.split("_") + if desc := artist_obj.get(f"strBiography{lang_country}") or ( + desc := artist_obj.get(f"strBiography{lang_code.upper()}") + ): metadata.description = desc else: metadata.description = artist_obj.get("strBiographyEN") @@ -226,7 +229,10 @@ class AudioDbMetadataProvider(MetadataProvider): ) # description - if desc := album_obj.get(f"strDescription{self.mass.metadata.preferred_language}"): + lang_code, lang_country = self.mass.metadata.locale.split("_") + if desc := album_obj.get(f"strDescription{lang_country}") or ( + desc := album_obj.get(f"strDescription{lang_code.upper()}") + ): metadata.description = desc else: metadata.description = album_obj.get("strDescriptionEN") @@ -251,7 +257,10 @@ class AudioDbMetadataProvider(MetadataProvider): metadata.genres = {genre} metadata.mood = track_obj.get("strMood") # description - if desc := track_obj.get(f"strDescription{self.mass.metadata.preferred_language}"): + lang_code, lang_country = self.mass.metadata.locale.split("_") + if desc := track_obj.get(f"strDescription{lang_country}") or ( + desc := track_obj.get(f"strDescription{lang_code.upper()}") + ): metadata.description = desc else: metadata.description = track_obj.get("strDescriptionEN") diff --git a/music_assistant/server/providers/tunein/__init__.py b/music_assistant/server/providers/tunein/__init__.py index 408599ec..549f59fc 100644 --- a/music_assistant/server/providers/tunein/__init__.py +++ b/music_assistant/server/providers/tunein/__init__.py @@ -258,9 +258,12 @@ class TuneInProvider(MusicProvider): kwargs["username"] = self.config.get_value(CONF_USERNAME) kwargs["partnerId"] = "1" kwargs["render"] = "json" + locale = self.mass.metadata.locale.replace("_", "-") + language = locale.split("-")[0] + headers = {"Accept-Language": f"{locale}, {language};q=0.9, *;q=0.5"} async with ( self._throttler, - self.mass.http_session.get(url, params=kwargs, ssl=False) as response, + self.mass.http_session.get(url, params=kwargs, headers=headers, ssl=False) as response, ): result = await response.json() if not result or "error" in result: diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py index 699ff3e5..6c516af1 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/server/providers/ytmusic/__init__.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING from urllib.parse import unquote import pytube +from ytmusicapi.constants import SUPPORTED_LANGUAGES from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType @@ -198,6 +199,14 @@ class YoutubeMusicProvider(MusicProvider): await self._initialize_context() self._cookies = {"CONSENT": "YES+1"} self._signature_timestamp = await self._get_signature_timestamp() + # get default language (that is supported by YTM) + mass_locale = self.mass.metadata.locale + for lang_code in SUPPORTED_LANGUAGES: + if lang_code in (mass_locale, mass_locale.split("_")[0]): + self.language = lang_code + break + else: + self.language = "en" @property def supported_features(self) -> tuple[ProviderFeature, ...]: @@ -224,7 +233,9 @@ class YoutubeMusicProvider(MusicProvider): ytm_filter = "songs" if media_types[0] == MediaType.PLAYLIST: ytm_filter = "playlists" - results = await search(query=search_query, ytm_filter=ytm_filter, limit=limit) + results = await search( + query=search_query, ytm_filter=ytm_filter, limit=limit, language=self.language + ) parsed_results = SearchResults() for result in results: try: @@ -245,36 +256,28 @@ class YoutubeMusicProvider(MusicProvider): async def get_library_artists(self) -> AsyncGenerator[Artist, None]: """Retrieve all library artists from Youtube Music.""" await self._check_oauth_token() - artists_obj = await get_library_artists( - headers=self._headers, - ) + artists_obj = await get_library_artists(headers=self._headers, language=self.language) for artist in artists_obj: yield await self._parse_artist(artist) async def get_library_albums(self) -> AsyncGenerator[Album, None]: """Retrieve all library albums from Youtube Music.""" await self._check_oauth_token() - albums_obj = await get_library_albums( - headers=self._headers, - ) + albums_obj = await get_library_albums(headers=self._headers, language=self.language) for album in albums_obj: yield await self._parse_album(album, album["browseId"]) async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: """Retrieve all library playlists from the provider.""" await self._check_oauth_token() - playlists_obj = await get_library_playlists( - headers=self._headers, - ) + playlists_obj = await get_library_playlists(headers=self._headers, language=self.language) for playlist in playlists_obj: yield await self._parse_playlist(playlist) async def get_library_tracks(self) -> AsyncGenerator[Track, None]: """Retrieve library tracks from Youtube Music.""" await self._check_oauth_token() - tracks_obj = await get_library_tracks( - headers=self._headers, - ) + tracks_obj = await get_library_tracks(headers=self._headers, language=self.language) for track in tracks_obj: # Library tracks sometimes do not have a valid artist id # In that case, call the API for track details based on track id @@ -287,7 +290,7 @@ class YoutubeMusicProvider(MusicProvider): async def get_album(self, prov_album_id) -> Album: """Get full album details by id.""" await self._check_oauth_token() - if album_obj := await get_album(prov_album_id=prov_album_id): + if album_obj := await get_album(prov_album_id=prov_album_id, language=self.language): return await self._parse_album(album_obj=album_obj, album_id=prov_album_id) msg = f"Item {prov_album_id} not found" raise MediaNotFoundError(msg) @@ -295,7 +298,7 @@ class YoutubeMusicProvider(MusicProvider): async def get_album_tracks(self, prov_album_id: str) -> list[AlbumTrack]: """Get album tracks for given album id.""" await self._check_oauth_token() - album_obj = await get_album(prov_album_id=prov_album_id) + album_obj = await get_album(prov_album_id=prov_album_id, language=self.language) if not album_obj.get("tracks"): return [] tracks = [] @@ -312,7 +315,9 @@ class YoutubeMusicProvider(MusicProvider): async def get_artist(self, prov_artist_id) -> Artist: """Get full artist details by id.""" await self._check_oauth_token() - if artist_obj := await get_artist(prov_artist_id=prov_artist_id, headers=self._headers): + if artist_obj := await get_artist( + prov_artist_id=prov_artist_id, headers=self._headers, language=self.language + ): return await self._parse_artist(artist_obj=artist_obj) msg = f"Item {prov_artist_id} not found" raise MediaNotFoundError(msg) @@ -324,6 +329,7 @@ class YoutubeMusicProvider(MusicProvider): prov_track_id=prov_track_id, headers=self._headers, signature_timestamp=self._signature_timestamp, + language=self.language, ): return await self._parse_track(track_obj) msg = f"Item {prov_track_id} not found" @@ -336,7 +342,7 @@ class YoutubeMusicProvider(MusicProvider): if YT_PLAYLIST_ID_DELIMITER in prov_playlist_id: prov_playlist_id = prov_playlist_id.split(YT_PLAYLIST_ID_DELIMITER)[0] if playlist_obj := await get_playlist( - prov_playlist_id=prov_playlist_id, headers=self._headers + prov_playlist_id=prov_playlist_id, headers=self._headers, language=self.language ): return await self._parse_playlist(playlist_obj) msg = f"Item {prov_playlist_id} not found" diff --git a/music_assistant/server/providers/ytmusic/helpers.py b/music_assistant/server/providers/ytmusic/helpers.py index c73bde9b..9fd1a217 100644 --- a/music_assistant/server/providers/ytmusic/helpers.py +++ b/music_assistant/server/providers/ytmusic/helpers.py @@ -23,11 +23,13 @@ from ytmusicapi.constants import ( from music_assistant.server.helpers.auth import AuthenticationHelper -async def get_artist(prov_artist_id: str, headers: dict[str, str]) -> dict[str, str]: +async def get_artist( + prov_artist_id: str, headers: dict[str, str], language: str = "en" +) -> dict[str, str]: """Async wrapper around the ytmusicapi get_artist function.""" def _get_artist(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) try: artist = ytm.get_artist(channelId=prov_artist_id) # ChannelId can sometimes be different and original ID is not part of the response @@ -40,21 +42,23 @@ async def get_artist(prov_artist_id: str, headers: dict[str, str]) -> dict[str, return await asyncio.to_thread(_get_artist) -async def get_album(prov_album_id: str) -> dict[str, str]: +async def get_album(prov_album_id: str, language: str = "en") -> dict[str, str]: """Async wrapper around the ytmusicapi get_album function.""" def _get_album(): - ytm = ytmusicapi.YTMusic() + ytm = ytmusicapi.YTMusic(language=language) return ytm.get_album(browseId=prov_album_id) return await asyncio.to_thread(_get_album) -async def get_playlist(prov_playlist_id: str, headers: dict[str, str]) -> dict[str, str]: +async def get_playlist( + prov_playlist_id: str, headers: dict[str, str], language: str = "en" +) -> dict[str, str]: """Async wrapper around the ytmusicapi get_playlist function.""" def _get_playlist(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) playlist = ytm.get_playlist(playlistId=prov_playlist_id, limit=None) playlist["checksum"] = get_playlist_checksum(playlist) return playlist @@ -63,12 +67,12 @@ async def get_playlist(prov_playlist_id: str, headers: dict[str, str]) -> dict[s async def get_track( - prov_track_id: str, headers: dict[str, str], signature_timestamp: str + prov_track_id: str, headers: dict[str, str], signature_timestamp: str, language: str = "en" ) -> dict[str, str] | None: """Async wrapper around the ytmusicapi get_playlist function.""" def _get_song(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) track_obj = ytm.get_song(videoId=prov_track_id, signatureTimestamp=signature_timestamp) track = {} if "videoDetails" not in track_obj: @@ -93,11 +97,11 @@ async def get_track( return await asyncio.to_thread(_get_song) -async def get_library_artists(headers: dict[str, str]) -> dict[str, str]: +async def get_library_artists(headers: dict[str, str], language: str = "en") -> dict[str, str]: """Async wrapper around the ytmusicapi get_library_artists function.""" def _get_library_artists(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) artists = ytm.get_library_subscriptions(limit=9999) # Sync properties with uniformal artist object for artist in artists: @@ -110,21 +114,21 @@ async def get_library_artists(headers: dict[str, str]) -> dict[str, str]: return await asyncio.to_thread(_get_library_artists) -async def get_library_albums(headers: dict[str, str]) -> dict[str, str]: +async def get_library_albums(headers: dict[str, str], language: str = "en") -> dict[str, str]: """Async wrapper around the ytmusicapi get_library_albums function.""" def _get_library_albums(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) return ytm.get_library_albums(limit=9999) return await asyncio.to_thread(_get_library_albums) -async def get_library_playlists(headers: dict[str, str]) -> dict[str, str]: +async def get_library_playlists(headers: dict[str, str], language: str = "en") -> dict[str, str]: """Async wrapper around the ytmusicapi get_library_playlists function.""" def _get_library_playlists(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) playlists = ytm.get_library_playlists(limit=9999) # Sync properties with uniformal playlist object for playlist in playlists: @@ -136,11 +140,11 @@ async def get_library_playlists(headers: dict[str, str]) -> dict[str, str]: return await asyncio.to_thread(_get_library_playlists) -async def get_library_tracks(headers: dict[str, str]) -> dict[str, str]: +async def get_library_tracks(headers: dict[str, str], language: str = "en") -> dict[str, str]: """Async wrapper around the ytmusicapi get_library_tracks function.""" def _get_library_tracks(): - ytm = ytmusicapi.YTMusic(auth=headers) + ytm = ytmusicapi.YTMusic(auth=headers, language=language) return ytm.get_library_songs(limit=9999) return await asyncio.to_thread(_get_library_tracks) @@ -235,11 +239,13 @@ async def get_song_radio_tracks( return await asyncio.to_thread(_get_song_radio_tracks) -async def search(query: str, ytm_filter: str | None = None, limit: int = 20) -> list[dict]: +async def search( + query: str, ytm_filter: str | None = None, limit: int = 20, language: str = "en" +) -> list[dict]: """Async wrapper around the ytmusicapi search function.""" def _search(): - ytm = ytmusicapi.YTMusic() + ytm = ytmusicapi.YTMusic(language=language) results = ytm.search(query=query, filter=ytm_filter, limit=limit) # Sync result properties with uniformal objects for result in results: