530 lines
20 KiB
Python
530 lines
20 KiB
Python
"""Actions for integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
|
from homeassistant.core import (
|
|
HomeAssistant,
|
|
ServiceCall,
|
|
ServiceResponse,
|
|
SupportsResponse,
|
|
callback,
|
|
)
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
from homeassistant.helpers import entity_registry as er
|
|
from music_assistant_models.errors import (
|
|
InvalidCommand,
|
|
MediaNotFoundError,
|
|
ProviderUnavailableError,
|
|
)
|
|
|
|
from .const import (
|
|
ATTR_COMMAND,
|
|
ATTR_DATA,
|
|
ATTR_DURATION,
|
|
ATTR_FAVORITE,
|
|
ATTR_LIMIT,
|
|
ATTR_LIMIT_AFTER,
|
|
ATTR_LIMIT_BEFORE,
|
|
ATTR_LOCAL_IMAGE_ENCODED,
|
|
ATTR_MEDIA_ALBUM_NAME,
|
|
ATTR_MEDIA_ARTIST,
|
|
ATTR_MEDIA_CONTENT_ID,
|
|
ATTR_MEDIA_IMAGE,
|
|
ATTR_MEDIA_TITLE,
|
|
ATTR_OFFSET,
|
|
ATTR_PLAYER_ENTITY,
|
|
ATTR_POSITION,
|
|
ATTR_PROVIDERS,
|
|
ATTR_QUEUE_ID,
|
|
ATTR_QUEUE_ITEM_ID,
|
|
ATTR_RELEASE_DATE,
|
|
ATTR_VOLUME_LEVEL,
|
|
CONF_DOWNLOAD_LOCAL,
|
|
DEFAULT_QUEUE_ITEMS_LIMIT,
|
|
DEFAULT_QUEUE_ITEMS_OFFSET,
|
|
DOMAIN,
|
|
LOGGER,
|
|
SERVICE_GET_GROUP_VOLUME,
|
|
SERVICE_GET_QUEUE_ITEMS,
|
|
SERVICE_GET_RECOMMENDATIONS,
|
|
SERVICE_MOVE_QUEUE_ITEM_DOWN,
|
|
SERVICE_MOVE_QUEUE_ITEM_NEXT,
|
|
SERVICE_MOVE_QUEUE_ITEM_UP,
|
|
SERVICE_PLAY_QUEUE_ITEM,
|
|
SERVICE_REMOVE_QUEUE_ITEM,
|
|
SERVICE_SEND_COMMAND,
|
|
SERVICE_SET_GROUP_VOLUME,
|
|
)
|
|
from .controller import MassQueueController
|
|
from .schemas import (
|
|
GET_GROUP_VOLUME_SERVICE_SCHEMA,
|
|
GET_RECOMMENDATIONS_SERVICE_SCHEMA,
|
|
MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA,
|
|
MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
|
|
MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA,
|
|
PLAY_QUEUE_ITEM_SERVICE_SCHEMA,
|
|
QUEUE_ITEM_SCHEMA,
|
|
QUEUE_ITEMS_SERVICE_SCHEMA,
|
|
REMOVE_QUEUE_ITEM_SERVICE_SCHEMA,
|
|
SEND_COMMAND_SERVICE_SCHEMA,
|
|
SET_GROUP_VOLUME_SERVICE_SCHEMA,
|
|
TRACK_ITEM_SCHEMA,
|
|
)
|
|
from .utils import (
|
|
find_image,
|
|
parse_uri,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from music_assistant_client import MusicAssistantClient
|
|
|
|
from . import MassQueueEntryData
|
|
|
|
|
|
class MassQueueActions:
|
|
"""Class to manage Music Assistant actions without passing `hass` and `mass_client` each time."""
|
|
|
|
def __init__(
|
|
self,
|
|
hass: HomeAssistant,
|
|
mass_client: MusicAssistantClient,
|
|
config_entry: ConfigEntry,
|
|
):
|
|
"""Initialize class."""
|
|
self._hass: HomeAssistant = hass
|
|
self._client: MusicAssistantClient = mass_client
|
|
self._controller = MassQueueController(self._hass, self._client, config_entry)
|
|
self._config_entry = config_entry
|
|
self._download_local = config_entry.options.get(CONF_DOWNLOAD_LOCAL)
|
|
|
|
def setup_controller(self):
|
|
"""Setup Music Assistant controller."""
|
|
self._controller.update_players()
|
|
self._controller.subscribe_events()
|
|
self._hass.loop.create_task(self._controller.update_queues())
|
|
|
|
@callback
|
|
def register_actions(self) -> None:
|
|
"""Register actions with Home Assistant."""
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_GET_QUEUE_ITEMS,
|
|
self.get_queue_items,
|
|
schema=QUEUE_ITEMS_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_PLAY_QUEUE_ITEM,
|
|
self.play_queue_item,
|
|
schema=PLAY_QUEUE_ITEM_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.NONE,
|
|
)
|
|
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_REMOVE_QUEUE_ITEM,
|
|
self.remove_queue_item,
|
|
schema=REMOVE_QUEUE_ITEM_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.NONE,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_MOVE_QUEUE_ITEM_UP,
|
|
self.move_queue_item_up,
|
|
schema=MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.NONE,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_MOVE_QUEUE_ITEM_DOWN,
|
|
self.move_queue_item_down,
|
|
schema=MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.NONE,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_MOVE_QUEUE_ITEM_NEXT,
|
|
self.move_queue_item_next,
|
|
schema=MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.NONE,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_SEND_COMMAND,
|
|
self.send_command,
|
|
schema=SEND_COMMAND_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.OPTIONAL,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_GET_RECOMMENDATIONS,
|
|
self.get_recommendations,
|
|
schema=GET_RECOMMENDATIONS_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_GET_GROUP_VOLUME,
|
|
self.get_group_volume,
|
|
schema=GET_GROUP_VOLUME_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
self._hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_SET_GROUP_VOLUME,
|
|
self.set_group_volume,
|
|
schema=SET_GROUP_VOLUME_SERVICE_SCHEMA,
|
|
supports_response=SupportsResponse.NONE,
|
|
)
|
|
|
|
def get_queue_id(self, entity_id: str):
|
|
"""Get the queue ID for a player."""
|
|
return self._hass.states.get(entity_id).attributes[ATTR_QUEUE_ID]
|
|
|
|
async def get_queue_index(self, entity_id: str):
|
|
"""Get the current index of the queue."""
|
|
active_queue = await self.get_active_queue(entity_id)
|
|
try:
|
|
return active_queue.current_index or 0
|
|
except AttributeError:
|
|
return 0
|
|
|
|
async def get_active_queue(self, entity_id: str):
|
|
"""Get active queue details."""
|
|
queue_id = self.get_queue_id(entity_id)
|
|
return await self._client.player_queues.get_active_queue(queue_id)
|
|
|
|
async def _format_queue_item(self, queue_item: dict) -> dict:
|
|
"""Format list of queue items for response."""
|
|
media = queue_item["media_item"]
|
|
|
|
queue_item_id = queue_item["queue_item_id"]
|
|
media_title = media["name"]
|
|
media_album = media.get("album")
|
|
media_album_name = "" if media_album is None else media_album.get("name", "")
|
|
media_content_id = media["uri"]
|
|
media_image = find_image(queue_item) or ""
|
|
local_image_encoded = queue_item.get(ATTR_LOCAL_IMAGE_ENCODED)
|
|
favorite = media["favorite"]
|
|
|
|
artists = media["artists"]
|
|
artist_names = [artist["name"] for artist in artists]
|
|
media_artist = ", ".join(artist_names)
|
|
response: ServiceResponse = QUEUE_ITEM_SCHEMA(
|
|
{
|
|
ATTR_QUEUE_ITEM_ID: queue_item_id,
|
|
ATTR_MEDIA_TITLE: media_title,
|
|
ATTR_MEDIA_ALBUM_NAME: media_album_name,
|
|
ATTR_MEDIA_ARTIST: media_artist,
|
|
ATTR_MEDIA_CONTENT_ID: media_content_id,
|
|
ATTR_MEDIA_IMAGE: media_image,
|
|
ATTR_FAVORITE: favorite,
|
|
},
|
|
)
|
|
if local_image_encoded:
|
|
response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded
|
|
return response
|
|
|
|
async def send_command(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Sends command to Music Assistant and returns response."""
|
|
command = call.data[ATTR_COMMAND]
|
|
data = call.data.get(ATTR_DATA)
|
|
response = await self._controller.send_command(command, data)
|
|
return {"response": response}
|
|
|
|
async def get_recommendations(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Pulls all recommendations for the providers given."""
|
|
providers = call.data.get(ATTR_PROVIDERS)
|
|
return await self._controller.get_recommendations(providers)
|
|
|
|
async def get_group_volume(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Gets the group volume for a single player."""
|
|
entity_id = call.data.get(ATTR_PLAYER_ENTITY)
|
|
queue_id = self.get_queue_id(entity_id)
|
|
try:
|
|
volume = await self._controller.get_grouped_volume(queue_id)
|
|
except: # noqa: E722
|
|
volume = None
|
|
return volume
|
|
|
|
async def set_group_volume(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Sets the group volume for a player."""
|
|
entity_id = call.data.get(ATTR_PLAYER_ENTITY)
|
|
queue_id = self.get_queue_id(entity_id)
|
|
volume_level = call.data.get(ATTR_VOLUME_LEVEL)
|
|
await self._controller.set_grouped_volume(queue_id, volume_level)
|
|
|
|
async def get_queue_items(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Get all items in queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
queue_id = self.get_queue_id(entity_id)
|
|
if queue_id is None:
|
|
return {entity_id: []}
|
|
offset = call.data.get(ATTR_OFFSET)
|
|
limit = call.data.get(ATTR_LIMIT)
|
|
limit_before = call.data.get(ATTR_LIMIT_BEFORE)
|
|
limit_after = call.data.get(ATTR_LIMIT_AFTER)
|
|
idx = await self.get_queue_index(entity_id)
|
|
if limit_before:
|
|
offset = idx - limit_before
|
|
if limit_after:
|
|
limit = limit_before + limit_after + 1 if limit_before else limit_after + 1
|
|
if offset is None:
|
|
offset = idx + DEFAULT_QUEUE_ITEMS_OFFSET
|
|
if limit is None:
|
|
limit = DEFAULT_QUEUE_ITEMS_LIMIT
|
|
offset = max(offset, 0)
|
|
queue_items = await self._controller.player_queue(queue_id, limit, offset)
|
|
response: ServiceResponse = {
|
|
entity_id: [await self._format_queue_item(item) for item in queue_items],
|
|
}
|
|
return response
|
|
|
|
async def play_queue_item(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Play selected item in queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
|
|
queue_id = self.get_queue_id(entity_id)
|
|
await self._client.send_command(
|
|
"player_queues/play_index",
|
|
queue_id=queue_id,
|
|
index=queue_item_id,
|
|
)
|
|
|
|
async def remove_queue_item(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Remove selected item from queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
|
|
queue_id = self.get_queue_id(entity_id)
|
|
await self._client.player_queues.queue_command_delete(queue_id, queue_item_id)
|
|
|
|
async def move_queue_item_up(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Move selected item up in queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
|
|
queue_id = self.get_queue_id(entity_id)
|
|
await self._client.player_queues.queue_command_move_up(queue_id, queue_item_id)
|
|
|
|
async def move_queue_item_down(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Move selected item down in queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
|
|
queue_id = self.get_queue_id(entity_id)
|
|
await self._client.player_queues.queue_command_move_down(
|
|
queue_id,
|
|
queue_item_id,
|
|
)
|
|
|
|
async def move_queue_item_next(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Move selected item next in queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
queue_item_id = call.data[ATTR_QUEUE_ITEM_ID]
|
|
queue_id = self.get_queue_id(entity_id)
|
|
await self._client.player_queues.queue_command_move_next(
|
|
queue_id,
|
|
queue_item_id,
|
|
)
|
|
|
|
async def unfavorite_item(self, call: ServiceCall) -> ServiceResponse:
|
|
"""Unfavorites currently playing item in queue."""
|
|
entity_id = call.data[ATTR_PLAYER_ENTITY]
|
|
attrs = self._hass.states.get(entity_id).attributes
|
|
content_id = attrs.get(ATTR_MEDIA_CONTENT_ID)
|
|
if not content_id:
|
|
msg = f"Cannot find media with content id {content_id}"
|
|
raise MediaNotFoundError(msg)
|
|
provider = content_id.split("://")[0]
|
|
if provider != "library":
|
|
msg = f"Unfavorite can only apply to library media items, not from provider {provider}"
|
|
raise InvalidCommand(msg)
|
|
item_id = str(content_id.split("/")[-1])
|
|
await self._client.send_command(
|
|
"music/favorites/remove_item",
|
|
media_type="track",
|
|
library_item_id=item_id,
|
|
)
|
|
|
|
async def get_artist_details(self, artist_uri):
|
|
"""Retrieves the details for an artist."""
|
|
provider, item_id = parse_uri(artist_uri)
|
|
LOGGER.debug(f"Getting artist details for provider {provider}")
|
|
return await self._client.music.get_artist(item_id, provider)
|
|
|
|
async def get_album_details(self, album_uri):
|
|
"""Retrieves the details for an album."""
|
|
provider, item_id = parse_uri(album_uri)
|
|
LOGGER.debug(f"Getting album details for provider {provider}")
|
|
return await self._client.music.get_album(item_id, provider)
|
|
|
|
async def get_playlist_details(self, playlist_uri):
|
|
"""Retrieves the details for a playlist."""
|
|
provider, item_id = parse_uri(playlist_uri)
|
|
LOGGER.debug(f"Getting album details for provider {provider}")
|
|
return await self._client.music.get_playlist(item_id, provider)
|
|
|
|
async def get_podcast_details(self, podcast_uri):
|
|
"""Retrieves the details for a podcast."""
|
|
provider, item_id = parse_uri(podcast_uri)
|
|
LOGGER.debug(f"Getting podcast details for provider {provider}")
|
|
return await self._client.music.get_podcast(item_id, provider)
|
|
|
|
async def get_artist_tracks(self, artist_uri: str, page: int | None = None):
|
|
"""Retrieves a limited number of tracks from an artist."""
|
|
details = await self.get_artist_details(artist_uri)
|
|
mappings = list(details.provider_mappings)
|
|
if not len(mappings) > 0:
|
|
msg = f"URI {artist_uri} returned no results!"
|
|
raise ProviderUnavailableError(msg)
|
|
mapping = mappings[0]
|
|
item_id = mapping.item_id
|
|
provider = mapping.provider_domain
|
|
resp = (
|
|
await self._client.music.get_artist_tracks(item_id, provider)
|
|
if not page
|
|
else await self._client.music.get_artist_tracks(item_id, provider, page)
|
|
)
|
|
return [self.format_track_item(item.to_dict()) for item in resp]
|
|
|
|
async def get_album_tracks(self, album_uri: str, page: int | None = None):
|
|
"""Retrieves all tracks from an album."""
|
|
details = await self.get_album_details(album_uri)
|
|
mappings = list(details.provider_mappings)
|
|
if not len(mappings) > 0:
|
|
msg = f"URI {album_uri} returned no results!"
|
|
raise ProviderUnavailableError(msg)
|
|
mapping = mappings[0]
|
|
item_id = mapping.item_id
|
|
provider = mapping.provider_domain
|
|
resp = (
|
|
await self._client.music.get_album_tracks(item_id, provider)
|
|
if not page
|
|
else await self._client.music.get_album_tracks(item_id, provider, page)
|
|
)
|
|
return [self.format_track_item(item.to_dict()) for item in resp]
|
|
|
|
async def get_podcast_episodes(self, podcast_uri):
|
|
"""Retrieves all episodes for a podcast."""
|
|
provider, item_id = parse_uri(podcast_uri)
|
|
LOGGER.debug(
|
|
f"Getting podcast episodes for provider {provider}, item_id {item_id}",
|
|
)
|
|
resp: list = await self._client.music.get_podcast_episodes(item_id, provider)
|
|
formatted = [self.format_podcast_episode(item.to_dict()) for item in resp]
|
|
formatted.sort(key=lambda x: x[ATTR_RELEASE_DATE], reverse=True)
|
|
return formatted
|
|
|
|
async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None):
|
|
"""Retrieves all playlist items."""
|
|
provider, item_id = parse_uri(playlist_uri)
|
|
LOGGER.debug(
|
|
f"Getting playlist items for provider {provider}, item_id {item_id}",
|
|
)
|
|
resp = (
|
|
await self._client.music.get_playlist_tracks(item_id, provider)
|
|
if not page
|
|
else await self._client.music.get_playlist_tracks(item_id, provider, page)
|
|
)
|
|
return [self.format_playlist_track(item.to_dict()) for item in resp]
|
|
|
|
def format_playlist_track(self, playlist_track: dict) -> TRACK_ITEM_SCHEMA:
|
|
"""Processes individual playlist tracks using format_track_item and adds position."""
|
|
result = self.format_track_item(playlist_track)
|
|
result[ATTR_POSITION] = playlist_track["position"]
|
|
return result
|
|
|
|
def format_track_item(self, track_item: dict) -> TRACK_ITEM_SCHEMA:
|
|
"""Process an individual track item."""
|
|
result = self.format_item(track_item)
|
|
media_album = track_item.get("album")
|
|
media_album_name = "" if media_album is None else media_album.get("name", "")
|
|
artists = track_item["artists"]
|
|
artist_names = [artist["name"] for artist in artists]
|
|
media_artist = ", ".join(artist_names)
|
|
result[ATTR_MEDIA_ALBUM_NAME] = media_album_name
|
|
result[ATTR_MEDIA_ARTIST] = media_artist
|
|
return result
|
|
|
|
def format_podcast_episode(self, podcast_episode: dict) -> TRACK_ITEM_SCHEMA:
|
|
"""Process an individual track item."""
|
|
result = self.format_item(podcast_episode)
|
|
result[ATTR_RELEASE_DATE] = podcast_episode.get("metadata", {}).get(
|
|
"release_date",
|
|
)
|
|
return result
|
|
|
|
def format_item(self, media_item: dict) -> TRACK_ITEM_SCHEMA:
|
|
"""Processes the individual items in a playlist."""
|
|
media_title = media_item.get("name") or "N/A"
|
|
media_content_id = media_item["uri"]
|
|
media_image = find_image(media_item) or ""
|
|
local_image_encoded = media_item.get(ATTR_LOCAL_IMAGE_ENCODED)
|
|
favorite = media_item["favorite"]
|
|
duration = media_item["duration"] or 0
|
|
response: ServiceResponse = TRACK_ITEM_SCHEMA(
|
|
{
|
|
ATTR_MEDIA_TITLE: media_title,
|
|
ATTR_MEDIA_CONTENT_ID: media_content_id,
|
|
ATTR_DURATION: duration,
|
|
ATTR_MEDIA_IMAGE: media_image,
|
|
ATTR_FAVORITE: favorite,
|
|
},
|
|
)
|
|
if local_image_encoded:
|
|
response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded
|
|
return response
|
|
|
|
async def remove_playlist_tracks(
|
|
self,
|
|
playlist_id: str | int,
|
|
positions_to_remove: list[int],
|
|
):
|
|
"""Removes one or more items from a playlist."""
|
|
await self._client.music.remove_playlist_tracks(
|
|
playlist_id,
|
|
positions_to_remove,
|
|
)
|
|
|
|
|
|
@callback
|
|
def get_music_assistant_client(
|
|
hass: HomeAssistant,
|
|
entity_id: str,
|
|
) -> MusicAssistantClient:
|
|
"""Get Music Assistant client from entity_id."""
|
|
registry = er.async_get(hass)
|
|
entity = registry.async_get(entity_id)
|
|
config_entry_id = entity.config_entry_id
|
|
return _get_music_assistant_client(hass, config_entry_id)
|
|
|
|
|
|
@callback
|
|
def _get_music_assistant_client(
|
|
hass: HomeAssistant,
|
|
config_entry_id: str,
|
|
) -> MusicAssistantClient:
|
|
"""Get Music Assistant Client from config_entry_id."""
|
|
entry: MassQueueEntryData | None
|
|
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
|
|
exc = "Entry not found."
|
|
raise ServiceValidationError(exc)
|
|
if entry.state is not ConfigEntryState.LOADED:
|
|
exc = "Entry not loaded"
|
|
raise ServiceValidationError(exc)
|
|
return entry.runtime_data.mass
|
|
|
|
|
|
@callback
|
|
async def setup_controller_and_actions(
|
|
hass: HomeAssistant,
|
|
mass_client: MusicAssistantClient,
|
|
entry: ConfigEntry,
|
|
) -> MassQueueActions:
|
|
"""Initialize client and actions class, add actions to Home Assistant."""
|
|
actions = MassQueueActions(hass, mass_client, entry)
|
|
actions.setup_controller()
|
|
return actions
|