Repository: dyc3/opentogethertube
Pull request #1829: feat(client): save sponsorblock settings in browser and apply them on room creation
Author: login: mayudev
Created: 2025-04-03T13:34:21Z | Merged: 2025-04-16T12:53:21Z

Related to https://github.com/dyc3/opentogethertube/issues/1828

- Saves SponsorBlock settings to localStorage/settings whenever they are changed
- Applies those settings on room creation (both temporary and permanent)


Diff:
diff --git a/client/src/components/AutoSkipSegmentSettings.vue b/client/src/components/AutoSkipSegmentSettings.vue
new file mode 100644
index 000000000..e93ac01a8
--- /dev/null
+++ b/client/src/components/AutoSkipSegmentSettings.vue
@@ -0,0 +1,23 @@
+<template>
+	<v-select
+		v-model="model"
+		:items="ALL_SKIP_CATEGORIES"
+		:loading="loading"
+		:disabled="disabled"
+		:label="$t('room-settings.auto-skip-text')"
+		chips
+		multiple
+		data-cy="input-auto-skip"
+	/>
+</template>
+
+<script setup lang="ts">
+import { ALL_SKIP_CATEGORIES } from "ott-common";
+import type { Category } from "sponsorblock-api";
+
+const model = defineModel<Category[]>();
+defineProps<{
+	loading?: boolean;
+	disabled?: boolean;
+}>();
+</script>
diff --git a/client/src/components/ClientSettingsDialog.vue b/client/src/components/ClientSettingsDialog.vue
index c9d60d8c1..406cde22e 100644
--- a/client/src/components/ClientSettingsDialog.vue
+++ b/client/src/components/ClientSettingsDialog.vue
@@ -44,6 +44,19 @@
 					max="1"
 					step="0.01"
 				/>
+
+				<v-checkbox
+					:label="$t('client-settings.room-settings')"
+					v-model="showRoomSettings"
+					false-icon="mdi-chevron-up"
+					true-icon="mdi-chevron-down"
+				/>
+
+				<v-expand-transition>
+					<div v-if="showRoomSettings">
+						<AutoSkipSegmentSettings v-model="autoSkipCategories" />
+					</div>
+				</v-expand-transition>
 			</v-card-text>
 
 			<v-divider />
@@ -68,6 +81,8 @@ import { SettingsState, RoomLayoutMode, Theme } from "@/stores/settings";
 import _ from "lodash";
 import { useSfx } from "@/plugins/sfx";
 import { enumKeys } from "@/util/misc";
+import AutoSkipSegmentSettings from "./AutoSkipSegmentSettings.vue";
+import { ALL_SKIP_CATEGORIES } from "ott-common";
 
 type ExcludedFields = "volume" | "locale";
 type ExposedSettings = Omit<SettingsState, ExcludedFields>;
@@ -78,6 +93,18 @@ const store = useStore();
 const settings: Ref<ExposedSettings> = ref(loadSettings());
 const sfx = useSfx();
 
+const showRoomSettings = ref(false);
+const autoSkipCategories = ref(
+	settings.value.defaultRoomSettings?.autoSkipSegmentCategories ?? ALL_SKIP_CATEGORIES
+);
+
+watch(autoSkipCategories, categories => {
+	settings.value.defaultRoomSettings = {
+		...settings.value.defaultRoomSettings,
+		autoSkipSegmentCategories: categories,
+	};
+});
+
 function loadSettings(): ExposedSettings {
 	const copy = _.cloneDeep(store.state.settings);
 	const filtered = _.omit(copy, EXCLUDED);
diff --git a/client/src/components/RoomSettingsForm.vue b/client/src/components/RoomSettingsForm.vue
index 21c7fe5f0..4ad53a72c 100644
--- a/client/src/components/RoomSettingsForm.vue
+++ b/client/src/components/RoomSettingsForm.vue
@@ -65,17 +65,12 @@
 					</v-list-item>
 				</template>
 			</v-select>
-			<v-select
-				v-model="settings.autoSkipSegmentCategories.value"
-				:items="ALL_SKIP_CATEGORIES"
+			<AutoSkipSegmentSettings
 				:loading="
 					isLoadingRoomSettings || dirtySettings.includes('autoSkipSegmentCategories')
 				"
 				:disabled="!granted('configure-room.other')"
-				:label="$t('room-settings.auto-skip-text')"
-				chips
-				multiple
-				data-cy="input-auto-skip"
+				v-model="settings.autoSkipSegmentCategories.value"
 			/>
 			<v-select
 				:label="$t('room-settings.restore-queue')"
@@ -163,11 +158,11 @@ import { Ref, onMounted, reactive, ref, toRefs } from "vue";
 import { useStore } from "@/store";
 import { useI18n } from "vue-i18n";
 import { OttApiResponseGetRoom } from "ott-common/models/rest-api";
-import { ALL_SKIP_CATEGORIES } from "ott-common/constants";
 import { useGrants } from "./composables/grants";
 import { useRoute } from "vue-router";
 import { watch } from "vue";
 import { watchDebounced } from "@vueuse/core";
+import AutoSkipSegmentSettings from "./AutoSkipSegmentSettings.vue";
 
 const store = useStore();
 const { t } = useI18n();
diff --git a/client/src/locales/de.ts b/client/src/locales/de.ts
index ffdcedafe..f31af92b9 100644
--- a/client/src/locales/de.ts
+++ b/client/src/locales/de.ts
@@ -284,9 +284,10 @@ export default {
 		"permission": "Berechtigung",
 	},
 	"client-settings": {
-		title: "Anpassen",
-		description: "Diese Einstellungen werden in deinem Browser gespeichert.",
-		activator: "@:client-settings.title",
+		"title": "Anpassen",
+		"description": "Diese Einstellungen werden in deinem Browser gespeichert.",
+		"activator": "@:client-settings.title",
+		"room-settings": "Standardraumeinstellungen",
 	},
 	"connect-overlay": {
 		"find-another": "Einen anderen Raum finden",
diff --git a/client/src/locales/en.ts b/client/src/locales/en.ts
index e069398d5..8c30b378c 100644
--- a/client/src/locales/en.ts
+++ b/client/src/locales/en.ts
@@ -360,6 +360,7 @@ export default {
 		"theme": "Theme",
 		"sfx-enable": "Enable Sound Effects",
 		"sfx-volume": "Sound Effect Volume",
+		"room-settings": "Default Room Settings",
 	},
 	"connect-overlay": {
 		"title": "Disconnected",
diff --git a/client/src/stores/settings.ts b/client/src/stores/settings.ts
index 4225946fb..ac97a6330 100644
--- a/client/src/stores/settings.ts
+++ b/client/src/stores/settings.ts
@@ -1,5 +1,6 @@
 import { Module } from "vuex/types";
 import vuetify from "@/plugins/vuetify";
+import { RoomSettings } from "ott-common";
 
 export interface SettingsState {
 	volume: number;
@@ -8,8 +9,11 @@ export interface SettingsState {
 	theme: Theme;
 	sfxEnabled: boolean;
 	sfxVolume: number;
+	defaultRoomSettings?: DefaultRoomSettings;
 }
 
+export type DefaultRoomSettings = Pick<RoomSettings, "autoSkipSegmentCategories">;
+
 export enum RoomLayoutMode {
 	default = "default",
 	theater = "theater",
diff --git a/client/src/util/roomcreator.ts b/client/src/util/roomcreator.ts
index 10f5cc240..76af4fa12 100644
--- a/client/src/util/roomcreator.ts
+++ b/client/src/util/roomcreator.ts
@@ -5,15 +5,16 @@ import {
 	OttApiResponseRoomGenerate,
 	OttApiResponseRoomCreate,
 	OttApiRequestRoomCreate,
+	OttApiRequestRoomGenerate,
 } from "ott-common/models/rest-api";
 import type { Store } from "vuex";
-import { reactive, Ref, ref } from "vue";
-import { AxiosResponse } from "axios";
 import type { FullOTTStoreState } from "@/store";
 
 /** Generate a temporary room. */
-export async function generateRoom(): Promise<OttApiResponseRoomGenerate> {
-	let resp = await API.post("/room/generate", undefined, {
+export async function generateRoom(
+	options: OttApiRequestRoomGenerate
+): Promise<OttApiResponseRoomGenerate> {
+	let resp = await API.post("/room/generate", options, {
 		validateStatus: status => status >= 200 && status < 400,
 	});
 	let data: OttResponseBody<OttApiResponseRoomGenerate> = resp.data;
@@ -49,14 +50,21 @@ export async function createRoomHelper(
 	store.commit("misc/CREATING_ROOM");
 	try {
 		if (options) {
-			await createRoom(options);
+			await createRoom({
+				...options,
+				autoSkipSegmentCategories:
+					store.state.settings.defaultRoomSettings?.autoSkipSegmentCategories,
+			});
 			if (store.state.misc.cancelledRoomCreation) {
 				return;
 			}
 			store.commit("misc/ROOM_CREATED", { name: options.name });
 			return options.name;
 		} else {
-			let resp = await generateRoom();
+			let resp = await generateRoom({
+				autoSkipSegmentCategories:
+					store.state.settings.defaultRoomSettings?.autoSkipSegmentCategories,
+			});
 			if (store.state.misc.cancelledRoomCreation) {
 				return;
 			}
diff --git a/common/models/rest-api.ts b/common/models/rest-api.ts
index 3d3476ca0..9813658a5 100644
--- a/common/models/rest-api.ts
+++ b/common/models/rest-api.ts
@@ -13,6 +13,7 @@ import {
 	OttApiRequestPatchRoomSchema,
 	ClaimSchema,
 	RoomSettingsSchema,
+	OttApiRequestRoomGenerateSchema,
 } from "./zod-schemas";
 import { z } from "zod";
 
@@ -40,6 +41,8 @@ export interface OttApiError {
 }
 
 /** Endpoint: `/api/room/generate` */
+export type OttApiRequestRoomGenerate = z.infer<typeof OttApiRequestRoomGenerateSchema>;
+
 export interface OttApiResponseRoomGenerate {
 	room: string;
 }
diff --git a/common/models/zod-schemas.ts b/common/models/zod-schemas.ts
index c7637f64e..e4c214ef3 100644
--- a/common/models/zod-schemas.ts
+++ b/common/models/zod-schemas.ts
@@ -6,20 +6,38 @@ import { z } from "zod";
 // These strings are not allowed to be used as room names.
 const RESERVED_ROOM_NAMES = ["list", "create", "generate"];
 
-export const OttApiRequestRoomCreateSchema = z.object({
-	name: z
-		.string()
-		.min(3, "too short, must be atleast 3 characters")
-		.max(32, "too long, must be at most 32 characters")
-		.regex(ROOM_NAME_REGEX)
-		.refine(name => !RESERVED_ROOM_NAMES.includes(name), { message: "not allowed (reserved)" }),
-	title: z.string().max(255, "too long, must be at most 255 characters").optional(),
-	description: z.string().optional(),
-	isTemporary: z.boolean().optional().default(true),
-	visibility: z.nativeEnum(Visibility).default(Visibility.Public).optional(),
-	queueMode: z.nativeEnum(QueueMode).optional(),
+const CategorySchema = z.enum([
+	"sponsor",
+	"intro",
+	"outro",
+	"interaction",
+	"selfpromo",
+	"music_offtopic",
+	"preview",
+]);
+
+export const OttApiRequestRoomGenerateSchema = z.object({
+	autoSkipSegmentCategories: z.array(CategorySchema).optional(),
 });
 
+export const OttApiRequestRoomCreateSchema = z
+	.object({
+		name: z
+			.string()
+			.min(3, "too short, must be atleast 3 characters")
+			.max(32, "too long, must be at most 32 characters")
+			.regex(ROOM_NAME_REGEX)
+			.refine(name => !RESERVED_ROOM_NAMES.includes(name), {
+				message: "not allowed (reserved)",
+			}),
+		title: z.string().max(255, "too long, must be at most 255 characters").optional(),
+		description: z.string().optional(),
+		isTemporary: z.boolean().optional().default(true),
+		visibility: z.nativeEnum(Visibility).default(Visibility.Public).optional(),
+		queueMode: z.nativeEnum(QueueMode).optional(),
+	})
+	.merge(OttApiRequestRoomGenerateSchema);
+
 const VideoIdSchema = z.object({
 	service: z.enum(ALL_VIDEO_SERVICES),
 	id: z.string(),
@@ -57,16 +75,6 @@ export const OttApiRequestAccountRecoveryVerifySchema = z.object({
 	newPassword: z.string(),
 });
 
-const CategorySchema = z.enum([
-	"sponsor",
-	"intro",
-	"outro",
-	"interaction",
-	"selfpromo",
-	"music_offtopic",
-	"preview",
-]);
-
 const GrantSchema = z.tuple([z.nativeEnum(Role), z.number()]);
 
 export const RoomSettingsSchema = z
diff --git a/server/api/room.ts b/server/api/room.ts
index 5670ed1f4..f623c6bac 100644
--- a/server/api/room.ts
+++ b/server/api/room.ts
@@ -40,6 +40,7 @@ import {
 	OttApiRequestAddToQueueSchema,
 	OttApiRequestRemoveFromQueueSchema,
 	OttApiRequestPatchRoomSchema,
+	OttApiRequestRoomGenerateSchema,
 } from "ott-common/models/zod-schemas";
 import { ZodError } from "zod";
 import { fromZodError } from "zod-validation-error";
@@ -105,6 +106,9 @@ const generateRoom: RequestHandler<unknown, OttResponseBody<OttApiResponseRoomGe
 	if (!conf.get("room.enable_create_temporary")) {
 		throw new FeatureDisabledException("Temporary rooms are disabled.");
 	}
+
+	const body = OttApiRequestRoomGenerateSchema.parse(req.body);
+
 	let points = 50;
 	if (!(await consumeRateLimitPoints(res, req.ip, points))) {
 		return;
@@ -115,6 +119,7 @@ const generateRoom: RequestHandler<unknown, OttResponseBody<OttApiResponseRoomGe
 		name: roomName,
 		isTemporary: true,
 		owner: req.user,
+		...body,
 	});
 	log.info(`room generated: ip=${req.ip} user-agent=${req.headers["user-agent"]}`);
 	res.status(201).json({


Reviews:
user: login: dyc3
body: Thanks for the PR! It's a good first draft.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-03T14:26:40Z
user: login: mayudev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T10:33:04Z
user: login: dyc3
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-04T11:39:54Z
user: login: dyc3
body: This looks great! Just a couple of minor nitpicks, and the CI failures need to be addressed.
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-15T19:27:09Z
user: login: dyc3
body: Great PR, thank you! Hope to see more from you!

It looks like the remaining CI failures are unrelated. I'll take a look.
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-16T12:44:09Z

Review comments:
diff_hunk: @@ -442,6 +443,18 @@ export default defineComponent({
 		onMounted(async () => {
 			await waitForToken(store);
 
+			if (history.state.created && store.state.settings.autoSkipSegmentCategories) {
+				try {
+					await API.patch(`/room/${route.params.roomId ?? store.state.room.name}`, {
+						autoSkipSegmentCategories: store.state.settings.autoSkipSegmentCategories,
+					});
+				} catch (e) {
+					console.error(e);
+				}
+
+				delete history.state.created;
+			}
+
path: client/src/views/Room.vue
user: login: dyc3
body: This feels very hacky. We can send the room settings directly to the `/api/room/create` and `/api/room/generate` endpoints, and avoid the extra round trip.
created_at: 2025-04-03T14:14:24Z
author_association: OWNER
diff_hunk: @@ -8,6 +9,7 @@ export interface SettingsState {
 	theme: Theme;
 	sfxEnabled: boolean;
 	sfxVolume: number;
+	autoSkipSegmentCategories?: Category[];
path: client/src/stores/settings.ts
user: login: dyc3
body: For the client settings here, it would be better if we could nest this under a `defaultRoomSettings` so we can add other settings like `grants` to it.
created_at: 2025-04-03T14:17:54Z
author_association: OWNER
diff_hunk: @@ -269,6 +276,7 @@ async function submitRoomSettings() {
 			`/room/${route.params.roomId ?? store.state.room.name}`,
 			getRoomSettingsSubmit()
 		);
+		updateClientSettings();
path: client/src/components/RoomSettingsForm.vue
user: login: dyc3
body: There's almost certainly a race condition here. You're grabbing the settings from the store before waiting for the sync message that actually updates the store.

Regardless, I don't really like this from a UX perspective. Modifying room settings shouldn't have side effects like this. The client settings are exposed in the "Preferences" dialog, which can be found here: https://github.com/dyc3/opentogethertube/blob/master/client/src/components/ClientSettingsDialog.vue
created_at: 2025-04-03T14:24:03Z
author_association: OWNER
diff_hunk: @@ -269,6 +276,7 @@ async function submitRoomSettings() {
 			`/room/${route.params.roomId ?? store.state.room.name}`,
 			getRoomSettingsSubmit()
 		);
+		updateClientSettings();
path: client/src/components/RoomSettingsForm.vue
user: login: mayudev
body: That's fair... I didn't think well enough about this because it works perfectly for my use case, but it makes sense that it's not really a good solution in general.
Would you suggest adding the default SponsorBlock settings to the client settings instead?
created_at: 2025-04-04T10:33:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -269,6 +276,7 @@ async function submitRoomSettings() {
 			`/room/${route.params.roomId ?? store.state.room.name}`,
 			getRoomSettingsSubmit()
 		);
+		updateClientSettings();
path: client/src/components/RoomSettingsForm.vue
user: login: dyc3
body: Yeah. We should have a "default room settings" setting in the client settings.
created_at: 2025-04-04T11:39:54Z
author_association: OWNER
diff_hunk: @@ -8,6 +9,11 @@ export interface SettingsState {
 	theme: Theme;
 	sfxEnabled: boolean;
 	sfxVolume: number;
+	defaultRoomSettings?: DefaultRoomSettings;
+}
+
+export interface DefaultRoomSettings {
+	autoSkipSegmentCategories: Category[];
path: client/src/stores/settings.ts
user: login: dyc3
body: We could instead define this type in terms of the `RoomSettings` type. Something like `Pick<RoomSettings, "autoSkipSegmentCategories">`.
created_at: 2025-04-15T19:21:09Z
author_association: OWNER
diff_hunk: @@ -189,7 +189,9 @@ export const App = defineComponent({
 			store.subscribe(mutation => {
 				if (mutation.type === "misc/ROOM_CREATED") {
 					try {
-						router.push(`/room/${mutation.payload.name}`);
+						router.push({
+							path: `/room/${mutation.payload.name}`,
+						});
path: client/src/App.vue
user: login: dyc3
body: nit: lets fully revert this change.
created_at: 2025-04-15T19:22:25Z
author_association: OWNER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-15T19:19:14Z
body: ## [Codecov](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) Report
Attention: Patch coverage is `54.38596%` with `52 lines` in your changes missing coverage. Please review.
> Project coverage is 61.6404%. Comparing base [(`e6b44c7`)](https://app.codecov.io/gh/dyc3/opentogethertube/commit/e6b44c716289a8babb2f70624e452542e45c2a1b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) to head [(`394590f`)](https://app.codecov.io/gh/dyc3/opentogethertube/commit/394590f4d073829d8297172daf3bd7941f796f4a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) | Patch % | Lines |
|---|---|---|
| [common/models/zod-schemas.ts](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&filepath=common%2Fmodels%2Fzod-schemas.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus#diff-Y29tbW9uL21vZGVscy96b2Qtc2NoZW1hcy50cw==) | 0.0000% | [30 Missing :warning: ](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) |
| [client/src/util/roomcreator.ts](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&filepath=client%2Fsrc%2Futil%2Froomcreator.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus#diff-Y2xpZW50L3NyYy91dGlsL3Jvb21jcmVhdG9yLnRz) | 0.0000% | [14 Missing :warning: ](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) |
| [client/src/locales/de.ts](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&filepath=client%2Fsrc%2Flocales%2Fde.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus#diff-Y2xpZW50L3NyYy9sb2NhbGVzL2RlLnRz) | 0.0000% | [4 Missing :warning: ](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) |
| [server/api/room.ts](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&filepath=server%2Fapi%2Froom.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus#diff-c2VydmVyL2FwaS9yb29tLnRz) | 20.0000% | [4 Missing :warning: ](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff                @@
##             master      #1829        +/-   ##
================================================
+ Coverage   61.6099%   61.6404%   +0.0304%     
================================================
  Files           169        170         +1     
  Lines         25504      25579        +75     
  Branches       1449       1448         -1     
================================================
+ Hits          15713      15767        +54     
- Misses         9737       9758        +21     
  Partials         54         54                
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/dyc3/opentogethertube/pull/1829?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Carson+McManus).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: cypress[bot]
created_at: 2025-04-16T13:01:39Z
body: 
<h3>
  <a href="https://cloud.cypress.io/projects/3utpz8?utm_source=github&utm_medium=failed&utm_campaign=project">OpenTogetherTube</a>&nbsp;
  <img src="https://assets.cypress.io/github-pr-comment-icons/chevron.svg" alt="" height="14" width="14" />&nbsp;
  <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/overview?utm_source=github&utm_medium=failed&utm_campaign=run%20number">Run #2039</a>
</h3>

<details>
  <summary>
    <strong>Run Properties:</strong>&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/errored.svg" alt="status check errored" height="10" width="10" />&nbsp;Errored&nbsp;<a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/overview?utm_source=github&utm_medium=failed&utm_campaign=run%20number">#2039</a></code> &nbsp;&bull;&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/dyc3/opentogethertube/commit/b23f085fba622150619bbae4f3e7164e744c1fb1">b23f085fba</a>: feat(client): save sponsorblock settings in browser and apply them on room creat...</code>
  </summary><br />

  <table width="744">
    <tbody>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Project</strong>
        </td>
        <td width="520">
          <code><a href="https://cloud.cypress.io/projects/3utpz8?utm_source=github&utm_medium=failed&utm_campaign=project">OpenTogetherTube</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Branch Review</strong>
        </th>
        <td>
          <code><a href="https://cloud.cypress.io/projects/3utpz8/branches/master/review?utm_source=github&utm_medium=failed&utm_campaign=review%20changes&changeRequest=1829">master</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run status</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/errored.svg" alt="status check errored" height="10" width="10" />&nbsp;Errored&nbsp;<a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/overview?utm_source=github&utm_medium=failed&utm_campaign=run%20number">#2039</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run duration</strong></td>
        <td><code>04m 34s</code></td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Commit</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/dyc3/opentogethertube/commit/b23f085fba622150619bbae4f3e7164e744c1fb1">b23f085fba</a>: feat(client): save sponsorblock settings in browser and apply them on room creat...</code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Committer</strong></td>
        <td><code>Maja</code></td>
      </tr>
    </tbody>
    <tfoot>
      <tr />
      <tr height="48">
        <td colspan="2">
          <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/properties?utm_source=github&utm_medium=failed&utm_campaign=properties">View all properties for this run ↗︎</a>
        </td>
      </tr>
    </tfoot>
  </table>
  <hr />
</details>

<table width="744">
  <thead>
    <tr />
    <tr height="48">
      <th colspan="2" align="left" width="744">Test results</th>
    </tr>
  </thead>
  <tbody>
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/failed.svg" alt="Tests that failed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/overview?reviewViewBy=FAILED&utm_source=github&utm_medium=failed&utm_campaign=failed%20tests">
          <strong>Failures</strong>
        </a>
      </td>
      <td>
        <code><strong>2</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/flaky-v3.svg" alt="Tests that were flaky" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/overview?reviewViewBy=FLAKY&utm_source=github&utm_medium=failed&utm_campaign=flaky%20tests">
          <strong>Flaky</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/pending.svg" alt="Tests that did not run due to a developer annotating a test with .skip" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/test-results?utm_source=github&utm_medium=failed&utm_campaign=pending%20tests&statuses=%5B%7B%22value%22%3A%22PENDING%22%2C%22label%22%3A%22PENDING%22%7D%5D">
          <strong>Pending</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/skipped-v3.svg" alt="Tests that did not run due to a failure in a mocha hook" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/test-results?utm_source=github&utm_medium=failed&utm_campaign=skipped%20tests&statuses=%5B%7B%22value%22%3A%22SKIPPED%22%2C%22label%22%3A%22SKIPPED%22%7D%5D">
          <strong>Skipped</strong>
        </a>
      </td>
      <td>
        <code><strong>1</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="Tests that passed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/3utpz8/runs/2039/overview?utm_source=github&utm_medium=failed&utm_campaign=passed%20tests">
          <strong>Passing</strong>
        </a>
      </td>
      <td>
        <code><strong>24</strong></code>
      </td>
    </tr><tfoot>
    <tr height="48"><td colspan="2"><a href="https://cloud.cypress.io/projects/3utpz8/branches/master/review?utm_source=github&utm_medium=failed&utm_campaign=review%20changes&changeRequest=1829">View all changes introduced in this branch ↗︎</a></td></tr></tfoot>
  </tbody>
</table>






author_association: NONE