diff --git a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java
index 9a8784e4..b182b1da 100644
--- a/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java
+++ b/common/src/main/java/ctbrec/recorder/download/dash/DashDownload.java
@@ -62,7 +62,6 @@ public class DashDownload extends AbstractDownload {
     private boolean running = false;
 
     private File targetFile;
-    private File finalFile;
 
     public DashDownload(HttpClient httpClient, String manifestUrl) {
         this.httpClient = httpClient;
@@ -92,7 +91,7 @@ public class DashDownload extends AbstractDownload {
                             throw httpException;
                         } else {
                             LOG.debug("Couldn't load manifest", httpException);
-                            waitSomeTime(100 * tries);
+                            waitSomeTime(100l * tries);
                         }
                     } else {
                         internalStop();
@@ -114,7 +113,7 @@ public class DashDownload extends AbstractDownload {
         int downloaded = downloadInitChunksForVideoAndAudio(isVideo, mpd, segmentTemplate, representation);
 
         String media = segmentTemplate.getMedia();
-        media = media.replaceAll("\\$RepresentationID\\$", representation.getId()); // NOSONAR
+        media = media.replace("$RepresentationID$", representation.getId()); // NOSONAR
 
         List<S> segments = segmentTemplate.getSegmentTimeline().getS();
         if (!segments.isEmpty()) {
@@ -128,7 +127,7 @@ public class DashDownload extends AbstractDownload {
                     } else {
                         lastAudioTimestamp = timestamp;
                     }
-                    String segmentUrl = media.replaceAll("\\$Time\\$", timestamp.toString());
+                    String segmentUrl = media.replace("$Time$", timestamp.toString());
                     duration = s.getD();
                     URL absUrl = new URL(new URL(mpd.getLocation().get(0)), segmentUrl);
                     download(downloadDir.toFile().getCanonicalPath(), absUrl, isVideo);
@@ -157,7 +156,7 @@ public class DashDownload extends AbstractDownload {
         int loadedFileCount = 0;
         if (isVideo && !videoInitLoaded || !isVideo && !audioInitLoaded) {
             String initialization = segmentTemplate.getInitializationAttribute();
-            initialization = initialization.replaceAll("\\$RepresentationID\\$", representation.getId());
+            initialization = initialization.replace("$RepresentationID$", representation.getId());
             URL initUrl = new URL(new URL(mpd.getLocation().get(0)), initialization);
             File file = download(downloadDir.toFile().getCanonicalPath(), initUrl, isVideo);
             if (file != null) {
@@ -198,7 +197,7 @@ public class DashDownload extends AbstractDownload {
             try (Response response = httpClient.execute(request)) {
                 if (!response.isSuccessful()) {
                     LOG.trace("Loading segment failed, try {}, {} size:{} {}", tries, response.code(), response.headers().values(CONTENT_LENGTH), url);
-                    waitSomeTime(tries * 80);
+                    waitSomeTime(tries * 80l);
                 } else {
                     InputStream in = response.body().byteStream();
                     String absFile = url.getFile();
@@ -229,7 +228,7 @@ public class DashDownload extends AbstractDownload {
         this.config = config;
         this.model = model;
         this.startTime = startTime;
-        finalFile = Config.getInstance().getFileForRecording(model, "mp4", startTime);
+        File finalFile = Config.getInstance().getFileForRecording(model, "mp4", startTime);
         targetFile = new File(finalFile.getParentFile(), finalFile.getName() + ".part");
         downloadDir = targetFile.toPath();
     }
@@ -260,6 +259,9 @@ public class DashDownload extends AbstractDownload {
             } else {
                 throw e;
             }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            LOG.error("Error while downloading dash stream", e);
         } catch (Exception e) {
             LOG.error("Error while downloading dash stream", e);
         } finally {
@@ -422,8 +424,7 @@ public class DashDownload extends AbstractDownload {
 
     @Override
     public void finalizeDownload() {
-        // TODO Auto-generated method stub
-
+        // nothing to do here
     }
 
 }
diff --git a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java
index 22a6e831..efb49d55 100644
--- a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java
+++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java
@@ -51,7 +51,7 @@ public class Flirt4FreeModel extends AbstractModel {
     private String streamHost;
     private String streamUrl;
     int[] resolution = new int[2];
-    private Object monitor = new Object();
+    private transient Object monitor = new Object();
     private boolean online = false;
     private boolean isInteractiveShow = false;
     private boolean isNew = false;
@@ -81,21 +81,7 @@ public class Flirt4FreeModel extends AbstractModel {
                         .build();
                 try (Response response = getSite().getHttpClient().execute(request)) {
                     if (response.isSuccessful()) {
-                        String body = response.body().string();
-                        if (body.trim().isEmpty()) {
-                            return false;
-                        }
-                        JSONObject json = new JSONObject(body);
-                        online = Objects.equals(json.optString("status"), "online"); // online is true, even if the model is in private or away
-                        updateModelId(json);
-                        if (online) {
-                            try {
-                                loadModelInfo();
-                            } catch (Exception e) {
-                                online = false;
-                                onlineState = Model.State.OFFLINE;
-                            }
-                        }
+                        parseOnlineState(response.body().string());
                     } else {
                         throw new HttpException(response.code(), response.message());
                     }
@@ -108,6 +94,23 @@ public class Flirt4FreeModel extends AbstractModel {
         return online;
     }
 
+    private void parseOnlineState(String body) {
+        if (body.trim().isEmpty()) {
+            return;
+        }
+        JSONObject json = new JSONObject(body);
+        online = Objects.equals(json.optString("status"), "online"); // online is true, even if the model is in private or away
+        updateModelId(json);
+        if (online) {
+            try {
+                loadModelInfo();
+            } catch (Exception e) {
+                online = false;
+                onlineState = Model.State.OFFLINE;
+            }
+        }
+    }
+
     private void updateModelId(JSONObject json) {
         if (json.has("model_id")) {
             Object modelId = json.get("model_id");
@@ -120,7 +123,7 @@ public class Flirt4FreeModel extends AbstractModel {
         }
     }
 
-    private void loadModelInfo() throws IOException, InterruptedException {
+    private void loadModelInfo() throws IOException {
         String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id;
         LOG.trace("Loading url {}", url);
         Request request = new Request.Builder()
@@ -135,7 +138,6 @@ public class Flirt4FreeModel extends AbstractModel {
             if (response.isSuccessful()) {
                 JSONObject json = new JSONObject(response.body().string());
                 if (json.optString("status").equals("success")) {
-                    // LOG.debug("chat-room-interface {}", json.toString(2));
                     JSONObject config = json.getJSONObject("config");
                     JSONObject performer = config.getJSONObject("performer");
                     setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/');
@@ -191,6 +193,7 @@ public class Flirt4FreeModel extends AbstractModel {
             }
             masterPlaylist = getMasterPlaylist();
         } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
             throw new ExecutionException(e);
         }
         List<StreamSource> sources = new ArrayList<>();
@@ -236,7 +239,7 @@ public class Flirt4FreeModel extends AbstractModel {
     private void loadStreamUrl() throws IOException, InterruptedException {
         loadModelInfo();
         Objects.requireNonNull(chatHost, "chatHost is null");
-        String h = chatHost.replaceAll("chat", "chat-vip");
+        String h = chatHost.replace("chat", "chat-vip");
         String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, "utf-8") + "&port_to_be=" + chatPort;
         LOG.trace("Opening chat websocket {}", url);
         Request req = new Request.Builder()
@@ -258,10 +261,9 @@ public class Flirt4FreeModel extends AbstractModel {
             public void onMessage(WebSocket webSocket, String text) {
                 LOG.trace("Chat wbesocket for {}: {}", getName(), text);
                 JSONObject json = new JSONObject(text);
-                //LOG.debug("WS {}", text);
                 if (json.optString("command").equals("8011")) {
                     JSONObject data = json.getJSONObject("data");
-                    streamHost = data.getString("stream_host"); // TODO look, if the stream_host is equal to the one encoded in base64 in some of the ajax requests (parameters)
+                    streamHost = data.getString("stream_host");
                     online = true;
                     isInteractiveShow = data.optString("devices").equals("1");
                     String roomState = data.optString("room_state");
@@ -285,7 +287,7 @@ public class Flirt4FreeModel extends AbstractModel {
             public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                 LOG.error("Chat websocket for {} failed", getName(), t);
                 synchronized (monitor) {
-                    monitor.notify();
+                    monitor.notifyAll();
                 }
                 response.close();
             }
@@ -294,7 +296,7 @@ public class Flirt4FreeModel extends AbstractModel {
             public void onClosed(WebSocket webSocket, int code, String reason) {
                 LOG.trace("Chat websocket for {} closed {} {}", getName(), code, reason);
                 synchronized (monitor) {
-                    monitor.notify();
+                    monitor.notifyAll();
                 }
             }
         });
@@ -311,25 +313,15 @@ public class Flirt4FreeModel extends AbstractModel {
 
     @Override
     public void invalidateCacheEntries() {
+        // nothing to do here
     }
 
     @Override
     public void receiveTip(Double tokens) throws IOException {
         try {
-            //        if(tokens < 50 || tokens > 750000) {
-            //            throw new RuntimeException("Tip amount has to be between 50 and 750000");
-            //        }
-
             // make sure we are logged in and all necessary model data is available
             getSite().login();
-            acquireSlot();
-            try {
-                loadStreamUrl();
-            } catch (InterruptedException e) {
-                throw new IOException("Couldn't send tip", e);
-            } finally {
-                releaseSlot();
-            }
+            fetchStreamUrl();
 
             // send the tip
             int giftId = isInteractiveShow ? 775 : 171;
@@ -370,10 +362,23 @@ public class Flirt4FreeModel extends AbstractModel {
                 }
             }
         } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
             throw new IOException("Couldn't acquire request slot", e);
         }
     }
 
+    private void fetchStreamUrl() throws InterruptedException, IOException {
+        acquireSlot();
+        try {
+            loadStreamUrl();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IOException("Couldn't send tip", e);
+        } finally {
+            releaseSlot();
+        }
+    }
+
     private String getUserIdt() throws IOException, InterruptedException {
         if (userIdt.isEmpty()) {
             acquireSlot();
@@ -429,6 +434,7 @@ public class Flirt4FreeModel extends AbstractModel {
         try {
             return changeFavoriteStatus(true);
         } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
             throw new IOException("Couldn't change follow status for model " + getName(), e);
         }
     }
@@ -438,7 +444,10 @@ public class Flirt4FreeModel extends AbstractModel {
         try {
             isOnline(true);
             return changeFavoriteStatus(false);
-        } catch (ExecutionException | InterruptedException e) {
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IOException("Couldn't change follow status for model " + getName(), e);
+        } catch (ExecutionException e) {
             throw new IOException("Couldn't change follow status for model " + getName(), e);
         }
     }
@@ -509,12 +518,10 @@ public class Flirt4FreeModel extends AbstractModel {
     }
 
     private void acquireSlot() throws InterruptedException {
-        //LOG.debug("Acquire: {} - Queue: {}", requestThrottle.availablePermits(), requestThrottle.getQueueLength());
         requestThrottle.acquire();
         long now = System.currentTimeMillis();
         long millisSinceLastRequest = now - lastRequest;
         if(millisSinceLastRequest < 500) {
-            //LOG.debug("Sleeping: {}", (500-millisSinceLastRequest));
             Thread.sleep(500 - millisSinceLastRequest);
         }
     }
@@ -522,6 +529,5 @@ public class Flirt4FreeModel extends AbstractModel {
     private void releaseSlot() {
         lastRequest = System.currentTimeMillis();
         requestThrottle.release();
-        //        LOG.debug("Release: {}", requestThrottle.availablePermits());
     }
 }
diff --git a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java
index ca1c032e..59af16b8 100644
--- a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java
+++ b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java
@@ -4,6 +4,7 @@ import static javax.servlet.http.HttpServletResponse.*;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
@@ -57,12 +58,11 @@ public class RecorderServlet extends AbstractCtbrecServlet {
 
         String json = null;
         try {
+            PrintWriter responseWriter = resp.getWriter();
             json = body(req);
             boolean isRequestAuthenticated = checkAuthentication(req, json);
             if (!isRequestAuthenticated) {
-                resp.setStatus(SC_UNAUTHORIZED);
-                String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}";
-                resp.getWriter().write(response);
+                sendError(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}");
                 return;
             }
 
@@ -76,204 +76,204 @@ public class RecorderServlet extends AbstractCtbrecServlet {
             JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
             Request request = requestAdapter.fromJson(json);
             if (request.action != null) {
-                switch (request.action) {
-                case "start":
-                    LOG.debug("Starting recording for model {} - {}", request.model.getName(), request.model.getUrl());
-                    recorder.addModel(request.model);
-                    String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "startByUrl":
-                    LOG.debug("Starting recording for model {}", request.model.getUrl());
-                    startByUrl(request);
-                    response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "startByName":
-                    LOG.debug("Starting recording for model {}", request.model.getUrl());
-                    startByName(request);
-                    response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "stop":
-                    GlobalThreadPool.submit(() -> {
-                        try {
-                            recorder.stopRecording(request.model);
-                        } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
-                            LOG.error("Couldn't stop recording for model {}", request.model, e);
-                        }
-                    });
-                    response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "stopAt":
-                    GlobalThreadPool.submit(() -> {
-                        try {
-                            recorder.stopRecordingAt(request.model);
-                        } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
-                            LOG.error("Couldn't stop recording for model {}", request.model, e);
-                        }
-                    });
-                    response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "list":
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
-                    JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();
-                    List<Model> models = recorder.getModels();
-                    for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
-                        Model model = iterator.next();
-                        resp.getWriter().write(modelAdapter.toJson(model));
-                        if(iterator.hasNext()) {
-                            resp.getWriter().write(',');
-                        }
-                    }
-                    resp.getWriter().write("]}");
-                    break;
-                case "listOnline":
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of online models\", \"models\": [");
-                    modelAdapter = new ModelJsonAdapter();
-                    models = recorder.getOnlineModels();
-                    for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
-                        Model model = iterator.next();
-                        resp.getWriter().write(modelAdapter.toJson(model));
-                        if(iterator.hasNext()) {
-                            resp.getWriter().write(',');
-                        }
-                    }
-                    resp.getWriter().write("]}");
-                    break;
-                case "recordings":
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
-                    JsonAdapter<Recording> recAdapter = moshi.adapter(Recording.class);
-                    List<Recording> recordings = recorder.getRecordings();
-                    for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
-                        Recording recording = iterator.next();
-                        String recJSON = recAdapter.toJson(recording);
-                        LOG.debug("Rec: {}", recJSON);
-                        resp.getWriter().write(recJSON);
-                        if (iterator.hasNext()) {
-                            resp.getWriter().write(',');
-                        }
-                    }
-                    resp.getWriter().write("]}");
-                    break;
-                case "delete":
-                    recorder.delete(request.recording);
-                    recAdapter = moshi.adapter(Recording.class);
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
-                    resp.getWriter().write(recAdapter.toJson(request.recording));
-                    resp.getWriter().write("]}");
-                    break;
-                case "pin":
-                    recorder.pin(request.recording);
-                    recAdapter = moshi.adapter(Recording.class);
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
-                    resp.getWriter().write(recAdapter.toJson(request.recording));
-                    resp.getWriter().write("]}");
-                    break;
-                case "unpin":
-                    recorder.unpin(request.recording);
-                    recAdapter = moshi.adapter(Recording.class);
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"Note saved\", \"recordings\": [");
-                    resp.getWriter().write(recAdapter.toJson(request.recording));
-                    resp.getWriter().write("]}");
-                    break;
-                case "setNote":
-                    recorder.setNote(request.recording, request.recording.getNote());
-                    recAdapter = moshi.adapter(Recording.class);
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
-                    resp.getWriter().write(recAdapter.toJson(request.recording));
-                    resp.getWriter().write("]}");
-                    break;
-                case "rerunPostProcessing":
-                    recorder.rerunPostProcessing(request.recording);
-                    recAdapter = moshi.adapter(Recording.class);
-                    resp.getWriter().write("{\"status\": \"success\", \"msg\": \"Post-Processing triggered\"}");
-                    break;
-                case "switch":
-                    recorder.switchStreamSource(request.model);
-                    response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "suspend":
-                    LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl());
-                    GlobalThreadPool.submit(() -> {
-                        try {
-                            recorder.suspendRecording(request.model);
-                        } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
-                            LOG.error("Couldn't suspend recording for model {}", request.model, e);
-                        }
-                    });
-                    response = "{\"status\": \"success\", \"msg\": \"Suspending recording\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "resume":
-                    LOG.debug("Resume recording for model {} - {}", request.model.getName(), request.model.getUrl());
-                    recorder.resumeRecording(request.model);
-                    response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "space":
-                    JSONObject jsonResponse = new JSONObject();
-                    jsonResponse.put("status", "success");
-                    jsonResponse.put("spaceTotal", recorder.getTotalSpaceBytes());
-                    jsonResponse.put("spaceFree", recorder.getFreeSpaceBytes());
-                    jsonResponse.put("throughput", BandwidthMeter.getThroughput());
-                    jsonResponse.put("throughputTimeframe", BandwidthMeter.getTimeframe().toMillis());
-                    jsonResponse.put("minimumSpaceLeftInBytes", Config.getInstance().getSettings().minimumSpaceLeftInBytes);
-                    resp.getWriter().write(jsonResponse.toString());
-                    break;
-                case "changePriority":
-                    recorder.priorityChanged(request.model);
-                    response = "{\"status\": \"success\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "pauseRecorder":
-                    recorder.pause();
-                    response = "{\"status\": \"success\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "resumeRecorder":
-                    recorder.resume();
-                    response = "{\"status\": \"success\"}";
-                    resp.getWriter().write(response);
-                    break;
-                case "saveModelGroup":
-                    recorder.saveModelGroup(request.modelGroup);
-                    sendModelGroups(resp, recorder.getModelGroups());
-                    break;
-                case "deleteModelGroup":
-                    recorder.deleteModelGroup(request.modelGroup);
-                    sendModelGroups(resp, recorder.getModelGroups());
-                    break;
-                case "listModelGroups":
-                    sendModelGroups(resp, recorder.getModelGroups());
-                    break;
-                default:
-                    resp.setStatus(SC_BAD_REQUEST);
-                    response = "{\"status\": \"error\", \"msg\": \"Unknown action ["+request.action+"]\"}";
-                    resp.getWriter().write(response);
-                    break;
-                }
-            } else {
-                resp.setStatus(SC_BAD_REQUEST);
-                String response = "{\"status\": \"error\", \"msg\": \"action is missing\"}";
-                resp.getWriter().write(response);
+                sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"action is missing\"}");
+                return;
             }
-        } catch(Throwable t) {
+            switch (request.action) {
+            case "start":
+                LOG.debug("Starting recording for model {} - {}", request.model.getName(), request.model.getUrl());
+                recorder.addModel(request.model);
+                String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
+                responseWriter.write(response);
+                break;
+            case "startByUrl":
+                LOG.debug("Starting recording for model {}", request.model.getUrl());
+                startByUrl(request);
+                response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
+                responseWriter.write(response);
+                break;
+            case "startByName":
+                LOG.debug("Starting recording for model {}", request.model.getUrl());
+                startByName(request);
+                response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
+                responseWriter.write(response);
+                break;
+            case "stop":
+                GlobalThreadPool.submit(() -> {
+                    try {
+                        recorder.stopRecording(request.model);
+                    } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
+                        LOG.error("Couldn't stop recording for model {}", request.model, e);
+                    }
+                });
+                response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
+                responseWriter.write(response);
+                break;
+            case "stopAt":
+                GlobalThreadPool.submit(() -> {
+                    try {
+                        recorder.stopRecordingAt(request.model);
+                    } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
+                        LOG.error("Couldn't stop recording for model {}", request.model, e);
+                    }
+                });
+                response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
+                responseWriter.write(response);
+                break;
+            case "list":
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
+                JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();
+                List<Model> models = recorder.getModels();
+                for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
+                    Model model = iterator.next();
+                    responseWriter.write(modelAdapter.toJson(model));
+                    if (iterator.hasNext()) {
+                        responseWriter.write(',');
+                    }
+                }
+                responseWriter.write("]}");
+                break;
+            case "listOnline":
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of online models\", \"models\": [");
+                modelAdapter = new ModelJsonAdapter();
+                models = recorder.getOnlineModels();
+                for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
+                    Model model = iterator.next();
+                    responseWriter.write(modelAdapter.toJson(model));
+                    if (iterator.hasNext()) {
+                        responseWriter.write(',');
+                    }
+                }
+                responseWriter.write("]}");
+                break;
+            case "recordings":
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
+                JsonAdapter<Recording> recAdapter = moshi.adapter(Recording.class);
+                List<Recording> recordings = recorder.getRecordings();
+                for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
+                    Recording recording = iterator.next();
+                    String recJSON = recAdapter.toJson(recording);
+                    LOG.debug("Rec: {}", recJSON);
+                    responseWriter.write(recJSON);
+                    if (iterator.hasNext()) {
+                        responseWriter.write(',');
+                    }
+                }
+                responseWriter.write("]}");
+                break;
+            case "delete":
+                recorder.delete(request.recording);
+                recAdapter = moshi.adapter(Recording.class);
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
+                responseWriter.write(recAdapter.toJson(request.recording));
+                responseWriter.write("]}");
+                break;
+            case "pin":
+                recorder.pin(request.recording);
+                recAdapter = moshi.adapter(Recording.class);
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
+                responseWriter.write(recAdapter.toJson(request.recording));
+                responseWriter.write("]}");
+                break;
+            case "unpin":
+                recorder.unpin(request.recording);
+                recAdapter = moshi.adapter(Recording.class);
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"Note saved\", \"recordings\": [");
+                responseWriter.write(recAdapter.toJson(request.recording));
+                responseWriter.write("]}");
+                break;
+            case "setNote":
+                recorder.setNote(request.recording, request.recording.getNote());
+                recAdapter = moshi.adapter(Recording.class);
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
+                responseWriter.write(recAdapter.toJson(request.recording));
+                responseWriter.write("]}");
+                break;
+            case "rerunPostProcessing":
+                recorder.rerunPostProcessing(request.recording);
+                responseWriter.write("{\"status\": \"success\", \"msg\": \"Post-Processing triggered\"}");
+                break;
+            case "switch":
+                recorder.switchStreamSource(request.model);
+                response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}";
+                responseWriter.write(response);
+                break;
+            case "suspend":
+                LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl());
+                GlobalThreadPool.submit(() -> {
+                    try {
+                        recorder.suspendRecording(request.model);
+                    } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
+                        LOG.error("Couldn't suspend recording for model {}", request.model, e);
+                    }
+                });
+                response = "{\"status\": \"success\", \"msg\": \"Suspending recording\"}";
+                responseWriter.write(response);
+                break;
+            case "resume":
+                LOG.debug("Resume recording for model {} - {}", request.model.getName(), request.model.getUrl());
+                recorder.resumeRecording(request.model);
+                response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
+                responseWriter.write(response);
+                break;
+            case "space":
+                JSONObject jsonResponse = new JSONObject();
+                jsonResponse.put("status", "success");
+                jsonResponse.put("spaceTotal", recorder.getTotalSpaceBytes());
+                jsonResponse.put("spaceFree", recorder.getFreeSpaceBytes());
+                jsonResponse.put("throughput", BandwidthMeter.getThroughput());
+                jsonResponse.put("throughputTimeframe", BandwidthMeter.getTimeframe().toMillis());
+                jsonResponse.put("minimumSpaceLeftInBytes", Config.getInstance().getSettings().minimumSpaceLeftInBytes);
+                responseWriter.write(jsonResponse.toString());
+                break;
+            case "changePriority":
+                recorder.priorityChanged(request.model);
+                response = "{\"status\": \"success\"}";
+                responseWriter.write(response);
+                break;
+            case "pauseRecorder":
+                recorder.pause();
+                response = "{\"status\": \"success\"}";
+                responseWriter.write(response);
+                break;
+            case "resumeRecorder":
+                recorder.resume();
+                response = "{\"status\": \"success\"}";
+                responseWriter.write(response);
+                break;
+            case "saveModelGroup":
+                recorder.saveModelGroup(request.modelGroup);
+                sendModelGroups(resp, recorder.getModelGroups());
+                break;
+            case "deleteModelGroup":
+                recorder.deleteModelGroup(request.modelGroup);
+                sendModelGroups(resp, recorder.getModelGroups());
+                break;
+            case "listModelGroups":
+                sendModelGroups(resp, recorder.getModelGroups());
+                break;
+            default:
+                sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"Unknown action [" + request.action + "]\"}");
+                break;
+            }
+        } catch(Exception e) {
             resp.setStatus(SC_INTERNAL_SERVER_ERROR);
             JSONObject response = new JSONObject();
             response.put("status", "error");
-            response.put("msg", t.getMessage());
+            response.put("msg", e.getMessage());
             resp.getWriter().write(response.toString());
-            LOG.error("Unexpected error", t);
+            LOG.error("Unexpected error", e);
             if (json != null) {
                 LOG.debug("Request: {}", json);
             }
         }
     }
 
+    private void sendError(HttpServletResponse resp, int code, String body) throws IOException {
+        resp.setStatus(code);
+        resp.getWriter().write(body);
+    }
+
     private void sendModelGroups(HttpServletResponse resp, Set<ModelGroup> modelGroups) throws IOException {
         JSONObject jsonResponse = new JSONObject();
         jsonResponse.put("status", "success");