From 3f7eba4b27c48fce61d4aa140a419c5e88b8f29c Mon Sep 17 00:00:00 2001 From: Jack Willis Date: Sun, 1 Feb 2026 21:47:42 +0000 Subject: [PATCH 1/3] feat(notifications): added a custom notify email field, managed account now get mail Some people, like me, like to manage peoples Plex account so all they need to worry about is signing in with provided username and password. The problem with this and Seerr is that Seerr assumes that people want to recieve mail on the email field. This is prefilled and non-changeable if users are synced from Plex you cannot change it to the users actual email. In my Plex environment I use my own domain for users login but they will never get mail from that, this now resolves that issue. BREAKING CHANGE: Database Migration Make null rather than empty string Remove Debug Code feat(e-mail notification changes): added a custom notify email field, managed account now get mail Some people, like me, like to manage peoples Plex account so all they need to worry about is signing in with provided username and password. The problem with this and Seerr is that Seerr assumes that people want to recieve mail on the email field. This is prefilled and non-changeable if users are synced from Plex you cannot change it to the users actual email. In my Plex environment I use my own domain for users login but they will never get mail from that, this now resolves that issue. BREAKING CHANGE: Database Migration fix(en.json): update Translations Update translations file due to failed GH Action --- seerr-api.yml | 3 + server/entity/UserSettings.ts | 3 + .../interfaces/api/userSettingsInterfaces.ts | 1 + server/lib/notifications/agents/email.ts | 10 +- ...769971931384-CustomNotifyEmailMigration.ts | 17 + ...769971922216-CustomNotifyEmailMigration.ts | 343 ++++++++++++++++++ server/routes/user/usersettings.ts | 4 + .../UserNotificationsDiscord.tsx | 1 + .../UserNotificationsEmail.tsx | 48 ++- .../UserNotificationsPushbullet.tsx | 1 + .../UserNotificationsPushover.tsx | 1 + .../UserNotificationsTelegram.tsx | 1 + .../UserNotificationsWebPush/index.tsx | 1 + src/i18n/locale/en.json | 3 + 14 files changed, 433 insertions(+), 4 deletions(-) create mode 100644 server/migration/postgres/1769971931384-CustomNotifyEmailMigration.ts create mode 100644 server/migration/sqlite/1769971922216-CustomNotifyEmailMigration.ts diff --git a/seerr-api.yml b/seerr-api.yml index 99ef16cc3c..8da9d073f9 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -1939,6 +1939,9 @@ components: pgpKey: type: string nullable: true + notifyEmil: + type: string + nullable: true discordEnabled: type: boolean discordEnabledTypes: diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 82671fe3b3..4dbeae0748 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -42,6 +42,9 @@ export class UserSettings { @Column({ nullable: true }) public pgpKey?: string; + @Column({ nullable: true }) + public notifyEmail?: string; + @Column({ nullable: true }) public discordId?: string; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 327764618e..19bdcce0da 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -24,6 +24,7 @@ export type NotificationAgentTypes = Record; export interface UserSettingsNotificationsResponse { emailEnabled?: boolean; pgpKey?: string; + notifyEmail?: string; discordEnabled?: boolean; discordEnabledTypes?: number; discordId?: string; diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index 0e373c855f..a397de5517 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -215,20 +215,24 @@ class EmailAgent type: Notification[type], subject: payload.subject, }); - try { const email = new PreparedEmail( this.getSettings(), payload.notifyUser.settings?.pgpKey ); if ( - validator.isEmail(payload.notifyUser.email, { require_tld: false }) + validator.isEmail( + payload.notifyUser.settings?.notifyEmail || + payload.notifyUser.email, + { require_tld: false } + ) ) { await email.send( this.buildMessage( type, payload, - payload.notifyUser.email, + payload.notifyUser.settings?.notifyEmail || + payload.notifyUser.email, payload.notifyUser.displayName ) ); diff --git a/server/migration/postgres/1769971931384-CustomNotifyEmailMigration.ts b/server/migration/postgres/1769971931384-CustomNotifyEmailMigration.ts new file mode 100644 index 0000000000..c6f4975bfd --- /dev/null +++ b/server/migration/postgres/1769971931384-CustomNotifyEmailMigration.ts @@ -0,0 +1,17 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CustomNotifyEmailMigration1769971931384 implements MigrationInterface { + name = 'CustomNotifyEmailMigration1769971931384'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" ADD "notifyEmail" character varying` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" DROP COLUMN "notifyEmail"` + ); + } +} diff --git a/server/migration/sqlite/1769971922216-CustomNotifyEmailMigration.ts b/server/migration/sqlite/1769971922216-CustomNotifyEmailMigration.ts new file mode 100644 index 0000000000..06f8f82c7f --- /dev/null +++ b/server/migration/sqlite/1769971922216-CustomNotifyEmailMigration.ts @@ -0,0 +1,343 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CustomNotifyEmailMigration1769971922216 implements MigrationInterface { + name = 'CustomNotifyEmailMigration1769971922216'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "UQ_6427d07d9a171a3a1ab87480005"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "telegramMessageThreadId" varchar, "notifyEmail" varchar, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId" FROM "user_settings"` + ); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"` + ); + await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`); + await queryRunner.query( + `CREATE TABLE "temporary_watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "watchlist"` + ); + await queryRunner.query(`DROP TABLE "watchlist"`); + await queryRunner.query( + `ALTER TABLE "temporary_watchlist" RENAME TO "watchlist"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE TABLE "temporary_issue_comment" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "message" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "issueId" integer, CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_issue_comment"("id", "message", "createdAt", "updatedAt", "userId", "issueId") SELECT "id", "message", "createdAt", "updatedAt", "userId", "issueId" FROM "issue_comment"` + ); + await queryRunner.query(`DROP TABLE "issue_comment"`); + await queryRunner.query( + `ALTER TABLE "temporary_issue_comment" RENAME TO "issue_comment"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_issue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "problemSeason" integer NOT NULL DEFAULT (0), "problemEpisode" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_issue"("id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById") SELECT "id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById" FROM "issue"` + ); + await queryRunner.query(`DROP TABLE "issue"`); + await queryRunner.query(`ALTER TABLE "temporary_issue" RENAME TO "issue"`); + await queryRunner.query( + `CREATE TABLE "temporary_override_rule" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" varchar, "genre" varchar, "language" varchar, "keywords" varchar, "profileId" integer, "rootFolder" varchar, "tags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP))` + ); + await queryRunner.query( + `INSERT INTO "temporary_override_rule"("id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt") SELECT "id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt" FROM "override_rule"` + ); + await queryRunner.query(`DROP TABLE "override_rule"`); + await queryRunner.query( + `ALTER TABLE "temporary_override_rule" RENAME TO "override_rule"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_season_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "requestId" integer, CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_season_request"("id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId" FROM "season_request"` + ); + await queryRunner.query(`DROP TABLE "season_request"`); + await queryRunner.query( + `ALTER TABLE "temporary_season_request" RENAME TO "season_request"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "media_request"` + ); + await queryRunner.query(`DROP TABLE "media_request"`); + await queryRunner.query( + `ALTER TABLE "temporary_media_request" RENAME TO "media_request"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "user"` + ); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`); + await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`); + await queryRunner.query( + `CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, "blacklistedTags" varchar, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags" FROM "blacklist"` + ); + await queryRunner.query(`DROP TABLE "blacklist"`); + await queryRunner.query( + `ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE TABLE "temporary_season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaId" integer, "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k" FROM "season"` + ); + await queryRunner.query(`DROP TABLE "season"`); + await queryRunner.query( + `ALTER TABLE "temporary_season" RENAME TO "season"` + ); + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query( + `CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "status4k" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaAddedAt" datetime DEFAULT (CURRENT_TIMESTAMP), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k" FROM "media"` + ); + await queryRunner.query(`DROP TABLE "media"`); + await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + await queryRunner.query( + `CREATE TABLE "temporary_discover_slider" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT (0), "enabled" boolean NOT NULL DEFAULT (1), "title" varchar, "data" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP))` + ); + await queryRunner.query( + `INSERT INTO "temporary_discover_slider"("id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt") SELECT "id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt" FROM "discover_slider"` + ); + await queryRunner.query(`DROP TABLE "discover_slider"`); + await queryRunner.query( + `ALTER TABLE "temporary_discover_slider" RENAME TO "discover_slider"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "discover_slider" RENAME TO "temporary_discover_slider"` + ); + await queryRunner.query( + `CREATE TABLE "discover_slider" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT (0), "enabled" boolean NOT NULL DEFAULT (1), "title" varchar, "data" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))` + ); + await queryRunner.query( + `INSERT INTO "discover_slider"("id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt") SELECT "id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt" FROM "temporary_discover_slider"` + ); + await queryRunner.query(`DROP TABLE "temporary_discover_slider"`); + await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`); + await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`); + await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`); + await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`); + await queryRunner.query( + `CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "status4k" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaAddedAt" datetime DEFAULT (CURRENT_TIMESTAMP), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))` + ); + await queryRunner.query( + `INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k" FROM "temporary_media"` + ); + await queryRunner.query(`DROP TABLE "temporary_media"`); + await queryRunner.query( + `CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") ` + ); + await queryRunner.query( + `CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") ` + ); + await queryRunner.query( + `ALTER TABLE "season" RENAME TO "temporary_season"` + ); + await queryRunner.query( + `CREATE TABLE "season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k" FROM "temporary_season"` + ); + await queryRunner.query(`DROP TABLE "temporary_season"`); + await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`); + await queryRunner.query( + `ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"` + ); + await queryRunner.query( + `CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, "blacklistedTags" varchar, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags" FROM "temporary_blacklist"` + ); + await queryRunner.query(`DROP TABLE "temporary_blacklist"`); + await queryRunner.query( + `CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") ` + ); + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`); + await queryRunner.query( + `CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "temporary_user"` + ); + await queryRunner.query(`DROP TABLE "temporary_user"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "media_request" RENAME TO "temporary_media_request"` + ); + await queryRunner.query( + `CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "temporary_media_request"` + ); + await queryRunner.query(`DROP TABLE "temporary_media_request"`); + await queryRunner.query( + `ALTER TABLE "season_request" RENAME TO "temporary_season_request"` + ); + await queryRunner.query( + `CREATE TABLE "season_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestId" integer, CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "season_request"("id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId" FROM "temporary_season_request"` + ); + await queryRunner.query(`DROP TABLE "temporary_season_request"`); + await queryRunner.query( + `ALTER TABLE "override_rule" RENAME TO "temporary_override_rule"` + ); + await queryRunner.query( + `CREATE TABLE "override_rule" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" varchar, "genre" varchar, "language" varchar, "keywords" varchar, "profileId" integer, "rootFolder" varchar, "tags" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))` + ); + await queryRunner.query( + `INSERT INTO "override_rule"("id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt") SELECT "id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt" FROM "temporary_override_rule"` + ); + await queryRunner.query(`DROP TABLE "temporary_override_rule"`); + await queryRunner.query(`ALTER TABLE "issue" RENAME TO "temporary_issue"`); + await queryRunner.query( + `CREATE TABLE "issue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "problemSeason" integer NOT NULL DEFAULT (0), "problemEpisode" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "issue"("id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById") SELECT "id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById" FROM "temporary_issue"` + ); + await queryRunner.query(`DROP TABLE "temporary_issue"`); + await queryRunner.query( + `ALTER TABLE "issue_comment" RENAME TO "temporary_issue_comment"` + ); + await queryRunner.query( + `CREATE TABLE "issue_comment" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "message" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "issueId" integer, CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "issue_comment"("id", "message", "createdAt", "updatedAt", "userId", "issueId") SELECT "id", "message", "createdAt", "updatedAt", "userId", "issueId" FROM "temporary_issue_comment"` + ); + await queryRunner.query(`DROP TABLE "temporary_issue_comment"`); + await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`); + await queryRunner.query( + `ALTER TABLE "watchlist" RENAME TO "temporary_watchlist"` + ); + await queryRunner.query( + `CREATE TABLE "watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "temporary_watchlist"` + ); + await queryRunner.query(`DROP TABLE "temporary_watchlist"`); + await queryRunner.query( + `CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") ` + ); + await queryRunner.query( + `ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "locale" varchar NOT NULL DEFAULT (''), "discoverRegion" varchar, "streamingRegion" varchar, "originalLanguage" varchar, "pgpKey" varchar, "discordId" varchar, "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" varchar, "pushoverSound" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, "telegramMessageThreadId" varchar, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_settings"("id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId") SELECT "id", "locale", "discoverRegion", "streamingRegion", "originalLanguage", "pgpKey", "discordId", "pushbulletAccessToken", "pushoverApplicationToken", "pushoverUserKey", "pushoverSound", "telegramChatId", "telegramSendSilently", "watchlistSyncMovies", "watchlistSyncTv", "notificationTypes", "userId", "telegramMessageThreadId" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE UNIQUE INDEX "UQ_6427d07d9a171a3a1ab87480005" ON "user_push_subscription" ("endpoint", "userId") ` + ); + } +} diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index bd5af746f5..262097e92e 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -537,6 +537,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( return res.status(200).json({ emailEnabled: settings.email.enabled, pgpKey: user.settings?.pgpKey, + notifyEmail: user.settings?.notifyEmail, discordEnabled: settings?.discord.enabled && settings.discord.options.enableMentions, discordEnabledTypes: @@ -589,6 +590,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( user.settings = new UserSettings({ user: req.user, pgpKey: req.body.pgpKey, + notifyEmail: req.body.notifyEmail, discordId: req.body.discordId, pushbulletAccessToken: req.body.pushbulletAccessToken, pushoverApplicationToken: req.body.pushoverApplicationToken, @@ -600,6 +602,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( }); } else { user.settings.pgpKey = req.body.pgpKey; + user.settings.notifyEmail = req.body.notifyEmail; user.settings.discordId = req.body.discordId; user.settings.pushbulletAccessToken = req.body.pushbulletAccessToken; user.settings.pushoverApplicationToken = @@ -621,6 +624,7 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>( return res.status(200).json({ pgpKey: user.settings.pgpKey, + notifyEmail: user.settings.notifyEmail, discordId: user.settings.discordId, pushbulletAccessToken: user.settings.pushbulletAccessToken, pushoverApplicationToken: user.settings.pushoverApplicationToken, diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx index 01650ac0f5..2d62820b7c 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx @@ -70,6 +70,7 @@ const UserNotificationsDiscord = () => { try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, + notifyEmail: data?.notifyEmail, discordId: values.discordId, pushbulletAccessToken: data?.pushbulletAccessToken, pushoverApplicationToken: data?.pushoverApplicationToken, diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx index c1196d5acf..8447aee5d7 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx @@ -12,11 +12,12 @@ import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; import axios from 'axios'; -import { Form, Formik } from 'formik'; +import { Field, Form, Formik } from 'formik'; import { useRouter } from 'next/router'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; +import validator from 'validator'; import * as Yup from 'yup'; const messages = defineMessages( @@ -27,6 +28,10 @@ const messages = defineMessages( pgpPublicKey: 'PGP Public Key', pgpPublicKeyTip: 'Encrypt email messages using OpenPGP', + notifyEmail: 'Custom Notify Email address', + notifyEmailTip: + "Email address to send notifications to if you don't want to send notifications to their login Email", + validationEmail: 'You must provide a valid email address', validationPgpPublicKey: 'You must provide a valid PGP public key', } ); @@ -51,6 +56,14 @@ const UserEmailSettings = () => { /-----BEGIN PGP PUBLIC KEY BLOCK-----.+-----END PGP PUBLIC KEY BLOCK-----/s, intl.formatMessage(messages.validationPgpPublicKey) ), + notifyEmail: Yup.string() + .nullable() + .notRequired() + .test( + 'email', + intl.formatMessage(messages.validationEmail), + (value) => !value || validator.isEmail(value, { require_tld: false }) + ), }); if (!data && !error) { @@ -61,6 +74,7 @@ const UserEmailSettings = () => { { try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: values.pgpKey, + notifyEmail: values.notifyEmail || null, discordId: data?.discordId, pushbulletAccessToken: data?.pushbulletAccessToken, pushoverApplicationToken: data?.pushoverApplicationToken, @@ -134,6 +149,37 @@ const UserEmailSettings = () => { )} +
+ +
+
+ +
+ {errors.notifyEmail && + touched.notifyEmail && + typeof errors.notifyEmail === 'string' && ( +
{errors.notifyEmail}
+ )} +
+
{ try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, + notifyEmail: data?.notifyEmail, discordId: data?.discordId, pushbulletAccessToken: values.pushbulletAccessToken, pushoverApplicationToken: data?.pushoverApplicationToken, diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx index 7ab9204ded..03cd7cd670 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover.tsx @@ -99,6 +99,7 @@ const UserPushoverSettings = () => { try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, + notifyEmail: data?.notifyEmail, discordId: data?.discordId, pushbulletAccessToken: data?.pushbulletAccessToken, pushoverApplicationToken: values.pushoverApplicationToken, diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx index 7d006b6769..07fdd0e41e 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx @@ -93,6 +93,7 @@ const UserTelegramSettings = () => { try { await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, + notifyEmail: data?.notifyEmail, discordId: data?.discordId, pushbulletAccessToken: data?.pushbulletAccessToken, pushoverApplicationToken: data?.pushoverApplicationToken, diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx index d534c0c262..7a7b89e076 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx @@ -256,6 +256,7 @@ const UserWebPushSettings = () => { `/api/v1/user/${user?.id}/settings/notifications`, { pgpKey: data?.pgpKey, + notifyEmail: data?.notifyEmail, discordId: data?.discordId, pushbulletAccessToken: data?.pushbulletAccessToken, pushoverApplicationToken: data?.pushoverApplicationToken, diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 06ad3fecbd..c6ad2e90e6 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1488,6 +1488,8 @@ "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Email notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings", + "components.UserProfile.UserSettings.UserNotificationSettings.notifyEmail": "Custom Notify Email address", + "components.UserProfile.UserSettings.UserNotificationSettings.notifyEmailTip": "Email address to send notifications to if you don't want to send notifications to their login Email", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Encrypt email messages using OpenPGP", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Access Token", @@ -1510,6 +1512,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram notification settings failed to save.", "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid user ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationEmail": "You must provide a valid email address", "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "You must provide a valid PGP public key", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushbulletAccessToken": "You must provide an access token", "components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverApplicationToken": "You must provide a valid application token", From 7ebb8818b7d6d35bcd5bc652f429c8fc9d7c2068 Mon Sep 17 00:00:00 2001 From: Jack Willis Date: Sat, 7 Feb 2026 18:51:03 +0000 Subject: [PATCH 2/3] fix(email.ts, seerr-api.yml): fix Bugs in new code Fixed bugs in new code to ensure notification email is validated, also fixed spelling mistake I made in the API --- seerr-api.yml | 2 +- server/lib/notifications/agents/email.ts | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/seerr-api.yml b/seerr-api.yml index 8da9d073f9..161959c3b2 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -1939,7 +1939,7 @@ components: pgpKey: type: string nullable: true - notifyEmil: + notifyEmail: type: string nullable: true discordEnabled: diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index a397de5517..e7f2aeeb3f 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -220,13 +220,10 @@ class EmailAgent this.getSettings(), payload.notifyUser.settings?.pgpKey ); - if ( - validator.isEmail( - payload.notifyUser.settings?.notifyEmail || - payload.notifyUser.email, - { require_tld: false } - ) - ) { + const userEmail = + payload.notifyUser.settings?.notifyEmail || + payload.notifyUser.email; + if (validator.isEmail(userEmail, { require_tld: false })) { await email.send( this.buildMessage( type, @@ -289,9 +286,10 @@ class EmailAgent this.getSettings(), user.settings?.pgpKey ); - if (validator.isEmail(user.email, { require_tld: false })) { + const userEmail = user.settings?.notifyEmail || user.email; + if (validator.isEmail(userEmail, { require_tld: false })) { await email.send( - this.buildMessage(type, payload, user.email, user.displayName) + this.buildMessage(type, payload, userEmail, user.displayName) ); } else { logger.warn('Invalid email address provided for user', { From 54336f7a9f993ad2c1350bc2b46dee0cc464ca3d Mon Sep 17 00:00:00 2001 From: JackW <53652452+JackW6809@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:08:23 +0000 Subject: [PATCH 3/3] feat(notifications): updated verbiage, use variable instead of re-computing, update api validation I have updated the new verbiage I added to ensure it is in the current users context and not the admins. Re-used a previously declared variable instead of re-computing it. Updated the acceptable format for the API call made. BREAKING CHANGE: API Updated, Notification Call Updated --- seerr-api.yml | 1 + server/lib/notifications/agents/email.ts | 3 +-- .../UserNotificationSettings/UserNotificationsEmail.tsx | 2 +- src/i18n/locale/en.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/seerr-api.yml b/seerr-api.yml index 161959c3b2..cc8f8129df 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -1941,6 +1941,7 @@ components: nullable: true notifyEmail: type: string + format: email nullable: true discordEnabled: type: boolean diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index e7f2aeeb3f..dc9cee94bc 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -228,8 +228,7 @@ class EmailAgent this.buildMessage( type, payload, - payload.notifyUser.settings?.notifyEmail || - payload.notifyUser.email, + userEmail, payload.notifyUser.displayName ) ); diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx index 8447aee5d7..ff3b7aaef8 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx @@ -30,7 +30,7 @@ const messages = defineMessages( 'Encrypt email messages using OpenPGP', notifyEmail: 'Custom Notify Email address', notifyEmailTip: - "Email address to send notifications to if you don't want to send notifications to their login Email", + "Email address to send notifications to if you don't want to send notifications to your login email", validationEmail: 'You must provide a valid email address', validationPgpPublicKey: 'You must provide a valid PGP public key', } diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index c6ad2e90e6..2886032325 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1489,7 +1489,7 @@ "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings", "components.UserProfile.UserSettings.UserNotificationSettings.notifyEmail": "Custom Notify Email address", - "components.UserProfile.UserSettings.UserNotificationSettings.notifyEmailTip": "Email address to send notifications to if you don't want to send notifications to their login Email", + "components.UserProfile.UserSettings.UserNotificationSettings.notifyEmailTip": "Email address to send notifications to if you don't want to send notifications to your login email", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key", "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Encrypt email messages using OpenPGP", "components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Access Token",