Skip to content

Commit

Permalink
Merge branch 'music-assistant:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
lokiberra authored May 10, 2024
2 parents 4abec32 + b71a3fe commit 6d78732
Show file tree
Hide file tree
Showing 55 changed files with 2,157 additions and 1,306 deletions.
71 changes: 45 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Publish releases
on:
release:
types: [published]
env:
PYTHON_VERSION: "3.11"

jobs:
build-and-publish-pypi:
Expand All @@ -11,14 +13,27 @@ jobs:
outputs:
version: ${{ steps.vars.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4.1.4
- name: Get tag
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Set up Python 3.11
- name: Validate version number
run: >-
if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
if ! [[ "${{ steps.vars.outputs.tag }}" =~ "b" ]]; then
echo "Pre-release: Tag is missing beta suffix (${{ steps.vars.outputs.tag }})"
exit 1
fi
else
if [[ "${{ steps.vars.outputs.tag }}" =~ "b" ]]; then
echo "Release: Tag must not have a beta suffix (${{ steps.vars.outputs.tag }})"
exit 1
fi
fi
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/[email protected]
with:
python-version: "3.11"
python-version: ${{ env.PYTHON_VERSION }}
- name: Install build
run: >-
pip install build tomli tomli-w
Expand All @@ -35,7 +50,7 @@ jobs:
with open("pyproject.toml", "wb") as f:
tomli_w.dump(pyproject, f)
- name: Build
- name: Build python package
run: >-
python3 -m build
- name: Publish release to PyPI
Expand All @@ -53,7 +68,7 @@ jobs:
packages: write
needs: build-and-publish-pypi
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4.1.4
- name: Log in to the GitHub container registry
uses: docker/[email protected]
with:
Expand All @@ -70,21 +85,9 @@ jobs:
echo "patch=${patch}" >> $GITHUB_OUTPUT
echo "minor=${patch%.*}" >> $GITHUB_OUTPUT
echo "major=${patch%.*.*}" >> $GITHUB_OUTPUT
if [[ $patch =~ "b" ]]; then
echo "channel=beta" >> $GITHUB_OUTPUT
else
echo "channel=stable" >> $GITHUB_OUTPUT
fi
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository_owner }}/server
- name: Build and Push images
- name: Build and Push release
uses: docker/[email protected]
if: github.event.release.prerelease == false
with:
context: .
platforms: linux/amd64,linux/arm64
Expand All @@ -93,31 +96,47 @@ jobs:
ghcr.io/${{ github.repository_owner }}/server:${{ steps.tags.outputs.patch }},
ghcr.io/${{ github.repository_owner }}/server:${{ steps.tags.outputs.minor }},
ghcr.io/${{ github.repository_owner }}/server:${{ steps.tags.outputs.major }},
ghcr.io/${{ github.repository_owner }}/server:${{ steps.tags.outputs.channel }},
ghcr.io/${{ github.repository_owner }}/server:latest
ghcr.io/${{ github.repository_owner }}/server:stable
push: true
build-args: "MASS_VERSION=${{ needs.build-and-publish-pypi.outputs.version }}"
- name: Build and Push pre-release
uses: docker/[email protected]
if: github.event.release.prerelease == true
with:
context: .
platforms: linux/amd64,linux/arm64
file: Dockerfile
tags: |-
ghcr.io/${{ github.repository_owner }}/server:${{ steps.tags.outputs.patch }},
ghcr.io/${{ github.repository_owner }}/server:beta
push: true
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"MASS_VERSION=${{ needs.build-and-publish-pypi.outputs.version }}"
build-args: "MASS_VERSION=${{ needs.build-and-publish-pypi.outputs.version }}"

release-notes-update:
name: Updates the release notes and changelog
needs: [ build-and-publish-pypi, build-and-push-container-image ]
needs: [build-and-publish-pypi, build-and-push-container-image]
runs-on: ubuntu-latest
steps:
- name: Update changelog and release notes including frontend notes
uses: music-assistant/release-notes-merge-action@main
if: github.event.release.prerelease == true
with:
github_token: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }}
release_tag: ${{ needs.build-and-publish-pypi.outputs.version }}

addon-version-update:
name: Updates the Addon repository with the new version
needs: [ build-and-publish-pypi, build-and-push-container-image, release-notes-update ]
needs:
[
build-and-publish-pypi,
build-and-push-container-image,
release-notes-update,
]
runs-on: ubuntu-latest
steps:
- name: Push new version number to addon config
uses: music-assistant/addon-update-action@main
if: github.event.release.prerelease == true
with:
github_token: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }}
new_server_version: ${{ needs.build-and-publish-pypi.outputs.version }}
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"source.organizeImports": "explicit"
}
},
"editor.defaultFormatter": "charliermarsh.ruff"
"editor.defaultFormatter": "charliermarsh.ruff",
"[github-actions-workflow]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
114 changes: 98 additions & 16 deletions music_assistant/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from music_assistant.client.exceptions import ConnectionClosed, InvalidServerVersion, InvalidState
from music_assistant.client.exceptions import (
ConnectionClosed,
InvalidServerVersion,
InvalidState,
)
from music_assistant.common.models.api import (
ChunkedResultMessage,
CommandMessage,
ErrorResultMessage,
EventMessage,
Expand All @@ -20,13 +23,18 @@
SuccessResultMessage,
parse_message,
)
from music_assistant.common.models.enums import EventType
from music_assistant.common.models.enums import EventType, ImageType
from music_assistant.common.models.errors import ERROR_MAP
from music_assistant.common.models.event import MassEvent
from music_assistant.common.models.media_items import ItemMapping, MediaItemType
from music_assistant.common.models.provider import ProviderInstance, ProviderManifest
from music_assistant.common.models.queue_item import QueueItem
from music_assistant.constants import API_SCHEMA_VERSION

from .config import Config
from .connection import WebsocketsConnection
from .music import Music
from .player_queues import PlayerQueues
from .players import Players

if TYPE_CHECKING:
Expand Down Expand Up @@ -54,37 +62,108 @@ def __init__(self, server_url: str, aiohttp_session: ClientSession | None) -> No
self._subscribers: list[EventSubscriptionType] = []
self._stop_called: bool = False
self._loop: asyncio.AbstractEventLoop | None = None
self._config = Config(self)
self._players = Players(self)
self._player_queues = PlayerQueues(self)
self._music = Music(self)
# below items are retrieved after connect
self._server_info: ServerInfoMessage | None = None
self._provider_manifests: dict[str, ProviderManifest] = {}
self._providers: dict[str, ProviderInstance] = {}

@property
def server_info(self) -> ServerInfoMessage | None:
"""Return info of the server we're currently connected to."""
return self._server_info

@property
def providers(self) -> list[ProviderInstance]:
"""Return all loaded/running Providers (instances)."""
return list(self._providers.values())

@property
def provider_manifests(self) -> list[ProviderManifest]:
"""Return all Provider manifests."""
return list(self._provider_manifests.values())

@property
def config(self) -> Config:
"""Return Config handler."""
return self._config

@property
def players(self) -> Players:
"""Return Players handler."""
return self._players

@property
def player_queues(self) -> PlayerQueues:
"""Return PlayerQueues handler."""
return self._player_queues

@property
def music(self) -> Music:
"""Return Music handler."""
return self._music

def get_image_url(self, image: MediaItemImage) -> str:
def get_provider_manifest(self, domain: str) -> ProviderManifest:
"""Return Provider manifests of single provider(domain)."""
return self._provider_manifests[domain]

def get_provider(
self, provider_instance_or_domain: str, return_unavailable: bool = False
) -> ProviderInstance | None:
"""Return provider by instance id or domain."""
# lookup by instance_id first
if prov := self._providers.get(provider_instance_or_domain):
if return_unavailable or prov.available:
return prov
if not prov.is_streaming_provider:
# no need to lookup other instances because this provider has unique data
return None
provider_instance_or_domain = prov.domain
# fallback to match on domain
for prov in self._providers.values():
if prov.domain != provider_instance_or_domain:
continue
if return_unavailable or prov.available:
return prov
self.logger.debug("Provider %s is not available", provider_instance_or_domain)
return None

def get_image_url(self, image: MediaItemImage, size: int = 0) -> str:
"""Get (proxied) URL for MediaItemImage."""
if image.remotely_accessible:
if image.remotely_accessible and not size:
return image.path
if image.remotely_accessible and size:
# get url to resized image(thumb) from weserv service
return (
f"https://images.weserv.nl/?url={urllib.parse.quote(image.path)}"
f"&w=${size}&h=${size}&fit=cover&a=attention"
)
# return imageproxy url for images that need to be resolved
# the original path is double encoded
encoded_url = urllib.parse.quote(urllib.parse.quote(image.path))
return (
f"{self.server_info.base_url}/imageproxy?path={encoded_url}&provider={image.provider}"
f"{self.server_info.base_url}/imageproxy?path={encoded_url}"
f"&provider={image.provider}&size={size}"
)

def get_media_item_image_url(
self,
item: MediaItemType | ItemMapping | QueueItem,
type: ImageType = ImageType.THUMB, # noqa: A002
size: int = 0,
) -> str | None:
"""Get image URL for MediaItem, QueueItem or ItemMapping."""
# handle queueitem with media_item attribute
if media_item := getattr(item, "media_item", None):
if img := self.music.get_media_item_image(media_item, type):
return self.get_image_url(img, size)
if img := self.music.get_media_item_image(item, type):
return self.get_image_url(img, size)
return None

def subscribe(
self,
cb_func: EventCallBackType,
Expand Down Expand Up @@ -135,9 +214,8 @@ async def connect(self) -> None:
self._server_info = info

self.logger.info(
"Connected to Music Assistant Server %s using %s, Version %s, Schema Version %s",
"Connected to Music Assistant Server %s, Version %s, Schema Version %s",
info.server_id,
self.connection.__class__.__name__,
info.server_version,
info.schema_version,
)
Expand Down Expand Up @@ -208,6 +286,15 @@ async def start_listening(self, init_ready: asyncio.Event | None = None) -> None
# fetch initial state
# we do this in a separate task to not block reading messages
async def fetch_initial_state() -> None:
self._providers = {
x["instance_id"]: ProviderInstance.from_dict(x)
for x in await self.send_command("providers")
}
self._provider_manifests = {
x["domain"]: ProviderManifest.from_dict(x)
for x in await self.send_command("providers/manifests")
}
await self._player_queues.fetch_state()
await self._players.fetch_state()

if init_ready is not None:
Expand Down Expand Up @@ -247,14 +334,6 @@ def _handle_incoming_message(self, raw: dict[str, Any]) -> None:
if future is None:
# no listener for this result
return
if isinstance(msg, ChunkedResultMessage):
# handle chunked response (for very large objects)
if not hasattr(future, "intermediate_result"):
future.intermediate_result = []
future.intermediate_result += msg.result
if msg.is_last_chunk:
future.set_result(future.intermediate_result)
return
if isinstance(msg, SuccessResultMessage):
future.set_result(msg.result)
return
Expand All @@ -281,6 +360,9 @@ def _handle_event(self, event: MassEvent) -> None:
if self._stop_called:
return

if event.event == EventType.PROVIDERS_UPDATED:
self._providers = {x["instance_id"]: ProviderInstance.from_dict(x) for x in event.data}

for cb_func, event_filter, id_filter in self._subscribers:
if not (event_filter is None or event.event in event_filter):
continue
Expand Down
Loading

0 comments on commit 6d78732

Please sign in to comment.