From 9755c65bb1e4eaf7419ae1464f20fd41b1d77241 Mon Sep 17 00:00:00 2001 From: David Trattnig <david.trattnig@o94.at> Date: Tue, 8 Sep 2020 19:53:00 +0200 Subject: [PATCH] Cleanup API artifacts. #27 --- .gitignore | 3 - Dockerfile | 5 - README.md | 9 +- configuration/sample-docker.gunicorn.conf.py | 217 --------- .../sample-production.gunicorn.conf.py | 217 --------- configuration/scheduler.xml | 31 -- configuration/supervisor/aura-engine-api.conf | 13 - configuration/systemd/aura-engine-api.service | 13 - docs/configuration-guide.md | 203 -------- docs/developer-guide.md | 102 ++-- docs/engine-features.md | 133 +----- docs/frequently-asked-questions.md | 49 +- docs/installation-development.md | 44 +- docs/installation-production.md | 74 ++- docs/running-docker.md | 3 +- docs/setup-audio-store.md | 1 - engine-api.py | 446 ------------------ install.sh | 11 - requirements.txt | 8 - run.sh | 36 +- script/build-web.sh | 17 - script/install-web.sh | 7 - script/kill-web.sh | 3 - web/css/aura-clock-bundle.css | 2 - web/css/aura-player-bundle.css | 2 - web/css/aura.css | 66 --- web/favicon.png | Bin 19295 -> 0 bytes web/js/aura-clock-bundle.js | 2 - web/js/aura-player-bundle.js | 2 - web/templates/clock.html | 24 - web/templates/trackservice.html | 18 - 31 files changed, 152 insertions(+), 1609 deletions(-) delete mode 100644 configuration/sample-docker.gunicorn.conf.py delete mode 100644 configuration/sample-production.gunicorn.conf.py delete mode 100644 configuration/scheduler.xml delete mode 100644 configuration/supervisor/aura-engine-api.conf delete mode 100644 configuration/systemd/aura-engine-api.service delete mode 100644 docs/configuration-guide.md delete mode 100644 engine-api.py delete mode 100755 script/build-web.sh delete mode 100755 script/install-web.sh delete mode 100755 script/kill-web.sh delete mode 100644 web/css/aura-clock-bundle.css delete mode 100644 web/css/aura-player-bundle.css delete mode 100644 web/css/aura.css delete mode 100644 web/favicon.png delete mode 100644 web/js/aura-clock-bundle.js delete mode 100644 web/js/aura-player-bundle.js delete mode 100644 web/templates/clock.html delete mode 100644 web/templates/trackservice.html diff --git a/.gitignore b/.gitignore index b179bf03..37e29808 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,5 @@ logs tmp .vscode/tags configuration/engine.ini -configuration/gunicorn.conf.py -web/clock.html -web/trackservice.html script/.engine.install-db.lock .engine.install-db.lock \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d7df05ed..44f3267c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,7 +72,6 @@ RUN pip3 install -r /tmp/requirements.txt # Default configuration COPY configuration/sample-docker.engine.ini /srv/configuration/engine.ini -COPY configuration/sample-docker.gunicorn.conf.py /srv/configuration/gunicorn.conf.py # Update OPAM @@ -86,7 +85,3 @@ RUN opam install depext -y RUN opam depext taglib mad lame vorbis flac opus cry samplerate pulseaudio bjack alsa ssl liquidsoap -y RUN opam install taglib mad lame vorbis flac opus cry samplerate pulseaudio bjack alsa ssl liquidsoap -y RUN eval $(opam env) - -# Expose the API - -EXPOSE 3333 \ No newline at end of file diff --git a/README.md b/README.md index 5b7f0014..36afba3a 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,9 @@ the requirements of community radios. - Multichannel Line-out - Blank Detenction / Silence Detecter - Auto Pilot a.k.a. Fallback Handling -- API to query Track-Service -- API to query monthly reports -- API to query data for a studio clock -- Web Application for displaying the Track-Service -- Web Application for displaying the studio clock +- API to query Track-Service, monthly reports and information for displaying the Studio Clock (see [Engine API](https://gitlab.servus.at/aura/engine-api)) +- Web Application for displaying the Track-Service (see [AURA Player](https://gitlab.servus.at/aura/player)) +- Web Application for displaying the Studio Clock (see [Engine Clock](https://gitlab.servus.at/aura/engine-clock)) Read more on the [Engine Features](docs/engine-features.md) page. @@ -50,7 +48,6 @@ To learn more, checkout the [Engine Developer Guide](docs/developer-guide.md) or - [Installation for Production](docs/installation-production.md) - [Running with Docker](docs/running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](docs/configuration-guide.md) ## Read more diff --git a/configuration/sample-docker.gunicorn.conf.py b/configuration/sample-docker.gunicorn.conf.py deleted file mode 100644 index 7034f54c..00000000 --- a/configuration/sample-docker.gunicorn.conf.py +++ /dev/null @@ -1,217 +0,0 @@ -# Sample Gunicorn configuration file. - -# -# Server socket -# -# bind - The socket to bind. -# -# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'. -# An IP is a valid HOST. -# -# backlog - The number of pending connections. This refers -# to the number of clients that can be waiting to be -# served. Exceeding this number results in the client -# getting an error when attempting to connect. It should -# only affect servers under significant load. -# -# Must be a positive integer. Generally set in the 64-2048 -# range. -# - -pythonpath = "/opt/aura/engine" -bind = '172.17.0.1:3333' -backlog = 2048 - -# -# Worker processes -# -# workers - The number of worker processes that this server -# should keep alive for handling requests. -# -# A positive integer generally in the 2-4 x $(NUM_CORES) -# range. You'll want to vary this a bit to find the best -# for your particular application's work load. -# -# worker_class - The type of workers to use. The default -# sync class should handle most 'normal' types of work -# loads. You'll want to read -# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type -# for information on when you might want to choose one -# of the other worker classes. -# -# A string referring to a Python path to a subclass of -# gunicorn.workers.base.Worker. The default provided values -# can be seen at -# http://docs.gunicorn.org/en/latest/settings.html#worker-class -# -# worker_connections - For the eventlet and gevent worker classes -# this limits the maximum number of simultaneous clients that -# a single process can handle. -# -# A positive integer generally set to around 1000. -# -# timeout - If a worker does not notify the master process in this -# number of seconds it is killed and a new worker is spawned -# to replace it. -# -# Generally set to thirty seconds. Only set this noticeably -# higher if you're sure of the repercussions for sync workers. -# For the non sync workers it just means that the worker -# process is still communicating and is not tied to the length -# of time required to handle a single request. -# -# keepalive - The number of seconds to wait for the next request -# on a Keep-Alive HTTP connection. -# -# A positive integer. Generally set in the 1-5 seconds range. -# - -workers = 4 -worker_class = 'sync' -worker_connections = 1000 -timeout = 30 -keepalive = 2 - -# -# spew - Install a trace function that spews every line of Python -# that is executed when running the server. This is the -# nuclear option. -# -# True or False -# - -spew = False - -# -# Server mechanics -# -# daemon - Detach the main Gunicorn process from the controlling -# terminal with a standard fork/fork sequence. -# -# True or False -# -# raw_env - Pass environment variables to the execution environment. -# -# pidfile - The path to a pid file to write -# -# A path string or None to not write a pid file. -# -# user - Switch worker processes to run as this user. -# -# A valid user id (as an integer) or the name of a user that -# can be retrieved with a call to pwd.getpwnam(value) or None -# to not change the worker process user. -# -# group - Switch worker process to run as this group. -# -# A valid group id (as an integer) or the name of a user that -# can be retrieved with a call to pwd.getgrnam(value) or None -# to change the worker processes group. -# -# umask - A mask for file permissions written by Gunicorn. Note that -# this affects unix socket permissions. -# -# A valid value for the os.umask(mode) call or a string -# compatible with int(value, 0) (0 means Python guesses -# the base, so values like "0", "0xFF", "0022" are valid -# for decimal, hex, and octal representations) -# -# tmp_upload_dir - A directory to store temporary request data when -# requests are read. This will most likely be disappearing soon. -# -# A path to a directory where the process owner can write. Or -# None to signal that Python should choose one on its own. -# - -daemon = False -raw_env = [ - 'DJANGO_SECRET_KEY=something', - 'SPAM=eggs', -] -pidfile = None -umask = 0 -user = None -group = None -tmp_upload_dir = None - -# -# Logging -# -# logfile - The path to a log file to write to. -# -# A path string. "-" means log to stdout. -# -# loglevel - The granularity of log output -# -# A string of "debug", "info", "warning", "error", "critical" -# - -errorlog = '-' -loglevel = 'info' -accesslog = '-' -access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' - -# -# Process naming -# -# proc_name - A base to use with setproctitle to change the way -# that Gunicorn processes are reported in the system process -# table. This affects things like 'ps' and 'top'. If you're -# going to be running more than one instance of Gunicorn you'll -# probably want to set a name to tell them apart. This requires -# that you install the setproctitle module. -# -# A string or None to choose a default of something like 'gunicorn'. -# - -proc_name = None - -# -# Server hooks -# -# post_fork - Called just after a worker has been forked. -# -# A callable that takes a server and worker instance -# as arguments. -# -# pre_fork - Called just prior to forking the worker subprocess. -# -# A callable that accepts the same arguments as after_fork -# -# pre_exec - Called just prior to forking off a secondary -# master process during things like config reloading. -# -# A callable that takes a server instance as the sole argument. -# - -def post_fork(server, worker): - server.log.info("Worker spawned (pid: %s)", worker.pid) - -def pre_fork(server, worker): - pass - -def pre_exec(server): - server.log.info("Forked child, re-executing.") - -def when_ready(server): - server.log.info("Server is ready. Spawning workers") - -def worker_int(worker): - worker.log.info("worker received INT or QUIT signal") - - ## get traceback info - import threading, sys, traceback - id2name = {th.ident: th.name for th in threading.enumerate()} - code = [] - for threadId, stack in sys._current_frames().items(): - code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), - threadId)) - for filename, lineno, name, line in traceback.extract_stack(stack): - code.append('File: "%s", line %d, in %s' % (filename, - lineno, name)) - if line: - code.append(" %s" % (line.strip())) - worker.log.debug("\n".join(code)) - -def worker_abort(worker): - worker.log.info("worker received SIGABRT signal") \ No newline at end of file diff --git a/configuration/sample-production.gunicorn.conf.py b/configuration/sample-production.gunicorn.conf.py deleted file mode 100644 index ec5cc956..00000000 --- a/configuration/sample-production.gunicorn.conf.py +++ /dev/null @@ -1,217 +0,0 @@ -# Sample Gunicorn configuration file. - -# -# Server socket -# -# bind - The socket to bind. -# -# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'. -# An IP is a valid HOST. -# -# backlog - The number of pending connections. This refers -# to the number of clients that can be waiting to be -# served. Exceeding this number results in the client -# getting an error when attempting to connect. It should -# only affect servers under significant load. -# -# Must be a positive integer. Generally set in the 64-2048 -# range. -# - -pythonpath = "/opt/aura/engine" -bind = '127.0.0.1:3333' -backlog = 2048 - -# -# Worker processes -# -# workers - The number of worker processes that this server -# should keep alive for handling requests. -# -# A positive integer generally in the 2-4 x $(NUM_CORES) -# range. You'll want to vary this a bit to find the best -# for your particular application's work load. -# -# worker_class - The type of workers to use. The default -# sync class should handle most 'normal' types of work -# loads. You'll want to read -# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type -# for information on when you might want to choose one -# of the other worker classes. -# -# A string referring to a Python path to a subclass of -# gunicorn.workers.base.Worker. The default provided values -# can be seen at -# http://docs.gunicorn.org/en/latest/settings.html#worker-class -# -# worker_connections - For the eventlet and gevent worker classes -# this limits the maximum number of simultaneous clients that -# a single process can handle. -# -# A positive integer generally set to around 1000. -# -# timeout - If a worker does not notify the master process in this -# number of seconds it is killed and a new worker is spawned -# to replace it. -# -# Generally set to thirty seconds. Only set this noticeably -# higher if you're sure of the repercussions for sync workers. -# For the non sync workers it just means that the worker -# process is still communicating and is not tied to the length -# of time required to handle a single request. -# -# keepalive - The number of seconds to wait for the next request -# on a Keep-Alive HTTP connection. -# -# A positive integer. Generally set in the 1-5 seconds range. -# - -workers = 4 -worker_class = 'sync' -worker_connections = 1000 -timeout = 30 -keepalive = 2 - -# -# spew - Install a trace function that spews every line of Python -# that is executed when running the server. This is the -# nuclear option. -# -# True or False -# - -spew = False - -# -# Server mechanics -# -# daemon - Detach the main Gunicorn process from the controlling -# terminal with a standard fork/fork sequence. -# -# True or False -# -# raw_env - Pass environment variables to the execution environment. -# -# pidfile - The path to a pid file to write -# -# A path string or None to not write a pid file. -# -# user - Switch worker processes to run as this user. -# -# A valid user id (as an integer) or the name of a user that -# can be retrieved with a call to pwd.getpwnam(value) or None -# to not change the worker process user. -# -# group - Switch worker process to run as this group. -# -# A valid group id (as an integer) or the name of a user that -# can be retrieved with a call to pwd.getgrnam(value) or None -# to change the worker processes group. -# -# umask - A mask for file permissions written by Gunicorn. Note that -# this affects unix socket permissions. -# -# A valid value for the os.umask(mode) call or a string -# compatible with int(value, 0) (0 means Python guesses -# the base, so values like "0", "0xFF", "0022" are valid -# for decimal, hex, and octal representations) -# -# tmp_upload_dir - A directory to store temporary request data when -# requests are read. This will most likely be disappearing soon. -# -# A path to a directory where the process owner can write. Or -# None to signal that Python should choose one on its own. -# - -daemon = False -raw_env = [ - 'DJANGO_SECRET_KEY=something', - 'SPAM=eggs', -] -pidfile = None -umask = 0 -user = None -group = None -tmp_upload_dir = None - -# -# Logging -# -# logfile - The path to a log file to write to. -# -# A path string. "-" means log to stdout. -# -# loglevel - The granularity of log output -# -# A string of "debug", "info", "warning", "error", "critical" -# - -errorlog = '-' -loglevel = 'info' -accesslog = '-' -access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' - -# -# Process naming -# -# proc_name - A base to use with setproctitle to change the way -# that Gunicorn processes are reported in the system process -# table. This affects things like 'ps' and 'top'. If you're -# going to be running more than one instance of Gunicorn you'll -# probably want to set a name to tell them apart. This requires -# that you install the setproctitle module. -# -# A string or None to choose a default of something like 'gunicorn'. -# - -proc_name = None - -# -# Server hooks -# -# post_fork - Called just after a worker has been forked. -# -# A callable that takes a server and worker instance -# as arguments. -# -# pre_fork - Called just prior to forking the worker subprocess. -# -# A callable that accepts the same arguments as after_fork -# -# pre_exec - Called just prior to forking off a secondary -# master process during things like config reloading. -# -# A callable that takes a server instance as the sole argument. -# - -def post_fork(server, worker): - server.log.info("Worker spawned (pid: %s)", worker.pid) - -def pre_fork(server, worker): - pass - -def pre_exec(server): - server.log.info("Forked child, re-executing.") - -def when_ready(server): - server.log.info("Server is ready. Spawning workers") - -def worker_int(worker): - worker.log.info("worker received INT or QUIT signal") - - ## get traceback info - import threading, sys, traceback - id2name = {th.ident: th.name for th in threading.enumerate()} - code = [] - for threadId, stack in sys._current_frames().items(): - code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), - threadId)) - for filename, lineno, name, line in traceback.extract_stack(stack): - code.append('File: "%s", line %d, in %s' % (filename, - lineno, name)) - if line: - code.append(" %s" % (line.strip())) - worker.log.debug("\n".join(code)) - -def worker_abort(worker): - worker.log.info("worker received SIGABRT signal") \ No newline at end of file diff --git a/configuration/scheduler.xml b/configuration/scheduler.xml deleted file mode 100644 index f8b4890d..00000000 --- a/configuration/scheduler.xml +++ /dev/null @@ -1,31 +0,0 @@ -<Config> - <Jobs multiple="true"> - <job> - <time>00:00</time> - <until>23:00</until> - <job>play_playlist</job> - <params>no_stop</params> - </job> - <job> - <job>start_recording</job> - <until>00:00</until> - <day>all</day> - <time>00:00</time> - <params>no_stop</params> - </job> - <job> - <daysolder>4</daysolder> - <job>clean_cached</job> - <day>1</day> - <time>00:03</time> - <params></params> - </job> - <job> - <time>01:00</time> - <day>all</day> - <job>precache</job> - <params></params> - </job> - </Jobs> -</Config> - diff --git a/configuration/supervisor/aura-engine-api.conf b/configuration/supervisor/aura-engine-api.conf deleted file mode 100644 index d4e52071..00000000 --- a/configuration/supervisor/aura-engine-api.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:aura-engine-api] -user = engineuser -directory = /opt/aura/engine -command = /opt/aura/engine/run.sh api - -priority = 999 -autostart = true -autorestart = true -stopsignal = TERM - -redirect_stderr = true -stdout_logfile = /var/log/aura/engine-api-stdout.log -stderr_logfile = /var/log/aura/engine-api-error.log \ No newline at end of file diff --git a/configuration/systemd/aura-engine-api.service b/configuration/systemd/aura-engine-api.service deleted file mode 100644 index 6cbca82a..00000000 --- a/configuration/systemd/aura-engine-api.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Aura Engine - API -After=network.target - -[Service] -Type=simple -User=engineuser -WorkingDirectory=/opt/aura/engine -ExecStart=/opt/aura/engine/run.sh api -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md deleted file mode 100644 index 53c24003..00000000 --- a/docs/configuration-guide.md +++ /dev/null @@ -1,203 +0,0 @@ - -# Aura Engine Configuration Guide - -This page goes into detail on what is possible to configure within the engine. - -<!-- TOC --> - -- [Aura Engine Configuration Guide](#aura-engine-configuration-guide) - - [Station](#station) - - [Soundcard](#soundcard) - - [Recordings](#recordings) - - [Streams](#streams) - - [Fallbacks](#fallbacks) - - [Database](#database) - - [Monitoring](#monitoring) - - [API Endpoints](#api-endpoints) - - [Fading](#fading) - - [Logging](#logging) - - [User](#user) - - [Socket](#socket) - - [Redis](#redis) - - [Frequently Asked Questions](#frequently-asked-questions) - - [Which Audio Interface / Soundcard is compatible with Aura?](#which-audio-interface--soundcard-is-compatible-with-aura) - - [ALSA Settings](#alsa-settings) - - [In the Liquidsoap Logs I get 'Error when starting output output_lineout_0: Failure("Error while setting open_pcm: Device or resource busy")!'. What does it mean?](#in-the-liquidsoap-logs-i-get-error-when-starting-output-output_lineout_0-failureerror-while-setting-open_pcm-device-or-resource-busy-what-does-it-mean) - - [How can I find the audio device IDs, required for settings in engine.ini?](#how-can-i-find-the-audio-device-ids-required-for-settings-in-engineini) - - [Read more](#read-more) - -<!-- /TOC --> - -## Station - -These properties are used to style the included web applications such as *Track Service* -and *Studio Clock* . - -Set the radio station name - -```ini -station_name="Radio Orange" -``` - -Set the URL to the radio station logo - -```ini -station_logo_url="https://your-radio.station/logo.png" -``` - -Set the `width` of the radio station logo - -```ini -station_logo_size="120px" -``` - -## Soundcard - -Configure your audio device in the `[soundcard]` section of `engine.ini`. - -You can configure up to **five** line IN and OUT stereo channels. Your hardware should -support that. When you use JACK, you will see the additional elements popping up when -viewing your connections (with e.g. Patchage). - -**Pulse Audio:** When using Ubuntu, Pulse Audio is selected by default. This is convenient, -as you won't have the need to adapt any Engine setting to get audio playing initially. - -**ALSA:** When you use ALSA, you will have to play around with ALSA settings. In the folder -`./modules/liquidsoap` is a scipt called alsa_settings_tester.liq. You can start it -with 'liquidsoap -v --debug alsa_settings_tester.liq'. Changing and playing with -settings may help you to find correct ALSA settings. - -**Jack Audio**: Beside ALSA the sound servers such as -is supported. - -Install the JACK daemon and GUI: -```bash - sudo apt-get install jackd qjackctl -``` - -Please ensure to enable "*realtime process priority*" when installing JACK to keep latency low. -Now, you are able to configure your hardware settings using following command: - -```bash - qjackctl -``` - -Next you need to install the JACK plugin for Liquidsoap: - -```bash -sudo apt install \ - liquidsoap-plugin-jack -``` - - -## Recordings - -You can configure up to **five** recorders in the `[recording]`. - -## Streams - -You can configure up to **five** streams in the `[streams]`. - -## Fallbacks - -Configure fallback handling in the `[fallback]` section. - -## Database - -Configure your engine database in the `[database]` section. - -## Monitoring - -Configure monitoring parameters such as admin emails in the `[monitoring]` section. - -## API Endpoints - -Configure connections to the other Aura components in the `[api]` section. - -Sets the API URL exposed to external clients. This is required by the included -web applications which access the API. - -```ini -exposed_api_url="https://your-radio.station/api/v3" -``` - -## Fading - -Configure fading parameters in the `[fading]` section. - -## Logging - -Configure log handling in the `[logging]` section. - -## User - -Configure the executing system user in the `[user]` section. - -## Socket - -Configure socket connectivity in the `[socket]` section. - -## Redis - -Configure Redis connectivity in the `[redis]` section. - - -## Frequently Asked Questions - -### Which Audio Interface / Soundcard is compatible with Aura? - -Basically any audio device which is supported by Linux Debian/Ubuntu and has ALSA drivers. -Engine has been tested with following audio interfaces: - -- ASUS Xonar DGX, -- Roland Duo-Capture Ex -- Onboard Soundcard (HDA Intel ALC262) -- Native Instruments Komplete Audio 6 - - -### ALSA Settings - -#### In the Liquidsoap Logs I get 'Error when starting output output_lineout_0: Failure("Error while setting open_pcm: Device or resource busy")!'. What does it mean? - -You probably have set a wrong or occupied device ID. - - -#### How can I find the audio device IDs, required for settings in engine.ini? - -* **ALSA**: You can get the device numbers or IDs by executing: - - cat /proc/asound/cards - -* **Pulse Audio**: You might not need this for Pulse Audio, but still, to see all available devices use: - - pactl list - - - -**If you cannot find correct ALSA settings** -Well, this is - at least for me - a hard one. I could not manage to find correct ALSA settings for the above mentioned soundcards. The best experience i had with the ASUS Xonar DGX, but still very problematic (especially the first couple of minutes after starting liquidsoap). Since i enabled JACK support i only use that. It is also a bit of trial and error, but works pretty much out of the box. - -**If you experience 'hangs' or other artefacts on the output signal** - * reduce the quality (especially, when hangs are on the stream) or - * install the realtime kernel with - - ```bash - apt install linux-image-rt-amd64 - reboot - ``` - - or - * invest in better hardware - - -## Read more - -- [Overview](/README.md) -- [Installation for Development](installation-development.md) -- [Installation for Production](installation-production.md) -- [Running with Docker](running-docker.md) -- [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) -- [Developer Guide](developer-guide.md) -- [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 1f822f85..6dae0345 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -8,12 +8,10 @@ This page gives insights on extending Aura Engine internals or through the API. - [AURA Componentes](#aura-componentes) - [Engine Components](#engine-components) - [API](#api) - - [Required Data Sources](#required-data-sources) - - [Provided API Endpoints](#provided-api-endpoints) - - [Web Applications using the Engine API](#web-applications-using-the-engine-api) - [More infos for debugging](#more-infos-for-debugging) - [Default ports used by Engine](#default-ports-used-by-engine) - [Debugging Liquidsoap](#debugging-liquidsoap) + - [Tips on configuring the audo interface](#tips-on-configuring-the-audo-interface) - [Read more](#read-more) <!-- /TOC --> @@ -50,66 +48,14 @@ There's a convenience script to start all of the three main dependencies (Steeri **engine-core.py**: It is the server which is connected to the external programme source (e.g. aura steering and tank), to liquidsoap and is listening for redis pubsub messages. This precious little server is telling liquidsoap what to play and when. -**Liquidsoap**: The heart of AuRa Engine. It uses the built in mixer, to switch between different sources. It records everything and streams everything depending on your settings in aura.ini. - -**engine-api.py**: A Flask web server which provides the API endpoints. This component can be (re-) started independently from the core engine. +**Liquidsoap**: The heart of AURA Engine. It uses the built in mixer, to switch between different sources. ## API -### Required Data Sources - -The AURA Project "**Dashboard**" provides the GUI to organize shows, schedules/timelsots -and organize uploads in form of playlists. Those playlists can be organized in timeslots -using a fancy calendar interface. - -These data-sources need to be configurated in the "engine.ini" configuration file: - - # STEERING - api_steering_status = "http://localhost:8000/api/v1/" - # The URL to get the Calendar via Steering - api_steering_calendar="http://localhost:8000/api/v1/playout" - # The URL to get show details via Steering - api_steering_show="http://localhost:8000/api/v1/shows/${ID}/" - - -The AURA Project "**Tank**" on the other hand delivers information on the tracks, related playlists -to be played and its meta-data: - - # TANK - api_tank_status = "http://localhost:8040/ui/" - # The URL to get playlist details via Tank - api_tank_playlist="http://localhost:8040/api/v1/shows/${SLUG}/playlists" - - -You can find more information here: <https://gitlab.servus.at/autoradio/meta/blob/master/api-definition.md> - -### Provided API Endpoints - -**Soundserverstate:** Returns true and false values of the internal In- and Outputs - - /api/v1/soundserver_state - -**Trackservice:** - -/api/v1/trackservice/<selected_date> -/api/v1/trackservice/ - - -## Web Applications using the Engine API - -Under `./contrib` you'll find two Web Applications which utilize the Engine API: - -- [Track Service](contrib/aura-player/README.md) -- [Studio Clock](contrib/aura-clock/README.md) - -When you start the engine-api using +You can find the AURA API definition here: https://gitlab.servus.at/autoradio/meta/blob/master/api-definition.md -```shell - ./run.sh api-dev -``` +OpenAPI definition for Engine API: https://app.swaggerhub.com/apis/AURA-Engine/engine-api/ -this automatically builds these web applications and copies the resulting assets to the -relevant `./web/**` folders. ## More infos for debugging @@ -151,6 +97,43 @@ Push some audio file to the filesystem `queue 0` in_filesystem_0.push /path/to/your/file.mp3 +### Tips on configuring the audo interface + +Configure your audio device in the `[soundcard]` section of `engine.ini`. + +You can configure up to **five** line IN and OUT stereo channels. Your hardware should +support that. When you use JACK, you will see the additional elements popping up when +viewing your connections (with e.g. Patchage). + +**Pulse Audio:** When using Ubuntu, Pulse Audio is selected by default. This is convenient, +as you won't have the need to adapt any Engine setting to get audio playing initially. + +**ALSA:** When you use ALSA, you will have to play around with ALSA settings. In the folder +`./modules/liquidsoap` is a scipt called alsa_settings_tester.liq. You can start it +with 'liquidsoap -v --debug alsa_settings_tester.liq'. Changing and playing with +settings may help you to find correct ALSA settings. + +**Jack Audio**: Install the JACK daemon and GUI: + +```bash + sudo apt-get install jackd qjackctl +``` + +Please ensure to enable "*realtime process priority*" when installing JACK to keep latency low. +Now, you are able to configure your hardware settings using following command: + +```bash + qjackctl +``` + +Next you need to install the JACK plugin for Liquidsoap: + +```bash +sudo apt install \ + liquidsoap-plugin-jack +``` + + ## Read more - [Overview](/README.md) @@ -158,7 +141,6 @@ Push some audio file to the filesystem `queue 0` - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) +- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) \ No newline at end of file diff --git a/docs/engine-features.md b/docs/engine-features.md index 9ca724f6..9488fc30 100644 --- a/docs/engine-features.md +++ b/docs/engine-features.md @@ -14,11 +14,9 @@ This page gives a more detailed overview of the Aura Engine features and how to - [Fallback Handling](#fallback-handling) - [Pro-active Fallback Handling (1st Level Fallback)](#pro-active-fallback-handling-1st-level-fallback) - [Fallback Handling using the Silence Detector (2nd Level Fallback)](#fallback-handling-using-the-silence-detector-2nd-level-fallback) - - [API Endpoints](#api-endpoints) - - [Web Applications](#web-applications) - [Monitoring](#monitoring) - [Send mails on errors and warnings](#send-mails-on-errors-and-warnings) - - [Engine Status Information](#engine-status-information) + - [Engine Health Information via Engine API](#engine-health-information-via-engine-api) - [Engine Heartbeat](#engine-heartbeat) - [Logging](#logging) - [Read more](#read-more) @@ -198,124 +196,6 @@ fallback_min_noise="0." fallback_threshold="-50." ``` - -## API Endpoints - -**Track Service API**: These endpoints provide information on the played tracks, their schedules and shows. - -* `/api/v1/trackservice` .............. Returns all Track Service entries for the current day -* `/api/v1/trackservice/$ID` .......... Returns a Track Service entry by ID -* `/api/v1/trackservice/current` ...... Returns the track currently playing -* `/api/v1/trackservice/day/$DAY` ..... Returns all tracks for a given day formated as `YYYY-MM-DD` - -The Swagger Specification of a Track Service entry as YAML looks like this: - -```yaml - components: - schemas: - TrackService: - properties: - album: {} - artist: {} - duration: {} - fallback: {} - id: {} - schedule_start: {} - title: {} - track_start: {} - type: object - info: - title: Swagger API Specification for Aura Engine - version: 1.0.0 - openapi: 3.0.2 - paths: {} -``` - -**Reporting API**: Create monthly reports using this endpoint. - -* `/api/v1/report/$MONTH` ...... Returns all playout details for the given month in the format `YYYY-MM` - -```yaml -schemas: - Report: - properties: - fallback_type: {} - id: {} - playlist_id: {} - schedule.category: {} - schedule.is_repetition: {} - schedule.languages: {} - schedule.musicfocus: {} - schedule.schedule_end: {} - schedule.schedule_id: {} - schedule.schedule_start: {} - schedule.show_funding_category: {} - schedule.show_hosts: {} - schedule.show_id: {} - schedule.show_name: {} - schedule.show_type: {} - schedule.topic: {} - schedule.type: {} - schedule_fallback_id: {} - show_fallback_id: {} - station_fallback_id: {} - track: {} - track_start: {} - type: object - info: - title: Swagger API Specification for Aura Engine - version: 1.0.0 - openapi: 3.0.2 -``` - -**Schedule API**: Retrieves information on the programme. - -* `/api/v1/schedule/upcoming` .. Returns the next three schedules, after the one currently playing. - -```yaml -schemas: - Schedule: - properties: - id: {} - schedule: {} - schedule_id: {} - schedule_start: {} - show_hosts: {} - show_id: {} - show_name: {} - show_type: {} - type: object - info: - title: Swagger API Specification for Aura Engine - version: 1.0.0 - openapi: 3.0.2 -``` - -**Clock API**: Retrieve all data relevant for a studio clock. - -* `/api/v1/clock` .............. Returns the current show, next show, playlist and time left until the next show. - -```yaml -schemas: - Clock: - properties: - current: {} - next: {} - track: {} - track_id: {} - track_start: {} - type: object - info: - title: Swagger API Specification for Aura Engine - version: 1.0.0 - openapi: 3.0.2 -``` - -## Web Applications - -* `/app/trackservice` ................. Web Application for displaying the Track-Service -* `/app/clock` ........................ Web Application for displaying the studio clock - ## Monitoring You have following options to monitor the Engine: @@ -346,12 +226,10 @@ from_mail="monitoring@aura.engine" mailsubject_prefix="[Aura Engine]" ``` -### Engine Status Information - -You can get various status fields & flags using the `/status` endpoint. +### Engine Health Information via Engine API -> Please note this is a rather expensive call. If you need to call this on a regular basis -for continious monitoring, the *Heartbeat* option might be preferred. +Whenever the Engine's status turns into some unhealthy state this is logged to [Engine API](https://gitlab.servus.at/aura/engine-api). +Also, when it returns to some valid state this is logged to the Engine API. ### Engine Heartbeat @@ -402,7 +280,6 @@ Additionally you'll finde Supervisor specific logs under`/var/log/supervisor`. - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) +- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) \ No newline at end of file diff --git a/docs/frequently-asked-questions.md b/docs/frequently-asked-questions.md index 0e4c9e29..44447bef 100644 --- a/docs/frequently-asked-questions.md +++ b/docs/frequently-asked-questions.md @@ -4,12 +4,58 @@ <!-- TOC --> - [Frequently Asked Questions](#frequently-asked-questions) + - [Which Audio Interface / Soundcard is compatible with Aura?](#which-audio-interface--soundcard-is-compatible-with-aura) + - [ALSA Settings](#alsa-settings) + - [In the Liquidsoap Logs I get 'Error when starting output output_lineout_0: Failure("Error while setting open_pcm: Device or resource busy")!'. What does it mean?](#in-the-liquidsoap-logs-i-get-error-when-starting-output-output_lineout_0-failureerror-while-setting-open_pcm-device-or-resource-busy-what-does-it-mean) + - [How can I find the audio device IDs, required for settings in engine.ini?](#how-can-i-find-the-audio-device-ids-required-for-settings-in-engineini) - [I have issues with starting the Engine](#i-have-issues-with-starting-the-engine) - [I have issues during some Engine play-out](#i-have-issues-during-some-engine-play-out) - [Read More](#read-more) <!-- /TOC --> +## Which Audio Interface / Soundcard is compatible with Aura? + +Basically any audio device which is supported by Linux Debian/Ubuntu and has ALSA drivers. +Engine has been tested with following audio interfaces: + +- ASUS Xonar DGX, +- Roland Duo-Capture Ex +- Onboard Soundcard (HDA Intel ALC262) +- Native Instruments Komplete Audio 6 + +## ALSA Settings + +### In the Liquidsoap Logs I get 'Error when starting output output_lineout_0: Failure("Error while setting open_pcm: Device or resource busy")!'. What does it mean? + +You probably have set a wrong or occupied device ID. + + +### How can I find the audio device IDs, required for settings in engine.ini? + +* **ALSA**: You can get the device numbers or IDs by executing: + + cat /proc/asound/cards + +* **Pulse Audio**: You might not need this for Pulse Audio, but still, to see all available devices use: + + pactl list + +**If you cannot find correct ALSA settings** +Well, this is - at least for me - a hard one. I could not manage to find correct ALSA settings for the above mentioned soundcards. The best experience i had with the ASUS Xonar DGX, but still very problematic (especially the first couple of minutes after starting liquidsoap). Since i enabled JACK support i only use that. It is also a bit of trial and error, but works pretty much out of the box. + +**If you experience 'hangs' or other artefacts on the output signal** + + * Reduce the quality (especially, when hangs are on the stream) or + * Check the logs (especially the Liquidsoap logs) for any configuration issues + * Check ther performance of your computer or audio hardware + * Install the realtime kernel with + + ```bash + apt install linux-image-rt-amd64 + reboot + ``` + ## I have issues with starting the Engine **Cannot connect to socketpath /opt/aura/engine/modules/liquidsoap/engine.sock. Reason: [Errno 111] Connection refused** @@ -38,7 +84,6 @@ - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) +- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) \ No newline at end of file diff --git a/docs/installation-development.md b/docs/installation-development.md index b5cf3fe0..a628aa58 100644 --- a/docs/installation-development.md +++ b/docs/installation-development.md @@ -126,17 +126,30 @@ Set the URLs to the *Steering* and *Tank* API: ```ini [api] -# STEERING -api_steering_status = "http://localhost:8000/api/v1/" +# The URL to get the health status +api_steering_status = "http://aura.local:8000/api/v1/" # The URL to get the Calendar via Steering -api_steering_calendar="http://localhost:8000/api/v1/playout" +api_steering_calendar="http://aura.local:8000/api/v1/playout" # The URL to get show details via Steering -api_steering_show="http://localhost:8000/api/v1/shows/${ID}/" +api_steering_show="http://aura.local:8000/api/v1/shows/${ID}/" -# TANK -api_tank_status = "http://localhost:8040/healthz" +## TANK ## + +# The URL to get the health status +api_tank_status = "http://aura.local:8040/healthz/" # The URL to get playlist details via Tank -api_tank_playlist="http://localhost:8040/api/v1/shows/${SLUG}/playlists" +api_tank_playlist="http://aura.local:8040/api/v1/playlists/${ID}" + +## ENGINE-API ## + +# Engine ID (1 or 2) +api_engine_number = 1 +# Engine API endpoint to store playlogs +api_engine_store_playlog = "http://localhost:8008/api/v1/playlog/store" +# Engine API endpoint to store clock information +api_engine_store_clock = "http://localhost:8008/api/v1/clock" +# Engine API endpoint to store health information +api_engine_store_health = "http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}" ``` Ensure that the Liquidsoap installation path is valid: @@ -163,7 +176,6 @@ Read about all other available settings in the [Configuration Guide](docs/config ## Running Engine - Use the convencience script `run.sh` to get engine started in different ways: **Run the Engine** @@ -192,19 +204,6 @@ Liquidsoap, you can run following: ./run.sh lqs ``` -**Run the Engine API** - -This requires to start the core component in another terminal. - -```shell - ./run.sh api-dev -``` - -In development mode Engine uses the default [Flask](https://palletsprojects.com/p/flask/) web server. -Please be careful not to use this type of server in your production environment. - -Check out more ways of running the engine in the [Developer Guide](docs/developer-guide.md). - ## Logging All Engine logs for development can be found under `./logs`. @@ -216,7 +215,6 @@ All Engine logs for development can be found under `./logs`. - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) +- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) \ No newline at end of file diff --git a/docs/installation-production.md b/docs/installation-production.md index dcd0b3fc..4afb67fa 100644 --- a/docs/installation-production.md +++ b/docs/installation-production.md @@ -7,8 +7,8 @@ - [Installation](#installation) - [Configuration](#configuration) - [Running Engine](#running-engine) - - [The API Server](#the-api-server) - - [Maintanence using Supervisor](#maintanence-using-supervisor) + - [Running with Systemd](#running-with-systemd) + - [Running with Supervisor](#running-with-supervisor) - [Logging](#logging) - [Read more](#read-more) @@ -136,7 +136,6 @@ This script does the following: - Install Liquidsoap components using OPAM (`script/install-opam-packages`) - Python Packages (`requirements.txt`) - Creates a default Engine configuration file in `/etc/aura/engine.ini` -- Creates a default Gunicorn configuration file in `gunicorn.conf.py` When this is completed, carefully check if any error occured. In case your database has been setup automatically, note the relevant credentials for later use in your `engine.ini` configuration. @@ -158,22 +157,35 @@ Now, specify at least following settings to get started: db_pass="---SECRET--PASSWORD---" ``` -Set the URLs to the *Steering* and *Tank* API: +Set the URLs to the *Steering*, *Tank* and *Engine* API: ```ini [api] -# STEERING -api_steering_status = "http://localhost:8000/api/v1/" +# The URL to get the health status +api_steering_status = "http://aura.local:8000/api/v1/" # The URL to get the Calendar via Steering -api_steering_calendar="http://localhost:8000/api/v1/playout" +api_steering_calendar="http://aura.local:8000/api/v1/playout" # The URL to get show details via Steering -api_steering_show="http://localhost:8000/api/v1/shows/${ID}/" +api_steering_show="http://aura.local:8000/api/v1/shows/${ID}/" -# TANK -api_tank_status = "http://localhost:8040/healthz" +## TANK ## + +# The URL to get the health status +api_tank_status = "http://aura.local:8040/healthz/" # The URL to get playlist details via Tank -api_tank_playlist="http://localhost:8040/api/v1/shows/${SLUG}/playlists" +api_tank_playlist="http://aura.local:8040/api/v1/playlists/${ID}" + +## ENGINE-API ## + +# Engine ID (1 or 2) +api_engine_number = 1 +# Engine API endpoint to store playlogs +api_engine_store_playlog = "http://localhost:8008/api/v1/playlog/store" +# Engine API endpoint to store clock information +api_engine_store_clock = "http://localhost:8008/api/v1/clock" +# Engine API endpoint to store health information +api_engine_store_health = "http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}" ``` Ensure that the Liquidsoap installation path is valid: @@ -183,17 +195,6 @@ Ensure that the Liquidsoap installation path is valid: liquidsoap_path="/home/engineuser/.opam/4.08.0/bin/liquidsoap" ``` -**Configuring the API Server** - -Set the correct IP in `/opt/aura/engine/configuration# nano gunicorn.conf.py` and -the exposed `exposed_api_url` in `engine.ini`. - -Also open the `api_port` defined in `engine.ini` in your `iptables` (Default is 3333) - -```shell -iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 3333 -j ACCEPT -``` - **Configuring the Audio Store** Finally Engine needs to be able to access the audio folder, where all the tracks of the playlists @@ -219,12 +220,13 @@ In production the process of starting the engine is slightly different compared This is due to the need of ensuring the engine's components are always running i.e. letting them to restart automatically after some system restart or crash has happened. -For this we utilize [Supervisor](http://supervisord.org/). +For this you can utilize either [Systemd](https://systemd.io/) or [Supervisor](http://supervisord.org/). -Also note, while running the engine might also work using a `systemd` service, the -recommened option to use in combination with Gunicorn ([API server](Running the API Server), see below), -is Supervisor. Beside others pros, Supervisor has the advantage that you are able to run services without -having superuser rights. +### Running with Systemd + +-- TO BE ADDED -- + +### Running with Supervisor Now, given you are in the engine's home directory `/opt/aura/engine/`, simply type following to start the services: @@ -251,25 +253,14 @@ starting services individually, please check-out the next section. engineuser:/opt/aura/engine$ supervisorctl avail ``` -You should get these two services with their actual state listed: +You should get these all services with their actual state listed: ```c++ aura-engine in use auto 666:666 aura-engine-api in use auto 999:999 ``` -## The API Server - -For production Engine API uses the WSGI HTTP Server [`Gunicorn`](https://gunicorn.org/). - -In production Gunicorn is used in combination with some proxy server, such as Nginx. - -> Although there are many HTTP proxies available, we strongly advise that you use Nginx. If you choose another proxy -server you need to make sure that it buffers slow clients when you use default Gunicorn workers. Without this buffering -Gunicorn will be easily susceptible to denial-of-service attacks. You can use Hey to check if your proxy is behaving properly. -— [**Gunicorn Docs**](http://docs.gunicorn.org/en/latest/deploy.html). - -## Maintanence using Supervisor +**Maintanence using Supervisor** Please remember to call all `supervisorctl` commands from within your engine home directory (`/opt/aura/engine/`), to pickup the correct `supervisord.conf`. @@ -336,7 +327,6 @@ Additionally you'll finde Supervisor specific logs under: - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) +- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) \ No newline at end of file diff --git a/docs/running-docker.md b/docs/running-docker.md index 624ed453..5553bf0c 100644 --- a/docs/running-docker.md +++ b/docs/running-docker.md @@ -54,7 +54,6 @@ This section is only relevant if you are an Engine Developer. - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) -- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) +- [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) \ No newline at end of file diff --git a/docs/setup-audio-store.md b/docs/setup-audio-store.md index d427e916..dffbceac 100644 --- a/docs/setup-audio-store.md +++ b/docs/setup-audio-store.md @@ -157,7 +157,6 @@ this file. Then restart your Tank Docker container and you should be good to go. - [Installation for Production](installation-production.md) - [Running with Docker](running-docker.md) - [Setup the Audio Store](docs/setup-audio-store.md) -- [Configuration Guide](configuration-guide.md) - [Developer Guide](developer-guide.md) - [Engine Features](engine-features.md) - [Frequently Asked Questions (FAQ)](docs/frequently-asked-questions.md) diff --git a/engine-api.py b/engine-api.py deleted file mode 100644 index 67eaeda5..00000000 --- a/engine-api.py +++ /dev/null @@ -1,446 +0,0 @@ -#!/usr/bin/env python3.7 - -# -# 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/>. - - -import logging -import os, os.path -import subprocess -import json - -from datetime import datetime, date, timedelta - -from flask import Flask, Response -from flask_caching import Cache -from flask_cors import CORS -from flask_sqlalchemy import SQLAlchemy -from flask_marshmallow import Marshmallow -from marshmallow import Schema, fields, post_dump -from flask_restful import Api, Resource, abort -from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin -from apispec_webframeworks.flask import FlaskPlugin -from werkzeug.exceptions import HTTPException, default_exceptions, Aborter - -from modules.base.logger import AuraLogger -from modules.base.config import AuraConfig -from modules.base.models import AuraDatabaseModel, Schedule, Playlist, PlaylistEntry, PlaylistEntryMetaData - - - - -# -# Initialize the Aura Web App and API. -# - -config = AuraConfig() -app = Flask(__name__, - static_url_path='', - static_folder='web/') - # static_folder='contrib/aura-player/public/') - -app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -app.config["SQLALCHEMY_DATABASE_URI"] = config.get_database_uri() -app.config["CACHE_TYPE"] = "simple" -app.config["CACHE_DEFAULT_TIMEOUT"] = 0 -app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 -cache = Cache(app) -cors = CORS(app, resources={r"/*": {"origins": "*"}}) # FIXME Update CORS for production use -db = SQLAlchemy(app) -ma = Marshmallow(app) -api = Api(app) - - -# -# Werkzeug HTTP code mappings -# -class NoDataAvailable(HTTPException): - code = 204 - description = "There is currently no content available." -default_exceptions[204] = NoDataAvailable -abort = Aborter() - - -class EngineApi: - """ - Provides the Aura Engine API services. - """ - config = None - api = None - logger = None - - # trackservice_schema = None - - - def __init__(self, config, api): - """ - Initializes the API. - - Args: - config (AuraConfig): The Engine configuration. - api (Api): The Flask restful API object. - """ - self.config = config - self.logger = AuraLogger(self.config, "engine-api") - self.logger = logging.getLogger("engine-api") - self.api = api - - # Generate HTML files - self.generate_html("web/templates/clock.html", "web/clock.html") - self.generate_html("web/templates/trackservice.html", "web/trackservice.html") - - # API Spec - spec.components.schema("TrackService", schema=TrackServiceSchema) - spec.components.schema("Report", schema=ReportSchema) - spec.components.schema("Schedule", schema=ScheduleSchema) - spec.components.schema("Clock", schema=ClockDataSchema) - spec.components.schema("Status", schema=StatusSchema) - - # TODO Generates HTML for specification - #self.logger.info(spec.to_yaml()) - - # Schema instances - EngineApi.trackservice_schema = TrackServiceSchema(many=True) - EngineApi.track_schema = TrackServiceSchema() - EngineApi.report_schema = ReportSchema(many=True) - EngineApi.schedule_schema = ScheduleSchema(many=True) - EngineApi.clockdata_schema = ClockDataSchema() - EngineApi.status_schema = StatusSchema() - - # Define API routes - self.api.add_resource(TrackServiceResource, config.api_prefix + "/trackservice/") - self.api.add_resource(TrackResource, config.api_prefix + "/trackservice/<int:track_id>") - self.api.add_resource(CurrentTrackResource, config.api_prefix + "/trackservice/current") - self.api.add_resource(TracksByDayResource, config.api_prefix + "/trackservice/date/<string:date_string>") - self.api.add_resource(ReportResource, config.api_prefix + "/report/<string:year_month>") - self.api.add_resource(UpcomingSchedulesResource, config.api_prefix + "/schedule/upcoming") - self.api.add_resource(ClockDataResource, config.api_prefix + "/clock") - self.api.add_resource(StatusResource, "/status") - - self.logger.info("Engine API routes successfully set!") - - # Static resources - @app.route('/app/trackservice', methods=['GET']) - def trackservice(): - content = open(os.path.join("web/", "trackservice.html")) - return Response(content, mimetype="text/html") - - # Static resources - @app.route('/app/clock', methods=['GET']) - def clock(): - content = open(os.path.join("web/", "clock.html")) - return Response(content, mimetype="text/html") - - - def generate_html(self, src_file, target_file): - """ - Generates HTML based on the configuration options and templates. - - Args: - src_file (String): The template file - target_file (String): The HTML file to be generated - """ - src_file = open(src_file, "r") - target_file = open(target_file, "w") - content = src_file.read() - - config_options = { - "CONFIG-STATION-NAME": config.get("station_name"), - "CONFIG-STATION-LOGO-URL": config.get("station_logo_url"), - "CONFIG-STATION-LOGO-SIZE": config.get("station_logo_size"), - "CONFIG-API-URL": config.get("exposed_api_url") - } - - for key, value in config_options.items(): - content = content.replace(":::"+key+":::", value) - - target_file.write(content) - src_file.close() - target_file.close() - - - def run(self): - """ - Starts the API server. - """ - # Set debug=False if you want to use your native IDE debugger - self.api.app.run(port=self.config.api_port, debug=False) - - - - -# -# API SPEC -# - - -spec = APISpec( - title="Swagger API Specification for Aura Engine", - version="1.0.0", - openapi_version="3.0.2", - plugins=[FlaskPlugin(), MarshmallowPlugin()], -) - - -# -# API SCHEMA -# - - -class TrackServiceSchema(ma.Schema): - class Meta: - fields = ( - "id", - "schedule.schedule_id", - "schedule.schedule_start", - "schedule.schedule_end", - "schedule.languages", - "schedule.type", - "schedule.category", - "schedule.topic", - "schedule.musicfocus", - "schedule.is_repetition", - - "track", - "track_start", - - "show" - ) - - -class ClockDataSchema(ma.Schema): - class Meta: - fields = ( - "current", - "next", - "track_id", - "track_start", - "track" - ) - - -class ScheduleSchema(ma.Schema): - class Meta: - fields = ( - "id", - "schedule_id", - "schedule_start", - "schedule", - - "show_id", - "show_name", - "show_hosts", - "show_type" - ) - - - -class ReportSchema(ma.Schema): - class Meta: - fields = ( - "id", - "schedule.schedule_id", - "schedule.schedule_start", - "schedule.schedule_end", - "schedule.languages", - "schedule.type", - "schedule.category", - "schedule.topic", - "schedule.musicfocus", - "schedule.is_repetition", - - "schedule.show_id", - "schedule.show_name", - "schedule.show_hosts", - "schedule.show_type", - "schedule.show_funding_category", - - "track", - "track_start", - - "playlist_id", - "fallback_type", - "schedule_fallback_id", - "show_fallback_id", - "station_fallback_id" - ) - - -class StatusSchema(ma.Schema): - class Meta: - fields = ( - "engine", - "soundsystem", - "api", - "redis_ready", - "audio_store" - ) - - -# -# API RESOURCES -# - - -class TrackServiceResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self): - today = date.today() - today = datetime(today.year, today.month, today.day) - tracks = TrackService.select_by_day(today) - return EngineApi.trackservice_schema.dump(tracks) - - -class TrackResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self, track_id): - track = TrackService.select_one(track_id) - return EngineApi.track_schema.dump(track) - - -class ClockDataResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self): - item = TrackService.select_current() - next_schedule = Schedule.select_upcoming(1) - if next_schedule: - next_schedule = next_schedule[0].as_dict() - next_schedule["playlist"] = None - else: - next_schedule = {} - - clockdata = { - "track_id": item.id, - "track_start": item.track_start, - "track": item.track, - "current": {}, - "next": next_schedule - } - - if item.schedule: - clockdata["current"] = item.schedule.as_dict() - if item.schedule.playlist: - clockdata["current"]["playlist"] = item.schedule.playlist[0].as_dict() - - clockdata["current"]["show"] = item.show - return EngineApi.clockdata_schema.dump(clockdata) - - -class CurrentTrackResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self): - track = TrackService.select_current() - if not track: - return abort(204) # No content available - return EngineApi.track_schema.dump(track) - - -class TracksByDayResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self, date_string): - date = datetime.strptime(date_string, "%Y-%m-%d") - self.logger.debug("Query track-service by day: %s" % str(date)) - tracks = TrackService.select_by_day(date) - if not tracks: - return abort(204) # No content available - return EngineApi.trackservice_schema.dump(tracks) - - -class UpcomingSchedulesResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self): - now = datetime.now() - self.logger.debug("Query upcoming schedules after %s" % str(now)) - schedules = Schedule.select_upcoming(3) - if not schedules: - return abort(204) # No content available - return EngineApi.schedule_schema.dump(schedules) - - -class ReportResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self, year_month): - year = int(year_month.split("-")[0]) - month = int(year_month.split("-")[1]) - - first_day = datetime(year, month, 1) - next_month = first_day.replace(day=28) + timedelta(days=4) - next_month - timedelta(days=next_month.day) - - self.logger.debug("Query report for month: %s - %s" % (str(first_day), str(next_month))) - report = TrackService.select_by_range(first_day, next_month) - if not report: - return abort(204) # No content available - return EngineApi.report_schema.dump(report) - - -class StatusResource(Resource): - logger = None - - def __init__(self): - self.logger = logging.getLogger("engine-api") - - def get(self): - status = subprocess.check_output(["python3", "guru.py", "-s", "-q"]) - status = status.decode("utf-8").replace("'", '"') - status = json.loads(status, strict=False) - - if not status: - return abort(204) # No content available - return EngineApi.status_schema.dump(status) - - - -# -# Initialization calls -# - - -engine_api = EngineApi(config, api) - -if __name__ == "__main__": - engine_api.run() diff --git a/install.sh b/install.sh index 10027f6a..a7718e29 100755 --- a/install.sh +++ b/install.sh @@ -33,8 +33,6 @@ if [ $mode == "dev" ]; then echo "Copy configuration to './configuration/engine.ini'" cp -n configuration/sample-development.engine.ini configuration/engine.ini - echo "Installing Web Application Packages ..." - bash script/install-web.sh fi @@ -48,15 +46,6 @@ if [ $mode == "prod" ]; then echo "Copy default Engine configuration to '/etc/aura/engine.ini'" cp -n configuration/sample-production.engine.ini /etc/aura/engine.ini - echo "Copy default Gunicorn configuration to '/etc/aura/engine.ini'" - cp -n configuration/sample-production.gunicorn.conf.py configuration/gunicorn.conf.py - - echo "Create Virtual Env for Gunicorn" - virtualenv -p /usr/bin/python3.7 ../python-env - source ../python-env/bin/activate - - echo "Install Requirements to Virtual Env" - pip3 install -r requirements.txt fi diff --git a/requirements.txt b/requirements.txt index 7db9e599..1c13beaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,6 @@ sqlalchemy==1.3.13 Flask==1.1.1 -Flask-Caching==1.8.0 Flask-SQLAlchemy==2.4.1 -Flask-RESTful==0.3.8 -flask-marshmallow==0.11.0 -flask-cors==3.0.8 -marshmallow-sqlalchemy==0.22.2 -apispec==3.3.0 -apispec-webframeworks==0.5.2 mysqlclient==1.3.12 redis==3.5.3 mutagen==1.44.0 @@ -15,5 +8,4 @@ validators==0.12.1 simplejson==3.17.0 accessify==0.3.1 librosa==0.7.2 -gunicorn==20.0.4 pyyaml==5.3.1 \ No newline at end of file diff --git a/run.sh b/run.sh index e7cf5ab1..1a769812 100755 --- a/run.sh +++ b/run.sh @@ -10,8 +10,6 @@ docker="false" # - engine # - core # - lqs -# - api-dev -# - api # - recreate-database # - docker:engine @@ -19,10 +17,9 @@ docker="false" # - docker:lqs # - docker:recreate-database # - docker:build -# - docker:api # -if [[ $* =~ ^(engine|core|lqs|api-dev|api)$ ]]; then +if [[ $* =~ ^(engine|core|lqs)$ ]]; then mode=$1 fi @@ -60,24 +57,6 @@ if [[ $docker == "false" ]]; then eval "$lqs" fi - ### Runs the API Server (Development) ### - - if [[ $mode == "api-dev" ]]; then - echo "Building Web Applications" - sh ./script/build-web.sh - echo "Starting API Server" - /usr/bin/env python3.7 engine-api.py - fi - - ### Runs the API Server (Production) ### - - if [[ $mode == "api" ]]; then - echo "Activating Python Environment" - source ../python-env/bin/activate - echo "Starting API Server" - gunicorn -c configuration/gunicorn.conf.py engine-api:app - fi - ### CAUTION: This deletes everything in your database ### if [[ $mode == "recreate-database" ]]; then @@ -130,19 +109,6 @@ if [[ $docker == "true" ]]; then fi - ### Runs Engine API using Gunicorn ### - - if [[ $mode == "api" ]]; then - exec sudo docker run --name aura-engine-api --rm -it \ - -u $UID:$GID \ - -p 127.0.0.1:8050:5000 \ - -v "$BASE_D":/srv \ - -v "$BASE_D/configuration/":/etc/aura \ - --tmpfs /var/log/aura/ autoradio/engine /srv/engine-api.py \ - --device autoradio/engine /bin/bash \ - -c "gunicorn -c configuration/gunicorn.conf.py engine-api:app" - fi - ### CAUTION: This deletes everything in your database ### if [[ $mode == "recreate-database" ]]; then diff --git a/script/build-web.sh b/script/build-web.sh deleted file mode 100755 index d42ecdbc..00000000 --- a/script/build-web.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -echo "Building AURA Clock ..." -( - cd contrib/aura-clock - npm run build -) -cp contrib/aura-clock/public/build/aura-clock-bundle.css web/css/aura-clock-bundle.css -cp contrib/aura-clock/public/build/aura-clock-bundle.js web/js/aura-clock-bundle.js - -echo "Building AURA Player ..." -( - cd contrib/aura-player - npm run build -) -cp contrib/aura-player/public/build/aura-player-bundle.css web/css/aura-player-bundle.css -cp contrib/aura-player/public/build/aura-player-bundle.js web/js/aura-player-bundle.js diff --git a/script/install-web.sh b/script/install-web.sh deleted file mode 100755 index 45f3818b..00000000 --- a/script/install-web.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -echo "Installing AURA Clock Packages ..." -(cd contrib/aura-clock && npm install) - -echo "Installing AURA Player Packages ..." -(cd contrib/aura-player && npm install) diff --git a/script/kill-web.sh b/script/kill-web.sh deleted file mode 100755 index f12fc1da..00000000 --- a/script/kill-web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -kill -9 `ps -eo pid,command | grep 'gunicorn.*engine-api:run_app()' | grep -v grep | sort | head -1 | awk '{print $1}'` \ No newline at end of file diff --git a/web/css/aura-clock-bundle.css b/web/css/aura-clock-bundle.css deleted file mode 100644 index bb60feb8..00000000 --- a/web/css/aura-clock-bundle.css +++ /dev/null @@ -1,2 +0,0 @@ - -/*# sourceMappingURL=aura-clock-bundle.css.map */ \ No newline at end of file diff --git a/web/css/aura-player-bundle.css b/web/css/aura-player-bundle.css deleted file mode 100644 index cfba9bb9..00000000 --- a/web/css/aura-player-bundle.css +++ /dev/null @@ -1,2 +0,0 @@ - -/*# sourceMappingURL=aura-player-bundle.css.map */ \ No newline at end of file diff --git a/web/css/aura.css b/web/css/aura.css deleted file mode 100644 index ec905f5e..00000000 --- a/web/css/aura.css +++ /dev/null @@ -1,66 +0,0 @@ -html, body { - position: relative; - width: 100%; - height: 100%; -} - -body { - color: #333; - margin: 0; - padding: 8px; - box-sizing: border-box; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; -} - -a { - color: rgb(0,100,200); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -a:visited { - color: rgb(0,80,160); -} - -label { - display: block; -} - -input, button, select, textarea { - font-family: inherit; - font-size: inherit; - padding: 0.4em; - margin: 0 0 0.5em 0; - box-sizing: border-box; - border: 1px solid #ccc; - border-radius: 2px; -} - -input:disabled { - color: #ccc; -} - -input[type="range"] { - height: 0; -} - -button { - color: #333; - background-color: #f4f4f4; - outline: none; -} - -button:disabled { - color: #999; -} - -button:not(:disabled):active { - background-color: #ddd; -} - -button:focus { - border-color: #666; -} diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index b210b336cd8f214dd83f2ea3dc6ce6d9cc413937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19295 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 diff --git a/web/js/aura-clock-bundle.js b/web/js/aura-clock-bundle.js deleted file mode 100644 index 75ce17c8..00000000 --- a/web/js/aura-clock-bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function r(t){t.forEach(e)}function o(t){return"function"==typeof t}function l(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function i(t,e){t.appendChild(e)}function s(t,e,n){t.insertBefore(e,n||null)}function c(t){t.parentNode.removeChild(t)}function a(t,e){for(let n=0;n<t.length;n+=1)t[n]&&t[n].d(e)}function u(t){return document.createElement(t)}function d(t){return document.createElementNS("http://www.w3.org/2000/svg",t)}function h(t){return document.createTextNode(t)}function f(){return h(" ")}function g(){return h("")}function p(t,e,n){null==n?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function m(t,e){e=""+e,t.data!==e&&(t.data=e)}function b(t,e,n,r){t.style.setProperty(e,n,r?"important":"")}class x{constructor(t,e=null){this.e=u("div"),this.a=e,this.u(t)}m(t,e=null){for(let n=0;n<this.n.length;n+=1)s(t,this.n[n],e);this.t=t}u(t){this.e.innerHTML=t,this.n=Array.from(this.e.childNodes)}p(t){this.d(),this.u(t),this.m(this.t,this.a)}d(){this.n.forEach(c)}}let y;function k(t){y=t}function w(){if(!y)throw new Error("Function called outside component initialization");return y}const $=[],v=[],S=[],_=[],E=Promise.resolve();let z=!1;function M(t){S.push(t)}let A=!1;const C=new Set;function N(){if(!A){A=!0;do{for(let t=0;t<$.length;t+=1){const e=$[t];k(e),L(e.$$)}for($.length=0;v.length;)v.pop()();for(let t=0;t<S.length;t+=1){const e=S[t];C.has(e)||(C.add(e),e())}S.length=0}while($.length);for(;_.length;)_.pop()();z=!1,A=!1,C.clear()}}function L(t){if(null!==t.fragment){t.update(),r(t.before_update);const e=t.dirty;t.dirty=[-1],t.fragment&&t.fragment.p(t.ctx,e),t.after_update.forEach(M)}}const T=new Set;let D,j;function H(t,e){t&&t.i&&(T.delete(t),t.i(e))}function I(t,e){const n=e.token={};function o(t,o,l,i){if(e.token!==n)return;e.resolved=i;let s=e.ctx;void 0!==l&&(s=s.slice(),s[l]=i);const c=t&&(e.current=t)(s);let a=!1;e.block&&(e.blocks?e.blocks.forEach((t,n)=>{n!==o&&t&&(D={r:0,c:[],p:D},function(t,e,n,r){if(t&&t.o){if(T.has(t))return;T.add(t),D.c.push(()=>{T.delete(t),r&&(n&&t.d(1),r())}),t.o(e)}}(t,1,1,()=>{e.blocks[n]=null}),D.r||r(D.c),D=D.p)}):e.block.d(1),c.c(),H(c,1),c.m(e.mount(),e.anchor),a=!0),e.block=c,e.blocks&&(e.blocks[o]=c),a&&N()}if((l=t)&&"object"==typeof l&&"function"==typeof l.then){const n=w();if(t.then(t=>{k(n),o(e.then,1,e.value,t),k(null)},t=>{k(n),o(e.catch,2,e.error,t),k(null)}),e.current!==e.pending)return o(e.pending,0),!0}else{if(e.current!==e.then)return o(e.then,1,e.value,t),!0;e.resolved=t}var l}function O(t,e){-1===t.$$.dirty[0]&&($.push(t),z||(z=!0,E.then(N)),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<<e%31}function R(l,i,s,c,a,u,d=[-1]){const h=y;k(l);const f=i.props||{},g=l.$$={fragment:null,ctx:null,props:u,update:t,not_equal:a,bound:n(),on_mount:[],on_destroy:[],before_update:[],after_update:[],context:new Map(h?h.$$.context:[]),callbacks:n(),dirty:d};let p=!1;g.ctx=s?s(l,f,(t,e,...n)=>{const r=n.length?n[0]:e;return g.ctx&&a(g.ctx[t],g.ctx[t]=r)&&(g.bound[t]&&g.bound[t](r),p&&O(l,t)),e}):[],g.update(),p=!0,r(g.before_update),g.fragment=!!c&&c(g.ctx),i.target&&(i.hydrate?g.fragment&&g.fragment.l(function(t){return Array.from(t.childNodes)}(i.target)):g.fragment&&g.fragment.c(),i.intro&&H(l.$$.fragment),function(t,n,l){const{fragment:i,on_mount:s,on_destroy:c,after_update:a}=t.$$;i&&i.m(n,l),M(()=>{const n=s.map(e).filter(o);c?c.push(...n):r(n),t.$$.on_mount=[]}),a.forEach(M)}(l,i.target,i.anchor),N()),k(h)}function B(t,e,n){const r=t.slice();return r[21]=e[n],r[23]=n,r}function P(t,e,n){const r=t.slice();return r[27]=e[n],r}function q(t,e,n){const r=t.slice();return r[24]=e[n],r}function F(t){let e,n;return{c(){e=d("line"),p(e,"class","minor"),p(e,"y1","42"),p(e,"y2","45"),p(e,"transform",n="rotate("+6*(t[24]+t[27])+")")},m(t,n){s(t,e,n)},d(t){t&&c(e)}}}function J(e){let n,r,o,l=[1,2,3,4],i=[];for(let t=0;t<4;t+=1)i[t]=F(P(e,l,t));return{c(){n=d("line");for(let t=0;t<4;t+=1)i[t].c();o=g(),p(n,"class","major"),p(n,"y1","35"),p(n,"y2","45"),p(n,"transform",r="rotate("+30*e[24]+")")},m(t,e){s(t,n,e);for(let n=0;n<4;n+=1)i[n].m(t,e);s(t,o,e)},p:t,d(t){t&&c(n),a(i,t),t&&c(o)}}}function G(t){let e,n,r,o=t[20]+"";return{c(){e=u("div"),n=u("p"),r=h(o),p(e,"class","error")},m(t,o){s(t,e,o),i(e,n),i(n,r)},p(t,e){16&e&&o!==(o=t[20]+"")&&m(r,o)},d(t){t&&c(e)}}}function K(t){let e,n,r,o=t[9](t[19])+"",l=t[19].current.show&&Q(t);return{c(){e=h(o),n=f(),l&&l.c(),r=g()},m(t,o){s(t,e,o),s(t,n,o),l&&l.m(t,o),s(t,r,o)},p(t,n){16&n&&o!==(o=t[9](t[19])+"")&&m(e,o),t[19].current.show?l?l.p(t,n):(l=Q(t),l.c(),l.m(r.parentNode,r)):l&&(l.d(1),l=null)},d(t){t&&c(e),t&&c(n),l&&l.d(t),t&&c(r)}}}function Q(t){let e,n,r,o,l,a,d,g,b,y,k,w,$,v,S=t[11](t[19].current.show)+"",_=nt(t[19].current)+"",E=t[11](t[19].next.show)+"",z=nt(t[19])+"";function M(t,e){return t[19].current.playlist?V:U}let A=M(t),C=A(t);return{c(){e=u("div"),n=u("h1"),o=f(),l=h(_),a=f(),d=u("div"),C.c(),g=f(),b=u("div"),y=u("h3"),k=h("Next: "),$=f(),v=h(z),r=new x(S,o),p(n,"class","schedule-title"),p(e,"id","current-schedule"),p(d,"id","playlist"),w=new x(E,$),p(y,"class","schedule-title"),p(b,"id","next-schedule")},m(t,c){s(t,e,c),i(e,n),r.m(n),i(n,o),i(n,l),s(t,a,c),s(t,d,c),C.m(d,null),s(t,g,c),s(t,b,c),i(b,y),i(y,k),w.m(y),i(y,$),i(y,v)},p(t,e){16&e&&S!==(S=t[11](t[19].current.show)+"")&&r.p(S),16&e&&_!==(_=nt(t[19].current)+"")&&m(l,_),A===(A=M(t))&&C?C.p(t,e):(C.d(1),C=A(t),C&&(C.c(),C.m(d,null))),16&e&&E!==(E=t[11](t[19].next.show)+"")&&w.p(E),16&e&&z!==(z=nt(t[19])+"")&&m(v,z)},d(t){t&&c(e),t&&c(a),t&&c(d),C.d(),t&&c(g),t&&c(b)}}}function U(t){let e,n,r,o,l,a,d,g=et(t[19].track)+"",b=t[10](t[5])+"";return{c(){e=u("div"),n=u("h2"),r=u("span"),o=h(g),l=f(),a=u("span"),d=h(b),p(r,"class","track-title"),p(a,"class","track-time-left"),p(e,"id","current-track"),p(e,"class","is-active")},m(t,c){s(t,e,c),i(e,n),i(n,r),i(r,o),i(n,l),i(n,a),i(a,d)},p(t,e){16&e&&g!==(g=et(t[19].track)+"")&&m(o,g),32&e&&b!==(b=t[10](t[5])+"")&&m(d,b)},d(t){t&&c(e)}}}function V(t){let e,n=t[19].current.playlist.entries,r=[];for(let e=0;e<n.length;e+=1)r[e]=Y(B(t,n,e));return{c(){e=u("ol");for(let t=0;t<r.length;t+=1)r[t].c()},m(t,n){s(t,e,n);for(let t=0;t<r.length;t+=1)r[t].m(e,null)},p(t,o){if(1072&o){let l;for(n=t[19].current.playlist.entries,l=0;l<n.length;l+=1){const i=B(t,n,l);r[l]?r[l].p(i,o):(r[l]=Y(i),r[l].c(),r[l].m(e,null))}for(;l<r.length;l+=1)r[l].d(1);r.length=n.length}},d(t){t&&c(e),a(r,t)}}}function W(t){let e,n,r,o,l,a,d,g,b,x=et(t[21])+"",y=t[10](t[21].duration)+"";return{c(){e=u("li"),n=u("span"),r=h(x),o=f(),l=u("span"),a=h("("),d=h(y),g=h(")"),b=f(),p(n,"class","track-title"),p(l,"class","track-duration"),p(e,"class","playlist-entry")},m(t,c){s(t,e,c),i(e,n),i(n,r),i(e,o),i(e,l),i(l,a),i(l,d),i(l,g),i(e,b)},p(t,e){16&e&&x!==(x=et(t[21])+"")&&m(r,x),16&e&&y!==(y=t[10](t[21].duration)+"")&&m(d,y)},d(t){t&&c(e)}}}function X(t){let e,n,r,o,l,a,d,g,b,x=et(t[21])+"",y=t[10](t[5])+"";return{c(){e=u("li"),n=u("span"),r=h(x),o=f(),l=u("span"),a=h("("),d=h(y),g=h(")"),b=f(),p(n,"class","track-title"),p(l,"class","track-time-left"),p(e,"id","current-playlist-entry"),p(e,"class","playlist-entry is-active")},m(t,c){s(t,e,c),i(e,n),i(n,r),i(e,o),i(e,l),i(l,a),i(l,d),i(l,g),i(e,b)},p(t,e){16&e&&x!==(x=et(t[21])+"")&&m(r,x),32&e&&y!==(y=t[10](t[5])+"")&&m(d,y)},d(t){t&&c(e)}}}function Y(t){let e,n;function r(t,n){return(null==e||16&n)&&(e=!!function(t,e){if(null!=e&&t.id==e.id)return location.hash="#current-playlist-entry",!0;return!1}(t[21],t[19].track)),e?X:W}let o=r(t,-1),l=o(t);return{c(){l.c(),n=g()},m(t,e){l.m(t,e),s(t,n,e)},p(t,e){o===(o=r(t,e))&&l?l.p(t,e):(l.d(1),l=o(t),l&&(l.c(),l.m(n.parentNode,n)))},d(t){l.d(t),t&&c(n)}}}function Z(e){let n;return{c(){n=u("div"),n.innerHTML='<span class="sr-only">Loading...</span>',p(n,"class","spinner-border mt-5"),p(n,"role","status")},m(t,e){s(t,n,e)},p:t,d(t){t&&c(n)}}}function tt(e){let n,r,o,l,g,x,y,k,w,$,v,S,_,E,z,M,A,C,N,L,T,D,j,H,O,R,B=[0,5,10,15,20,25,30,35,40,45,50,55],P=[];for(let t=0;t<12;t+=1)P[t]=J(q(e,B,t));let F={ctx:e,current:null,token:null,pending:Z,then:K,catch:G,value:19,error:20};return I(H=e[4],F),{c(){n=u("main"),r=f(),o=u("div"),l=u("img"),x=f(),y=u("h1"),k=h(e[0]),w=f(),$=u("div"),v=u("div"),S=d("svg"),_=d("circle");for(let t=0;t<12;t+=1)P[t].c();E=d("line"),M=d("line"),C=d("g"),N=d("line"),L=d("line"),D=f(),j=u("div"),F.block.c(),O=f(),R=u("footer"),R.innerHTML='<a href="https://gitlab.servus.at/aura/meta"><img id="aura-logo" src="https://gitlab.servus.at/aura/meta/-/raw/master/images/aura-logo.png" alt="Aura Logo"></a> \n\t<br>\n\tStudio Clock is powered by <a href="https://gitlab.servus.at/autoradio">Aura Engine</a>',this.c=t,p(l,"id","station-logo"),l.src!==(g=e[1])&&p(l,"src",g),b(l,"width",e[2]),p(l,"alt","Radio Station"),p(l,"align","left"),p(y,"id","station-name"),p(o,"id","station-header"),p(_,"class","clock-face"),p(_,"r","48"),p(E,"class","hour"),p(E,"y1","2"),p(E,"y2","-20"),p(E,"transform",z="rotate("+(30*e[6]+e[7]/2)+")"),p(M,"class","minute"),p(M,"y1","4"),p(M,"y2","-30"),p(M,"transform",A="rotate("+(6*e[7]+e[8]/10)+")"),p(N,"class","second"),p(N,"y1","10"),p(N,"y2","-38"),p(L,"class","second-counterweight"),p(L,"y1","10"),p(L,"y2","2"),p(C,"transform",T="rotate("+6*e[8]+")"),p(S,"viewBox","-50 -50 100 100"),p(v,"id","left-column"),p(v,"class","column"),p(j,"id","right-column"),p(j,"class","column"),p($,"id","studio-clock")},m(t,c){s(t,n,c),e[18](n),s(t,r,c),s(t,o,c),i(o,l),i(o,x),i(o,y),i(y,k),s(t,w,c),s(t,$,c),i($,v),i(v,S),i(S,_);for(let t=0;t<12;t+=1)P[t].m(S,null);i(S,E),i(S,M),i(S,C),i(C,N),i(C,L),i($,D),i($,j),F.block.m(j,F.anchor=null),F.mount=()=>j,F.anchor=null,s(t,O,c),s(t,R,c)},p(t,[n]){if(e=t,2&n&&l.src!==(g=e[1])&&p(l,"src",g),4&n&&b(l,"width",e[2]),1&n&&m(k,e[0]),192&n&&z!==(z="rotate("+(30*e[6]+e[7]/2)+")")&&p(E,"transform",z),384&n&&A!==(A="rotate("+(6*e[7]+e[8]/10)+")")&&p(M,"transform",A),256&n&&T!==(T="rotate("+6*e[8]+")")&&p(C,"transform",T),F.ctx=e,16&n&&H!==(H=e[4])&&I(H,F));else{const t=e.slice();t[19]=F.resolved,F.block.p(t,n)}},i:t,o:t,d(t){t&&c(n),e[18](null),t&&c(r),t&&c(o),t&&c(w),t&&c($),a(P,t),F.block.d(),F.token=null,F=null,t&&c(O),t&&c(R)}}}"function"==typeof HTMLElement&&(j=class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){for(const t in this.$$.slotted)this.appendChild(this.$$.slotted[t])}attributeChangedCallback(t,e,n){this[t]=n}$destroy(){!function(t,e){const n=t.$$;null!==n.fragment&&(r(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}(this,1),this.$destroy=t}$on(t,e){const n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}$set(){}});function et(t){if(null!=t){let e="";return""!=t.artist&&(e=t.artist+" - "),e+t.title}return""}function nt(t){let e="";if(null!=t&&null!=t.schedule_start){let n="";if(null!=t.schedule_start){let n=new Date(Date.parse(t.schedule_start));n=n.toLocaleTimeString(navigator.language,{hour:"2-digit",minute:"2-digit"}),e="("+n}null!=t.schedule_end?(n=new Date(Date.parse(t.schedule_end)),n=n.toLocaleTimeString(navigator.language,{hour:"2-digit",minute:"2-digit"}),e=e+" - "+n+")"):e+=")"}return e}function rt(t,e,n){let r,o,l,{css:i=""}=e,{api:s="http://localhost:3333/api/v1"}=e,{name:c="Studio Clock"}=e,{logo:a="https://gitlab.servus.at/aura/meta/-/raw/master/images/aura-logo.png"}=e,{logosize:u="100px"}=e,{noScheduleMessage:d="Nothing scheduled!"}=e,h=new Date,f=null;var g;async function p(t){let e,n;try{e=await fetch(s+t)}catch{throw new Error("Cannot connect to Engine!")}try{n=await e.json()}catch(t){throw console.log("Error while converting response to JSON!",t),new Error(e.statusText)}if(e.ok)return n;throw console.log("Error:",n),new Error(n.message)}let m,b,x;return o=p("/clock"),g=()=>{const t=setInterval(()=>{n(15,h=new Date),n(5,l-=1),(l<=0||null==o)&&(f=null,n(4,o=null),n(4,o=p("/clock")))},1e3);return()=>{clearInterval(t)}},w().$$.on_mount.push(g),t.$set=t=>{"css"in t&&n(12,i=t.css),"api"in t&&n(13,s=t.api),"name"in t&&n(0,c=t.name),"logo"in t&&n(1,a=t.logo),"logosize"in t&&n(2,u=t.logosize),"noScheduleMessage"in t&&n(14,d=t.noScheduleMessage)},t.$$.update=()=>{32768&t.$$.dirty&&n(6,m=h.getHours()),32768&t.$$.dirty&&n(7,b=h.getMinutes()),32768&t.$$.dirty&&n(8,x=h.getSeconds())},[c,a,u,r,o,l,m,b,x,function(t){if(null!=i&&function(t,e){let n=document.createElement("link");n.setAttribute("rel","stylesheet"),n.setAttribute("type","text/css"),n.setAttribute("href",e),t.appendChild(n)}(r,i),null==f&&null!=t&&null!=t.track){f=t;let e=h-Date.parse(t.track_start);e=parseInt(e/1e3),n(5,l=t.track.duration-e-3),console.log("Current Data",t)}return""},function(t){if(null!=t&&Number.isInteger(t)){let e,n=new Date(null);return n.setSeconds(t),e=t>3600?n.toISOString().substr(11,8):n.toISOString().substr(14,5),e}return""},function(t){let e="";return e=null==t||null==t.name?'<span class="error">'+d+"</span>":t.name,e},i,s,d,h,f,p,function(t){v[t?"unshift":"push"](()=>{n(3,r=t)})}]}customElements.define("aura-clock",class extends j{constructor(t){super(),this.shadowRoot.innerHTML='<style>#station-header{width:100%;height:50px;padding:40px 100px}#station-name{margin:0;font-size:3em;line-height:80px}#station-logo{align-content:left;text-align:right;margin:0 40px 0 10px;opacity:0.5;filter:invert(100%)}#studio-clock{width:calc(100% - 200px);height:calc(100% - 500px);margin:100px;display:-webkit-flex;display:-ms-flexbox;display:flex;flex-direction:row}#left-column{width:30%;padding:25px}#right-column{width:70%;padding:25px 25px 25px 50px}#current-schedule,#next-schedule{margin:0 0 40px 20px}#next-schedule{background-color:rgb(24, 24, 24);margin-right:20px;padding:12px}#current-schedule .schedule-title{color:#ccc;font-size:3.5em}#next-schedule .schedule-title{color:gray !important;font-size:2em}#playlist{border:2px solid #333;margin:20px 20px 40px 20px;padding:10px;height:calc(80% - 100px);overflow-y:auto;scroll-behavior:smooth;background-color:#111;display:flex;align-items:center}#playlist::-webkit-scrollbar-track{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.3);background-color:rgb(77, 73, 73)}#playlist::-webkit-scrollbar{width:12px;background-color:rgb(0, 0, 0)}#playlist::-webkit-scrollbar-thumb{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,.3);background-color:rgb(34, 32, 32)}.playlist-entry{font-size:1.9em;padding-left:53px}#current-track *{font-size:1.5em}.track-time-left{margin:25px 50px}.is-active{color:green;padding-left:0}.is-active .track-title::before{content:"\\00a0\\00a0▶\\00a0\\00a0\\00a0";font-size:larger;color:green}.is-active .track-time-left{color:rgb(43, 241, 36);background-color:#222;padding:5px 15px}.error{font-size:1.3em;color:red;height:100%;display:flex;align-items:center;justify-content:center}svg{width:100%;height:100%}.clock-face{stroke:rgb(66, 66, 66);fill:black}.minor{stroke:rgb(132, 132, 132);stroke-width:0.5}.major{stroke:rgb(162, 162, 162);stroke-width:1}.hour{stroke:rgba(255, 255, 255, 0.705)}.minute{stroke:rgba(255, 255, 255, 0.705)}.second,.second-counterweight{stroke:rgb(180,0,0)}footer{width:100%;text-align:center;font-size:0.8em;color:gray;opacity:0.5}footer a{color:gray;text-decoration:underline}footer #aura-logo{filter:invert(100%);width:75px;margin:0 0 20px 0}</style>',R(this,{target:this.shadowRoot},rt,tt,l,{css:12,api:13,name:0,logo:1,logosize:2,noScheduleMessage:14}),t&&(t.target&&s(t.target,this,t.anchor),t.props&&(this.$set(t.props),N()))}static get observedAttributes(){return["css","api","name","logo","logosize","noScheduleMessage"]}get css(){return this.$$.ctx[12]}set css(t){this.$set({css:t}),N()}get api(){return this.$$.ctx[13]}set api(t){this.$set({api:t}),N()}get name(){return this.$$.ctx[0]}set name(t){this.$set({name:t}),N()}get logo(){return this.$$.ctx[1]}set logo(t){this.$set({logo:t}),N()}get logosize(){return this.$$.ctx[2]}set logosize(t){this.$set({logosize:t}),N()}get noScheduleMessage(){return this.$$.ctx[14]}set noScheduleMessage(t){this.$set({noScheduleMessage:t}),N()}})}(); -//# sourceMappingURL=aura-clock-bundle.js.map diff --git a/web/js/aura-player-bundle.js b/web/js/aura-player-bundle.js deleted file mode 100644 index 377e1aaa..00000000 --- a/web/js/aura-player-bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function r(t){t.forEach(e)}function o(t){return"function"==typeof t}function a(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function c(t,e){t.appendChild(e)}function i(t,e,n){t.insertBefore(e,n||null)}function l(t){t.parentNode.removeChild(t)}function s(t){return document.createElement(t)}function u(t){return document.createTextNode(t)}function d(){return u(" ")}function f(t,e,n){null==n?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function p(t,e){e=""+e,t.data!==e&&(t.data=e)}let h;function m(t){h=t}function g(){if(!h)throw new Error("Function called outside component initialization");return h}const b=[],k=[],$=[],y=[],x=Promise.resolve();let v=!1;function w(t){$.push(t)}function _(){const t=new Set;do{for(;b.length;){const t=b.shift();m(t),S(t.$$)}for(;k.length;)k.pop()();for(let e=0;e<$.length;e+=1){const n=$[e];t.has(n)||(n(),t.add(n))}$.length=0}while(b.length);for(;y.length;)y.pop()();v=!1}function S(t){if(null!==t.fragment){t.update(),r(t.before_update);const e=t.dirty;t.dirty=[-1],t.fragment&&t.fragment.p(t.ctx,e),t.after_update.forEach(w)}}const T=new Set;let E,L;function D(t,e){t&&t.i&&(T.delete(t),t.i(e))}function A(t,e){const n=e.token={};function o(t,o,a,c){if(e.token!==n)return;e.resolved=c;let i=e.ctx;void 0!==a&&(i=i.slice(),i[a]=c);const l=t&&(e.current=t)(i);let s=!1;e.block&&(e.blocks?e.blocks.forEach((t,n)=>{n!==o&&t&&(E={r:0,c:[],p:E},function(t,e,n,r){if(t&&t.o){if(T.has(t))return;T.add(t),E.c.push(()=>{T.delete(t),r&&(n&&t.d(1),r())}),t.o(e)}}(t,1,1,()=>{e.blocks[n]=null}),E.r||r(E.c),E=E.p)}):e.block.d(1),l.c(),D(l,1),l.m(e.mount(),e.anchor),s=!0),e.block=l,e.blocks&&(e.blocks[o]=l),s&&_()}if((a=t)&&"object"==typeof a&&"function"==typeof a.then){const n=g();if(t.then(t=>{m(n),o(e.then,1,e.value,t),m(null)},t=>{m(n),o(e.catch,2,e.error,t),m(null)}),e.current!==e.pending)return o(e.pending,0),!0}else{if(e.current!==e.then)return o(e.then,1,e.value,t),!0;e.resolved=t}var a}function C(t,e){-1===t.$$.dirty[0]&&(b.push(t),v||(v=!0,x.then(_)),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<<e%31}function I(a,c,i,l,s,u,d=[-1]){const f=h;m(a);const p=c.props||{},g=a.$$={fragment:null,ctx:null,props:u,update:t,not_equal:s,bound:n(),on_mount:[],on_destroy:[],before_update:[],after_update:[],context:new Map(f?f.$$.context:[]),callbacks:n(),dirty:d};let b=!1;g.ctx=i?i(a,p,(t,e,n=e)=>(g.ctx&&s(g.ctx[t],g.ctx[t]=n)&&(g.bound[t]&&g.bound[t](n),b&&C(a,t)),e)):[],g.update(),b=!0,r(g.before_update),g.fragment=!!l&&l(g.ctx),c.target&&(c.hydrate?g.fragment&&g.fragment.l(function(t){return Array.from(t.childNodes)}(c.target)):g.fragment&&g.fragment.c(),c.intro&&D(a.$$.fragment),function(t,n,a){const{fragment:c,on_mount:i,on_destroy:l,after_update:s}=t.$$;c&&c.m(n,a),w(()=>{const n=i.map(e).filter(o);l?l.push(...n):r(n),t.$$.on_mount=[]}),s.forEach(w)}(a,c.target,c.anchor),_()),m(f)}function M(t,e,n){const r=t.slice();return r[9]=e[n],r}function H(t){let e,n,r=t[8].message+"";return{c(){var t,o,a;e=s("p"),n=u(r),t="color",o="red",e.style.setProperty(t,o,a?"important":"")},m(t,r){i(t,e,r),c(e,n)},p(t,e){2&e&&r!==(r=t[8].message+"")&&p(n,r)},d(t){t&&l(e)}}}function N(t){let e,n=t[7],r=[];for(let e=0;e<n.length;e+=1)r[e]=j(M(t,n,e));return{c(){for(let t=0;t<r.length;t+=1)r[t].c();e=u("")},m(t,n){for(let e=0;e<r.length;e+=1)r[e].m(t,n);i(t,e,n)},p(t,o){if(6&o){let a;for(n=t[7],a=0;a<n.length;a+=1){const c=M(t,n,a);r[a]?r[a].p(c,o):(r[a]=j(c),r[a].c(),r[a].m(e.parentNode,e))}for(;a<r.length;a+=1)r[a].d(1);r.length=n.length}},d(t){!function(t,e){for(let n=0;n<t.length;n+=1)t[n]&&t[n].d(e)}(r,t),t&&l(e)}}}function j(t){let e,n,r,o,a,h,m,g,b,k,$,y,x,v,w,_,S=t[2](t[9])+"",T=R(t[9])+"",E=t[9].track.artist+"",L=t[9].track.title+"",D=B(t[9])+"";return{c(){e=s("h4"),n=u(S),r=d(),o=s("div"),a=s("div"),h=s("h5"),m=s("b"),g=u(T),b=u(" | "),k=u(E),$=u(" - "),y=u(L),x=d(),v=u(D),w=d(),f(e,"class","current-date"),f(h,"class","card-title"),f(a,"class","card-body"),f(o,"class",_="card mt-5 "+q(t[9]))},m(t,l){i(t,e,l),c(e,n),i(t,r,l),i(t,o,l),c(o,a),c(a,h),c(h,m),c(m,g),c(h,b),c(h,k),c(h,$),c(h,y),c(h,x),c(h,v),c(o,w)},p(t,e){2&e&&S!==(S=t[2](t[9])+"")&&p(n,S),2&e&&T!==(T=R(t[9])+"")&&p(g,T),2&e&&E!==(E=t[9].track.artist+"")&&p(k,E),2&e&&L!==(L=t[9].track.title+"")&&p(y,L),2&e&&D!==(D=B(t[9])+"")&&p(v,D),2&e&&_!==(_="card mt-5 "+q(t[9]))&&f(o,"class",_)},d(t){t&&l(e),t&&l(r),t&&l(o)}}}function z(e){let n;return{c(){n=s("div"),n.innerHTML='<div class="lds-dual-ring"></div> \n\t\t\t\t\t<div class="loading">Loading...</div>',f(n,"class","spinner"),f(n,"role","status")},m(t,e){i(t,n,e)},p:t,d(t){t&&l(n)}}}function O(e){let n,r,o,a,h,m,g,b,k,$,y,x,v,w={ctx:e,current:null,token:null,pending:z,then:N,catch:H,value:7,error:8};return A(k=e[1],w),{c(){n=s("div"),r=s("div"),o=s("div"),a=d(),h=s("div"),m=s("h1"),g=u(e[0]),b=d(),w.block.c(),$=d(),y=s("div"),x=d(),v=s("footer"),v.innerHTML='Track Service is powered by <a href="https://gitlab.servus.at/autoradio">Aura</a>',this.c=t,f(o,"class","col-md"),f(m,"class","display-4"),f(h,"class","col-md-8 text-center"),f(y,"class","col-md"),f(r,"class","row"),f(n,"class","container mt-5")},m(t,e){i(t,n,e),c(n,r),c(r,o),c(r,a),c(r,h),c(h,m),c(m,g),c(h,b),w.block.m(h,w.anchor=null),w.mount=()=>h,w.anchor=null,c(r,$),c(r,y),i(t,x,e),i(t,v,e)},p(t,[n]){if(e=t,1&n&&p(g,e[0]),w.ctx=e,2&n&&k!==(k=e[1])&&A(k,w));else{const t=e.slice();t[7]=w.resolved,w.block.p(t,n)}},i:t,o:t,d(t){t&&l(n),w.block.d(),w.token=null,w=null,t&&l(x),t&&l(v)}}}"function"==typeof HTMLElement&&(L=class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){for(const t in this.$$.slotted)this.appendChild(this.$$.slotted[t])}attributeChangedCallback(t,e,n){this[t]=n}$destroy(){!function(t,e){const n=t.$$;null!==n.fragment&&(r(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}(this,1),this.$destroy=t}$on(t,e){const n=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return n.push(e),()=>{const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}$set(){}});let P="trackservice";function R(t){return new Date(t.track_start).toLocaleTimeString("de-at",{hour:"2-digit",minute:"2-digit"})}function q(t){if(null!=t.track.duration&&parseInt(t.track.duration)>0){let e=new Date(t.track_start),n=new Date(e.getTime());n.setSeconds(n.getSeconds()+parseInt(t.track.duration));let r=new Date;return e<r&&r<n?"active-track":""}return""}function B(t){return null!=t.track.duration&&parseInt(t.track.duration)>0?"("+function(t){if(null!=t&&Number.isInteger(t)){let e,n=new Date(null);return n.setSeconds(t),e=t>3600?n.toISOString().substr(11,8):n.toISOString().substr(14,5),e}return""}(t.track.duration)+")":""}function F(t,e,n){let r,{api:o="http://localhost:3333/api/v1/"}=e,{name:a="Track Service"}=e,c="";return r=async function(t){let e=await fetch(`${o}${t}`),n=await e.json();if(e.ok)return n;throw new Error(n)}(P),t.$set=t=>{"api"in t&&n(3,o=t.api),"name"in t&&n(0,a=t.name)},[a,r,function(t){let e="";if(null!=t.track_start&&(e=t.track_start.split("T")[0]),c!=e){c=e;let t={weekday:"long",year:"numeric",month:"short",day:"numeric"};return new Date(c).toLocaleDateString("de-at",t)}return""},o]}customElements.define("aura-trackservice",class extends L{constructor(t){super(),this.shadowRoot.innerHTML='<style>h1,h4,h5{text-align:center}.card{margin-bottom:38px;font-size:1.3em}.card.active-track{border:3px solid greenyellow}.spinner{text-align:center}.spinner .loading{margin:10px 0 0 0;color:gray}.lds-dual-ring{display:inline-block;width:80px;height:80px}.lds-dual-ring:after{content:" ";display:block;width:64px;height:64px;margin:8px;border-radius:50%;border:6px solid #000;border-color:#aaa transparent #aaa transparent;animation:lds-dual-ring 1.2s linear infinite}@keyframes lds-dual-ring{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.current-date{font-size:1.3em}footer{font-size:0.8em;margin:40px 0;text-align:center;color:gray}footer a{color:gray;text-decoration:underline}</style>',I(this,{target:this.shadowRoot},F,O,a,{api:3,name:0}),t&&(t.target&&i(t.target,this,t.anchor),t.props&&(this.$set(t.props),_()))}static get observedAttributes(){return["api","name"]}get api(){return this.$$.ctx[3]}set api(t){this.$set({api:t}),_()}get name(){return this.$$.ctx[0]}set name(t){this.$set({name:t}),_()}})}(); -//# sourceMappingURL=aura-player-bundle.js.map diff --git a/web/templates/clock.html b/web/templates/clock.html deleted file mode 100644 index 7770d882..00000000 --- a/web/templates/clock.html +++ /dev/null @@ -1,24 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset='utf-8'> - <meta name='viewport' content='width=device-width,initial-scale=1'> - - <title>Aura Engine - Studio Clock</title> - - <link rel='icon' type='image/png' href='/favicon.png'> - <link rel='stylesheet' href='/css/aura.css'> - <link rel='stylesheet' href='/css/aura-clock-bundle.css'> - - <script defer src='/js/aura-clock-bundle.js'></script> -</head> - -<body style="background-color: black;"> - <aura-clock - css="/css/aura.css" - name=":::CONFIG-STATION-NAME:::" - logo=":::CONFIG-STATION-LOGO-URL:::" - logosize=":::CONFIG-STATION-LOGO-SIZE:::" - api=":::CONFIG-API-URL:::" /> -</body> -</html> diff --git a/web/templates/trackservice.html b/web/templates/trackservice.html deleted file mode 100644 index bea6aee7..00000000 --- a/web/templates/trackservice.html +++ /dev/null @@ -1,18 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset='utf-8'> - <meta name='viewport' content='width=device-width,initial-scale=1'> - - <title>Aura Engine - Track Service</title> - - <link rel='icon' type='image/png' href='/favicon.png'> - <link rel='stylesheet' href='/css/aura.css'> - <link rel='stylesheet' href='/css/aura-player-bundle.css'> - - <script defer src='/js/aura-player-bundle.js'></script> -</head> -<body> - <aura-trackservice api=":::CONFIG-API-URL:::" name=":::CONFIG-STATION-NAME::: - Track Service" /> -</body> -</html> -- GitLab