Commit 8bec4d4d authored by David Trattnig's avatar David Trattnig
Browse files

Custom channel and resource routing. #43

parent e652d724
......@@ -19,6 +19,8 @@
from enum import Enum
from modules.base.utils import SimpleUtil as SU
from modules.core.resources import ResourceType
class TransitionType(Enum):
......@@ -28,14 +30,14 @@ class TransitionType(Enum):
INSTANT = "instant"
FADE = "fade"
class Channel(Enum):
"""
Channel name mappings to the Liqidsoap channel IDs
Channel name mappings to the Liqidsoap channel IDs.
"""
FILESYSTEM_A = "in_filesystem_0"
FILESYSTEM_B = "in_filesystem_1"
SCHEDULED_FALLBACK = "playlist_fallback_scheduled"
QUEUE_A = "in_filesystem_0"
QUEUE_B = "in_filesystem_1"
FALLBACK_QUEUE_A = "in_fallback_scheduled_0"
FALLBACK_QUEUE_B = "in_fallback_scheduled_1"
HTTP_A = "in_http_0"
HTTP_B = "in_http_1"
HTTPS_A = "in_https_0"
......@@ -54,10 +56,15 @@ class ChannelType(Enum):
"""
Engine channel types mapped to `Entry` source types.
"""
FILESYSTEM = {
QUEUE = {
"id": "fs",
"numeric": 0,
"channels": [Channel.FILESYSTEM_A, Channel.FILESYSTEM_B, Channel.SCHEDULED_FALLBACK]
"channels": [Channel.QUEUE_A, Channel.QUEUE_B]
}
FALLBACK_QUEUE = {
"id": "fallback_queue",
"numeric": 0,
"channels": [Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B]
}
HTTP = {
"id": "http",
......@@ -106,3 +113,126 @@ class LiquidsoapResponse(Enum):
STREAM_STATUS_STOPPED = "stopped"
STREAM_STATUS_CONNECTED = "connected"
class ChannelRouter():
"""
Wires source types with channels and channel-types.
"""
config = None
logger = None
resource_mapping = None
active_channels = None
def __init__(self, config, logger):
"""
Constructor
Args:
config (AuraConfig): The configuration
"""
self.config = config
self.logger = logger
self.resource_mapping = {
ResourceType.FILE: ChannelType.QUEUE,
ResourceType.STREAM_HTTP: ChannelType.HTTP,
ResourceType.STREAM_HTTPS: ChannelType.HTTPS,
ResourceType.LINE: ChannelType.LIVE,
ResourceType.PLAYLIST: ChannelType.QUEUE,
ResourceType.POOL: ChannelType.QUEUE
}
self.active_channels = {
ChannelType.QUEUE: Channel.QUEUE_A,
ChannelType.FALLBACK_QUEUE: Channel.FALLBACK_QUEUE_A,
ChannelType.HTTP: Channel.HTTP_A,
ChannelType.HTTPS: Channel.HTTPS_A,
ChannelType.LIVE: Channel.LIVE_0
}
def set_active(self, channel_type, channel):
"""
Set the channel for the given resource type active
"""
self.active_channels[channel_type] = channel
def get_active(self, channel_type):
"""
Retrieves the active channel for the given resource type
"""
return self.active_channels[channel_type]
def type_for_resource(self, resource_type):
"""
Retrieves a `ChannelType` for the given `ResourceType`.
Only default mappings can be evaluatated. Custom variations
like fallback channels are not respected.
"""
return self.resource_mapping.get(resource_type)
def channel_swap(self, channel_type):
"""
Returns the currently inactive channel for a given type. For example if the currently some
file on channel QUEUE A is playing, the channel QUEUE B is returned for being used
to queue new entries.
Args:
entry_type (ResourceType): The resource type such es file, stream or live source
Returns:
(Channel, Channel): The previous and new channel
"""
previous_channel = self.active_channels[channel_type]
new_channel = None
msg = None
if channel_type == ChannelType.QUEUE:
if previous_channel == Channel.QUEUE_A:
new_channel = Channel.QUEUE_B
msg = "Swapped queue channel from A > B"
else:
new_channel = Channel.QUEUE_A
msg = "Swapped queue channel from B > A"
elif channel_type == ChannelType.FALLBACK_QUEUE:
if previous_channel == Channel.FALLBACK_QUEUE_A:
new_channel = Channel.FALLBACK_QUEUE_B
msg = "Swapped fallback queue channel from A > B"
else:
new_channel = Channel.FALLBACK_QUEUE_A
msg = "Swapped fallback channel from B > A"
elif channel_type == ChannelType.HTTP:
if previous_channel == Channel.HTTP_A:
new_channel = Channel.HTTP_B
msg = "Swapped HTTP Stream channel from A > B"
else:
new_channel = Channel.HTTP_A
msg = "Swapped HTTP Stream channel from B > A"
elif channel_type == ChannelType.HTTPS:
if previous_channel == Channel.HTTPS_A:
new_channel = Channel.HTTPS_B
msg = "Swapped HTTPS Stream channel from A > B"
else:
new_channel = Channel.HTTPS_A
msg = "Swapped HTTPS Stream channel from B > A"
else:
self.logger.warning(SU.red(f"No channel to swap - invalid entry_type '{channel_type}'"))
if msg: self.logger.info(SU.pink(msg))
return (previous_channel, new_channel)
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
class ResourceType(Enum):
"""
Media content types.
"""
FILE = "file:"
STREAM_HTTP = "http:"
STREAM_HTTPS = "https:"
LINE = "line:"
PLAYLIST = "playlist:"
POOL = "pool:"
class ResourceClass(Enum):
"""
Media content classes.
"""
FILE = {
"id": "fs",
"numeric": 0,
"types": [ResourceType.FILE]
}
STREAM = {
"id": "fs",
"numeric": 0,
"types": [ResourceType.STREAM_HTTP, ResourceType.STREAM_HTTPS]
}
LIVE = {
"id": "http",
"numeric": 1,
"types": [ResourceType.LINE]
}
PLAYLIST = {
"id": "https",
"numeric": 2,
"types": [ResourceType.PLAYLIST, ResourceType.POOL]
}
@property
def types(self):
return self.value["types"]
@property
def numeric(self):
return self.value["numeric"]
def __str__(self):
return str(self.value["id"])
class ResourceUtil(Enum):
"""
Utilities for different resource types.
"""
@staticmethod
def get_content_type(uri):
"""
Returns the content type identified by the passed URI.
Args:
uri (String): The URI of the source
Returns:
(ResourceType)
"""
if uri.startswith(ResourceType.STREAM_HTTPS.value):
return ResourceType.STREAM_HTTPS
if uri.startswith(ResourceType.STREAM_HTTP.value):
return ResourceType.STREAM_HTTP
if uri.startswith(ResourceType.POOL.value):
return ResourceType.POOL
if uri.startswith(ResourceType.FILE.value):
return ResourceType.FILE
if uri.startswith(ResourceType.LINE.value):
return ResourceType.LINE
@staticmethod
def get_content_class(content_type):
"""
Returns the content class identified by the passed type.
Args:
content_type (ContentType): The content type
Returns:
(ResourceType)
"""
if content_type in ResourceClass.FILE.types:
return ResourceClass.FILE
if content_type in ResourceClass.STREAM.types:
return ResourceClass.STREAM
if content_type in ResourceClass.LIVE.types:
return ResourceClass.LIVE
if content_type in ResourceClass.PLAYLIST.types:
return ResourceClass.PLAYLIST
@staticmethod
def generate_m3u_file(target_file, audio_store_path, entries, entry_extension):
"""
Writes a M3U file based on the given playlist object.
Args:
target_file (File): The M3U playlist to write
audio_store_path (String): Folder containing the source files
entries (PlaylistEntry): Entries of the playlist
entry_extension (String): The file extension of the playlist entries
"""
file = open(target_file, "w")
fb = [ "#EXTM3U" ]
for entry in entries:
if ResourceUtil.get_content_type(entry.source) == ResourceType.FILE:
path = ResourceUtil.uri_to_filepath(audio_store_path, entry.source, entry_extension)
fb.append(f"#EXTINF:{entry.duration},{entry.meta_data.artist} - {entry.meta_data.title}")
fb.append(path)
file.writelines(fb)
file.close()
@staticmethod
def uri_to_filepath(base_dir, uri, source_extension):
"""
Converts a file-system URI to an actual, absolute path to the file.
Args:
basi_dir (String): The location of the audio store.
uri (String): The URI of the file
source_extension (String): The file extension of audio sources
Returns:
path (String): Absolute file path
"""
return base_dir + "/" + uri[7:] + source_extension
@staticmethod
def get_entries_string(entries):
"""
Returns a list of entries as String for logging purposes.
"""
s = ""
if isinstance(entries, list):
for entry in entries:
s += str(entry)
if entry != entries[-1]: s += ", "
else:
s = str(entries)
return s
@staticmethod
def lqs_annotate_cuein(uri, cue_in):
"""
Wraps the given URI with a Liquidsoap Cue In annotation.
Args:
uri (String): The path to the audio source
cue_in (Float): The value in seconds wher the cue in should start
Returns:
(String): The annotated URI
"""
if cue_in > 0.0:
uri = "annotate:liq_cue_in=\"%s\":%s" % (str(cue_in), uri)
return uri
\ No newline at end of file
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2020 - The Aura Engine Team.
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
......@@ -21,8 +21,8 @@ import logging
from collections import deque
from modules.base.exceptions import NoActiveEntryException
from modules.base.utils import SimpleUtil as SU, EngineUtil
from modules.core.channels import ChannelType
from modules.base.utils import SimpleUtil as SU
from modules.core.resources import ResourceClass, ResourceUtil
......@@ -31,8 +31,8 @@ class PlayerStateService:
PlayerStateService keeps a short history of currently playing entries. It stores the recent
active entries to a local cache `entry_history` being able to manage concurrently playing entries.
It also is in charge of storing relevant meta information of the currently playing entry to
the TrackService table.
It also is in charge of resolving relevant meta information of the currently playing entry for
the TrackService plugin.
"""
config = None
......@@ -90,11 +90,12 @@ class PlayerStateService:
for entry in entries:
entry_source = entry.source
if entry.channel in ChannelType.FILESYSTEM.channels:
base_dir = self.config.get("audiofolder")
entry_source = EngineUtil.uri_to_filepath(base_dir, entry.source)
if entry.get_content_type() in ResourceClass.FILE.types:
base_dir = self.config.get("audio_source_folder")
extension = self.config.get("audio_source_extension")
entry_source = ResourceUtil.uri_to_filepath(base_dir, entry.source, extension)
if entry_source == source:
self.logger.info("Resolved '%s' entry '%s' for URI '%s'" % (entry.get_type(), entry, source))
self.logger.info("Resolved '%s' entry '%s' for URI '%s'" % (entry.get_content_type(), entry, source))
result = entry
break
......
......@@ -22,7 +22,7 @@ import logging
import requests
from modules.base.utils import SimpleUtil as SU
from modules.core.resources import ResourceUtil
class TrackServiceHandler():
......@@ -65,7 +65,8 @@ class TrackServiceHandler():
data["track_title"] = entry.meta_data.title
data["track_duration"] = entry.duration
data["track_num"] = entry.entry_num
data["track_type"] = entry.get_type().numeric
content_class = ResourceUtil.get_content_class(entry.get_content_type())
data["track_type"] = content_class.numeric
data["playlist_id"] = entry.playlist.playlist_id
data["schedule_id"] = entry.playlist.schedule.schedule_id
data["show_id"] = entry.playlist.schedule.show_id
......@@ -106,7 +107,8 @@ class TrackServiceHandler():
entry["track_title"] = e.meta_data.title
entry["track_num"] = e.entry_num
entry["track_duration"] = e.duration
entry["track_type"] = e.get_type().numeric
content_class = ResourceUtil.get_content_class(e.get_content_type)
entry["track_type"] = content_class.numeric
entry = SU.clean_dictionary(entry)
data["current_playlist"]["entries"].append(entry)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment