Compare commits
2 commits
68609f3d07
...
3ef3112014
Author | SHA1 | Date | |
---|---|---|---|
3ef3112014 | |||
c29f4b9b27 |
6 changed files with 103 additions and 6 deletions
|
@ -14,6 +14,26 @@
|
||||||
"title": "HasArtwork",
|
"title": "HasArtwork",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"MixRamp": {
|
||||||
|
"properties": {
|
||||||
|
"db": {
|
||||||
|
"description": "The volume threshold at which MPD will overlap MixRamp-analysed songs, measured in decibels. Can be set to any float, but sensible values are typically negative.",
|
||||||
|
"title": "Db",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"delay": {
|
||||||
|
"description": "A delay time in seconds which will be subtracted from the MixRamp overlap. Must be set to a positive value for MixRamp to work at all - will be zero if it's disabled.",
|
||||||
|
"title": "Delay",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"db",
|
||||||
|
"delay"
|
||||||
|
],
|
||||||
|
"title": "MixRamp",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"MusicBrainzIds": {
|
"MusicBrainzIds": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"artist": {
|
"artist": {
|
||||||
|
@ -125,6 +145,16 @@
|
||||||
"description": "Remove songs from the queue as they're played. This flag can also be set to \"oneshot\", which means the currently playing song will be consumed, and then the flag will automatically be switched off.",
|
"description": "Remove songs from the queue as they're played. This flag can also be set to \"oneshot\", which means the currently playing song will be consumed, and then the flag will automatically be switched off.",
|
||||||
"title": "Consume"
|
"title": "Consume"
|
||||||
},
|
},
|
||||||
|
"crossfade": {
|
||||||
|
"description": "The number of seconds to overlap songs when cross-fading between the current song and the next. Will be zero when the cross-fading feature is disabled entirely. Curiously, fractional seconds are not supported here, unlike many other places MPD uses seconds.",
|
||||||
|
"minimum": 0,
|
||||||
|
"title": "Crossfade",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"mixramp": {
|
||||||
|
"$ref": "#/$defs/MixRamp",
|
||||||
|
"description": "Settings for MixRamp-powered cross-fading, which analyses your songs' volume levels to choose optimal places for cross-fading. This requires either that the songs have previously been analysed and tagged with MixRamp information, or that MPD's on the fly mixramp_analyzer has been enabled."
|
||||||
|
},
|
||||||
"random": {
|
"random": {
|
||||||
"description": "Play the queued songs in random order. This is distinct from shuffling the queue, which randomises the queue's order once when you send the shuffle command and will then play the queue in that new order repeatedly if asked. If MPD is asked to both repeat and randomise, the queue is effectively shuffled each time it loops.",
|
"description": "Play the queued songs in random order. This is distinct from shuffling the queue, which randomises the queue's order once when you send the shuffle command and will then play the queue in that new order repeatedly if asked. If MPD is asked to both repeat and randomise, the queue is effectively shuffled each time it loops.",
|
||||||
"title": "Random",
|
"title": "Random",
|
||||||
|
@ -162,7 +192,9 @@
|
||||||
"repeat",
|
"repeat",
|
||||||
"random",
|
"random",
|
||||||
"single",
|
"single",
|
||||||
"consume"
|
"consume",
|
||||||
|
"crossfade",
|
||||||
|
"mixramp"
|
||||||
],
|
],
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from ...config.model import MpdConfig
|
from ...config.model import MpdConfig
|
||||||
from ...playback import Playback
|
from ...playback import Playback
|
||||||
from ...playback.queue import Queue
|
from ...playback.queue import Queue
|
||||||
from ...playback.settings import Settings, to_oneshot
|
from ...playback.settings import MixRamp, Settings, to_oneshot
|
||||||
from ...tools.types import option_fmap
|
from ...tools.types import option_fmap
|
||||||
from ..types import MpdState
|
from ..types import MpdState
|
||||||
from .to_song import to_song
|
from .to_song import to_song
|
||||||
|
@ -15,6 +15,16 @@ def to_queue(mpd: MpdState) -> Queue:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_mixramp(mpd: MpdState) -> MixRamp:
|
||||||
|
delay = mpd.status.get("mixrampdelay", 0)
|
||||||
|
if delay == "nan":
|
||||||
|
delay = 0
|
||||||
|
return MixRamp(
|
||||||
|
db=float(mpd.status.get("mixrampdb", 0)),
|
||||||
|
delay=float(delay),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def to_settings(mpd: MpdState) -> Settings:
|
def to_settings(mpd: MpdState) -> Settings:
|
||||||
return Settings(
|
return Settings(
|
||||||
volume=option_fmap(int, mpd.status.get("volume")),
|
volume=option_fmap(int, mpd.status.get("volume")),
|
||||||
|
@ -22,6 +32,8 @@ def to_settings(mpd: MpdState) -> Settings:
|
||||||
random=mpd.status["random"] == "1",
|
random=mpd.status["random"] == "1",
|
||||||
single=to_oneshot(mpd.status["single"]),
|
single=to_oneshot(mpd.status["single"]),
|
||||||
consume=to_oneshot(mpd.status["consume"]),
|
consume=to_oneshot(mpd.status["consume"]),
|
||||||
|
crossfade=int(mpd.status.get("xfade", 0)),
|
||||||
|
mixramp=to_mixramp(mpd),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from ..playback import Playback
|
||||||
from ..playback.state import PlaybackState
|
from ..playback.state import PlaybackState
|
||||||
from ..player import Player
|
from ..player import Player
|
||||||
from ..song_receiver import Receiver
|
from ..song_receiver import Receiver
|
||||||
|
from ..tools.asyncio import run_background_task
|
||||||
from .artwork_cache import MpdArtworkCache
|
from .artwork_cache import MpdArtworkCache
|
||||||
from .convert.to_playback import to_playback
|
from .convert.to_playback import to_playback
|
||||||
from .types import MpdState
|
from .types import MpdState
|
||||||
|
@ -37,16 +38,25 @@ class MpdStateListener(Player):
|
||||||
print("Authorising to MPD with your password...")
|
print("Authorising to MPD with your password...")
|
||||||
await self.client.password(conf.password.get_secret_value())
|
await self.client.password(conf.password.get_secret_value())
|
||||||
print(f"Connected to MPD v{self.client.mpd_version}")
|
print(f"Connected to MPD v{self.client.mpd_version}")
|
||||||
|
run_background_task(self.heartbeat())
|
||||||
|
|
||||||
|
async def heartbeat(self) -> None:
|
||||||
|
while True:
|
||||||
|
await self.client.ping()
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
async def refresh(self) -> None:
|
async def refresh(self) -> None:
|
||||||
await self.update_receivers()
|
await self.update_receivers()
|
||||||
|
|
||||||
async def loop(self, receivers: Iterable[Receiver]) -> None:
|
async def loop(self, receivers: Iterable[Receiver]) -> None:
|
||||||
self.receivers = receivers
|
self.receivers = receivers
|
||||||
# notify our receivers of the initial state MPD is in when this script loads up.
|
# Notify our receivers of the initial state MPD is in when this script loads up.
|
||||||
await self.update_receivers()
|
await self.update_receivers()
|
||||||
# then wait for stuff to change in MPD. :)
|
# And then wait for stuff to change in MPD. :)
|
||||||
async for _ in self.client.idle():
|
async for subsystems in self.client.idle():
|
||||||
|
# If no subsystems actually changed, we don't need to update the receivers.
|
||||||
|
if not subsystems:
|
||||||
|
continue
|
||||||
self.idle_count += 1
|
self.idle_count += 1
|
||||||
await self.update_receivers()
|
await self.update_receivers()
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,20 @@ class StatusResponse(TypedDict):
|
||||||
single: OneshotFlag
|
single: OneshotFlag
|
||||||
consume: OneshotFlag
|
consume: OneshotFlag
|
||||||
|
|
||||||
|
# The configured crossfade time in seconds. Omitted if crossfading isn't
|
||||||
|
# enabled. Fractional seconds are *not* allowed for this field.
|
||||||
|
xfade: NotRequired[str]
|
||||||
|
|
||||||
|
# The volume threshold at which MixRamp-compatible songs will be
|
||||||
|
# overlapped, measured in decibels. Will usually be negative, and is
|
||||||
|
# permitted to be fractional.
|
||||||
|
mixrampdb: NotRequired[str]
|
||||||
|
|
||||||
|
# A number of seconds to subtract from the overlap computed by MixRamp.
|
||||||
|
# Must be positive for MixRamp to work and is permitted to be fractional.
|
||||||
|
# Can be set to "nan" to disable MixRamp and use basic crossfading instead.
|
||||||
|
mixrampdelay: NotRequired[str]
|
||||||
|
|
||||||
# Partitions essentially let one MPD server act as multiple music players.
|
# Partitions essentially let one MPD server act as multiple music players.
|
||||||
# For most folks, this will just be "default", but mpd-now-playable will
|
# For most folks, this will just be "default", but mpd-now-playable will
|
||||||
# eventually support addressing specific partitions. Eventually.
|
# eventually support addressing specific partitions. Eventually.
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Literal
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from annotated_types import Ge
|
||||||
|
|
||||||
OneShotFlag = bool | Literal["oneshot"]
|
OneShotFlag = bool | Literal["oneshot"]
|
||||||
|
|
||||||
|
@ -15,6 +17,19 @@ def to_oneshot(value: str) -> OneShotFlag:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, kw_only=True)
|
||||||
|
class MixRamp:
|
||||||
|
#: The volume threshold at which MPD will overlap MixRamp-analysed songs,
|
||||||
|
#: measured in decibels. Can be set to any float, but sensible values are
|
||||||
|
#: typically negative.
|
||||||
|
db: float
|
||||||
|
|
||||||
|
#: A delay time in seconds which will be subtracted from the MixRamp
|
||||||
|
#: overlap. Must be set to a positive value for MixRamp to work at all -
|
||||||
|
#: will be zero if it's disabled.
|
||||||
|
delay: float
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, kw_only=True)
|
@dataclass(slots=True, kw_only=True)
|
||||||
class Settings:
|
class Settings:
|
||||||
#: The playback volume ranging from 0 to 100 - it will only be available if
|
#: The playback volume ranging from 0 to 100 - it will only be available if
|
||||||
|
@ -46,3 +61,16 @@ class Settings:
|
||||||
#: to "oneshot", which means the currently playing song will be consumed,
|
#: to "oneshot", which means the currently playing song will be consumed,
|
||||||
#: and then the flag will automatically be switched off.
|
#: and then the flag will automatically be switched off.
|
||||||
consume: OneShotFlag
|
consume: OneShotFlag
|
||||||
|
|
||||||
|
#: The number of seconds to overlap songs when cross-fading between the
|
||||||
|
#: current song and the next. Will be zero when the cross-fading feature is
|
||||||
|
#: disabled entirely. Curiously, fractional seconds are not supported here,
|
||||||
|
#: unlike many other places MPD uses seconds.
|
||||||
|
crossfade: Annotated[int, Ge(0)]
|
||||||
|
|
||||||
|
#: Settings for MixRamp-powered cross-fading, which analyses your songs'
|
||||||
|
#: volume levels to choose optimal places for cross-fading. This requires
|
||||||
|
#: either that the songs have previously been analysed and tagged with
|
||||||
|
#: MixRamp information, or that MPD's on the fly mixramp_analyzer has been
|
||||||
|
#: enabled.
|
||||||
|
mixramp: MixRamp
|
||||||
|
|
|
@ -9,6 +9,7 @@ class MPDClient(MPDClientBase):
|
||||||
|
|
||||||
def __init__(self) -> None: ...
|
def __init__(self) -> None: ...
|
||||||
async def connect(self, host: str, port: int = ...) -> None: ...
|
async def connect(self, host: str, port: int = ...) -> None: ...
|
||||||
|
async def ping(self) -> None: ...
|
||||||
async def password(self, password: str) -> None: ...
|
async def password(self, password: str) -> None: ...
|
||||||
def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ...
|
def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ...
|
||||||
async def status(self) -> types.StatusResponse: ...
|
async def status(self) -> types.StatusResponse: ...
|
||||||
|
|
Loading…
Reference in a new issue