from __future__ import annotations
import hashlib
-import mimetypes
import socket
from asyncio import TaskGroup
from collections.abc import AsyncGenerator
from aiojellyfin import MediaLibrary as JellyMediaLibrary
from aiojellyfin import NotFound, authenticate_by_name
-from aiojellyfin import Track as JellyTrack
from aiojellyfin.session import SessionConfiguration
from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig
-from music_assistant_models.enums import (
- ConfigEntryType,
- ContentType,
- MediaType,
- ProviderFeature,
- StreamType,
-)
+from music_assistant_models.enums import ConfigEntryType, MediaType, ProviderFeature, StreamType
from music_assistant_models.errors import LoginFailed, MediaNotFoundError
from music_assistant_models.media_items import (
Album,
Artist,
- AudioFormat,
Playlist,
ProviderMapping,
SearchResults,
from music_assistant.models import ProviderInstanceType
from music_assistant.models.music_provider import MusicProvider
from music_assistant.providers.jellyfin.parsers import (
+ audio_format,
parse_album,
parse_artist,
parse_playlist,
ARTIST_FIELDS,
ITEM_KEY_COLLECTION_TYPE,
ITEM_KEY_ID,
- ITEM_KEY_MEDIA_CHANNELS,
- ITEM_KEY_MEDIA_CODEC,
- ITEM_KEY_MEDIA_SOURCES,
ITEM_KEY_MEDIA_STREAMS,
ITEM_KEY_NAME,
ITEM_KEY_RUNTIME_TICKS,
) -> StreamDetails:
"""Return the content details for the given track when it will be streamed."""
jellyfin_track = await self._client.get_track(item_id)
- mimetype = self._media_mime_type(jellyfin_track)
- media_stream = jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0]
- url = self._client.audio_url(jellyfin_track[ITEM_KEY_ID], SUPPORTED_CONTAINER_FORMATS)
- if ITEM_KEY_MEDIA_CODEC in media_stream:
- content_type = ContentType.try_parse(media_stream[ITEM_KEY_MEDIA_CODEC])
- else:
- content_type = ContentType.try_parse(mimetype) if mimetype else ContentType.UNKNOWN
+ url = self._client.audio_url(
+ jellyfin_track[ITEM_KEY_ID], container=SUPPORTED_CONTAINER_FORMATS
+ )
return StreamDetails(
item_id=jellyfin_track[ITEM_KEY_ID],
provider=self.lookup_key,
- audio_format=AudioFormat(
- content_type=content_type,
- channels=jellyfin_track[ITEM_KEY_MEDIA_STREAMS][0][ITEM_KEY_MEDIA_CHANNELS],
- ),
+ audio_format=audio_format(jellyfin_track),
stream_type=StreamType.HTTP,
duration=int(
jellyfin_track[ITEM_KEY_RUNTIME_TICKS] / 10000000
):
result.append(library)
return result
-
- def _media_mime_type(self, media_item: JellyTrack) -> str | None:
- """Return the mime type of a media item."""
- if not media_item.get(ITEM_KEY_MEDIA_SOURCES):
- return None
-
- media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0]
-
- if "Path" not in media_source:
- return None
-
- path = media_source["Path"]
- mime_type, _ = mimetypes.guess_type(path)
-
- return mime_type
'version': '',
})
# ---
+# name: test_parse_tracks[do_i_wanna_know]
+ dict({
+ 'album': dict({
+ 'available': True,
+ 'external_ids': list([
+ ]),
+ 'image': None,
+ 'item_id': 'd42d74e134693184e7adc73106238e89',
+ 'media_type': 'album',
+ 'name': 'AM',
+ 'provider': 'xx-instance-id-xx',
+ 'sort_name': 'am',
+ 'uri': 'xx-instance-id-xx://album/d42d74e134693184e7adc73106238e89',
+ 'version': '',
+ }),
+ 'artists': list([
+ dict({
+ 'available': True,
+ 'external_ids': list([
+ ]),
+ 'image': None,
+ 'item_id': 'cc940aeb8a99149f159fe9794f136071',
+ 'media_type': 'artist',
+ 'name': 'Arctic Monkeys',
+ 'provider': 'xx-instance-id-xx',
+ 'sort_name': 'arctic monkeys',
+ 'uri': 'xx-instance-id-xx://artist/cc940aeb8a99149f159fe9794f136071',
+ 'version': '',
+ }),
+ ]),
+ 'disc_number': 1,
+ 'duration': 272,
+ 'external_ids': list([
+ ]),
+ 'favorite': False,
+ 'item_id': 'da9c458e425584680765ddc3a89cbc0c',
+ 'media_type': 'track',
+ 'metadata': dict({
+ 'chapters': None,
+ 'copyright': None,
+ 'description': None,
+ 'explicit': None,
+ 'genres': None,
+ 'images': list([
+ dict({
+ 'path': 'http://localhost:1234/Items/da9c458e425584680765ddc3a89cbc0c/Images/Primary?api_key=ACCESS_TOKEN',
+ 'provider': 'xx-instance-id-xx',
+ 'remotely_accessible': False,
+ 'type': 'thumb',
+ }),
+ ]),
+ 'label': None,
+ 'languages': None,
+ 'last_refresh': None,
+ 'links': None,
+ 'lyrics': None,
+ 'mood': None,
+ 'performers': None,
+ 'popularity': None,
+ 'preview': None,
+ 'release_date': None,
+ 'review': None,
+ 'style': None,
+ }),
+ 'name': 'Do I Wanna Know?',
+ 'position': 1,
+ 'provider': 'xx-instance-id-xx',
+ 'provider_mappings': list([
+ dict({
+ 'audio_format': dict({
+ 'bit_depth': 24,
+ 'bit_rate': 1546,
+ 'channels': 2,
+ 'content_type': 'flac',
+ 'output_format_str': 'flac',
+ 'sample_rate': 44100,
+ }),
+ 'available': True,
+ 'details': None,
+ 'item_id': 'da9c458e425584680765ddc3a89cbc0c',
+ 'provider_domain': 'jellyfin',
+ 'provider_instance': 'xx-instance-id-xx',
+ 'url': 'http://localhost:1234/Audio/da9c458e425584680765ddc3a89cbc0c/universal?userId=USER_ID&deviceId=X&maxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
+ }),
+ ]),
+ 'sort_name': 'do i wanna know?',
+ 'track_number': 1,
+ 'uri': 'xx-instance-id-xx://track/da9c458e425584680765ddc3a89cbc0c',
+ 'version': '',
+ })
+# ---
# name: test_parse_tracks[thrown_away]
dict({
'album': dict({
dict({
'audio_format': dict({
'bit_depth': 16,
- 'bit_rate': 0,
+ 'bit_rate': 156,
'channels': 2,
'content_type': 'mp3',
'output_format_str': 'mp3',
'item_id': 'b5319fb11cde39fca2023184fcfa9862',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
- 'url': 'http://localhost:1234/Audio/b5319fb11cde39fca2023184fcfa9862/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
+ 'url': 'http://localhost:1234/Audio/b5319fb11cde39fca2023184fcfa9862/universal?userId=USER_ID&deviceId=X&maxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
}),
]),
'sort_name': '11 thrown away',
dict({
'audio_format': dict({
'bit_depth': 16,
- 'bit_rate': 0,
+ 'bit_rate': 278,
'channels': 2,
'content_type': 'aac',
'output_format_str': 'aac',
'item_id': '54918f75ee8f6c8b8dc5efd680644f29',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
- 'url': 'http://localhost:1234/Audio/54918f75ee8f6c8b8dc5efd680644f29/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
+ 'url': 'http://localhost:1234/Audio/54918f75ee8f6c8b8dc5efd680644f29/universal?userId=USER_ID&deviceId=X&maxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
}),
]),
'sort_name': 'where the bands are (2018 version)',
dict({
'audio_format': dict({
'bit_depth': 16,
- 'bit_rate': 0,
+ 'bit_rate': 267,
'channels': 2,
'content_type': 'aac',
'output_format_str': 'aac',
'item_id': 'fb12a77f49616a9fc56a6fab2b94174c',
'provider_domain': 'jellyfin',
'provider_instance': 'xx-instance-id-xx',
- 'url': 'http://localhost:1234/Audio/fb12a77f49616a9fc56a6fab2b94174c/universal?UserId=USER_ID&DeviceId=X&MaxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
+ 'url': 'http://localhost:1234/Audio/fb12a77f49616a9fc56a6fab2b94174c/universal?userId=USER_ID&deviceId=X&maxStreamingBitrate=140000000&api_key=ACCESS_TOKEN',
}),
]),
'sort_name': 'zombie christmas',
--- /dev/null
+{
+ "Name": "Do I Wanna Know?",
+ "ServerId": "a9038ca202a94606b3764740074fec3a",
+ "Id": "da9c458e425584680765ddc3a89cbc0c",
+ "Etag": "6a569c1c1d153acad95ffcaf531b6ccb",
+ "DateCreated": "2025-01-07T20:04:36.5322321Z",
+ "CanDelete": false,
+ "CanDownload": true,
+ "HasLyrics": true,
+ "Container": "flac",
+ "SortName": "0001 - 0001 - Do I Wanna Know?",
+ "PremiereDate": "2013-01-01T00:00:00.0000000Z",
+ "ExternalUrls": [],
+ "MediaSources": [
+ {
+ "Protocol": "File",
+ "Id": "da9c458e425584680765ddc3a89cbc0c",
+ "Path": "/media/music/Arctic Monkeys/AM/01-01 Do I Wanna Know.flac",
+ "Type": "Default",
+ "Container": "flac",
+ "Size": 52672047,
+ "Name": "01-01 Do I Wanna Know",
+ "IsRemote": false,
+ "ETag": "09f1d9b8dcbcd5b61c65d4fdc2aadc32",
+ "RunTimeTicks": 2723941040,
+ "ReadAtNativeFramerate": false,
+ "IgnoreDts": false,
+ "IgnoreIndex": false,
+ "GenPtsInput": false,
+ "SupportsTranscoding": true,
+ "SupportsDirectStream": true,
+ "SupportsDirectPlay": true,
+ "IsInfiniteStream": false,
+ "UseMostCompatibleTranscodingProfile": false,
+ "RequiresOpening": false,
+ "RequiresClosing": false,
+ "RequiresLooping": false,
+ "SupportsProbing": true,
+ "MediaStreams": [
+ {
+ "Codec": "flac",
+ "TimeBase": "1/44100",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "FLAC - Stereo",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "stereo",
+ "BitRate": 1546936,
+ "BitDepth": 24,
+ "Channels": 2,
+ "SampleRate": 44100,
+ "IsDefault": false,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Type": "Audio",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ },
+ {
+ "Codec": "mjpeg",
+ "ColorSpace": "bt470bg",
+ "Comment": "Cover (front)",
+ "TimeBase": "1/90000",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": false,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 600,
+ "Width": 600,
+ "RealFrameRate": 90000,
+ "ReferenceFrameRate": 90000,
+ "Profile": "Baseline",
+ "Type": "EmbeddedImage",
+ "AspectRatio": "1:1",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuvj444p",
+ "Level": -99,
+ "IsAnamorphic": false
+ },
+ {
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "IsInterlaced": false,
+ "IsDefault": false,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Type": "Lyric",
+ "Index": 2,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Path": "/var/lib/jellyfin/metadata/library/da/da9c458e425584680765ddc3a89cbc0c/01-01 Do I Wanna Know.lrc"
+ }
+ ],
+ "MediaAttachments": [],
+ "Formats": [],
+ "Bitrate": 1546936,
+ "RequiredHttpHeaders": {},
+ "TranscodingSubProtocol": "http",
+ "DefaultAudioStreamIndex": 0,
+ "HasSegments": false
+ }
+ ],
+ "Path": "/media/music/Arctic Monkeys/AM/01-01 Do I Wanna Know.flac",
+ "EnableMediaSourceDisplay": true,
+ "ChannelId": null,
+ "Taglines": [],
+ "Genres": ["Alternative \u0026 Indie"],
+ "RunTimeTicks": 2723941040,
+ "PlayAccess": "Full",
+ "ProductionYear": 2013,
+ "IndexNumber": 1,
+ "ParentIndexNumber": 1,
+ "RemoteTrailers": [],
+ "ProviderIds": {},
+ "IsFolder": false,
+ "ParentId": "d42d74e134693184e7adc73106238e89",
+ "Type": "Audio",
+ "People": [],
+ "Studios": [],
+ "GenreItems": [
+ {
+ "Name": "Alternative \u0026 Indie",
+ "Id": "daf2f5737c11cea90c2ef7376801d804"
+ }
+ ],
+ "ParentLogoItemId": "cc940aeb8a99149f159fe9794f136071",
+ "ParentBackdropItemId": "cc940aeb8a99149f159fe9794f136071",
+ "ParentBackdropImageTags": ["c0bbf52814fd50ec5417617da2766e43"],
+ "LocalTrailerCount": 0,
+ "UserData": {
+ "PlaybackPositionTicks": 0,
+ "PlayCount": 4,
+ "IsFavorite": false,
+ "LastPlayedDate": "2025-01-18T12:09:39.3190466Z",
+ "Played": true,
+ "Key": "Arctic Monkeys-AM-0001-0001Do I Wanna Know?",
+ "ItemId": "00000000000000000000000000000000"
+ },
+ "SpecialFeatureCount": 0,
+ "DisplayPreferencesId": "61bba315f137702baa296a1c417faada",
+ "Tags": [],
+ "PrimaryImageAspectRatio": 1,
+ "Artists": ["Arctic Monkeys"],
+ "ArtistItems": [
+ { "Name": "Arctic Monkeys", "Id": "cc940aeb8a99149f159fe9794f136071" }
+ ],
+ "Album": "AM",
+ "AlbumId": "d42d74e134693184e7adc73106238e89",
+ "AlbumPrimaryImageTag": "0e9e1da3e4efc8f37917cc6a4d8f626d",
+ "AlbumArtist": "Arctic Monkeys",
+ "AlbumArtists": [
+ { "Name": "Arctic Monkeys", "Id": "cc940aeb8a99149f159fe9794f136071" }
+ ],
+ "MediaStreams": [
+ {
+ "Codec": "flac",
+ "TimeBase": "1/44100",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "LocalizedDefault": "Default",
+ "LocalizedExternal": "External",
+ "DisplayTitle": "FLAC - Stereo",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "ChannelLayout": "stereo",
+ "BitRate": 1546936,
+ "BitDepth": 24,
+ "Channels": 2,
+ "SampleRate": 44100,
+ "IsDefault": false,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Type": "Audio",
+ "Index": 0,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Level": 0
+ },
+ {
+ "Codec": "mjpeg",
+ "ColorSpace": "bt470bg",
+ "Comment": "Cover (front)",
+ "TimeBase": "1/90000",
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "IsInterlaced": false,
+ "IsAVC": false,
+ "BitDepth": 8,
+ "RefFrames": 1,
+ "IsDefault": false,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Height": 600,
+ "Width": 600,
+ "RealFrameRate": 90000,
+ "ReferenceFrameRate": 90000,
+ "Profile": "Baseline",
+ "Type": "EmbeddedImage",
+ "AspectRatio": "1:1",
+ "Index": 1,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "PixelFormat": "yuvj444p",
+ "Level": -99,
+ "IsAnamorphic": false
+ },
+ {
+ "VideoRange": "Unknown",
+ "VideoRangeType": "Unknown",
+ "AudioSpatialFormat": "None",
+ "IsInterlaced": false,
+ "IsDefault": false,
+ "IsForced": false,
+ "IsHearingImpaired": false,
+ "Type": "Lyric",
+ "Index": 2,
+ "IsExternal": false,
+ "IsTextSubtitleStream": false,
+ "SupportsExternalStream": false,
+ "Path": "/var/lib/jellyfin/metadata/library/da/da9c458e425584680765ddc3a89cbc0c/01-01 Do I Wanna Know.lrc"
+ }
+ ],
+ "ImageTags": { "Primary": "7225206d6903fc4ab214375fd25d9308" },
+ "BackdropImageTags": [],
+ "ParentLogoImageTag": "3203dbc243aec2cc2e17d054a13a51cf",
+ "ImageBlurHashes": {
+ "Primary": {
+ "7225206d6903fc4ab214375fd25d9308": "eG7_4nofayofIUozfQj[j[WB00WBj[WB%MM{ayWBayt7_3j[WBofRj",
+ "0e9e1da3e4efc8f37917cc6a4d8f626d": "eG7_4nt7kCt7D%xuj[ofofWB00WBf6Rj%MD%WVRjWBt7?bofWBofM{"
+ },
+ "Logo": {
+ "3203dbc243aec2cc2e17d054a13a51cf": "OlJ8V0M{00ofD%xuRjIUM{j[xuoft7t7D%ayxut7t7RjWB"
+ },
+ "Backdrop": {
+ "c0bbf52814fd50ec5417617da2766e43": "WYEVg7?Hoeo}X9%L~qxuV@s:W=oz%gozRPjFWBWB%3xaR*S2R*ae"
+ }
+ },
+ "LocationType": "FileSystem",
+ "MediaType": "Audio",
+ "LockedFields": [],
+ "LockData": false,
+ "NormalizationGain": -8.4
+}