diff --git a/fixtures/sample/radiosettings.json b/fixtures/sample/radiosettings.json
index 61278a91937674ad3f3f538f76e7bd8404c318db..75f81aa0b131d7b718bfc671a07b5393f3bf5f6c 100644
--- a/fixtures/sample/radiosettings.json
+++ b/fixtures/sample/radiosettings.json
@@ -3,18 +3,28 @@
     "model": "program.radiosettings",
     "pk": 1,
     "fields": {
-      "cba_api_key": "dea",
+      "cba_api_key": "",
       "cba_domains": [
         "cba.media"
       ],
+      "fallback_default_pool": "",
       "fallback_show": null,
+      "host_image_aspect_ratio": "1:1",
+      "host_image_shape": "round",
       "line_in_channels": {
-        "1": "live",
-        "2": "preprod"
+        "0": "live",
+        "1": "preprod"
       },
+      "micro_show": null,
+      "note_image_aspect_ratio": "16:9",
+      "note_image_shape": "rect",
+      "show_image_aspect_ratio": "16:9",
+      "show_image_shape": "rect",
+      "show_logo_aspect_ratio": "1:1",
+      "show_logo_shape": "rect",
       "station_logo": null,
-      "station_name": "Radio Helsinki",
-      "station_website": "https://helsinki.at/"
+      "station_name": "Radio AURA",
+      "station_website": "https://aura.radio"
     }
   }
 ]
diff --git a/poetry.lock b/poetry.lock
index 1f31997bd5148d4e14ba2a6d18936f0c5e66ba69..1c1eccf42d7ce7598b11881fff1878af3dc2728c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand.
 
 [[package]]
 name = "argcomplete"
@@ -93,13 +93,13 @@ uvloop = ["uvloop (>=0.15.2)"]
 
 [[package]]
 name = "certifi"
-version = "2024.2.2"
+version = "2024.6.2"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
-    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+    {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+    {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
 ]
 
 [[package]]
@@ -239,63 +239,63 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.5.1"
+version = "7.5.3"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"},
-    {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"},
-    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"},
-    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"},
-    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"},
-    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"},
-    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"},
-    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"},
-    {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"},
-    {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"},
-    {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"},
-    {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"},
-    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"},
-    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"},
-    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"},
-    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"},
-    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"},
-    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"},
-    {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"},
-    {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"},
-    {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"},
-    {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"},
-    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"},
-    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"},
-    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"},
-    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"},
-    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"},
-    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"},
-    {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"},
-    {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"},
-    {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"},
-    {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"},
-    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"},
-    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"},
-    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"},
-    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"},
-    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"},
-    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"},
-    {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"},
-    {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"},
-    {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"},
-    {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"},
-    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"},
-    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"},
-    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"},
-    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"},
-    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"},
-    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"},
-    {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"},
-    {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"},
-    {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"},
-    {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"},
+    {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"},
+    {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"},
+    {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"},
+    {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"},
+    {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"},
+    {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"},
+    {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"},
+    {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"},
+    {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"},
+    {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"},
+    {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"},
+    {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"},
+    {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"},
+    {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"},
+    {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"},
+    {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"},
+    {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"},
+    {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"},
+    {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"},
+    {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"},
+    {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"},
+    {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"},
+    {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"},
+    {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"},
+    {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"},
+    {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"},
+    {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"},
+    {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"},
+    {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"},
+    {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"},
+    {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"},
+    {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"},
+    {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"},
+    {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"},
+    {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"},
+    {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"},
+    {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"},
+    {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"},
+    {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"},
+    {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"},
+    {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"},
+    {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"},
+    {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"},
+    {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"},
+    {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"},
+    {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"},
+    {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"},
+    {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"},
+    {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"},
+    {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"},
+    {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"},
+    {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"},
 ]
 
 [package.extras]
@@ -520,13 +520,13 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
 
 [[package]]
 name = "faker"
-version = "25.2.0"
+version = "25.5.0"
 description = "Faker is a Python package that generates fake data for you."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "Faker-25.2.0-py3-none-any.whl", hash = "sha256:cfe97c4857c4c36ee32ea4aaabef884895992e209bae4cbd26807cf3e05c6918"},
-    {file = "Faker-25.2.0.tar.gz", hash = "sha256:45b84f47ff1ef86e3d1a8d11583ca871ecf6730fad0660edadc02576583a2423"},
+    {file = "Faker-25.5.0-py3-none-any.whl", hash = "sha256:edb85040a47ef1b30ccd8c4b6f07ee3cb4bd64aab1483be4efe75816ee2e2e36"},
+    {file = "Faker-25.5.0.tar.gz", hash = "sha256:84d454fc9fef0b73428e00bdf45a36c04568c75f22727e990071580840cfbb84"},
 ]
 
 [package.dependencies]
@@ -784,18 +784,15 @@ files = [
 
 [[package]]
 name = "nodeenv"
-version = "1.8.0"
+version = "1.9.1"
 description = "Node.js virtual environment builder"
 optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
-    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
-    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+    {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
+    {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
 ]
 
-[package.dependencies]
-setuptools = "*"
-
 [[package]]
 name = "packaging"
 version = "24.0"
@@ -1173,13 +1170,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
 
 [[package]]
 name = "pytest"
-version = "8.2.1"
+version = "8.2.2"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
-    {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
+    {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
+    {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
 ]
 
 [package.dependencies]
@@ -1372,13 +1369,13 @@ rpds-py = ">=0.7.0"
 
 [[package]]
 name = "requests"
-version = "2.32.2"
+version = "2.32.3"
 description = "Python HTTP for Humans."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
-    {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
+    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
 ]
 
 [package.dependencies]
@@ -1499,21 +1496,6 @@ files = [
     {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"},
 ]
 
-[[package]]
-name = "setuptools"
-version = "70.0.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
-    {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -1553,13 +1535,13 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.11.0"
+version = "4.12.1"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
-    {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+    {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"},
+    {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"},
 ]
 
 [[package]]
@@ -1672,4 +1654,4 @@ tests = ["build", "coverage", "mypy", "ruff", "wheel"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.11"
-content-hash = "08207d73bde29fc310e646d91845a97cce45fb708ee3b20a089436d7e18dfee3"
+content-hash = "e92284084d976e62cd8b2760be953c54b10a6cc94af62cc631218ddd916753a1"
diff --git a/program/admin.py b/program/admin.py
index 804236289733b158e081d4f3d45879263606c93f..8836b0fd1260eacae30442bc2c1e5d5cbfb88cee 100644
--- a/program/admin.py
+++ b/program/admin.py
@@ -105,7 +105,21 @@ admin.site.register(User, UserProfileUserAdmin)
 class RadioSettingsAdmin(admin.ModelAdmin):
     fieldsets = [
         (None, {"fields": ["station_name", "station_website", "station_logo"]}),
-        ("Programme", {"fields": ["fallback_show"]}),
+        (
+            "Image requirements",
+            {
+                "fields": [
+                    ("host_image_aspect_ratio", "host_image_shape"),
+                    (
+                        "note_image_aspect_ratio",
+                        "note_image_shape",
+                    ),
+                    ("show_image_aspect_ratio", "show_image_shape"),
+                    ("show_logo_aspect_ratio", "show_logo_shape"),
+                ]
+            },
+        ),
+        ("Programme", {"fields": [("fallback_show", "fallback_default_pool"), "micro_show"]}),
         ("CBA", {"fields": ["cba_api_key", "cba_domains"]}),
         ("Playout", {"fields": ["line_in_channels"]}),
     ]
diff --git a/program/migrations/0091_radiosettings_host_image_aspect_ratio_and_more.py b/program/migrations/0091_radiosettings_host_image_aspect_ratio_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..c6032dfccbcfc74eccfa8c131c18b2ab14a5fcbb
--- /dev/null
+++ b/program/migrations/0091_radiosettings_host_image_aspect_ratio_and_more.py
@@ -0,0 +1,70 @@
+# Generated by Django 4.2.13 on 2024-06-03 16:43
+
+import program.models
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("program", "0090_alter_playlist_options"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="radiosettings",
+            name="host_image_aspect_ratio",
+            field=program.models.ImageAspectRadioField(
+                choices=[("1:1", "1:1"), ("16:9", "16:9")], default="1:1", max_length=4
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="host_image_shape",
+            field=program.models.ImageShapeField(
+                choices=[("rect", "rect"), ("round", "round")], default="round", max_length=5
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="note_image_aspect_ratio",
+            field=program.models.ImageAspectRadioField(
+                choices=[("1:1", "1:1"), ("16:9", "16:9")], default="16:9", max_length=4
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="note_image_shape",
+            field=program.models.ImageShapeField(
+                choices=[("rect", "rect"), ("round", "round")], default="rect", max_length=5
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="show_image_aspect_ratio",
+            field=program.models.ImageAspectRadioField(
+                choices=[("1:1", "1:1"), ("16:9", "16:9")], default="16:9", max_length=4
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="show_image_shape",
+            field=program.models.ImageShapeField(
+                choices=[("rect", "rect"), ("round", "round")], default="rect", max_length=5
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="show_logo_aspect_ratio",
+            field=program.models.ImageAspectRadioField(
+                choices=[("1:1", "1:1"), ("16:9", "16:9")], default="1:1", max_length=4
+            ),
+        ),
+        migrations.AddField(
+            model_name="radiosettings",
+            name="show_logo_shape",
+            field=program.models.ImageShapeField(
+                choices=[("rect", "rect"), ("round", "round")], default="rect", max_length=5
+            ),
+        ),
+    ]
diff --git a/program/migrations/0092_radiosettings_micro_show.py b/program/migrations/0092_radiosettings_micro_show.py
new file mode 100644
index 0000000000000000000000000000000000000000..d584218ccb84af64d246ee71b2359b2b4e45ce54
--- /dev/null
+++ b/program/migrations/0092_radiosettings_micro_show.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.13 on 2024-06-03 16:50
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("program", "0091_radiosettings_host_image_aspect_ratio_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="radiosettings",
+            name="micro_show",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="program.show",
+            ),
+        ),
+    ]
diff --git a/program/migrations/0093_radiosettings_fallback_default_pool.py b/program/migrations/0093_radiosettings_fallback_default_pool.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a2906b8769ebebc34dbb1b2481db5b9cf5130ea
--- /dev/null
+++ b/program/migrations/0093_radiosettings_fallback_default_pool.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.13 on 2024-06-03 16:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("program", "0092_radiosettings_micro_show"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="radiosettings",
+            name="fallback_default_pool",
+            field=models.CharField(blank=True, max_length=32),
+        ),
+    ]
diff --git a/program/migrations/0094_alter_radiosettings_fallback_show_and_more.py b/program/migrations/0094_alter_radiosettings_fallback_show_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..15e71367d12ea7d5cf7086fc80251e636a0d866c
--- /dev/null
+++ b/program/migrations/0094_alter_radiosettings_fallback_show_and_more.py
@@ -0,0 +1,36 @@
+# Generated by Django 4.2.13 on 2024-06-03 16:57
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("program", "0093_radiosettings_fallback_default_pool"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="radiosettings",
+            name="fallback_show",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="program.show",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="radiosettings",
+            name="station_logo",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="+",
+                to="program.image",
+            ),
+        ),
+    ]
diff --git a/program/migrations/0095_alter_radiosettings_cba_domains_and_more.py b/program/migrations/0095_alter_radiosettings_cba_domains_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..a40f5fd8047e1ff852d21386d65509a49e56305a
--- /dev/null
+++ b/program/migrations/0095_alter_radiosettings_cba_domains_and_more.py
@@ -0,0 +1,35 @@
+# Generated by Django 4.2.13 on 2024-06-04 18:36
+
+import program.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("program", "0094_alter_radiosettings_fallback_show_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="radiosettings",
+            name="cba_domains",
+            field=models.JSONField(
+                blank=True,
+                default=list,
+                help_text="JSON array of strings",
+                validators=[program.models.validate_cba_domains],
+                verbose_name="CBA domains",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="radiosettings",
+            name="line_in_channels",
+            field=models.JSONField(
+                blank=True,
+                default=dict,
+                help_text="JSON key/value pairs",
+                validators=[program.models.validate_line_in_channels],
+            ),
+        ),
+    ]
diff --git a/program/models.py b/program/models.py
index 06b4a775377cd16728627525303cc3e551a72260..16886c5b7af4e30d50ed4b20fb1d2c95663fa78b 100644
--- a/program/models.py
+++ b/program/models.py
@@ -17,11 +17,12 @@
 # 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 jsonschema
 from rest_framework.exceptions import ValidationError
 from versatileimagefield.fields import PPOIField, VersatileImageField
 
 from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError as DjangoValidationError
 from django.db import models
 from django.db.models import Max, Q
 from django.utils import timezone
@@ -539,14 +540,87 @@ class Playlist(models.Model):
         ]
 
 
+class ImageAspectRadioField(models.CharField):
+    def __init__(self, *args, **kwargs):
+        kwargs["choices"] = [
+            ("1:1", "1:1"),
+            ("16:9", "16:9"),
+        ]
+        kwargs["max_length"] = 4
+
+        super().__init__(*args, **kwargs)
+
+
+class ImageShapeField(models.CharField):
+    def __init__(self, *args, **kwargs):
+        kwargs["choices"] = [
+            ("rect", "rect"),
+            ("round", "round"),
+        ]
+        kwargs["max_length"] = 5
+
+        super().__init__(*args, **kwargs)
+
+
+def validate_cba_domains(value):
+    schema = {
+        "type": "array",
+        "items": {"type": "string"},
+    }
+
+    try:
+        jsonschema.validate(value, schema)
+    except jsonschema.exceptions.ValidationError as e:
+        raise DjangoValidationError(e.args[0])
+
+
+def validate_line_in_channels(value):
+    schema = {
+        "type": "object",
+        "patternProperties": {
+            "^.*$": {"type": "string"},
+        },
+    }
+
+    try:
+        jsonschema.validate(value, schema)
+    except jsonschema.exceptions.ValidationError as e:
+        raise DjangoValidationError(e.args[0])
+
+
 class RadioSettings(models.Model):
     cba_api_key = models.CharField(blank=True, max_length=64, verbose_name="CBA API key")
     cba_domains = models.JSONField(
-        blank=True, default=list, help_text="JSON array of strings", verbose_name="CBA domains"
+        blank=True,
+        default=list,
+        help_text="JSON array of strings",
+        validators=[validate_cba_domains],
+        verbose_name="CBA domains",
+    )
+    fallback_default_pool = models.CharField(blank=True, max_length=32)
+    fallback_show = models.ForeignKey(
+        Show, blank=True, null=True, on_delete=models.CASCADE, related_name="+"
+    )
+    host_image_aspect_ratio = ImageAspectRadioField(default="1:1")
+    host_image_shape = ImageShapeField(default="round")
+    line_in_channels = models.JSONField(
+        blank=True,
+        default=dict,
+        help_text="JSON key/value pairs",
+        validators=[validate_line_in_channels],
+    )
+    micro_show = models.ForeignKey(
+        Show, blank=True, null=True, on_delete=models.CASCADE, related_name="+"
+    )
+    note_image_aspect_ratio = ImageAspectRadioField(default="16:9")
+    note_image_shape = ImageShapeField(default="rect")
+    show_image_aspect_ratio = ImageAspectRadioField(default="16:9")
+    show_image_shape = ImageShapeField(default="rect")
+    show_logo_aspect_ratio = ImageAspectRadioField(default="1:1")
+    show_logo_shape = ImageShapeField(default="rect")
+    station_logo = models.ForeignKey(
+        Image, blank=True, null=True, on_delete=models.CASCADE, related_name="+"
     )
-    fallback_show = models.ForeignKey(Show, blank=True, null=True, on_delete=models.CASCADE)
-    line_in_channels = models.JSONField(blank=True, default=dict, help_text="JSON key/value pairs")
-    station_logo = models.ForeignKey(Image, blank=True, null=True, on_delete=models.CASCADE)
     station_name = models.CharField(max_length=256, unique=True)
     station_website = models.URLField()
 
diff --git a/program/serializers.py b/program/serializers.py
index ac67e886150abb69d03fa14da240f9051dc242bd..8f30cecfccbfd7ee71ae4395730c491a072f6380 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -19,7 +19,7 @@
 #
 
 import re
-from typing import NotRequired, TypedDict
+from typing import Literal, NotRequired, TypedDict
 
 from drf_jsonschema_serializer import JSONSchemaField
 from rest_framework import serializers
@@ -110,6 +110,9 @@ class UserSerializer(serializers.ModelSerializer):
     permissions = serializers.SerializerMethodField()
     # Add profile fields to JSON
     profile = ProfileSerializer(required=False)
+    host_ids = serializers.PrimaryKeyRelatedField(
+        many=True, queryset=Host.objects.all(), source="hosts"
+    )
 
     class Meta:
         model = User
@@ -117,6 +120,7 @@ class UserSerializer(serializers.ModelSerializer):
             "id",
             "is_privileged",
             "permissions",
+            "host_ids",
         )
         fields = (
             "email",
@@ -344,7 +348,6 @@ class HostSerializer(serializers.ModelSerializer):
         many=True, required=False, help_text="Array of `HostLink` objects. Can be empty."
     )
     owner_ids = serializers.PrimaryKeyRelatedField(
-        allow_null=True,
         many=True,
         queryset=User.objects.all(),
         source="owners",
@@ -1166,12 +1169,27 @@ class RadioCBASettings(TypedDict):
     domains: list[str]
 
 
+class ProgrammeFallback(TypedDict):
+    default_pool: Literal["fallback"] | None
+    show_id: int | None
+
+
+class MicroProgramme(TypedDict):
+    show_id: int | None
+
+
 class RadioProgrammeSettings(TypedDict):
-    fallback_show_id: int | None
+    fallback: ProgrammeFallback
+    micro: MicroProgramme
+
+
+class PlayoutPools(TypedDict):
+    fallback: str | None
 
 
 class RadioPlayoutSettings(TypedDict):
     line_in_channels: dict[str, str]
+    pools: PlayoutPools
 
 
 class RadioStationSettings(TypedDict):
@@ -1180,15 +1198,43 @@ class RadioStationSettings(TypedDict):
     website: str
 
 
+class ImageFrame(TypedDict):
+    aspect_ratio: tuple[int, int]
+    shape: Literal["rect", "round"]
+
+
+class ImageRequirements(TypedDict):
+    frame: ImageFrame
+
+
+# done this way, because the keys have dots (".")
+RadioImageRequirementsSettings = TypedDict(
+    "RadioImageRequirementsSettings",
+    {
+        "host.image": ImageRequirements,
+        "note.image": ImageRequirements,
+        "show.image": ImageRequirements,
+        "show.logo": ImageRequirements,
+    },
+)
+
+
 class RadioSettingsSerializer(serializers.ModelSerializer):
     cba = serializers.SerializerMethodField()
+    image_requirements = serializers.SerializerMethodField()
     playout = serializers.SerializerMethodField()
     programme = serializers.SerializerMethodField()
     station = serializers.SerializerMethodField()
 
     class Meta:
-        read_only_fields = ("id", "cba", "playout", "programme", "station")
-        fields = read_only_fields
+        fields = read_only_fields = (
+            "id",
+            "cba",
+            "image_requirements",
+            "playout",
+            "programme",
+            "station",
+        )
         model = RadioSettings
 
     def get_cba(self, obj) -> RadioCBASettings:
@@ -1200,13 +1246,67 @@ class RadioSettingsSerializer(serializers.ModelSerializer):
         else:
             return {"domains": obj.cba_domains}
 
+    @staticmethod
+    def get_image_requirements(obj) -> RadioImageRequirementsSettings:
+        def get_aspect_ration(field) -> tuple[int, int]:
+            """return the tuple of ints representing the aspect ratio of the image."""
+
+            values = field.split(":")
+
+            return int(values[0]), int(values[1])
+
+        aspect_ratios = {
+            "host.image": get_aspect_ration(obj.host_image_aspect_ratio),
+            "note.image": get_aspect_ration(obj.note_image_aspect_ratio),
+            "show.image": get_aspect_ration(obj.show_image_aspect_ratio),
+            "show.logo": get_aspect_ration(obj.show_logo_aspect_ratio),
+        }
+
+        return {
+            "host.image": {
+                "frame": {
+                    "aspect_ratio": aspect_ratios["host.image"],
+                    "shape": obj.host_image_shape,
+                }
+            },
+            "note.image": {
+                "frame": {
+                    "aspect_ratio": aspect_ratios["note.image"],
+                    "shape": obj.host_image_shape,
+                }
+            },
+            "show.image": {
+                "frame": {
+                    "aspect_ratio": aspect_ratios["show.image"],
+                    "shape": obj.host_image_shape,
+                }
+            },
+            "show.logo": {
+                "frame": {
+                    "aspect_ratio": aspect_ratios["show.logo"],
+                    "shape": obj.host_image_shape,
+                }
+            },
+        }
+
     @staticmethod
     def get_programme(obj) -> RadioProgrammeSettings:
-        return {"fallback_show_id": obj.fallback_show.id if obj.fallback_show else None}
+        return {
+            "micro": {"show_id": obj.micro_show.id if obj.micro_show else None},
+            "fallback": {
+                "show_id": obj.fallback_show.id if obj.fallback_show else None,
+                "default_pool": "fallback",
+            },
+        }
 
     @staticmethod
     def get_playout(obj) -> RadioPlayoutSettings:
-        return {"line_in_channels": obj.line_in_channels}
+        return {
+            "line_in_channels": obj.line_in_channels,
+            "pools": {
+                "fallback": obj.fallback_default_pool,
+            },
+        }
 
     @staticmethod
     def get_station(obj) -> RadioStationSettings:
diff --git a/program/services.py b/program/services.py
index 038f3089355ef1c27a12053364a1d317225fa423..df3a45f27591081083974005778f41fec15a44a4 100644
--- a/program/services.py
+++ b/program/services.py
@@ -19,7 +19,8 @@
 
 import copy
 from datetime import datetime, time, timedelta
-from typing import TypedDict
+from itertools import pairwise
+from typing import Literal, TypedDict
 
 from dateutil.relativedelta import relativedelta
 from dateutil.rrule import rrule
@@ -30,7 +31,15 @@ from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import Q, QuerySet
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
-from program.models import Note, RRule, Schedule, ScheduleConflictError, Show, TimeSlot
+from program.models import (
+    Note,
+    RadioSettings,
+    RRule,
+    Schedule,
+    ScheduleConflictError,
+    Show,
+    TimeSlot,
+)
 from program.serializers import ScheduleSerializer, TimeSlotSerializer
 from program.utils import parse_date, parse_datetime, parse_time
 
@@ -86,6 +95,36 @@ class ScheduleCreateUpdateData(TypedDict):
     solutions: dict[str, str]
 
 
+class ScheduleEntry(TypedDict):
+    end: str
+    is_virtual: bool
+    show_id: int
+    start: str
+    title: str
+
+
+class TimeslotEntry(TypedDict):
+    end: str
+    id: int
+    is_virtual: Literal[False]
+    playlist_id: int | None
+    repetition_of_id: int | None
+    schedule_default_playlist_id: int | None
+    schedule_id: int
+    show_default_playlist_id: int | None
+    show_id: int
+    start: str
+    title: str
+
+
+class VirtualTimeslotEntry(TypedDict):
+    end: str
+    is_virtual: Literal[True]
+    show_id: int
+    start: str
+    title: str
+
+
 def create_timeslot(start: str, end: str, schedule: Schedule) -> TimeSlot:
     """Creates and returns an unsaved timeslot with the given `start`, `end` and `schedule`."""
 
@@ -740,14 +779,93 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts:
     return conflicts
 
 
-def get_timerange_timeslots(start: datetime, end: datetime) -> QuerySet[TimeSlot]:
-    """Gets a queryset of timeslots between the given `start` and `end` datetime."""
+def make_schedule_entry(*, timeslot_entry: TimeslotEntry) -> ScheduleEntry:
+    """returns a schedule entry for the given timeslot entry."""
+
+    return {
+        "end": timeslot_entry["end"],
+        "show_id": timeslot_entry["show_id"],
+        "is_virtual": timeslot_entry["is_virtual"],
+        "start": timeslot_entry["start"],
+        "title": timeslot_entry["title"],
+    }
+
+
+def make_timeslot_entry(*, timeslot: TimeSlot) -> TimeslotEntry:
+    """returns a timeslot entry for the given timeslot."""
+
+    schedule = timeslot.schedule
+    show = timeslot.schedule.show
+
+    return {
+        "end": timeslot.end.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "id": timeslot.id,
+        "is_virtual": False,
+        "playlist_id": timeslot.playlist_id,
+        # 'timeslot.repetition_of` is a foreign key that can be null
+        "repetition_of_id": timeslot.repetition_of.id if timeslot.repetition_of else None,
+        "schedule_default_playlist_id": schedule.default_playlist_id,
+        "schedule_id": schedule.id,
+        "show_default_playlist_id": show.default_playlist_id,
+        "show_id": show.id,
+        "start": timeslot.start.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "title": f"{show.name} {_('REP')}" if schedule.is_repetition else show.name,
+    }
+
+
+def make_virtual_timeslot_entry(*, gap_start: datetime, gap_end: datetime) -> VirtualTimeslotEntry:
+    """returns a virtual timeslot entry to fill the gap in between `gap_start` and `gap_end`."""
+
+    return {
+        "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "is_virtual": True,
+        "show_id": RadioSettings.objects.first().fallback_show.id,
+        "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S %z"),
+        "title": RadioSettings.objects.first().fallback_default_pool,
+    }
+
+
+def get_timerange_timeslot_entries(
+    timerange_start: datetime, timerange_end: datetime, include_virtual: bool = False
+) -> list[TimeslotEntry | VirtualTimeslotEntry]:
+    """Gets list of timeslot entries between the given `timerange_start` and `timerange_end`.
+
+    Include virtual timeslots if requested."""
+
+    timeslots = TimeSlot.objects.filter(
+        # start before `timerange_start` and end after `timerange_start`
+        Q(start__lt=timerange_start, end__gt=timerange_start)
+        # start after/at `timerange_start`, end before/at `timerange_end`
+        | Q(start__gte=timerange_start, end__lte=timerange_end)
+        # start before `timerange_end`, end after/at `timerange_end`
+        | Q(start__lt=timerange_end, end__gte=timerange_end)
+    ).select_related("schedule")
+
+    if not include_virtual:
+        return [make_timeslot_entry(timeslot=timeslot) for timeslot in timeslots]
+
+    timeslot_entries = []
+    # gap before the first timeslot
+    first_timeslot = timeslots.first()
+    if first_timeslot.start > timerange_start:
+        timeslot_entries.append(
+            make_virtual_timeslot_entry(gap_start=timerange_start, gap_end=first_timeslot.start)
+        )
 
-    return TimeSlot.objects.filter(
-        # start before `start` and end after `start`
-        Q(start__lt=start, end__gt=start)
-        # start after/at `start`, end before/at `end`
-        | Q(start__gte=start, end__lte=end)
-        # start before `end`, end after/at `end`
-        | Q(start__lt=end, end__gte=end)
-    )
+    for index, (current, upcoming) in enumerate(pairwise(timeslots)):
+        timeslot_entries.append(make_timeslot_entry(timeslot=current))
+
+        # gap between the timeslots
+        if current.end != upcoming.start:
+            timeslot_entries.append(
+                make_virtual_timeslot_entry(gap_start=current.end, gap_end=upcoming.start)
+            )
+
+    # gap after the last timeslot
+    last_timeslot = timeslots.last()
+    if last_timeslot.end < timerange_end:
+        timeslot_entries.append(
+            make_virtual_timeslot_entry(gap_start=last_timeslot.end, gap_end=timerange_end)
+        )
+
+    return timeslot_entries
diff --git a/program/views.py b/program/views.py
index b70e0d78c41c7103bdcb2ca424ba5b0bc5ff606e..ecfa6794e3e5ef0e7998e4b4d724330fa56a8a2a 100644
--- a/program/views.py
+++ b/program/views.py
@@ -20,7 +20,6 @@
 
 import logging
 from datetime import date, datetime, time, timedelta
-from itertools import pairwise
 from textwrap import dedent
 
 from django_filters.rest_framework import DjangoFilterBackend
@@ -43,7 +42,6 @@ from django.db import IntegrityError
 from django.http import HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
-from django.utils.translation import gettext as _
 from program import filters
 from program.models import (
     Category,
@@ -89,59 +87,16 @@ from program.serializers import (
     TypeSerializer,
     UserSerializer,
 )
-from program.services import get_timerange_timeslots, resolve_conflicts
+from program.services import (
+    get_timerange_timeslot_entries,
+    make_schedule_entry,
+    resolve_conflicts,
+)
 from program.utils import get_values, parse_date
 
 logger = logging.getLogger(__name__)
 
 
-def timeslot_entry(*, timeslot: TimeSlot) -> dict:
-    """return a timeslot entry as a dict"""
-
-    schedule = timeslot.schedule
-    show = timeslot.schedule.show
-    playlist_id = timeslot.playlist_id
-
-    title = show_name = f"{show.name} {_('REP')}" if schedule.is_repetition else show.name
-    # we start and end as timezone naive datetime objects
-    start = timezone.make_naive(timeslot.start).strftime("%Y-%m-%dT%H:%M:%S")
-    end = timezone.make_naive(timeslot.end).strftime("%Y-%m-%dT%H:%M:%S")
-
-    return {
-        "end": end,
-        "id": timeslot.id,
-        "playlistId": playlist_id,
-        # `Timeslot.repetition_of` is a foreign key that can be null
-        "repetitionOfId": timeslot.repetition_of.id if timeslot.repetition_of else None,
-        "scheduleDefaultPlaylistId": schedule.default_playlist_id,
-        "scheduleId": schedule.id,
-        "showCategories": ", ".join(show.category.values_list("name", flat=True)),
-        "showDefaultPlaylistId": show.default_playlist_id,
-        # `Show.funding_category` is a foreign key can be null
-        "showFundingCategory": show.funding_category.name if show.funding_category_id else "",
-        "showHosts": ", ".join(show.hosts.values_list("name", flat=True)),
-        "showId": show.id,
-        "showLanguages": ", ".join(show.language.values_list("name", flat=True)),
-        "showMusicFocus": ", ".join(show.music_focus.values_list("name", flat=True)),
-        "showName": show_name,
-        "showTopics": ", ".join(show.topic.values_list("name", flat=True)),
-        # `Show.type` is a foreign key that can be null
-        "showType": show.type.name if show.type_id else "",
-        "start": start,
-        "title": title,
-    }
-
-
-def gap_entry(*, gap_start: datetime, gap_end: datetime) -> dict:
-    """return a virtual timeslot to fill the gap in between `gap_start` and `gap_end` as a dict"""
-
-    return {
-        "end": gap_end.strftime("%Y-%m-%dT%H:%M:%S"),
-        "start": gap_start.strftime("%Y-%m-%dT%H:%M:%S"),
-        "virtual": True,
-    }
-
-
 @extend_schema_view(
     list=extend_schema(
         summary="List schedule for a specific date.",
@@ -168,18 +123,12 @@ class APIDayScheduleViewSet(
 
         end = start + timedelta(hours=24)
 
-        timeslots = get_timerange_timeslots(start, end).select_related("schedule")
-        schedule = []
-
-        for ts in timeslots:
-            entry = {
-                "start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"),
-                "end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"),
-                "title": ts.schedule.show.name,
-                "id": ts.schedule.show.id,
-            }
+        include_virtual = request.GET.get("include_virtual") == "true"
 
-            schedule.append(entry)
+        schedule = [
+            make_schedule_entry(timeslot_entry=timeslot_entry)
+            for timeslot_entry in get_timerange_timeslot_entries(start, end, include_virtual)
+        ]
 
         return JsonResponse(schedule, safe=False)
 
@@ -229,33 +178,9 @@ class APIPlayoutViewSet(
 
         include_virtual = request.GET.get("include_virtual") == "true"
 
-        timeslots = get_timerange_timeslots(schedule_start, schedule_end).select_related(
-            "schedule"
-        )
-
-        schedule = []
-
-        first_timeslot = timeslots.first()
+        playout = get_timerange_timeslot_entries(schedule_start, schedule_end, include_virtual)
 
-        if include_virtual and first_timeslot.start > schedule_start:
-            schedule.append(gap_entry(gap_start=schedule_start, gap_end=first_timeslot.start))
-
-        for current, upcoming in pairwise(timeslots):
-            schedule.append(timeslot_entry(timeslot=current))
-
-            if include_virtual and current.end != upcoming.start:
-                schedule.append(gap_entry(gap_start=current.end, gap_end=upcoming.start))
-
-        last_timeslot = timeslots.last()
-
-        # we need to append the last timeslot to the schedule to complete it
-        if last_timeslot:
-            schedule.append(timeslot_entry(timeslot=last_timeslot))
-
-        if include_virtual and last_timeslot.end < schedule_end:
-            schedule.append(gap_entry(gap_start=last_timeslot.end, gap_end=schedule_end))
-
-        return JsonResponse(schedule, safe=False)
+        return JsonResponse(playout, safe=False)
 
 
 @extend_schema_view(
diff --git a/pyproject.toml b/pyproject.toml
index c81c122aab153851f87d3c5c22801de94af74bf5..7b3be6ed39c58c4405954551ee6d98b771c3434b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ django-versatileimagefield = "^3.0"
 drf-jsonschema-serializer = "^2.0.0"
 drf-spectacular = "^0.27.1"
 gunicorn = "^21.2.0"
+jsonschema = "^4.22.0"
 Pillow = "^10.1.0"
 psycopg2-binary = "^2.9.3"
 pydot = "^2.0.0"