diff --git a/CHANGELOG.md b/CHANGELOG.md
index 959dd25d..d2aea796 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,13 +19,15 @@
   * Streamate:
     - Fixed "Couldn't load model ID" error while adding models by URL or by
       nickname
-    - Online/Offline switch on all tabs. Up to 10 000 offline models in each 
+    - Online / Offline switch on all tabs. Up to 10 000 offline models in each 
       category. How do you like it, Elon Musk?
-    - Added "New Girls" tab and adjusted others. All same models on less tabs.
+    - Added "New Girls" tab and adjusted others. All same models on less tabs
   * Stripchat: 
-    - Added "Private" tab.
+    - Added "Private" tab
     - CTBRec can record your Spy/Private/Ticket shows (login required)
-  * 
+  * Streamray:
+    - Added models tags
+    - Added Online / Offline switch on "Favorites" tab
 
 5.2.3
 ========================
diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java
index 907f82c9..f38a04eb 100644
--- a/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java
+++ b/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java
@@ -2,17 +2,95 @@ package ctbrec.ui.sites.streamray;
 
 import ctbrec.sites.streamray.Streamray;
 import ctbrec.ui.tabs.PaginatedScheduledService;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.json.JSONArray;
 
-abstract class AbstractStreamrayUpdateService extends PaginatedScheduledService {
+import java.net.URLEncoder;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
 
-    protected int modelsPerPage = 48;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@RequiredArgsConstructor
+public abstract class AbstractStreamrayUpdateService extends PaginatedScheduledService {
+
+    @Getter
+    @Setter
+    private static JSONArray mapping;
+    protected static final int MODELS_PER_PAGE = 48;
+    protected static final String API_URL = "https://beta-api.cams.com/won/compressed/";
     protected final Streamray site;
 
-    AbstractStreamrayUpdateService(Streamray site) {
-        this.site = site;
+    protected String getPreviewURL(String name) {
+        String lname = name.toLowerCase();
+        String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0, 1), lname.substring(lname.length() - 1), lname);
+        try {
+            return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:640::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, UTF_8));
+        } catch (Exception ex) {
+            return url;
+        }
     }
 
-    boolean isLoggedIn() {
-        return site.isLoggedIn();
+    protected List<String> createTags(JSONArray m) {
+        List<String> tags = new ArrayList<>();
+        int idx1 = mappingIndex("gender");
+        switch (m.optString(idx1)) {
+            case "M" -> tags.add("male");
+            case "F" -> tags.add("female");
+            case "TS" -> tags.add("trans");
+            default -> {
+                // don't add anything
+            }
+        }
+        int idx2 = mappingIndex("ethnicity");
+        switch (m.optString(idx2)) {
+            case "02" -> tags.add("asian");
+            case "03" -> tags.add("ebony");
+            case "04" -> tags.add("white");
+            case "05" -> tags.add("indian");
+            case "06" -> tags.add("latina");
+            case "07" -> tags.add("middle-eastern");
+            default -> {
+                // don't add anything
+            }
+        }
+        int idx3 = mappingIndex("hair_color");
+        switch (m.optString(idx3)) {
+            case "01" -> tags.add("black-hair");
+            case "02" -> tags.add("blonde");
+            case "03" -> tags.add("brunette");
+            case "06" -> tags.add("redhead");
+            default -> {
+                // don't add anything
+            }
+        }
+        int idx4 = mappingIndex("chat_type");
+        switch (m.optString(idx4)) {
+            case "0" -> tags.add("offline");
+            case "1" -> tags.add("public");
+            case "2" -> tags.add("nude-show");
+            case "3" -> tags.add("private");
+            case "4" -> tags.add("exclusive");
+            case "6" -> tags.add("ticket-show");
+            case "7" -> tags.add("voyeur");
+            case "10" -> tags.add("party");
+            case "13" -> tags.add("group");
+            case "14" -> tags.add("c2c");
+            default -> {
+                // don't add anything
+            }
+        }
+        return tags;
+    }
+
+    protected int mappingIndex(String s) {
+        for (var i = 0; i < mapping.length(); i++) {
+            if (Objects.equals(s, mapping.get(i))) return i;
+        }
+        return -1;
     }
 }
diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java
index 93b2087b..da03f3a9 100644
--- a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java
+++ b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java
@@ -1,18 +1,36 @@
 package ctbrec.ui.sites.streamray;
 
+import ctbrec.Config;
 import ctbrec.Model;
+import ctbrec.StringUtil;
+import ctbrec.io.HttpException;
 import ctbrec.sites.streamray.Streamray;
+import ctbrec.sites.streamray.StreamrayHttpClient;
 import ctbrec.sites.streamray.StreamrayModel;
 import javafx.concurrent.Task;
+import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.json.JSONArray;
+import org.json.JSONObject;
 
 import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+
+import static ctbrec.io.HttpConstants.*;
 
 @Slf4j
 public class StreamrayFavoritesService extends AbstractStreamrayUpdateService {
 
+    private List<StreamrayModel> modelsList;
+    private Instant lastListInfoRequest = Instant.EPOCH;
+    @Getter
+    private boolean loggedIn = false;
+    private boolean showOnline = true;
+
     public StreamrayFavoritesService(Streamray site) {
         super(site);
     }
@@ -22,20 +40,120 @@ public class StreamrayFavoritesService extends AbstractStreamrayUpdateService {
         return new Task<>() {
             @Override
             public List<Model> call() throws IOException {
-                return getModelList().stream()
-                        .skip((page - 1) * (long) modelsPerPage)
-                        .limit(modelsPerPage)
-                        .map(Model.class::cast)
-                        .toList();
+                if (showOnline) {
+                    return getModelList().stream()
+                            .skip((page - 1) * (long) MODELS_PER_PAGE)
+                            .limit(MODELS_PER_PAGE)
+                            .map(Model.class::cast)
+                            .toList();
+                } else {
+                    return loadOfflineModelList().stream()
+                            .map(Model.class::cast)
+                            .toList();
+                }
             }
         };
     }
 
     private List<StreamrayModel> getModelList() throws IOException {
-        List<StreamrayModel> models = site.loadModelList(true).stream().filter(StreamrayModel::isFavorite).toList();
-        if (models == null) {
-            models = Collections.emptyList();
+        if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
+            return Objects.nonNull(modelsList) ? modelsList : loadModelList(API_URL);
         }
-        return models;
+        modelsList = loadModelList(API_URL);
+        return modelsList;
+    }
+
+    private List<StreamrayModel> loadOfflineModelList() throws IOException {
+        String url = "https://beta-api.cams.com/favorites/member_favorites/?gender=female";
+        int offset = (getPage() - 1) * MODELS_PER_PAGE;
+        String paginatedUrl = url + "&offset=" + offset + "&limit=" + MODELS_PER_PAGE;
+        return loadModelList(paginatedUrl);
+    }
+
+    private List<StreamrayModel> loadModelList(String url) throws IOException {
+        log.debug("Fetching page {}", url);
+        lastListInfoRequest = Instant.now();
+        StreamrayHttpClient client = (StreamrayHttpClient) site.getHttpClient();
+        String token;
+        if (site.login()) {
+            loggedIn = true;
+            token = client.getUserToken();
+        } else {
+            loggedIn = false;
+            return Collections.emptyList();
+        }
+        Request req = new Request.Builder()
+                .url(url)
+                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
+                .header(ACCEPT, MIMETYPE_APPLICATION_JSON)
+                .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
+                .header(REFERER, site.getBaseUrl() + "/")
+                .header(ORIGIN, site.getBaseUrl())
+                .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
+                .header(AUTHORIZATION, "Bearer " + token)
+                .build();
+        try (Response response = client.execute(req)) {
+            if (response.isSuccessful()) {
+                List<StreamrayModel> models = new ArrayList<>();
+                JSONObject json = new JSONObject(response.body().string());
+                if (showOnline) {
+                    if (json.has("models")) {
+                        JSONArray modelNodes = json.getJSONArray("models");
+                        AbstractStreamrayUpdateService.setMapping(json.getJSONArray("mapping"));
+                        parseModels(modelNodes, models);
+                    }
+                } else {
+                    if (json.has("results")) {
+                        JSONArray modelNodes = json.getJSONArray("results");
+                        parseOfflineModels(modelNodes, models);
+                    }
+                }
+                return models;
+            } else {
+                throw new HttpException(response.code(), response.message());
+            }
+        }
+    }
+
+    private void parseModels(JSONArray jsonModels, List<StreamrayModel> models) {
+        int nameIdx = mappingIndex("stream_name");
+        int favIdx = mappingIndex("is_favorite");
+        for (int i = 0; i < jsonModels.length(); i++) {
+            JSONArray m = jsonModels.getJSONArray(i);
+            String name = m.optString(nameIdx);
+            boolean favorite = m.optBoolean(favIdx);
+            if (favorite) {
+                StreamrayModel model = site.createModel(name);
+                String preview = getPreviewURL(name);
+                model.setPreview(preview);
+                model.setTags(createTags(m));
+                StringBuilder description = new StringBuilder();
+                for (String tag : model.getTags()) {
+                    description.append("#").append(tag).append(" ");
+                }
+                model.setDescription(description.toString());
+                models.add(model);
+            }
+        }
+    }
+
+    private void parseOfflineModels(JSONArray jsonModels, List<StreamrayModel> models) {
+        for (int i = 0; i < jsonModels.length(); i++) {
+            JSONObject m = jsonModels.getJSONObject(i);
+            String name = m.optString("stream_name");
+            if (StringUtil.isBlank(name) || m.optBoolean("is_online")) {
+                continue;
+            }
+            StreamrayModel model = site.createModel(name);
+            String preview = getPreviewURL(name);
+            model.setPreview(preview);
+            model.setDisplayName(m.getString("screen_name"));
+            model.setOnlineState(Model.State.OFFLINE);
+            models.add(model);
+        }
+    }
+
+    public void setOnline(boolean online) {
+        showOnline = online;
     }
 }
diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java
index dbe1449c..2265564a 100644
--- a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java
+++ b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java
@@ -9,8 +9,11 @@ import javafx.geometry.Pos;
 import javafx.scene.Scene;
 import javafx.scene.control.Button;
 import javafx.scene.control.Label;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.ToggleGroup;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.HBox;
 
 public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedTab {
     private final Label status;
@@ -29,12 +32,38 @@ public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedT
         loginButton.setOnAction(e -> {
             try {
                 new StreamrayElectronLoginDialog(site.getHttpClient().getCookieJar());
+                queue.clear();
                 updateService.restart();
             } catch (Exception ex) {
+                // fail silently
             }
         });
     }
 
+    @Override
+    protected void createGui() {
+        super.createGui();
+        addOnlineOfflineSelector();
+    }
+
+    private void addOnlineOfflineSelector() {
+        var group = new ToggleGroup();
+        var online = new RadioButton("online");
+        online.setToggleGroup(group);
+        var offline = new RadioButton("offline");
+        offline.setToggleGroup(group);
+        pagination.getChildren().add(online);
+        pagination.getChildren().add(offline);
+        HBox.setMargin(online, new Insets(5, 5, 5, 40));
+        HBox.setMargin(offline, new Insets(5, 5, 5, 5));
+        online.setSelected(true);
+        group.selectedToggleProperty().addListener(e -> {
+            ((StreamrayFavoritesService) updateService).setOnline(online.isSelected());
+            queue.clear();
+            updateService.restart();
+        });
+    }
+
     protected void addLoginButton() {
         grid.getChildren().clear();
         grid.setAlignment(Pos.CENTER);
@@ -45,7 +74,7 @@ public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedT
     protected void onSuccess() {
         grid.getChildren().removeAll(status, loginButton);
         grid.setAlignment(Pos.TOP_LEFT);
-        if (!((AbstractStreamrayUpdateService) updateService).isLoggedIn()) {
+        if (!streamrayFavoritesService.isLoggedIn()) {
             addLoginButton();
         } else {
             super.onSuccess();
diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java
index 623dc213..aec262d5 100644
--- a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java
+++ b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java
@@ -1,22 +1,37 @@
 package ctbrec.ui.sites.streamray;
 
+import ctbrec.Config;
 import ctbrec.Model;
+import ctbrec.io.HttpException;
 import ctbrec.sites.streamray.Streamray;
+import ctbrec.sites.streamray.StreamrayHttpClient;
 import ctbrec.sites.streamray.StreamrayModel;
 import javafx.concurrent.Task;
 import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.json.JSONArray;
+import org.json.JSONObject;
 
 import java.io.IOException;
 import java.time.Duration;
 import java.time.Instant;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
 import java.util.function.Predicate;
 
+import static ctbrec.io.HttpConstants.*;
+
 @Slf4j
 public class StreamrayUpdateService extends AbstractStreamrayUpdateService {
 
-    private List<StreamrayModel> models;
+    private List<StreamrayModel> modelsList;
     private Instant lastListInfoRequest = Instant.EPOCH;
 
     @Setter
@@ -32,21 +47,78 @@ public class StreamrayUpdateService extends AbstractStreamrayUpdateService {
         return new Task<>() {
             @Override
             public List<Model> call() throws IOException {
-                return getModels().stream()
+                return getModelList().stream()
                         .filter(filter)
-                        .skip((page - 1) * (long) modelsPerPage)
-                        .limit(modelsPerPage)
+                        .skip((page - 1) * (long) MODELS_PER_PAGE)
+                        .limit(MODELS_PER_PAGE)
                         .map(Model.class::cast)
                         .toList();
             }
         };
     }
 
-    private List<StreamrayModel> getModels() throws IOException {
-        if (models == null || Duration.between(lastListInfoRequest, Instant.now()).getSeconds() >= 10) {
-            models = site.loadModelList();
-            lastListInfoRequest = Instant.now();
+    private List<StreamrayModel> getModelList() throws IOException {
+        if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
+            return Objects.nonNull(modelsList) ? modelsList : loadModelList();
+        }
+        modelsList = loadModelList();
+        return modelsList;
+    }
+
+    private List<StreamrayModel> loadModelList() throws IOException {
+        log.debug("Fetching page {}", API_URL);
+        lastListInfoRequest = Instant.now();
+        StreamrayHttpClient client = (StreamrayHttpClient) site.getHttpClient();
+        Request req = new Request.Builder()
+                .url(API_URL)
+                .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
+                .header(ACCEPT, MIMETYPE_APPLICATION_JSON)
+                .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
+                .header(REFERER, site.getBaseUrl() + "/")
+                .header(ORIGIN, site.getBaseUrl())
+                .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
+                .build();
+        try (Response response = client.execute(req)) {
+            if (response.isSuccessful()) {
+                List<StreamrayModel> models = new ArrayList<>();
+                String content = response.body().string();
+                JSONObject json = new JSONObject(content);
+                JSONArray modelNodes = json.getJSONArray("models");
+                AbstractStreamrayUpdateService.setMapping(json.getJSONArray("mapping"));
+                parseModels(modelNodes, models);
+                return models;
+            } else {
+                throw new HttpException(response.code(), response.message());
+            }
+        }
+    }
+
+    private void parseModels(JSONArray jsonModels, List<StreamrayModel> models) {
+        int nameIdx = mappingIndex("stream_name");
+        int dateIdx = mappingIndex("create_date");
+        int genIdx = mappingIndex("gender");
+        for (var i = 0; i < jsonModels.length(); i++) {
+            var m = jsonModels.getJSONArray(i);
+            String name = m.optString(nameIdx);
+            String gender = m.optString(genIdx);
+            String reg = m.optString(dateIdx);
+            StreamrayModel model = site.createModel(name);
+            try {
+                LocalDate regDate = LocalDate.parse(reg, DateTimeFormatter.BASIC_ISO_DATE);
+                model.setRegDate(regDate);
+            } catch (DateTimeParseException e) {
+                model.setRegDate(LocalDate.EPOCH);
+            }
+            String preview = getPreviewURL(name);
+            model.setPreview(preview);
+            model.setGender(gender);
+            model.setTags(createTags(m));
+            StringBuilder description = new StringBuilder();
+            for (String tag : model.getTags()) {
+                description.append("#").append(tag).append(" ");
+            }
+            model.setDescription(description.toString());
+            models.add(model);
         }
-        return models;
     }
 }
diff --git a/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java b/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java
index a8a3e4b1..bd1afb7e 100644
--- a/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java
+++ b/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java
@@ -1,19 +1,15 @@
 package ctbrec.ui.tabs;
 
-import java.util.List;
-
 import ctbrec.Model;
 import javafx.concurrent.ScheduledService;
+import lombok.Getter;
+import lombok.Setter;
 
+import java.util.List;
+
+@Getter
+@Setter
 public abstract class PaginatedScheduledService extends ScheduledService<List<Model>> {
 
     protected int page = 1;
-
-    public int getPage() {
-        return page;
-    }
-
-    public void setPage(int page) {
-        this.page = page;
-    }
 }