Compare commits

..

No commits in common. "fcf7254e64863d13822bfa3f72351b34c7695a46" and "2f70c6f7fa1ff16a8a79f6f977785ba37c296870" have entirely different histories.

8 changed files with 87 additions and 142 deletions

116
pdm.lock
View file

@ -183,106 +183,100 @@ files = [
[[package]]
name = "pyobjc-core"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Python<->ObjC Interoperability Module"
files = [
{file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"},
{file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"},
{file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"},
{file = "pyobjc-core-10.2.tar.gz", hash = "sha256:0153206e15d0e0d7abd53ee8a7fbaf5606602a032e177a028fc8589516a8771c"},
{file = "pyobjc_core-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a70546246177c23acb323c9324330e37638f1a0a3d13664abcba3bb75e43012c"},
]
[[package]]
name = "pyobjc-framework-avfoundation"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Wrappers for the framework AVFoundation on macOS"
dependencies = [
"pyobjc-core>=10.3.1",
"pyobjc-framework-Cocoa>=10.3.1",
"pyobjc-framework-CoreAudio>=10.3.1",
"pyobjc-framework-CoreMedia>=10.3.1",
"pyobjc-framework-Quartz>=10.3.1",
"pyobjc-core>=10.2",
"pyobjc-framework-Cocoa>=10.2",
"pyobjc-framework-CoreAudio>=10.2",
"pyobjc-framework-CoreMedia>=10.2",
"pyobjc-framework-Quartz>=10.2",
]
files = [
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:0896f6650df35f0229d1fb3aa3fbf632647dd815d4921cb61d9eb7fa26be6237"},
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:0cb27cc95288d95df7504adf474596f8855de7fa7798bbc1bbfbdfbbcb940952"},
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb606ef0806d952a04db45ae691167678121df1d8d7c2f8cc73745695097033"},
{file = "pyobjc_framework_AVFoundation-10.3.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:00889eb915479aa9ea392cdd241e4b635ae0fa3114f043d08cf3e1d1b5a23bd4"},
{file = "pyobjc_framework_avfoundation-10.3.1.tar.gz", hash = "sha256:2f94bee3a4217b46d9416cad066e4f357bf0f344079c328736114451ae19ae94"},
{file = "pyobjc-framework-AVFoundation-10.2.tar.gz", hash = "sha256:4d394014f2477c0c6a596dbb01ef5d92944058d0e0d954ce6121a676ae9395ce"},
{file = "pyobjc_framework_AVFoundation-10.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:5f20c11a8870d7d58f0e4f20f918e45e922520aa5c9dbee61dc59ca4bc4bd26d"},
{file = "pyobjc_framework_AVFoundation-10.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:283355d1f96c184e5f5f479870eb3bf510747307697616737bbc5d224af3abcb"},
{file = "pyobjc_framework_AVFoundation-10.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:a63a4e26c088023b0b1cb29d7da2c2246aa8eca2b56767fe1cc36a18c6fb650b"},
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Wrappers for the Cocoa frameworks on macOS"
dependencies = [
"pyobjc-core>=10.3.1",
"pyobjc-core>=10.2",
]
files = [
{file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"},
{file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"},
{file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"},
{file = "pyobjc-framework-Cocoa-10.2.tar.gz", hash = "sha256:6383141379636b13855dca1b39c032752862b829f93a49d7ddb35046abfdc035"},
{file = "pyobjc_framework_Cocoa-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:18886d5013cd7dc7ecd6e0df5134c767569b5247fc10a5e293c72ee3937b217b"},
]
[[package]]
name = "pyobjc-framework-coreaudio"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Wrappers for the framework CoreAudio on macOS"
dependencies = [
"pyobjc-core>=10.3.1",
"pyobjc-framework-Cocoa>=10.3.1",
"pyobjc-core>=10.2",
"pyobjc-framework-Cocoa>=10.2",
]
files = [
{file = "pyobjc_framework_CoreAudio-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e0aeca61a425d846afc92350ffba970e1e503469182f5f0ea436de98cfd00d96"},
{file = "pyobjc_framework_CoreAudio-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:21cecd1b023b6960d1071c106345656de45a399196701b07c7e5c076321f25ad"},
{file = "pyobjc_framework_coreaudio-10.3.1.tar.gz", hash = "sha256:c81c709bf955aea474a4de380b187f3c2e56c864ca7de520b08362b73070c795"},
{file = "pyobjc-framework-CoreAudio-10.2.tar.gz", hash = "sha256:5e97ae7a65be85aee83aef004b31146c5fbf28325d870362959f7312b303fb67"},
{file = "pyobjc_framework_CoreAudio-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32608ce881b5e6a7cb332c2732762fa93829ac495c5344c33e8e8b72a2431b23"},
]
[[package]]
name = "pyobjc-framework-coremedia"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Wrappers for the framework CoreMedia on macOS"
dependencies = [
"pyobjc-core>=10.3.1",
"pyobjc-framework-Cocoa>=10.3.1",
"pyobjc-core>=10.2",
"pyobjc-framework-Cocoa>=10.2",
]
files = [
{file = "pyobjc_framework_CoreMedia-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c6eaf48f202becab10679e3b5dd62607ddec2739495db45882524592cabf3997"},
{file = "pyobjc_framework_CoreMedia-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98b8ab02e6ec979007b706e05166e16bd61121e47fbc6e449f4b2de2c58f3cb6"},
{file = "pyobjc_framework_coremedia-10.3.1.tar.gz", hash = "sha256:bc3e0cddf5f546b5d8407d8f46b203f1bd4396ad5dbfdc0d064a560b3fe31221"},
{file = "pyobjc-framework-CoreMedia-10.2.tar.gz", hash = "sha256:d726d86636217eaa135e5626d05c7eb0f9b4529ce1ed504e08069fe1e0421483"},
{file = "pyobjc_framework_CoreMedia-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7fa13166a14d384bb6442e5f635310dd075c2a4b3b3bd67ac63b1e2e1fd2d65e"},
]
[[package]]
name = "pyobjc-framework-mediaplayer"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Wrappers for the framework MediaPlayer on macOS"
dependencies = [
"pyobjc-core>=10.3.1",
"pyobjc-framework-AVFoundation>=10.3.1",
"pyobjc-core>=10.2",
"pyobjc-framework-AVFoundation>=10.2",
]
files = [
{file = "pyobjc_framework_MediaPlayer-10.3.1-py2.py3-none-any.whl", hash = "sha256:5b428cc28e57c1778bd431156c3adb948650f7503f266689559d0ece94b34e8a"},
{file = "pyobjc_framework_mediaplayer-10.3.1.tar.gz", hash = "sha256:97043df5ef89d4fbe217813e8f4ee1e226d8a43dee4eac00fff95e6b8a7772be"},
{file = "pyobjc-framework-MediaPlayer-10.2.tar.gz", hash = "sha256:4b6d296b084e01fb6e5c782b7b6308077db09f4051f50b0a6c3298ffbd1f1d70"},
{file = "pyobjc_framework_MediaPlayer-10.2-py2.py3-none-any.whl", hash = "sha256:c501ea19380bfbf6b04fbe909fcfe9a78c5ff2a9b58dae87be259066b1ae3521"},
]
[[package]]
name = "pyobjc-framework-quartz"
version = "10.3.1"
version = "10.2"
requires_python = ">=3.8"
summary = "Wrappers for the Quartz frameworks on macOS"
dependencies = [
"pyobjc-core>=10.3.1",
"pyobjc-framework-Cocoa>=10.3.1",
"pyobjc-core>=10.2",
"pyobjc-framework-Cocoa>=10.2",
]
files = [
{file = "pyobjc_framework_Quartz-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ca35f92486869a41847a1703bb176aab8a53dbfd8e678d1f4d68d8e6e1581c71"},
{file = "pyobjc_framework_Quartz-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:00a0933267e3a46ea4afcc35d117b2efb920f06de797fa66279c52e7057e3590"},
{file = "pyobjc_framework_quartz-10.3.1.tar.gz", hash = "sha256:b6d7e346d735c9a7f147cd78e6da79eeae416a0b7d3874644c83a23786c6f886"},
{file = "pyobjc-framework-Quartz-10.2.tar.gz", hash = "sha256:9b947e081f5bd6cd01c99ab5d62c36500d2d6e8d3b87421c1cbb7f9c885555eb"},
{file = "pyobjc_framework_Quartz-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3e8e33246d966c2bd7f5ee2cf3b431582fa434a6ec2b6dbe580045ebf1f55be5"},
]
[[package]]
@ -327,27 +321,27 @@ files = [
[[package]]
name = "ruff"
version = "0.4.10"
version = "0.4.4"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"},
{file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"},
{file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"},
{file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"},
{file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"},
{file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"},
{file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
{file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
{file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
{file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
{file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
{file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
]
[[package]]

View file

@ -52,6 +52,7 @@ build-backend = "pdm.backend"
mypy_path = 'stubs'
[tool.ruff.lint]
ignore-init-module-imports = true
select = [
# pycodestyle
"E4", # import

View file

@ -78,12 +78,6 @@ def playback_state_to_cocoa(state: PlaybackState) -> MPMusicPlaybackState:
return mapping[state]
def join_plural_field(field: list[str]) -> str | None:
if field:
return ", ".join(field)
return None
def song_to_media_item(song: Song) -> NSMutableDictionary:
nowplaying_info = nothing_to_media_item()
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeAudio
@ -94,12 +88,12 @@ def song_to_media_item(song: Song) -> NSMutableDictionary:
nowplaying_info[MPMediaItemPropertyPersistentID] = song_to_persistent_id(song)
nowplaying_info[MPMediaItemPropertyTitle] = song.title
nowplaying_info[MPMediaItemPropertyArtist] = join_plural_field(song.artist)
nowplaying_info[MPMediaItemPropertyAlbumTitle] = join_plural_field(song.album)
nowplaying_info[MPMediaItemPropertyArtist] = song.artist
nowplaying_info[MPMediaItemPropertyAlbumTitle] = song.album
nowplaying_info[MPMediaItemPropertyAlbumTrackNumber] = song.track
nowplaying_info[MPMediaItemPropertyDiscNumber] = song.disc
nowplaying_info[MPMediaItemPropertyGenre] = join_plural_field(song.genre)
nowplaying_info[MPMediaItemPropertyComposer] = join_plural_field(song.composer)
nowplaying_info[MPMediaItemPropertyGenre] = song.genre
nowplaying_info[MPMediaItemPropertyComposer] = song.composer
nowplaying_info[MPMediaItemPropertyPlaybackDuration] = song.duration
# MPD can't play back music at different rates, so we just want to set it

View file

@ -6,7 +6,6 @@ from yarl import URL
from ..cache import Cache, make_cache
from ..tools.asyncio import run_background_task
from ..tools.types import un_maybe_plural
from .types import CurrentSongResponse, MpdStateHandler
CACHE_TTL = 60 * 60 # seconds = 1 hour
@ -17,11 +16,9 @@ class ArtCacheEntry(TypedDict):
def calc_album_key(song: CurrentSongResponse) -> str:
artist = sorted(
un_maybe_plural(song.get("albumartist", song.get("artist", "Unknown Artist")))
)
album = sorted(un_maybe_plural(song.get("album", "Unknown Album")))
return ":".join(";".join(t).replace(":", "-") for t in (artist, album))
artist = song.get("albumartist", song.get("artist", "Unknown Artist"))
album = song.get("album", "Unknown Album")
return ":".join(t.replace(":", "-") for t in (artist, album))
def calc_track_key(song: CurrentSongResponse) -> str:

View file

@ -9,7 +9,7 @@ from yarl import URL
from ..config.model import MpdConfig
from ..player import Player
from ..song import PlaybackState, Song, SongListener
from ..tools.types import convert_if_exists, un_maybe_plural
from ..tools.types import convert_if_exists
from .artwork_cache import MpdArtworkCache
from .types import CurrentSongResponse, StatusResponse
@ -27,11 +27,11 @@ def mpd_current_to_song(
current.get("musicbrainz_releasetrackid"), UUID
),
title=current.get("title"),
artist=un_maybe_plural(current.get("artist")),
album=un_maybe_plural(current.get("album")),
album_artist=un_maybe_plural(current.get("albumartist")),
composer=un_maybe_plural(current.get("composer")),
genre=un_maybe_plural(current.get("genre")),
artist=current.get("artist"),
album=current.get("album"),
album_artist=current.get("albumartist"),
composer=current.get("composer"),
genre=current.get("genre"),
track=convert_if_exists(current.get("track"), int),
disc=convert_if_exists(current.get("disc"), int),
duration=float(status["duration"]),

View file

@ -1,7 +1,5 @@
from typing import Literal, NotRequired, Protocol, TypedDict
from ..tools.types import MaybePlural
class MpdStateHandler(Protocol):
async def get_art(self, file: str) -> bytes | None: ...
@ -49,19 +47,19 @@ class StatusResponse(TypedDict):
# tagged, since then it can pass more information on to Now Playing, but it
# should work fine with completely untagged music too.
class CurrentSongTags(TypedDict, total=False):
artist: MaybePlural[str]
albumartist: MaybePlural[str]
artistsort: MaybePlural[str]
albumartistsort: MaybePlural[str]
artist: str
albumartist: str
artistsort: str
albumartistsort: str
title: str
album: MaybePlural[str]
album: str
track: str
date: str
originaldate: str
composer: MaybePlural[str]
composer: str
disc: str
label: str
genre: MaybePlural[str]
genre: str
musicbrainz_albumid: str
musicbrainz_albumartistid: str
musicbrainz_releasetrackid: str

View file

@ -21,17 +21,18 @@ class Song:
musicbrainz_trackid: UUID | None
musicbrainz_releasetrackid: UUID | None
title: str | None
artist: list[str]
composer: list[str]
album: list[str]
album_artist: list[str]
artist: str | None
composer: str | None
album: str | None
album_artist: str | None
track: int | None
disc: int | None
genre: list[str]
genre: str | None
duration: float
elapsed: float
art: bytes | None = field(repr=lambda a: "<has art>" if a else "<no art>")
class SongListener(Protocol):
def update(self, song: Song | None) -> None: ...
def update(self, song: Song | None) -> None:
...

View file

@ -1,51 +1,11 @@
from collections.abc import Callable
from typing import Any, TypeAlias, TypeVar
from typing import Callable, TypeVar
__all__ = (
"AnyExceptList",
"MaybePlural",
"convert_if_exists",
"un_maybe_plural",
)
__all__ = ("convert_if_exists",)
# Accept as many types as possible that are not lists. Yes, having to identify
# them manually like this is kind of a pain! TypeScript can express this
# restriction using a conditional type, and with another conditional type it
# can correctly type a version of un_maybe_plural that *does* accept lists, but
# Python's type system isn't quite that bonkers powerful. Yet?
AnyExceptList = (
int
| float
| complex
| bool
| str
| bytes
| set[Any]
| dict[Any, Any]
| tuple[Any, ...]
| Callable[[Any], Any]
| type
)
T = TypeVar("T")
U = TypeVar("U")
def convert_if_exists(value: str | None, converter: Callable[[str], U]) -> U | None:
def convert_if_exists(value: str | None, converter: Callable[[str], T]) -> T | None:
if value is None:
return None
return converter(value)
T = TypeVar("T", bound=AnyExceptList)
MaybePlural: TypeAlias = list[T] | T
def un_maybe_plural(value: MaybePlural[T] | None) -> list[T]:
match value:
case None:
return []
case list(values):
return values[:]
case item:
return [item]