+
@@ -232,6 +206,48 @@ function SettingsPage() {
)}
+
+
);
}
diff --git a/bun.lock b/bun.lock
index 5497eff..2687db6 100644
--- a/bun.lock
+++ b/bun.lock
@@ -27,7 +27,7 @@
},
"apps/native": {
"name": "@sofa/native",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@better-auth/expo": "catalog:",
"@expo/metro-runtime": "55.0.6",
@@ -112,7 +112,7 @@
},
"apps/public-api": {
"name": "@sofa/public-api",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@hono/zod-validator": "0.7.6",
"@vercel/firewall": "1.1.2",
@@ -127,7 +127,7 @@
},
"apps/server": {
"name": "@sofa/server",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@orpc/contract": "catalog:",
"@orpc/json-schema": "catalog:",
@@ -153,7 +153,7 @@
},
"apps/web": {
"name": "@sofa/web",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@base-ui/react": "1.3.0",
"@fontsource-variable/dm-sans": "5.2.8",
@@ -215,7 +215,7 @@
},
"packages/api": {
"name": "@sofa/api",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@orpc/contract": "catalog:",
"zod": "catalog:",
@@ -227,7 +227,7 @@
},
"packages/auth": {
"name": "@sofa/auth",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@better-auth/drizzle-adapter": "1.5.6",
"@better-auth/expo": "catalog:",
@@ -244,7 +244,7 @@
},
"packages/config": {
"name": "@sofa/config",
- "version": "0.1.1",
+ "version": "0.1.2",
"devDependencies": {
"@sofa/tsconfig": "workspace:*",
"@types/bun": "catalog:",
@@ -253,7 +253,7 @@
},
"packages/core": {
"name": "@sofa/core",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@orpc/server": "catalog:",
"@sofa/api": "workspace:*",
@@ -277,7 +277,7 @@
},
"packages/db": {
"name": "@sofa/db",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@sofa/config": "workspace:*",
"@sofa/logger": "workspace:*",
@@ -292,7 +292,7 @@
},
"packages/i18n": {
"name": "@sofa/i18n",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@lingui/core": "catalog:",
"@lingui/react": "catalog:",
@@ -310,7 +310,7 @@
},
"packages/logger": {
"name": "@sofa/logger",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"pino": "10.3.1",
"pino-pretty": "13.1.3",
@@ -323,7 +323,7 @@
},
"packages/test": {
"name": "@sofa/test",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@sofa/db": "workspace:*",
"better-sqlite3": "12.8.0",
@@ -338,7 +338,7 @@
},
"packages/tmdb": {
"name": "@sofa/tmdb",
- "version": "0.1.1",
+ "version": "0.1.2",
"dependencies": {
"@sofa/logger": "workspace:*",
"openapi-fetch": "0.17.0",
@@ -351,10 +351,14 @@
},
"packages/tsconfig": {
"name": "@sofa/tsconfig",
- "version": "0.1.1",
+ "version": "0.1.2",
},
},
"trustedDependencies": [
+ "sharp",
+ "better-sqlite3",
+ "esbuild",
+ "msw",
"@sentry/cli",
],
"catalog": {
@@ -383,8 +387,8 @@
"@vitest/browser-playwright": "4.1.1",
"@vitest/coverage-v8": "4.1.1",
"better-auth": "1.5.6",
- "drizzle-kit": "1.0.0-beta.18-7eb39f0",
- "drizzle-orm": "1.0.0-beta.18-7eb39f0",
+ "drizzle-kit": "1.0.0-beta.19",
+ "drizzle-orm": "1.0.0-beta.19",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "3.5.0",
@@ -1875,9 +1879,9 @@
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
- "drizzle-kit": ["drizzle-kit@1.0.0-beta.18-7eb39f0", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-F3ozFBjlUr/VWHR0NIz06jM9cKmfFafoXnCDhKvwjhH91hRK/l6sxIjiDOwMxjGuoJH+/p9W8DE6juoGvn7kXA=="],
+ "drizzle-kit": ["drizzle-kit@1.0.0-beta.19", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-LO9cz8iipjyOROaBLmVAhu1WT4JGd8QKSc0XYhnO0FGXP5FappHII52lHcxfj2j+vVuDCTjoqeTTvW1N9NJYZQ=="],
- "drizzle-orm": ["drizzle-orm@1.0.0-beta.18-7eb39f0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-piQ+esfm3krAiwXw3d8U559VdBjzA178jqazAkb0tSo7lJPZUL4SbB5uqg77ZUuCvY37hJuQ/o/gQoMX4m0E6Q=="],
+ "drizzle-orm": ["drizzle-orm@1.0.0-beta.19", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-wL8FoHl9NRVYdrescFWyAVY5rNuEmbY4KDqO6Qo4RTVt1rYMbz7btq5Mhg91GN3x+qeUbQ83l9QkHYd4QmVTvg=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -3199,6 +3203,8 @@
"@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "@better-auth/drizzle-adapter/drizzle-orm": ["drizzle-orm@1.0.0-beta.18-7eb39f0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-piQ+esfm3krAiwXw3d8U559VdBjzA178jqazAkb0tSo7lJPZUL4SbB5uqg77ZUuCvY37hJuQ/o/gQoMX4m0E6Q=="],
+
"@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
@@ -3351,6 +3357,10 @@
"babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
+ "better-auth/drizzle-kit": ["drizzle-kit@1.0.0-beta.18-7eb39f0", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-F3ozFBjlUr/VWHR0NIz06jM9cKmfFafoXnCDhKvwjhH91hRK/l6sxIjiDOwMxjGuoJH+/p9W8DE6juoGvn7kXA=="],
+
+ "better-auth/drizzle-orm": ["drizzle-orm@1.0.0-beta.18-7eb39f0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-piQ+esfm3krAiwXw3d8U559VdBjzA178jqazAkb0tSo7lJPZUL4SbB5uqg77ZUuCvY37hJuQ/o/gQoMX4m0E6Q=="],
+
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
diff --git a/docs/public/openapi.json b/docs/public/openapi.json
index f1390cf..3d5bcc5 100644
--- a/docs/public/openapi.json
+++ b/docs/public/openapi.json
@@ -2046,7 +2046,7 @@
},
"offerType": {
"type": "string",
- "description": "Offer type: flatrate, rent, buy, free, ads"
+ "description": "Offer category: stream or purchase"
},
"watchUrl": {
"anyOf": [
@@ -4343,16 +4343,12 @@
"type": "string",
"description": "Platform ID"
},
- "tmdbProviderId": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "description": "TMDB provider ID"
+ "tmdbProviderIds": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ },
+ "description": "TMDB provider IDs"
},
"name": {
"type": "string",
@@ -4372,7 +4368,7 @@
},
"required": [
"id",
- "tmdbProviderId",
+ "tmdbProviderIds",
"name",
"logoPath"
]
@@ -4861,14 +4857,12 @@
"allowReserved": true
},
{
- "name": "providerId",
+ "name": "platformId",
"in": "query",
"required": false,
"schema": {
- "type": "integer",
- "minimum": -9007199254740991,
- "maximum": 9007199254740991,
- "description": "TMDB watch provider ID"
+ "type": "string",
+ "description": "Platform ID to filter by"
},
"allowEmptyValue": true,
"allowReserved": true
@@ -6773,16 +6767,12 @@
"type": "string",
"description": "Display name"
},
- "tmdbProviderId": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ],
- "description": "TMDB provider ID (null for custom platforms)"
+ "tmdbProviderIds": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ },
+ "description": "TMDB provider IDs mapped to this platform"
},
"logoPath": {
"anyOf": [
@@ -6795,17 +6785,17 @@
],
"description": "Logo image path"
},
- "displayOrder": {
- "type": "number",
- "description": "Sort order"
+ "isSubscription": {
+ "type": "boolean",
+ "description": "True for subscription services, false for purchase/rental stores"
}
},
"required": [
"id",
"name",
- "tmdbProviderId",
+ "tmdbProviderIds",
"logoPath",
- "displayOrder"
+ "isSubscription"
],
"description": "A streaming platform"
}
diff --git a/package.json b/package.json
index f308629..ff367ee 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,8 @@
"@vitest/browser-playwright": "4.1.1",
"@vitest/coverage-v8": "4.1.1",
"better-auth": "1.5.6",
- "drizzle-kit": "1.0.0-beta.18-7eb39f0",
- "drizzle-orm": "1.0.0-beta.18-7eb39f0",
+ "drizzle-kit": "1.0.0-beta.19",
+ "drizzle-orm": "1.0.0-beta.19",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "3.5.0",
@@ -58,7 +58,8 @@
"clean": "rm -rf .turbo \"apps/*/.turbo\" \"apps/*/node_modules\" \"apps/*/*.tsbuildinfo\" \"apps/*/dist\" \"packages/*/.turbo\" \"packages/*/node_modules\" \"packages/*/*.tsbuildinfo\" node_modules bun.lock",
"generate:openapi": "bun scripts/generate-openapi-spec.ts && cd docs && bun run generate:api-docs",
"i18n:extract": "lingui extract",
- "i18n:claude": "bun packages/i18n/scripts/claude.ts"
+ "i18n:claude": "bun packages/i18n/scripts/claude.ts",
+ "sync:providers": "bun scripts/sync-tmdb-providers.ts"
},
"devDependencies": {
"@e18e/eslint-plugin": "0.3.0",
@@ -82,6 +83,10 @@
},
"packageManager": "bun@1.3.11",
"trustedDependencies": [
- "@sentry/cli"
+ "@sentry/cli",
+ "better-sqlite3",
+ "esbuild",
+ "msw",
+ "sharp"
]
}
diff --git a/packages/api/src/schemas.ts b/packages/api/src/schemas.ts
index 1f86714..76f8e33 100644
--- a/packages/api/src/schemas.ts
+++ b/packages/api/src/schemas.ts
@@ -106,7 +106,7 @@ export const DiscoverInput = z
.regex(/^[a-z]{2}$/)
.optional()
.describe("ISO 639-1 original language code"),
- providerId: z.number().int().optional().describe("TMDB watch provider ID"),
+ platformId: z.string().optional().describe("Platform ID to filter by"),
})
.merge(PageParam)
.meta({ description: "Genre-based discovery filters" });
@@ -283,7 +283,7 @@ export const AvailabilityOfferSchema = z
platformId: z.string().describe("Platform ID"),
providerName: z.string().describe("Display name (e.g. Netflix, Hulu)"),
logoPath: z.string().nullable().describe("Provider logo image path"),
- offerType: z.string().describe("Offer type: flatrate, rent, buy, free, ads"),
+ offerType: z.string().describe("Offer category: stream or purchase"),
watchUrl: z.string().nullable().describe("Direct link to watch on this provider"),
isUserSubscribed: z.boolean().describe("Whether the user subscribes to this platform"),
})
@@ -293,9 +293,11 @@ export const PlatformSchema = z
.object({
id: z.string().describe("Platform ID"),
name: z.string().describe("Display name"),
- tmdbProviderId: z.number().nullable().describe("TMDB provider ID (null for custom platforms)"),
+ tmdbProviderIds: z.array(z.number()).describe("TMDB provider IDs mapped to this platform"),
logoPath: z.string().nullable().describe("Logo image path"),
- displayOrder: z.number().describe("Sort order"),
+ isSubscription: z
+ .boolean()
+ .describe("True for subscription services, false for purchase/rental stores"),
})
.meta({ description: "A streaming platform" });
@@ -621,7 +623,7 @@ export const WatchProvidersOutput = z
providers: z.array(
z.object({
id: z.string().describe("Platform ID"),
- tmdbProviderId: z.number().nullable().describe("TMDB provider ID"),
+ tmdbProviderIds: z.array(z.number()).describe("TMDB provider IDs"),
name: z.string().describe("Provider display name"),
logoPath: z.string().nullable().describe("Provider logo image path"),
}),
diff --git a/packages/core/src/integrations.ts b/packages/core/src/integrations.ts
index efa9afe..45321a1 100644
--- a/packages/core/src/integrations.ts
+++ b/packages/core/src/integrations.ts
@@ -2,7 +2,7 @@ import {
deleteIntegrationByUserAndProvider,
getIntegrationByToken,
getIntegrationByUserAndProvider,
- getRecentEventsForIntegration,
+ getRecentEventsForIntegrations,
getUserIntegrations,
insertIntegration,
regenerateIntegrationToken,
@@ -38,11 +38,8 @@ function serializeIntegration(row: {
export function listUserIntegrations(userId: string) {
const userIntegrations = getUserIntegrations(userId);
- const eventsByIntegration = new Map
>();
- for (const integration of userIntegrations) {
- const events = getRecentEventsForIntegration(integration.id);
- eventsByIntegration.set(integration.id, events);
- }
+ const integrationIds = userIntegrations.map((i) => i.id);
+ const eventsByIntegration = getRecentEventsForIntegrations(integrationIds);
const result = userIntegrations.map((integration) => {
const events = eventsByIntegration.get(integration.id) ?? [];
diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts
index 259f666..282eb2d 100644
--- a/packages/core/src/metadata.ts
+++ b/packages/core/src/metadata.ts
@@ -684,19 +684,66 @@ async function ensureEnriched(
return false;
}
+const STREAM_TYPES = new Set(["flatrate", "free", "ads"]);
+const STREAM_PRIORITY: Record = { flatrate: 0, free: 1, ads: 2 };
+const PURCHASE_PRIORITY: Record = { rent: 0, buy: 1 };
+
function readAvailability(
titleId: string,
titleName: string,
userPlatformIds?: Set,
): AvailabilityOffer[] {
- return getAvailabilityOffersForTitle(titleId).map((a) => ({
- platformId: a.platformId,
- providerName: a.providerName,
- logoPath: tmdbImageUrl(a.logoPath, "logos"),
- offerType: a.offerType,
- watchUrl: generateProviderUrl(a.urlTemplate, titleName),
- isUserSubscribed: userPlatformIds ? userPlatformIds.has(a.platformId) : false,
- }));
+ const raw = getAvailabilityOffersForTitle(titleId);
+
+ // Group by platformId to deduplicate
+ const byPlatform = new Map();
+ for (const offer of raw) {
+ let list = byPlatform.get(offer.platformId);
+ if (!list) {
+ list = [];
+ byPlatform.set(offer.platformId, list);
+ }
+ list.push(offer);
+ }
+
+ const result: AvailabilityOffer[] = [];
+
+ for (const [platformId, offers] of byPlatform) {
+ const streamOffers = offers.filter((o) => STREAM_TYPES.has(o.offerType));
+ const purchaseOffers = offers.filter((o) => !STREAM_TYPES.has(o.offerType));
+
+ // Emit one "stream" entry per platform (best offer type wins)
+ if (streamOffers.length > 0) {
+ const best = streamOffers.sort(
+ (a, b) => (STREAM_PRIORITY[a.offerType] ?? 99) - (STREAM_PRIORITY[b.offerType] ?? 99),
+ )[0];
+ result.push({
+ platformId,
+ providerName: best.providerName,
+ logoPath: tmdbImageUrl(best.logoPath, "logos"),
+ offerType: "stream",
+ watchUrl: generateProviderUrl(best.urlTemplate, titleName),
+ isUserSubscribed: userPlatformIds ? userPlatformIds.has(platformId) : false,
+ });
+ }
+
+ // Emit one "purchase" entry per platform (rent preferred over buy)
+ if (purchaseOffers.length > 0) {
+ const best = purchaseOffers.sort(
+ (a, b) => (PURCHASE_PRIORITY[a.offerType] ?? 99) - (PURCHASE_PRIORITY[b.offerType] ?? 99),
+ )[0];
+ result.push({
+ platformId,
+ providerName: best.providerName,
+ logoPath: tmdbImageUrl(best.logoPath, "logos"),
+ offerType: "purchase",
+ watchUrl: generateProviderUrl(best.urlTemplate, titleName),
+ isUserSubscribed: userPlatformIds ? userPlatformIds.has(platformId) : false,
+ });
+ }
+ }
+
+ return result;
}
export async function getOrFetchTitle(
diff --git a/packages/core/src/platforms.ts b/packages/core/src/platforms.ts
index a2f199b..d0a6e25 100644
--- a/packages/core/src/platforms.ts
+++ b/packages/core/src/platforms.ts
@@ -1,5 +1,7 @@
import {
getAllPlatforms,
+ getTmdbProviderIdsByPlatformIds,
+ getTmdbProviderIdsForPlatform,
getUserPlatformIds,
getUserPlatforms,
hasUserPlatforms,
@@ -29,3 +31,11 @@ export function updateUserPlatforms(userId: string, platformIds: string[]): void
export function hasUserSetPlatforms(userId: string): boolean {
return hasUserPlatforms(userId);
}
+
+export function getPlatformTmdbIds(platformId: string): number[] {
+ return getTmdbProviderIdsForPlatform(platformId);
+}
+
+export function getPlatformTmdbIdMap(platformIds: string[]): Map {
+ return getTmdbProviderIdsByPlatformIds(platformIds);
+}
diff --git a/packages/core/src/system-health.ts b/packages/core/src/system-health.ts
index a884de9..dbc3f38 100644
--- a/packages/core/src/system-health.ts
+++ b/packages/core/src/system-health.ts
@@ -2,8 +2,7 @@ import { access, constants, readdir } from "node:fs/promises";
import path from "node:path";
import { CACHE_DIR, DATA_DIR, DATABASE_URL, TMDB_API_BASE_URL } from "@sofa/config";
-import { getLatestCronRun, getTableCounts } from "@sofa/db/queries/system-health";
-import type { cronRuns } from "@sofa/db/schema";
+import { getLatestCronRuns, getTableCounts } from "@sofa/db/queries/system-health";
import { listBackups } from "./backup";
import { imageCacheEnabled } from "./image-cache";
@@ -153,12 +152,7 @@ function getJobsHealth(): SystemHealthData["jobs"] {
const schedules = _getJobSchedules?.() ?? [];
const scheduleMap = new Map(schedules.map((s) => [s.jobName, s]));
- // Fetch only the latest cron run per job (index-optimized LIMIT 1 each)
- const latestByJob = new Map();
- for (const jobName of JOB_NAMES) {
- const latest = getLatestCronRun(jobName);
- if (latest) latestByJob.set(jobName, latest);
- }
+ const latestByJob = getLatestCronRuns([...JOB_NAMES]);
return JOB_NAMES.map((jobName) => {
const latest = latestByJob.get(jobName);
diff --git a/packages/core/src/tracking.ts b/packages/core/src/tracking.ts
index 717ba0a..60aa448 100644
--- a/packages/core/src/tracking.ts
+++ b/packages/core/src/tracking.ts
@@ -11,9 +11,10 @@ import {
getAllEpisodeIdsForTitle,
getEpisodeProgressByTitleIds as getEpisodeProgressByTitleIdsQuery,
getEpisodeTitleId,
+ getEpisodeTitleIds,
getExistingEpisodeWatchIds,
getSeasonById,
- getSeasonEpisodes,
+ getSeasonEpisodeIds,
getTitleStatus,
getUserStatusesByTitleIds as getUserStatusesByTitleIdsQuery,
getUserTitleInfo as getUserTitleInfoQuery,
@@ -91,11 +92,8 @@ export function logEpisodeWatchBatch(
batchInsertEpisodeWatchesTransaction(userId, episodeIds, source, watchedAt);
// Auto-set title status to in_progress for affected titles
- const titleIds = new Set();
- for (const episodeId of episodeIds) {
- const titleId = getEpisodeTitleId(episodeId);
- if (titleId) titleIds.add(titleId);
- }
+ const episodeTitleMap = getEpisodeTitleIds(episodeIds);
+ const titleIds = new Set(episodeTitleMap.values());
for (const titleId of titleIds) {
const existing = getTitleStatus(userId, titleId);
if (!existing || existing.status === "watchlist") {
@@ -140,9 +138,7 @@ export function unwatchEpisode(userId: string, episodeId: string) {
}
export function unwatchSeason(userId: string, seasonId: string) {
- const seasonEps = getSeasonEpisodes(seasonId);
-
- const epIds = seasonEps.map((ep) => ep.id);
+ const epIds = getSeasonEpisodeIds(seasonId);
if (epIds.length > 0) {
deleteEpisodeWatches(userId, epIds);
}
@@ -271,9 +267,5 @@ export function quickAddTitle(
}
export function watchSeason(userId: string, seasonId: string): void {
- const seasonEps = getSeasonEpisodes(seasonId);
- logEpisodeWatchBatch(
- userId,
- seasonEps.map((ep) => ep.id),
- );
+ logEpisodeWatchBatch(userId, getSeasonEpisodeIds(seasonId));
}
diff --git a/packages/core/src/webhooks.ts b/packages/core/src/webhooks.ts
index 817aea3..2bb383c 100644
--- a/packages/core/src/webhooks.ts
+++ b/packages/core/src/webhooks.ts
@@ -2,8 +2,7 @@ import { findEpisodeBySeasonAndNumber, findSeasonByTitleAndNumber } from "@sofa/
import {
getRecentEpisodeWatch,
getRecentMovieWatch,
- insertIntegrationEvent,
- updateIntegrationLastEvent,
+ insertIntegrationEventTransaction,
} from "@sofa/db/queries/webhooks";
import { createLogger } from "@sofa/logger";
import { getTvDetails } from "@sofa/tmdb/client";
@@ -189,22 +188,23 @@ function logEvent(
status: "success" | "ignored" | "error",
errorMessage?: string,
) {
- insertIntegrationEvent({
- integrationId: connectionId,
- eventType:
- event?.provider === "plex"
- ? "media.scrobble"
- : event?.provider === "emby"
- ? "playback.stop"
- : "PlaybackStop",
- mediaType: event?.mediaType ?? null,
- mediaTitle: event?.title ?? null,
- status,
- errorMessage: errorMessage ?? null,
- receivedAt: new Date(),
- });
-
- updateIntegrationLastEvent(connectionId);
+ insertIntegrationEventTransaction(
+ {
+ integrationId: connectionId,
+ eventType:
+ event?.provider === "plex"
+ ? "media.scrobble"
+ : event?.provider === "emby"
+ ? "playback.stop"
+ : "PlaybackStop",
+ mediaType: event?.mediaType ?? null,
+ mediaTitle: event?.title ?? null,
+ status,
+ errorMessage: errorMessage ?? null,
+ receivedAt: new Date(),
+ },
+ connectionId,
+ );
}
// ─── Main Processing ────────────────────────────────────────────────
diff --git a/packages/core/test/upcoming.test.ts b/packages/core/test/upcoming.test.ts
index e740307..450d3e1 100644
--- a/packages/core/test/upcoming.test.ts
+++ b/packages/core/test/upcoming.test.ts
@@ -294,7 +294,35 @@ describe("streaming provider", () => {
});
});
- test("returns null when no flatrate provider exists", () => {
+ test("attaches ads-supported streaming provider", () => {
+ const tomorrow = daysFromNow(1);
+ insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
+ insertStatus("user-1", "tv-1", "in_progress");
+ const pId = insertPlatform({ id: "p-tubi", name: "Tubi", tmdbProviderId: 73 });
+ insertTitleAvailability("tv-1", pId, { offerType: "ads" });
+
+ const result = getUpcomingFeed("user-1", { days: 7 });
+ expect(result.items[0].streamingProvider).toEqual({
+ platformId: "p-tubi",
+ providerName: "Tubi",
+ logoPath: "/logo.png",
+ });
+ });
+
+ test("prefers flatrate over ads when both exist", () => {
+ const tomorrow = daysFromNow(1);
+ insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
+ insertStatus("user-1", "tv-1", "in_progress");
+ const pAds = insertPlatform({ id: "p-tubi", name: "Tubi", tmdbProviderId: 73 });
+ const pFlat = insertPlatform({ id: "p-netflix", name: "Netflix", tmdbProviderId: 8 });
+ insertTitleAvailability("tv-1", pAds, { offerType: "ads" });
+ insertTitleAvailability("tv-1", pFlat, { offerType: "flatrate" });
+
+ const result = getUpcomingFeed("user-1", { days: 7 });
+ expect(result.items[0].streamingProvider!.platformId).toBe("p-netflix");
+ });
+
+ test("returns null when only purchase providers exist", () => {
const tomorrow = daysFromNow(1);
insertTvShow("tv-1", 100, 1, 1, { airDates: [tomorrow] });
insertStatus("user-1", "tv-1", "in_progress");
diff --git a/packages/db/drizzle/20260324210721_small_prowler/migration.sql b/packages/db/drizzle/20260324210721_small_prowler/migration.sql
new file mode 100644
index 0000000..06c0054
--- /dev/null
+++ b/packages/db/drizzle/20260324210721_small_prowler/migration.sql
@@ -0,0 +1,13 @@
+CREATE TABLE `platformTmdbIds` (
+ `platformId` text NOT NULL,
+ `tmdbProviderId` integer NOT NULL,
+ CONSTRAINT `fk_platformTmdbIds_platformId_platforms_id_fk` FOREIGN KEY (`platformId`) REFERENCES `platforms`(`id`) ON DELETE CASCADE
+);
+--> statement-breakpoint
+INSERT INTO `platformTmdbIds` (`platformId`, `tmdbProviderId`)
+SELECT `id`, `tmdbProviderId` FROM `platforms` WHERE `tmdbProviderId` IS NOT NULL;
+--> statement-breakpoint
+DROP INDEX IF EXISTS `platforms_tmdbProviderId_unique`;--> statement-breakpoint
+CREATE UNIQUE INDEX `platformTmdbIds_tmdbProviderId_unique` ON `platformTmdbIds` (`tmdbProviderId`);--> statement-breakpoint
+CREATE INDEX `platformTmdbIds_platformId` ON `platformTmdbIds` (`platformId`);--> statement-breakpoint
+ALTER TABLE `platforms` DROP COLUMN `tmdbProviderId`;
diff --git a/packages/db/drizzle/20260324210721_small_prowler/snapshot.json b/packages/db/drizzle/20260324210721_small_prowler/snapshot.json
new file mode 100644
index 0000000..e974267
--- /dev/null
+++ b/packages/db/drizzle/20260324210721_small_prowler/snapshot.json
@@ -0,0 +1,3420 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "364cbbc8-2e6e-4ebb-9e52-4fea9e88b994",
+ "prevIds": [
+ "09684273-fbee-43c5-ac9c-95a0882e6302"
+ ],
+ "ddl": [
+ {
+ "name": "account",
+ "entityType": "tables"
+ },
+ {
+ "name": "appSettings",
+ "entityType": "tables"
+ },
+ {
+ "name": "cronRuns",
+ "entityType": "tables"
+ },
+ {
+ "name": "episodes",
+ "entityType": "tables"
+ },
+ {
+ "name": "genres",
+ "entityType": "tables"
+ },
+ {
+ "name": "importJobs",
+ "entityType": "tables"
+ },
+ {
+ "name": "integrationEvents",
+ "entityType": "tables"
+ },
+ {
+ "name": "integrations",
+ "entityType": "tables"
+ },
+ {
+ "name": "personFilmography",
+ "entityType": "tables"
+ },
+ {
+ "name": "persons",
+ "entityType": "tables"
+ },
+ {
+ "name": "platformTmdbIds",
+ "entityType": "tables"
+ },
+ {
+ "name": "platforms",
+ "entityType": "tables"
+ },
+ {
+ "name": "seasons",
+ "entityType": "tables"
+ },
+ {
+ "name": "session",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleAvailability",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleCast",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleGenres",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleRecommendations",
+ "entityType": "tables"
+ },
+ {
+ "name": "titles",
+ "entityType": "tables"
+ },
+ {
+ "name": "user",
+ "entityType": "tables"
+ },
+ {
+ "name": "userEpisodeWatches",
+ "entityType": "tables"
+ },
+ {
+ "name": "userMovieWatches",
+ "entityType": "tables"
+ },
+ {
+ "name": "userPlatforms",
+ "entityType": "tables"
+ },
+ {
+ "name": "userRatings",
+ "entityType": "tables"
+ },
+ {
+ "name": "userTitleStatus",
+ "entityType": "tables"
+ },
+ {
+ "name": "verification",
+ "entityType": "tables"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "providerId",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "accessToken",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refreshToken",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "idToken",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "accessTokenExpiresAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refreshTokenExpiresAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "scope",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "password",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "key",
+ "entityType": "columns",
+ "table": "appSettings"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "appSettings"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "jobName",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "startedAt",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "finishedAt",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "durationMs",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "errorMessage",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seasonId",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "episodeNumber",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "overview",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stillPath",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stillThumbHash",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "airDate",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "runtimeMinutes",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "genres"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "genres"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "payload",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "importWatches",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "importWatchlist",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "importRatings",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "totalItems",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "processedItems",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "importedCount",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "skippedCount",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "failedCount",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "currentMessage",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "errors",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "warnings",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "startedAt",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "finishedAt",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "integrationId",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "eventType",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "mediaType",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "mediaTitle",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "errorMessage",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "receivedAt",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provider",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastEventAt",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "personId",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "character",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Acting'",
+ "generated": null,
+ "name": "department",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "job",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "displayOrder",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tmdbId",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "biography",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "birthday",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "deathday",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "placeOfBirth",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "profilePath",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "profileThumbHash",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "knownForDepartment",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "real",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "popularity",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "imdbId",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "filmographyLastFetchedAt",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "platformId",
+ "entityType": "columns",
+ "table": "platformTmdbIds"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tmdbProviderId",
+ "entityType": "columns",
+ "table": "platformTmdbIds"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "logoPath",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "urlTemplate",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "displayOrder",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seasonNumber",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "overview",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterPath",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterThumbHash",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "airDate",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expiresAt",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ipAddress",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userAgent",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "impersonatedBy",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "platformId",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "offerType",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'US'",
+ "generated": null,
+ "name": "region",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "personId",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "character",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Acting'",
+ "generated": null,
+ "name": "department",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "job",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "displayOrder",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "episodeCount",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleGenres"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "genreId",
+ "entityType": "columns",
+ "table": "titleGenres"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "recommendedTitleId",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "rank",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tmdbId",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tvdbId",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "title",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "originalTitle",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "overview",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "releaseDate",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "firstAirDate",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterPath",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterThumbHash",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backdropPath",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backdropThumbHash",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "real",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "popularity",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "real",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "voteAverage",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "voteCount",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "contentRating",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "imdbId",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "originalLanguage",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "runtimeMinutes",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "colorPalette",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "trailerVideoKey",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "emailVerified",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "image",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'user'",
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "banned",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "banReason",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "banExpires",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "episodeId",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "watchedAt",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'manual'",
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "watchedAt",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'manual'",
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userPlatforms"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "platformId",
+ "entityType": "columns",
+ "table": "userPlatforms"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ratingStars",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ratedAt",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "addedAt",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "identifier",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expiresAt",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_account_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "account"
+ },
+ {
+ "columns": [
+ "seasonId"
+ ],
+ "tableTo": "seasons",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_episodes_seasonId_seasons_id_fk",
+ "entityType": "fks",
+ "table": "episodes"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_importJobs_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ "integrationId"
+ ],
+ "tableTo": "integrations",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_integrationEvents_integrationId_integrations_id_fk",
+ "entityType": "fks",
+ "table": "integrationEvents"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_integrations_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "integrations"
+ },
+ {
+ "columns": [
+ "personId"
+ ],
+ "tableTo": "persons",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_personFilmography_personId_persons_id_fk",
+ "entityType": "fks",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_personFilmography_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ "platformId"
+ ],
+ "tableTo": "platforms",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_platformTmdbIds_platformId_platforms_id_fk",
+ "entityType": "fks",
+ "table": "platformTmdbIds"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_seasons_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "seasons"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_session_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "session"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleAvailability_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ "platformId"
+ ],
+ "tableTo": "platforms",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleAvailability_platformId_platforms_id_fk",
+ "entityType": "fks",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleCast_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ "personId"
+ ],
+ "tableTo": "persons",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleCast_personId_persons_id_fk",
+ "entityType": "fks",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleGenres_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ "genreId"
+ ],
+ "tableTo": "genres",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleGenres_genreId_genres_id_fk",
+ "entityType": "fks",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleRecommendations_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ "recommendedTitleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleRecommendations_recommendedTitleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userEpisodeWatches_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ "episodeId"
+ ],
+ "tableTo": "episodes",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userEpisodeWatches_episodeId_episodes_id_fk",
+ "entityType": "fks",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userMovieWatches_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userMovieWatches_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userPlatforms_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ "platformId"
+ ],
+ "tableTo": "platforms",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userPlatforms_platformId_platforms_id_fk",
+ "entityType": "fks",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userRatings_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userRatings"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userRatings_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "userRatings"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userTitleStatus_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userTitleStatus_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "account_pk",
+ "table": "account",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "key"
+ ],
+ "nameExplicit": false,
+ "name": "appSettings_pk",
+ "table": "appSettings",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "cronRuns_pk",
+ "table": "cronRuns",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "episodes_pk",
+ "table": "episodes",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "genres_pk",
+ "table": "genres",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "importJobs_pk",
+ "table": "importJobs",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "integrationEvents_pk",
+ "table": "integrationEvents",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "integrations_pk",
+ "table": "integrations",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "personFilmography_pk",
+ "table": "personFilmography",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "persons_pk",
+ "table": "persons",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "platforms_pk",
+ "table": "platforms",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "seasons_pk",
+ "table": "seasons",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "session_pk",
+ "table": "session",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "titleCast_pk",
+ "table": "titleCast",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "titles_pk",
+ "table": "titles",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "user_pk",
+ "table": "user",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "userEpisodeWatches_pk",
+ "table": "userEpisodeWatches",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "userMovieWatches_pk",
+ "table": "userMovieWatches",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "verification_pk",
+ "table": "verification",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "jobName",
+ "isExpression": false
+ },
+ {
+ "value": "startedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "cronRuns_jobName_startedAt",
+ "entityType": "indexes",
+ "table": "cronRuns"
+ },
+ {
+ "columns": [
+ {
+ "value": "seasonId",
+ "isExpression": false
+ },
+ {
+ "value": "episodeNumber",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "episodes_seasonId_episodeNumber",
+ "entityType": "indexes",
+ "table": "episodes"
+ },
+ {
+ "columns": [
+ {
+ "value": "airDate",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "episodes_airDate",
+ "entityType": "indexes",
+ "table": "episodes"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "createdAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "importJobs_userId_createdAt",
+ "entityType": "indexes",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ {
+ "value": "status",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "importJobs_status",
+ "entityType": "indexes",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": "\"importJobs\".\"status\" in ('pending', 'running')",
+ "origin": "manual",
+ "name": "importJobs_active_user",
+ "entityType": "indexes",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ {
+ "value": "integrationId",
+ "isExpression": false
+ },
+ {
+ "value": "receivedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "integrationEvents_integrationId_receivedAt",
+ "entityType": "indexes",
+ "table": "integrationEvents"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "provider",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "integrations_userId_provider",
+ "entityType": "indexes",
+ "table": "integrations"
+ },
+ {
+ "columns": [
+ {
+ "value": "token",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "integrations_token",
+ "entityType": "indexes",
+ "table": "integrations"
+ },
+ {
+ "columns": [
+ {
+ "value": "personId",
+ "isExpression": false
+ },
+ {
+ "value": "displayOrder",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "personFilmography_personId_displayOrder",
+ "entityType": "indexes",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "personFilmography_titleId",
+ "entityType": "indexes",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ {
+ "value": "tmdbId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "persons_tmdbId_unique",
+ "entityType": "indexes",
+ "table": "persons"
+ },
+ {
+ "columns": [
+ {
+ "value": "name",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "persons_name",
+ "entityType": "indexes",
+ "table": "persons"
+ },
+ {
+ "columns": [
+ {
+ "value": "tmdbProviderId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "platformTmdbIds_tmdbProviderId_unique",
+ "entityType": "indexes",
+ "table": "platformTmdbIds"
+ },
+ {
+ "columns": [
+ {
+ "value": "platformId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "platformTmdbIds_platformId",
+ "entityType": "indexes",
+ "table": "platformTmdbIds"
+ },
+ {
+ "columns": [
+ {
+ "value": "displayOrder",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "platforms_displayOrder",
+ "entityType": "indexes",
+ "table": "platforms"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "seasonNumber",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "seasons_titleId_seasonNumber",
+ "entityType": "indexes",
+ "table": "seasons"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "platformId",
+ "isExpression": false
+ },
+ {
+ "value": "offerType",
+ "isExpression": false
+ },
+ {
+ "value": "region",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleAvailability_unique",
+ "entityType": "indexes",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleAvailability_titleId",
+ "entityType": "indexes",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ {
+ "value": "platformId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleAvailability_platformId",
+ "entityType": "indexes",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "personId",
+ "isExpression": false
+ },
+ {
+ "value": "department",
+ "isExpression": false
+ },
+ {
+ "value": "character",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleCast_unique",
+ "entityType": "indexes",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "displayOrder",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleCast_titleId_displayOrder",
+ "entityType": "indexes",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ {
+ "value": "personId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleCast_personId",
+ "entityType": "indexes",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "genreId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleGenres_titleId_genreId",
+ "entityType": "indexes",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ {
+ "value": "genreId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleGenres_genreId",
+ "entityType": "indexes",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "recommendedTitleId",
+ "isExpression": false
+ },
+ {
+ "value": "source",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleRecommendations_unique",
+ "entityType": "indexes",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "rank",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleRecommendations_titleId_rank",
+ "entityType": "indexes",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ {
+ "value": "tmdbId",
+ "isExpression": false
+ },
+ {
+ "value": "type",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_tmdbId_type_unique",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "type",
+ "isExpression": false
+ },
+ {
+ "value": "releaseDate",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_type_releaseDate",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "type",
+ "isExpression": false
+ },
+ {
+ "value": "firstAirDate",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_type_firstAirDate",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "lastFetchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_lastFetchedAt",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "type",
+ "isExpression": false
+ },
+ {
+ "value": "status",
+ "isExpression": false
+ },
+ {
+ "value": "lastFetchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_type_status_lastFetchedAt",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "watchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userEpisodeWatches_userId_watchedAt",
+ "entityType": "indexes",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "episodeId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userEpisodeWatches_episodeId",
+ "entityType": "indexes",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "episodeId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userEpisodeWatches_userId_episodeId",
+ "entityType": "indexes",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "watchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userMovieWatches_userId_watchedAt",
+ "entityType": "indexes",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userMovieWatches_titleId",
+ "entityType": "indexes",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userMovieWatches_userId_titleId",
+ "entityType": "indexes",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "platformId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "userPlatforms_userId_platformId",
+ "entityType": "indexes",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userPlatforms_userId",
+ "entityType": "indexes",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "userRatings_userId_titleId",
+ "entityType": "indexes",
+ "table": "userRatings"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "userTitleStatus_userId_titleId",
+ "entityType": "indexes",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "status",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userTitleStatus_userId_status",
+ "entityType": "indexes",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ "token"
+ ],
+ "nameExplicit": false,
+ "name": "session_token_unique",
+ "entityType": "uniques",
+ "table": "session"
+ },
+ {
+ "columns": [
+ "email"
+ ],
+ "nameExplicit": false,
+ "name": "user_email_unique",
+ "entityType": "uniques",
+ "table": "user"
+ }
+ ],
+ "renames": []
+}
\ No newline at end of file
diff --git a/packages/db/drizzle/20260324223903_secret_rhodey/migration.sql b/packages/db/drizzle/20260324223903_secret_rhodey/migration.sql
new file mode 100644
index 0000000..b70c2c9
--- /dev/null
+++ b/packages/db/drizzle/20260324223903_secret_rhodey/migration.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `platforms` ADD `isSubscription` integer DEFAULT true NOT NULL;--> statement-breakpoint
+DROP INDEX IF EXISTS `platforms_displayOrder`;--> statement-breakpoint
+ALTER TABLE `platforms` DROP COLUMN `displayOrder`;
\ No newline at end of file
diff --git a/packages/db/drizzle/20260324223903_secret_rhodey/snapshot.json b/packages/db/drizzle/20260324223903_secret_rhodey/snapshot.json
new file mode 100644
index 0000000..518c2c9
--- /dev/null
+++ b/packages/db/drizzle/20260324223903_secret_rhodey/snapshot.json
@@ -0,0 +1,3406 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "f50d9b53-3ac7-4fd2-95e1-3f8480b5f636",
+ "prevIds": [
+ "364cbbc8-2e6e-4ebb-9e52-4fea9e88b994"
+ ],
+ "ddl": [
+ {
+ "name": "account",
+ "entityType": "tables"
+ },
+ {
+ "name": "appSettings",
+ "entityType": "tables"
+ },
+ {
+ "name": "cronRuns",
+ "entityType": "tables"
+ },
+ {
+ "name": "episodes",
+ "entityType": "tables"
+ },
+ {
+ "name": "genres",
+ "entityType": "tables"
+ },
+ {
+ "name": "importJobs",
+ "entityType": "tables"
+ },
+ {
+ "name": "integrationEvents",
+ "entityType": "tables"
+ },
+ {
+ "name": "integrations",
+ "entityType": "tables"
+ },
+ {
+ "name": "personFilmography",
+ "entityType": "tables"
+ },
+ {
+ "name": "persons",
+ "entityType": "tables"
+ },
+ {
+ "name": "platformTmdbIds",
+ "entityType": "tables"
+ },
+ {
+ "name": "platforms",
+ "entityType": "tables"
+ },
+ {
+ "name": "seasons",
+ "entityType": "tables"
+ },
+ {
+ "name": "session",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleAvailability",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleCast",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleGenres",
+ "entityType": "tables"
+ },
+ {
+ "name": "titleRecommendations",
+ "entityType": "tables"
+ },
+ {
+ "name": "titles",
+ "entityType": "tables"
+ },
+ {
+ "name": "user",
+ "entityType": "tables"
+ },
+ {
+ "name": "userEpisodeWatches",
+ "entityType": "tables"
+ },
+ {
+ "name": "userMovieWatches",
+ "entityType": "tables"
+ },
+ {
+ "name": "userPlatforms",
+ "entityType": "tables"
+ },
+ {
+ "name": "userRatings",
+ "entityType": "tables"
+ },
+ {
+ "name": "userTitleStatus",
+ "entityType": "tables"
+ },
+ {
+ "name": "verification",
+ "entityType": "tables"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "accountId",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "providerId",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "accessToken",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refreshToken",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "idToken",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "accessTokenExpiresAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refreshTokenExpiresAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "scope",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "password",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "key",
+ "entityType": "columns",
+ "table": "appSettings"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "appSettings"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "jobName",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "startedAt",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "finishedAt",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "durationMs",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "errorMessage",
+ "entityType": "columns",
+ "table": "cronRuns"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seasonId",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "episodeNumber",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "overview",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stillPath",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stillThumbHash",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "airDate",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "runtimeMinutes",
+ "entityType": "columns",
+ "table": "episodes"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "genres"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "genres"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "payload",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "importWatches",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "importWatchlist",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "importRatings",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "totalItems",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "processedItems",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "importedCount",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "skippedCount",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "failedCount",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "currentMessage",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "errors",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "warnings",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "startedAt",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "finishedAt",
+ "entityType": "columns",
+ "table": "importJobs"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "integrationId",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "eventType",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "mediaType",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "mediaTitle",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "errorMessage",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "receivedAt",
+ "entityType": "columns",
+ "table": "integrationEvents"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provider",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastEventAt",
+ "entityType": "columns",
+ "table": "integrations"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "personId",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "character",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Acting'",
+ "generated": null,
+ "name": "department",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "job",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "displayOrder",
+ "entityType": "columns",
+ "table": "personFilmography"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tmdbId",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "biography",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "birthday",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "deathday",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "placeOfBirth",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "profilePath",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "profileThumbHash",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "knownForDepartment",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "real",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "popularity",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "imdbId",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "filmographyLastFetchedAt",
+ "entityType": "columns",
+ "table": "persons"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "platformId",
+ "entityType": "columns",
+ "table": "platformTmdbIds"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tmdbProviderId",
+ "entityType": "columns",
+ "table": "platformTmdbIds"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "logoPath",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "urlTemplate",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "isSubscription",
+ "entityType": "columns",
+ "table": "platforms"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seasonNumber",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "overview",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterPath",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterThumbHash",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "airDate",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "seasons"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expiresAt",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ipAddress",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userAgent",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "impersonatedBy",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "platformId",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "offerType",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'US'",
+ "generated": null,
+ "name": "region",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titleAvailability"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "personId",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "character",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Acting'",
+ "generated": null,
+ "name": "department",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "job",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "displayOrder",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "episodeCount",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titleCast"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleGenres"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "genreId",
+ "entityType": "columns",
+ "table": "titleGenres"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "recommendedTitleId",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "rank",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titleRecommendations"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tmdbId",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "tvdbId",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "title",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "originalTitle",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "overview",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "releaseDate",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "firstAirDate",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterPath",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "posterThumbHash",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backdropPath",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backdropThumbHash",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "real",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "popularity",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "real",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "voteAverage",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "voteCount",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "contentRating",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "imdbId",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "originalLanguage",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "runtimeMinutes",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "colorPalette",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "trailerVideoKey",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "lastFetchedAt",
+ "entityType": "columns",
+ "table": "titles"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "emailVerified",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "image",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'user'",
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "banned",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "banReason",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "banExpires",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "user"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "episodeId",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "watchedAt",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'manual'",
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "watchedAt",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'manual'",
+ "generated": null,
+ "name": "source",
+ "entityType": "columns",
+ "table": "userMovieWatches"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userPlatforms"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "platformId",
+ "entityType": "columns",
+ "table": "userPlatforms"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ratingStars",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ratedAt",
+ "entityType": "columns",
+ "table": "userRatings"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "userId",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "titleId",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "addedAt",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "userTitleStatus"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "identifier",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expiresAt",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "createdAt",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "updatedAt",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_account_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "account"
+ },
+ {
+ "columns": [
+ "seasonId"
+ ],
+ "tableTo": "seasons",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_episodes_seasonId_seasons_id_fk",
+ "entityType": "fks",
+ "table": "episodes"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_importJobs_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ "integrationId"
+ ],
+ "tableTo": "integrations",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_integrationEvents_integrationId_integrations_id_fk",
+ "entityType": "fks",
+ "table": "integrationEvents"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_integrations_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "integrations"
+ },
+ {
+ "columns": [
+ "personId"
+ ],
+ "tableTo": "persons",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_personFilmography_personId_persons_id_fk",
+ "entityType": "fks",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_personFilmography_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ "platformId"
+ ],
+ "tableTo": "platforms",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_platformTmdbIds_platformId_platforms_id_fk",
+ "entityType": "fks",
+ "table": "platformTmdbIds"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_seasons_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "seasons"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_session_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "session"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleAvailability_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ "platformId"
+ ],
+ "tableTo": "platforms",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleAvailability_platformId_platforms_id_fk",
+ "entityType": "fks",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleCast_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ "personId"
+ ],
+ "tableTo": "persons",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleCast_personId_persons_id_fk",
+ "entityType": "fks",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleGenres_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ "genreId"
+ ],
+ "tableTo": "genres",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleGenres_genreId_genres_id_fk",
+ "entityType": "fks",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleRecommendations_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ "recommendedTitleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_titleRecommendations_recommendedTitleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userEpisodeWatches_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ "episodeId"
+ ],
+ "tableTo": "episodes",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userEpisodeWatches_episodeId_episodes_id_fk",
+ "entityType": "fks",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userMovieWatches_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userMovieWatches_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userPlatforms_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ "platformId"
+ ],
+ "tableTo": "platforms",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userPlatforms_platformId_platforms_id_fk",
+ "entityType": "fks",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userRatings_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userRatings"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userRatings_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "userRatings"
+ },
+ {
+ "columns": [
+ "userId"
+ ],
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userTitleStatus_userId_user_id_fk",
+ "entityType": "fks",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ "titleId"
+ ],
+ "tableTo": "titles",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_userTitleStatus_titleId_titles_id_fk",
+ "entityType": "fks",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "account_pk",
+ "table": "account",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "key"
+ ],
+ "nameExplicit": false,
+ "name": "appSettings_pk",
+ "table": "appSettings",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "cronRuns_pk",
+ "table": "cronRuns",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "episodes_pk",
+ "table": "episodes",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "genres_pk",
+ "table": "genres",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "importJobs_pk",
+ "table": "importJobs",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "integrationEvents_pk",
+ "table": "integrationEvents",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "integrations_pk",
+ "table": "integrations",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "personFilmography_pk",
+ "table": "personFilmography",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "persons_pk",
+ "table": "persons",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "platforms_pk",
+ "table": "platforms",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "seasons_pk",
+ "table": "seasons",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "session_pk",
+ "table": "session",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "titleCast_pk",
+ "table": "titleCast",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "titles_pk",
+ "table": "titles",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "user_pk",
+ "table": "user",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "userEpisodeWatches_pk",
+ "table": "userEpisodeWatches",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "userMovieWatches_pk",
+ "table": "userMovieWatches",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "verification_pk",
+ "table": "verification",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "jobName",
+ "isExpression": false
+ },
+ {
+ "value": "startedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "cronRuns_jobName_startedAt",
+ "entityType": "indexes",
+ "table": "cronRuns"
+ },
+ {
+ "columns": [
+ {
+ "value": "seasonId",
+ "isExpression": false
+ },
+ {
+ "value": "episodeNumber",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "episodes_seasonId_episodeNumber",
+ "entityType": "indexes",
+ "table": "episodes"
+ },
+ {
+ "columns": [
+ {
+ "value": "airDate",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "episodes_airDate",
+ "entityType": "indexes",
+ "table": "episodes"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "createdAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "importJobs_userId_createdAt",
+ "entityType": "indexes",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ {
+ "value": "status",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "importJobs_status",
+ "entityType": "indexes",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": "\"importJobs\".\"status\" in ('pending', 'running')",
+ "origin": "manual",
+ "name": "importJobs_active_user",
+ "entityType": "indexes",
+ "table": "importJobs"
+ },
+ {
+ "columns": [
+ {
+ "value": "integrationId",
+ "isExpression": false
+ },
+ {
+ "value": "receivedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "integrationEvents_integrationId_receivedAt",
+ "entityType": "indexes",
+ "table": "integrationEvents"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "provider",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "integrations_userId_provider",
+ "entityType": "indexes",
+ "table": "integrations"
+ },
+ {
+ "columns": [
+ {
+ "value": "token",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "integrations_token",
+ "entityType": "indexes",
+ "table": "integrations"
+ },
+ {
+ "columns": [
+ {
+ "value": "personId",
+ "isExpression": false
+ },
+ {
+ "value": "displayOrder",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "personFilmography_personId_displayOrder",
+ "entityType": "indexes",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "personFilmography_titleId",
+ "entityType": "indexes",
+ "table": "personFilmography"
+ },
+ {
+ "columns": [
+ {
+ "value": "tmdbId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "persons_tmdbId_unique",
+ "entityType": "indexes",
+ "table": "persons"
+ },
+ {
+ "columns": [
+ {
+ "value": "name",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "persons_name",
+ "entityType": "indexes",
+ "table": "persons"
+ },
+ {
+ "columns": [
+ {
+ "value": "tmdbProviderId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "platformTmdbIds_tmdbProviderId_unique",
+ "entityType": "indexes",
+ "table": "platformTmdbIds"
+ },
+ {
+ "columns": [
+ {
+ "value": "platformId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "platformTmdbIds_platformId",
+ "entityType": "indexes",
+ "table": "platformTmdbIds"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "seasonNumber",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "seasons_titleId_seasonNumber",
+ "entityType": "indexes",
+ "table": "seasons"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "platformId",
+ "isExpression": false
+ },
+ {
+ "value": "offerType",
+ "isExpression": false
+ },
+ {
+ "value": "region",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleAvailability_unique",
+ "entityType": "indexes",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleAvailability_titleId",
+ "entityType": "indexes",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ {
+ "value": "platformId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleAvailability_platformId",
+ "entityType": "indexes",
+ "table": "titleAvailability"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "personId",
+ "isExpression": false
+ },
+ {
+ "value": "department",
+ "isExpression": false
+ },
+ {
+ "value": "character",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleCast_unique",
+ "entityType": "indexes",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "displayOrder",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleCast_titleId_displayOrder",
+ "entityType": "indexes",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ {
+ "value": "personId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleCast_personId",
+ "entityType": "indexes",
+ "table": "titleCast"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "genreId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleGenres_titleId_genreId",
+ "entityType": "indexes",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ {
+ "value": "genreId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleGenres_genreId",
+ "entityType": "indexes",
+ "table": "titleGenres"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "recommendedTitleId",
+ "isExpression": false
+ },
+ {
+ "value": "source",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titleRecommendations_unique",
+ "entityType": "indexes",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ },
+ {
+ "value": "rank",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titleRecommendations_titleId_rank",
+ "entityType": "indexes",
+ "table": "titleRecommendations"
+ },
+ {
+ "columns": [
+ {
+ "value": "tmdbId",
+ "isExpression": false
+ },
+ {
+ "value": "type",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_tmdbId_type_unique",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "type",
+ "isExpression": false
+ },
+ {
+ "value": "releaseDate",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_type_releaseDate",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "type",
+ "isExpression": false
+ },
+ {
+ "value": "firstAirDate",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_type_firstAirDate",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "lastFetchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_lastFetchedAt",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "type",
+ "isExpression": false
+ },
+ {
+ "value": "status",
+ "isExpression": false
+ },
+ {
+ "value": "lastFetchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "titles_type_status_lastFetchedAt",
+ "entityType": "indexes",
+ "table": "titles"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "watchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userEpisodeWatches_userId_watchedAt",
+ "entityType": "indexes",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "episodeId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userEpisodeWatches_episodeId",
+ "entityType": "indexes",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "episodeId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userEpisodeWatches_userId_episodeId",
+ "entityType": "indexes",
+ "table": "userEpisodeWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "watchedAt",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userMovieWatches_userId_watchedAt",
+ "entityType": "indexes",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userMovieWatches_titleId",
+ "entityType": "indexes",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userMovieWatches_userId_titleId",
+ "entityType": "indexes",
+ "table": "userMovieWatches"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "platformId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "userPlatforms_userId_platformId",
+ "entityType": "indexes",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userPlatforms_userId",
+ "entityType": "indexes",
+ "table": "userPlatforms"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "userRatings_userId_titleId",
+ "entityType": "indexes",
+ "table": "userRatings"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "titleId",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "userTitleStatus_userId_titleId",
+ "entityType": "indexes",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ {
+ "value": "userId",
+ "isExpression": false
+ },
+ {
+ "value": "status",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "userTitleStatus_userId_status",
+ "entityType": "indexes",
+ "table": "userTitleStatus"
+ },
+ {
+ "columns": [
+ "token"
+ ],
+ "nameExplicit": false,
+ "name": "session_token_unique",
+ "entityType": "uniques",
+ "table": "session"
+ },
+ {
+ "columns": [
+ "email"
+ ],
+ "nameExplicit": false,
+ "name": "user_email_unique",
+ "entityType": "uniques",
+ "table": "user"
+ }
+ ],
+ "renames": []
+}
\ No newline at end of file
diff --git a/packages/db/src/migrate.ts b/packages/db/src/migrate.ts
index 6c7a479..c44d658 100644
--- a/packages/db/src/migrate.ts
+++ b/packages/db/src/migrate.ts
@@ -9,7 +9,7 @@ import { db } from "./client";
const log = createLogger("db");
export function runMigrations(migrationsFolder = path.join(import.meta.dir, "../drizzle")) {
- log.info("Running database migrations...");
+ log.debug("Running database migrations...");
migrate(db, { migrationsFolder });
- log.info("Database migrations complete");
+ log.debug("Database migrations complete");
}
diff --git a/packages/db/src/platforms.json b/packages/db/src/platforms.json
new file mode 100644
index 0000000..bac57e2
--- /dev/null
+++ b/packages/db/src/platforms.json
@@ -0,0 +1,275 @@
+[
+ {
+ "tmdbProviderIds": [8, 175, 1796],
+ "name": "Netflix",
+ "logoPath": "/pbpMk2JmcoNnQwx5JGpXngfoWtp.jpg",
+ "urlTemplate": "https://www.netflix.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [9, 119, 2100],
+ "name": "Amazon Prime Video",
+ "logoPath": "/qR6FKvnPBx2O37FDg8PNM7efwF3.jpg",
+ "urlTemplate": "https://www.amazon.com/s?i=instant-video&k={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [10],
+ "name": "Amazon Video",
+ "logoPath": "/seGSXajazLMCKGB5hnRCidtjay1.jpg",
+ "urlTemplate": "https://www.amazon.com/s?i=instant-video&k={title}",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [337, 2739],
+ "name": "Disney+",
+ "logoPath": "/97yvRBw1GzX7fXprcF80er19ot.jpg",
+ "urlTemplate": "https://www.disneyplus.com/search/{title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [350],
+ "name": "Apple TV+",
+ "logoPath": "/SPnB1qiCkYfirS2it3hZORwGVn.jpg",
+ "urlTemplate": "https://tv.apple.com/search?term={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [2],
+ "name": "Apple TV Store",
+ "logoPath": "/mcbz1LgtErU9p4UdbZ0rG6RTWHX.jpg",
+ "urlTemplate": "https://tv.apple.com/search?term={title}",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [384, 1825, 1899],
+ "name": "HBO Max",
+ "logoPath": "/jbe4gVSfRlbPTdESXhEKpornsfu.jpg",
+ "urlTemplate": "https://play.max.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [15],
+ "name": "Hulu",
+ "logoPath": "/bxBlRPEPpMVDc4jMhSrTf2339DW.jpg",
+ "urlTemplate": "https://www.hulu.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [531, 582, 633, 1770, 1853, 2303, 2616, 37],
+ "name": "Paramount+",
+ "logoPath": "/fts6X10Jn4QT0X6ac3udKEn2tJA.jpg",
+ "urlTemplate": "https://www.paramountplus.com/search/?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [386, 387],
+ "name": "Peacock",
+ "logoPath": "/2aGrp1xw3qhwCYvNGAJZPdjfeeX.jpg",
+ "urlTemplate": "https://www.peacocktv.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [3],
+ "name": "Google Play Movies",
+ "logoPath": "/8z7rC8uIDaTM91X0ZfkRf04ydj2.jpg",
+ "urlTemplate": "https://play.google.com/store/search?q={title}&c=movies",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [7, 332],
+ "name": "Fandango at Home",
+ "logoPath": "/19fkcOz0xeUgCVW8tO85uOYnYK9.jpg",
+ "urlTemplate": "https://www.vudu.com/content/movies/search?searchString={title}",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [68],
+ "name": "Microsoft Store",
+ "logoPath": "/shq88b09gTBYC4hA7K7MUL8Q4zP.jpg",
+ "urlTemplate": "https://www.microsoft.com/en-us/store/movies-and-tv",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [188, 192],
+ "name": "YouTube",
+ "logoPath": "/pTnn5JwWr4p3pG8H6VrpiQo7Vs0.jpg",
+ "urlTemplate": "https://www.youtube.com/results?search_query={title}",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [2528],
+ "name": "YouTube TV",
+ "logoPath": "/x9zOHTUkQzt3PgPVKbMH9CKBwLK.jpg",
+ "urlTemplate": "https://tv.youtube.com/",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [283, 1968],
+ "name": "Crunchyroll",
+ "logoPath": "/fzN5Jok5Ig1eJ7gyNGoMhnLSCfh.jpg",
+ "urlTemplate": "https://www.crunchyroll.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [43, 634, 1794, 1855],
+ "name": "Starz",
+ "logoPath": "/yIKwylTLP1u8gl84Is7FItpYLGL.jpg",
+ "urlTemplate": "https://www.starz.com/search?query={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [526, 528, 635, 1854, 352],
+ "name": "AMC+",
+ "logoPath": "/ovmu6uot1XVvsemM2dDySXLiX57.jpg",
+ "urlTemplate": "https://www.amcplus.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [520, 584],
+ "name": "Discovery+",
+ "logoPath": "/eMTnWwNVtThkjvQA6zwxaoJG9NE.jpg",
+ "urlTemplate": "https://www.discoveryplus.com/search/{title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [11, 201],
+ "name": "MUBI",
+ "logoPath": "/x570VpH2C9EKDf1riP83rYc5dnL.jpg",
+ "urlTemplate": "https://mubi.com/search?query={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [99, 204],
+ "name": "Shudder",
+ "logoPath": "/vEtdiYRPRbDCp1Tcn3BEPF1Ni76.jpg",
+ "urlTemplate": "https://www.shudder.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [151, 1852, 197],
+ "name": "BritBox",
+ "logoPath": "/8oA7IcDNNUtBa9JYB5kQ8hrDz5o.jpg",
+ "urlTemplate": "https://www.britbox.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [87, 196],
+ "name": "Acorn TV",
+ "logoPath": "/doCc555FPPgGtuaZJxf9QZVpIp5.jpg",
+ "urlTemplate": "https://acorn.tv/search/?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [190],
+ "name": "Curiosity Stream",
+ "logoPath": "/oR1aNm1Qu9jQBkW4VrGPWhqbC3P.jpg",
+ "urlTemplate": "https://curiositystream.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [34, 583, 636],
+ "name": "MGM+",
+ "logoPath": "/ctiRpS16dlaTXQBSsiFncMrgWmh.jpg",
+ "urlTemplate": "https://www.mgmplus.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [143, 205],
+ "name": "Sundance Now",
+ "logoPath": "/1Edma9SrJnqkQW3BqFd2rJNHZvX.jpg",
+ "urlTemplate": "https://www.sundancenow.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [251, 343],
+ "name": "ALLBLK",
+ "logoPath": "/4cKdiYEPW1BsWLb9UmNzAyUlD5p.jpg",
+ "urlTemplate": "https://www.allblk.tv/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [73],
+ "name": "Tubi",
+ "logoPath": "/zLYr7OPvpskMA4S79E3vlCi71iC.jpg",
+ "urlTemplate": "https://tubitv.com/search/{title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [300],
+ "name": "Pluto TV",
+ "logoPath": "/dB8G41Q6tSL5NBisrIeqByfepBc.jpg",
+ "urlTemplate": "https://pluto.tv/search/details?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [257],
+ "name": "fuboTV",
+ "logoPath": "/9BgaNQRMDvVlji1JBZi6tcfxpKx.jpg",
+ "urlTemplate": "https://www.fubo.tv/search/{title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [2383],
+ "name": "Philo",
+ "logoPath": "/ptmbGSttkyzawLbxx9MElmxKuVo.jpg",
+ "urlTemplate": "https://www.philo.com/",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [191],
+ "name": "Kanopy",
+ "logoPath": "/rcBwnERpNfPfWB5DaSTyEMCZbCA.jpg",
+ "urlTemplate": "https://www.kanopy.com/search?q={title}",
+ "isSubscription": false
+ },
+ {
+ "tmdbProviderIds": [212],
+ "name": "Hoopla",
+ "logoPath": "/j7D006Uy3UWwZ6G0xH6BMgIWTzH.jpg",
+ "urlTemplate": "https://www.hoopladigital.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [207],
+ "name": "The Roku Channel",
+ "logoPath": "/wQzSN83BnWVgO7xEh0SeTVqtrFv.jpg",
+ "urlTemplate": "https://therokuchannel.roku.com/search/{title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [258],
+ "name": "Criterion Channel",
+ "logoPath": "/yhrtzYd43pFIhRq0ruO8umJPuyn.jpg",
+ "urlTemplate": "https://www.criterionchannel.com/search?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [83],
+ "name": "The CW",
+ "logoPath": "/spcwROYevucLluqZZ8Fv75UuTBt.jpg",
+ "urlTemplate": "https://www.cwtv.com/search/?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [209, 293, 294],
+ "name": "PBS",
+ "logoPath": "/iLjStQKQwzyxXJb3jyNpvDmW9mx.jpg",
+ "urlTemplate": "https://www.pbs.org/search/?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [538, 2077],
+ "name": "Plex",
+ "logoPath": "/vLZKlXUNDcZR7ilvfY9Wr9k80FZ.jpg",
+ "urlTemplate": "https://www.plex.tv/search/?q={title}",
+ "isSubscription": true
+ },
+ {
+ "tmdbProviderIds": [457],
+ "name": "ViX",
+ "logoPath": "/jwRPknT20dfU1GeVqbcDXFyvtdG.jpg",
+ "urlTemplate": "https://vix.com/es-es/search?q={title}",
+ "isSubscription": true
+ }
+]
diff --git a/packages/db/src/queries/auth-cleanup.ts b/packages/db/src/queries/auth-cleanup.ts
index 2678c50..349b2ee 100644
--- a/packages/db/src/queries/auth-cleanup.ts
+++ b/packages/db/src/queries/auth-cleanup.ts
@@ -4,25 +4,17 @@ import { db } from "../client";
import { session, verification } from "../schema";
export function deleteExpiredSessions(): number {
- const now = new Date();
- const count = db
- .select({ id: session.id })
- .from(session)
- .where(lt(session.expiresAt, now))
+ return db
+ .delete(session)
+ .where(lt(session.expiresAt, new Date()))
+ .returning({ id: session.id })
.all().length;
- if (count === 0) return 0;
- db.delete(session).where(lt(session.expiresAt, now)).run();
- return count;
}
export function deleteExpiredVerifications(): number {
- const now = new Date();
- const count = db
- .select({ id: verification.id })
- .from(verification)
- .where(lt(verification.expiresAt, now))
+ return db
+ .delete(verification)
+ .where(lt(verification.expiresAt, new Date()))
+ .returning({ id: verification.id })
.all().length;
- if (count === 0) return 0;
- db.delete(verification).where(lt(verification.expiresAt, now)).run();
- return count;
}
diff --git a/packages/db/src/queries/availability.ts b/packages/db/src/queries/availability.ts
index c758a42..e71aa23 100644
--- a/packages/db/src/queries/availability.ts
+++ b/packages/db/src/queries/availability.ts
@@ -1,7 +1,7 @@
-import { and, eq, sql } from "drizzle-orm";
+import { and, eq } from "drizzle-orm";
import { db } from "../client";
-import { platforms, titleAvailability } from "../schema";
+import { platformTmdbIds, platforms, titleAvailability } from "../schema";
export function replaceAvailabilityTransaction(
titleId: string,
@@ -26,7 +26,6 @@ export function getAvailabilityForTitle(titleId: string) {
providerName: platforms.name,
logoPath: platforms.logoPath,
urlTemplate: platforms.urlTemplate,
- tmdbProviderId: platforms.tmdbProviderId,
offerType: titleAvailability.offerType,
})
.from(titleAvailability)
@@ -36,7 +35,9 @@ export function getAvailabilityForTitle(titleId: string) {
}
/**
- * Ensure a platform row exists for a TMDB provider. Upserts by tmdbProviderId.
+ * Ensure a platform row exists for a TMDB provider.
+ * Looks up via the platformTmdbIds junction table.
+ * Uses a transaction to prevent orphaned platform rows under concurrent refreshes.
* Returns the platform ID.
*/
export function ensurePlatformForTmdbProvider(
@@ -44,23 +45,38 @@ export function ensurePlatformForTmdbProvider(
name: string,
logoPath: string | null,
): string {
- const row = db
- .insert(platforms)
- .values({
- tmdbProviderId,
- name,
- logoPath,
- displayOrder: 999,
- })
- .onConflictDoUpdate({
- target: platforms.tmdbProviderId,
- set: {
- name: sql`excluded.name`,
- logoPath: sql`excluded.logoPath`,
- },
- })
- .returning({ id: platforms.id })
- .get();
+ return db.transaction((tx) => {
+ const existing = tx
+ .select({ platformId: platformTmdbIds.platformId })
+ .from(platformTmdbIds)
+ .where(eq(platformTmdbIds.tmdbProviderId, tmdbProviderId))
+ .get();
- return row!.id;
+ if (existing) {
+ tx.update(platforms).set({ logoPath }).where(eq(platforms.id, existing.platformId)).run();
+ return existing.platformId;
+ }
+
+ const id = Bun.randomUUIDv7();
+ tx.insert(platforms).values({ id, name, logoPath }).run();
+ tx.insert(platformTmdbIds)
+ .values({ platformId: id, tmdbProviderId })
+ .onConflictDoNothing()
+ .run();
+
+ // Verify our insert won (handles race with another writer)
+ const mapping = tx
+ .select({ platformId: platformTmdbIds.platformId })
+ .from(platformTmdbIds)
+ .where(eq(platformTmdbIds.tmdbProviderId, tmdbProviderId))
+ .get()!;
+
+ if (mapping.platformId !== id) {
+ // Another transaction beat us — clean up our orphan
+ tx.delete(platforms).where(eq(platforms.id, id)).run();
+ tx.update(platforms).set({ logoPath }).where(eq(platforms.id, mapping.platformId)).run();
+ }
+
+ return mapping.platformId;
+ });
}
diff --git a/packages/db/src/queries/cache.ts b/packages/db/src/queries/cache.ts
index 3b82083..7340253 100644
--- a/packages/db/src/queries/cache.ts
+++ b/packages/db/src/queries/cache.ts
@@ -34,9 +34,9 @@ function collectTitleImagePaths(titleIds: string[]): OrphanedImage[] {
if (r.backdropPath) images.push({ category: "backdrops", path: r.backdropPath });
}
- // Season posters
+ // Season posters + IDs
const seasonRows = db
- .select({ posterPath: seasons.posterPath })
+ .select({ id: seasons.id, posterPath: seasons.posterPath })
.from(seasons)
.where(inArray(seasons.titleId, batch))
.all();
@@ -45,12 +45,7 @@ function collectTitleImagePaths(titleIds: string[]): OrphanedImage[] {
}
// Episode stills (via seasons)
- const seasonIds = db
- .select({ id: seasons.id })
- .from(seasons)
- .where(inArray(seasons.titleId, batch))
- .all()
- .map((s) => s.id);
+ const seasonIds = seasonRows.map((s) => s.id);
for (let j = 0; j < seasonIds.length; j += BATCH_SIZE) {
const sBatch = seasonIds.slice(j, j + BATCH_SIZE);
@@ -68,19 +63,44 @@ function collectTitleImagePaths(titleIds: string[]): OrphanedImage[] {
return images;
}
-/** Collect profile paths for persons that will be orphaned (no remaining titleCast). */
-function collectOrphanedPersonImages(): OrphanedImage[] {
- const orphaned = db
- .select({ profilePath: persons.profilePath })
+/** Find person IDs that have no remaining titleCast entries. */
+function getOrphanedPersonIds(): string[] {
+ return db
+ .select({ id: persons.id })
.from(persons)
.where(
notInArray(persons.id, db.selectDistinct({ personId: titleCast.personId }).from(titleCast)),
)
- .all();
+ .all()
+ .map((p) => p.id);
+}
- return orphaned
- .filter((p): p is { profilePath: string } => p.profilePath != null)
- .map((p) => ({ category: "profiles" as const, path: p.profilePath }));
+/** Collect profile paths for a set of orphaned person IDs. */
+function collectOrphanedPersonImages(orphanedIds: string[]): OrphanedImage[] {
+ if (orphanedIds.length === 0) return [];
+ const images: OrphanedImage[] = [];
+ for (let i = 0; i < orphanedIds.length; i += BATCH_SIZE) {
+ const batch = orphanedIds.slice(i, i + BATCH_SIZE);
+ const rows = db
+ .select({ profilePath: persons.profilePath })
+ .from(persons)
+ .where(inArray(persons.id, batch))
+ .all();
+ for (const r of rows) {
+ if (r.profilePath) images.push({ category: "profiles", path: r.profilePath });
+ }
+ }
+ return images;
+}
+
+/** Delete orphaned persons by IDs, returning the count deleted. */
+function deleteOrphanedPersons(orphanedIds: string[]): number {
+ if (orphanedIds.length === 0) return 0;
+ for (let i = 0; i < orphanedIds.length; i += BATCH_SIZE) {
+ const batch = orphanedIds.slice(i, i + BATCH_SIZE);
+ db.delete(persons).where(inArray(persons.id, batch)).run();
+ }
+ return orphanedIds.length;
}
export function purgeShellTitlesTransaction(): PurgeResult {
@@ -92,8 +112,13 @@ export function purgeShellTitlesTransaction(): PurgeResult {
.all();
if (shellTitles.length === 0) {
- const orphanedImages = collectOrphanedPersonImages();
- return { deletedTitles: 0, deletedPersons: purgeOrphanedPersons(), orphanedImages };
+ const orphanedIds = getOrphanedPersonIds();
+ const orphanedImages = collectOrphanedPersonImages(orphanedIds);
+ return {
+ deletedTitles: 0,
+ deletedPersons: deleteOrphanedPersons(orphanedIds),
+ orphanedImages,
+ };
}
const libraryTitleIds = new Set(
@@ -107,8 +132,13 @@ export function purgeShellTitlesTransaction(): PurgeResult {
const toDelete = shellTitles.map((t) => t.id).filter((id) => !libraryTitleIds.has(id));
if (toDelete.length === 0) {
- const orphanedImages = collectOrphanedPersonImages();
- return { deletedTitles: 0, deletedPersons: purgeOrphanedPersons(), orphanedImages };
+ const orphanedIds = getOrphanedPersonIds();
+ const orphanedImages = collectOrphanedPersonImages(orphanedIds);
+ return {
+ deletedTitles: 0,
+ deletedPersons: deleteOrphanedPersons(orphanedIds),
+ orphanedImages,
+ };
}
// Collect image paths BEFORE cascade-deleting the title rows
@@ -119,32 +149,17 @@ export function purgeShellTitlesTransaction(): PurgeResult {
db.delete(titles).where(inArray(titles.id, batch)).run();
}
- // After title cascade deletes, collect orphaned person images then delete persons
- orphanedImages.push(...collectOrphanedPersonImages());
+ // After title cascade deletes, find orphaned persons once and use for both operations
+ const orphanedIds = getOrphanedPersonIds();
+ orphanedImages.push(...collectOrphanedPersonImages(orphanedIds));
return {
deletedTitles: toDelete.length,
- deletedPersons: purgeOrphanedPersons(),
+ deletedPersons: deleteOrphanedPersons(orphanedIds),
orphanedImages,
};
});
}
export function purgeOrphanedPersons(): number {
- const orphanedPersons = db
- .select({ id: persons.id })
- .from(persons)
- .where(
- notInArray(persons.id, db.selectDistinct({ personId: titleCast.personId }).from(titleCast)),
- )
- .all();
-
- if (orphanedPersons.length === 0) return 0;
-
- const ids = orphanedPersons.map((p) => p.id);
- for (let i = 0; i < ids.length; i += BATCH_SIZE) {
- const batch = ids.slice(i, i + BATCH_SIZE);
- db.delete(persons).where(inArray(persons.id, batch)).run();
- }
-
- return ids.length;
+ return deleteOrphanedPersons(getOrphanedPersonIds());
}
diff --git a/packages/db/src/queries/cron.ts b/packages/db/src/queries/cron.ts
index 7c1863c..06f6574 100644
--- a/packages/db/src/queries/cron.ts
+++ b/packages/db/src/queries/cron.ts
@@ -53,7 +53,7 @@ export function getStaleTitles(titleIds: string[], staleDate: Date) {
export function getStaleNonLibraryTitles(staleDate: Date, limit: number) {
return db
- .select()
+ .select({ id: titles.id })
.from(titles)
.where(and(isNotNull(titles.lastFetchedAt), lt(titles.lastFetchedAt, staleDate)))
.limit(limit)
@@ -94,7 +94,7 @@ export function getTitlesWithStaleOffersFetchedBefore(titleIds: string[], staleD
export function getReturningTvShows() {
const returningStatuses = ["Returning Series", "In Production"];
return db
- .select()
+ .select({ id: titles.id, tmdbId: titles.tmdbId })
.from(titles)
.where(
and(
@@ -128,19 +128,11 @@ export function getCastEntryForTitle(titleId: string) {
}
export function deleteOldCronRuns(beforeDate: Date): number {
- const old = db
- .select({ id: cronRuns.id })
- .from(cronRuns)
+ return db
+ .delete(cronRuns)
.where(lt(cronRuns.startedAt, beforeDate))
- .all();
- if (old.length === 0) return 0;
- const ids = old.map((r) => r.id);
- for (let i = 0; i < ids.length; i += 500) {
- db.delete(cronRuns)
- .where(inArray(cronRuns.id, ids.slice(i, i + 500)))
- .run();
- }
- return old.length;
+ .returning({ id: cronRuns.id })
+ .all().length;
}
export function getTitlesWithFreshRecommendations(
diff --git a/packages/db/src/queries/discovery.ts b/packages/db/src/queries/discovery.ts
index a4bd799..2015872 100644
--- a/packages/db/src/queries/discovery.ts
+++ b/packages/db/src/queries/discovery.ts
@@ -351,6 +351,7 @@ export function getUpcomingMovies(
export function getAvailabilityByTitleIds(titleIds: string[]) {
if (titleIds.length === 0) return [];
+ // Order by offerType so flatrate is picked first when the caller deduplicates
return db
.select({
titleId: titleAvailability.titleId,
@@ -363,8 +364,11 @@ export function getAvailabilityByTitleIds(titleIds: string[]) {
.where(
and(
inArray(titleAvailability.titleId, titleIds),
- eq(titleAvailability.offerType, "flatrate"),
+ inArray(titleAvailability.offerType, ["flatrate", "free", "ads"]),
),
)
+ .orderBy(
+ sql`CASE ${titleAvailability.offerType} WHEN 'flatrate' THEN 0 WHEN 'free' THEN 1 ELSE 2 END`,
+ )
.all();
}
diff --git a/packages/db/src/queries/image-cache.ts b/packages/db/src/queries/image-cache.ts
index da91f0c..eb6fdfd 100644
--- a/packages/db/src/queries/image-cache.ts
+++ b/packages/db/src/queries/image-cache.ts
@@ -1,4 +1,4 @@
-import { eq, inArray } from "drizzle-orm";
+import { eq } from "drizzle-orm";
import { db } from "../client";
import {
@@ -39,19 +39,11 @@ export function getSeasonPostersForTitle(titleId: string) {
// ─── Episode stills ──────────────────────────────────────────────────
export function getEpisodeStillsForTitle(titleId: string) {
- const allSeasons = db
- .select({ id: seasons.id })
- .from(seasons)
- .where(eq(seasons.titleId, titleId))
- .all();
-
- const seasonIds = allSeasons.map((s) => s.id);
- if (seasonIds.length === 0) return [];
-
return db
.select({ stillPath: episodes.stillPath })
.from(episodes)
- .where(inArray(episodes.seasonId, seasonIds))
+ .innerJoin(seasons, eq(episodes.seasonId, seasons.id))
+ .where(eq(seasons.titleId, titleId))
.all();
}
diff --git a/packages/db/src/queries/imports.ts b/packages/db/src/queries/imports.ts
index 18501f1..05c6f82 100644
--- a/packages/db/src/queries/imports.ts
+++ b/packages/db/src/queries/imports.ts
@@ -89,22 +89,14 @@ export function getActiveImportJobForUser(userId: string) {
/** Mark any running/pending import jobs as errored — called on server startup to recover from crashes. */
export function recoverStaleImportJobs(): number {
- const stale = db
- .select({ id: importJobs.id })
- .from(importJobs)
+ return db
+ .update(importJobs)
+ .set({
+ status: "error",
+ finishedAt: new Date(),
+ errors: JSON.stringify(["Import interrupted by server restart"]),
+ })
.where(inArray(importJobs.status, ["pending", "running"]))
- .all();
- if (stale.length === 0) return 0;
- const now = new Date();
- for (const { id } of stale) {
- db.update(importJobs)
- .set({
- status: "error",
- finishedAt: now,
- errors: JSON.stringify(["Import interrupted by server restart"]),
- })
- .where(eq(importJobs.id, id))
- .run();
- }
- return stale.length;
+ .returning({ id: importJobs.id })
+ .all().length;
}
diff --git a/packages/db/src/queries/integrations.ts b/packages/db/src/queries/integrations.ts
index 49f1fcb..ed1d308 100644
--- a/packages/db/src/queries/integrations.ts
+++ b/packages/db/src/queries/integrations.ts
@@ -17,6 +17,18 @@ export function getRecentEventsForIntegration(integrationId: string, limit = 10)
.all();
}
+export function getRecentEventsForIntegrations(integrationIds: string[], limit = 10) {
+ if (integrationIds.length === 0)
+ return new Map>();
+
+ const result = new Map>();
+ for (const integrationId of integrationIds) {
+ const events = getRecentEventsForIntegration(integrationId, limit);
+ if (events.length > 0) result.set(integrationId, events);
+ }
+ return result;
+}
+
export function getIntegrationByUserAndProvider(userId: string, provider: string) {
return db
.select()
diff --git a/packages/db/src/queries/library.ts b/packages/db/src/queries/library.ts
index ce3600b..54c9ba6 100644
--- a/packages/db/src/queries/library.ts
+++ b/packages/db/src/queries/library.ts
@@ -224,6 +224,10 @@ export function getFilteredLibrary(userId: string, filters: LibraryFilters) {
// Build query with display status when needed for filtering
if (needsDisplayStatus) {
+ // Filter by display status in SQL so pagination works at the DB level
+ const statusValues = filters.statuses!.map((s) => sql`${s}`);
+ conditions.push(sql`(${displayStatusExpr()}) IN (${sql.join(statusValues, sql`, `)})`);
+
const rows = db
.select({
titleId: titles.id,
@@ -239,6 +243,7 @@ export function getFilteredLibrary(userId: string, filters: LibraryFilters) {
userStatus: userTitleStatus.status,
userRating: userRatings.ratingStars,
displayStatus: displayStatusExpr().as("display_status"),
+ totalCount: sql`count(*) over()`.as("totalCount"),
})
.from(titles)
.innerJoin(
@@ -251,18 +256,15 @@ export function getFilteredLibrary(userId: string, filters: LibraryFilters) {
)
.where(and(...conditions))
.orderBy(...sortExpressions)
+ .limit(filters.limit)
+ .offset(offset)
.all();
- // Post-filter by display status
- const filtered = rows.filter((r) => filters.statuses!.includes(r.displayStatus));
- const totalResults = filtered.length;
- const paged = filtered.slice(offset, offset + filters.limit);
+ const totalResults = rows[0]?.totalCount ?? 0;
+ const items = rows.map(({ totalCount: _, ...item }) => item);
return {
- items: paged.map((row) => {
- const { displayStatus, ...item } = row;
- return Object.assign(item, { displayStatus });
- }),
+ items,
page: filters.page,
totalPages: Math.max(1, Math.ceil(totalResults / filters.limit)),
totalResults,
diff --git a/packages/db/src/queries/metadata.ts b/packages/db/src/queries/metadata.ts
index a103fae..695d1ab 100644
--- a/packages/db/src/queries/metadata.ts
+++ b/packages/db/src/queries/metadata.ts
@@ -267,7 +267,6 @@ export function getAvailabilityOffersForTitle(titleId: string) {
providerName: platforms.name,
logoPath: platforms.logoPath,
urlTemplate: platforms.urlTemplate,
- tmdbProviderId: platforms.tmdbProviderId,
offerType: titleAvailability.offerType,
})
.from(titleAvailability)
diff --git a/packages/db/src/queries/system-health.ts b/packages/db/src/queries/system-health.ts
index 4244791..93abe4e 100644
--- a/packages/db/src/queries/system-health.ts
+++ b/packages/db/src/queries/system-health.ts
@@ -1,4 +1,4 @@
-import { count, desc, eq } from "drizzle-orm";
+import { count, desc, eq, inArray, sql } from "drizzle-orm";
import { db } from "../client";
import { cronRuns, episodes, titles, user } from "../schema";
@@ -23,3 +23,26 @@ export function getLatestCronRun(jobName: string) {
.limit(1)
.get();
}
+
+export function getLatestCronRuns(jobNames: string[]) {
+ if (jobNames.length === 0) return new Map();
+ const rows = db
+ .select({
+ id: cronRuns.id,
+ jobName: cronRuns.jobName,
+ status: cronRuns.status,
+ startedAt: cronRuns.startedAt,
+ finishedAt: cronRuns.finishedAt,
+ durationMs: cronRuns.durationMs,
+ errorMessage: cronRuns.errorMessage,
+ rn: sql`row_number() over (partition by ${cronRuns.jobName} order by ${cronRuns.startedAt} desc)`.as(
+ "rn",
+ ),
+ })
+ .from(cronRuns)
+ .where(inArray(cronRuns.jobName, jobNames))
+ .all()
+ .filter((r) => r.rn === 1);
+
+ return new Map(rows.map((r) => [r.jobName, r]));
+}
diff --git a/packages/db/src/queries/tracking.ts b/packages/db/src/queries/tracking.ts
index 2e321df..bf92102 100644
--- a/packages/db/src/queries/tracking.ts
+++ b/packages/db/src/queries/tracking.ts
@@ -69,6 +69,17 @@ export function getEpisodeTitleId(episodeId: string): string | null {
return row?.titleId ?? null;
}
+export function getEpisodeTitleIds(episodeIds: string[]): Map {
+ if (episodeIds.length === 0) return new Map();
+ const rows = db
+ .select({ episodeId: episodes.id, titleId: seasons.titleId })
+ .from(episodes)
+ .innerJoin(seasons, eq(episodes.seasonId, seasons.id))
+ .where(inArray(episodes.id, episodeIds))
+ .all();
+ return new Map(rows.map((r) => [r.episodeId, r.titleId]));
+}
+
export function batchInsertEpisodeWatchesTransaction(
userId: string,
episodeIds: string[],
@@ -168,7 +179,7 @@ export function batchInsertMissingEpisodeWatches(
export function countDistinctEpisodeWatches(userId: string, episodeIds: string[]): number {
if (episodeIds.length === 0) return 0;
- const [result] = db
+ const result = db
.select({
count: sql`count(distinct ${userEpisodeWatches.episodeId})`,
})
@@ -176,8 +187,8 @@ export function countDistinctEpisodeWatches(userId: string, episodeIds: string[]
.where(
and(eq(userEpisodeWatches.userId, userId), inArray(userEpisodeWatches.episodeId, episodeIds)),
)
- .all();
- return result.count;
+ .get();
+ return result?.count ?? 0;
}
export function upsertRating(
@@ -277,6 +288,15 @@ export function getSeasonEpisodes(seasonId: string) {
return db.select().from(episodes).where(eq(episodes.seasonId, seasonId)).all();
}
+export function getSeasonEpisodeIds(seasonId: string): string[] {
+ return db
+ .select({ id: episodes.id })
+ .from(episodes)
+ .where(eq(episodes.seasonId, seasonId))
+ .all()
+ .map((e) => e.id);
+}
+
export function getSeasonById(seasonId: string) {
return db.select().from(seasons).where(eq(seasons.id, seasonId)).get();
}
diff --git a/packages/db/src/queries/user-platforms.ts b/packages/db/src/queries/user-platforms.ts
index 37ecb39..62f5232 100644
--- a/packages/db/src/queries/user-platforms.ts
+++ b/packages/db/src/queries/user-platforms.ts
@@ -1,7 +1,7 @@
-import { eq, inArray } from "drizzle-orm";
+import { asc, desc, eq, inArray } from "drizzle-orm";
import { db } from "../client";
-import { platforms, userPlatforms } from "../schema";
+import { platformTmdbIds, platforms, userPlatforms } from "../schema";
export function getUserPlatformIds(userId: string): string[] {
return db
@@ -17,15 +17,14 @@ export function getUserPlatforms(userId: string) {
.select({
id: platforms.id,
name: platforms.name,
- tmdbProviderId: platforms.tmdbProviderId,
logoPath: platforms.logoPath,
urlTemplate: platforms.urlTemplate,
- displayOrder: platforms.displayOrder,
+ isSubscription: platforms.isSubscription,
})
.from(userPlatforms)
.innerJoin(platforms, eq(userPlatforms.platformId, platforms.id))
.where(eq(userPlatforms.userId, userId))
- .orderBy(platforms.displayOrder)
+ .orderBy(desc(platforms.isSubscription), asc(platforms.name))
.all();
}
@@ -42,7 +41,11 @@ export function setUserPlatforms(userId: string, platformIds: string[]): void {
}
export function getAllPlatforms() {
- return db.select().from(platforms).orderBy(platforms.displayOrder).all();
+ return db
+ .select()
+ .from(platforms)
+ .orderBy(desc(platforms.isSubscription), asc(platforms.name))
+ .all();
}
export function hasUserPlatforms(userId: string): boolean {
@@ -66,3 +69,32 @@ export function platformIdsExist(platformIds: string[]): boolean {
.all();
return found.length === unique.length;
}
+
+export function getTmdbProviderIdsForPlatform(platformId: string): number[] {
+ return db
+ .select({ tmdbProviderId: platformTmdbIds.tmdbProviderId })
+ .from(platformTmdbIds)
+ .where(eq(platformTmdbIds.platformId, platformId))
+ .all()
+ .map((r) => r.tmdbProviderId);
+}
+
+export function getTmdbProviderIdsByPlatformIds(platformIds: string[]): Map {
+ if (platformIds.length === 0) return new Map();
+ const rows = db
+ .select({
+ platformId: platformTmdbIds.platformId,
+ tmdbProviderId: platformTmdbIds.tmdbProviderId,
+ })
+ .from(platformTmdbIds)
+ .where(inArray(platformTmdbIds.platformId, platformIds))
+ .all();
+
+ const map = new Map();
+ for (const row of rows) {
+ const arr = map.get(row.platformId) ?? [];
+ arr.push(row.tmdbProviderId);
+ map.set(row.platformId, arr);
+ }
+ return map;
+}
diff --git a/packages/db/src/queries/webhooks.ts b/packages/db/src/queries/webhooks.ts
index 3c1dbca..f55f3ce 100644
--- a/packages/db/src/queries/webhooks.ts
+++ b/packages/db/src/queries/webhooks.ts
@@ -1,4 +1,4 @@
-import { and, eq, gte, inArray, lt } from "drizzle-orm";
+import { and, eq, gte, lt } from "drizzle-orm";
import { db } from "../client";
import { integrationEvents, integrations, userEpisodeWatches, userMovieWatches } from "../schema";
@@ -31,29 +31,23 @@ export function getRecentEpisodeWatch(userId: string, episodeId: string, since:
.get();
}
-export function insertIntegrationEvent(values: typeof integrationEvents.$inferInsert): void {
- db.insert(integrationEvents).values(values).run();
-}
-
-export function updateIntegrationLastEvent(connectionId: string): void {
- db.update(integrations)
- .set({ lastEventAt: new Date() })
- .where(eq(integrations.id, connectionId))
- .run();
+export function insertIntegrationEventTransaction(
+ values: typeof integrationEvents.$inferInsert,
+ connectionId: string,
+): void {
+ db.transaction((tx) => {
+ tx.insert(integrationEvents).values(values).run();
+ tx.update(integrations)
+ .set({ lastEventAt: new Date() })
+ .where(eq(integrations.id, connectionId))
+ .run();
+ });
}
export function deleteOldIntegrationEvents(beforeDate: Date): number {
- const old = db
- .select({ id: integrationEvents.id })
- .from(integrationEvents)
+ return db
+ .delete(integrationEvents)
.where(lt(integrationEvents.receivedAt, beforeDate))
- .all();
- if (old.length === 0) return 0;
- const ids = old.map((r) => r.id);
- for (let i = 0; i < ids.length; i += 500) {
- db.delete(integrationEvents)
- .where(inArray(integrationEvents.id, ids.slice(i, i + 500)))
- .run();
- }
- return old.length;
+ .returning({ id: integrationEvents.id })
+ .all().length;
}
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
index e9050fa..0d27a9b 100644
--- a/packages/db/src/schema.ts
+++ b/packages/db/src/schema.ts
@@ -251,19 +251,25 @@ export const userRatings = sqliteTable(
// ─── Platforms & Availability ────────────────────────────────────────
-export const platforms = sqliteTable(
- "platforms",
+export const platforms = sqliteTable("platforms", {
+ id: uuidPk(),
+ name: text("name").notNull(),
+ logoPath: text("logoPath"),
+ urlTemplate: text("urlTemplate"),
+ isSubscription: int("isSubscription", { mode: "boolean" }).notNull().default(true),
+});
+
+export const platformTmdbIds = sqliteTable(
+ "platformTmdbIds",
{
- id: uuidPk(),
- name: text("name").notNull(),
- tmdbProviderId: int("tmdbProviderId"),
- logoPath: text("logoPath"),
- urlTemplate: text("urlTemplate"),
- displayOrder: int("displayOrder").notNull().default(0),
+ platformId: text("platformId")
+ .notNull()
+ .references(() => platforms.id, { onDelete: "cascade" }),
+ tmdbProviderId: int("tmdbProviderId").notNull(),
},
(table) => [
- uniqueIndex("platforms_tmdbProviderId_unique").on(table.tmdbProviderId),
- index("platforms_displayOrder").on(table.displayOrder),
+ uniqueIndex("platformTmdbIds_tmdbProviderId_unique").on(table.tmdbProviderId),
+ index("platformTmdbIds_platformId").on(table.platformId),
],
);
diff --git a/packages/db/src/seed-platforms.ts b/packages/db/src/seed-platforms.ts
index 6d9cdb6..68e80ec 100644
--- a/packages/db/src/seed-platforms.ts
+++ b/packages/db/src/seed-platforms.ts
@@ -1,198 +1,126 @@
-import { sql } from "drizzle-orm";
+import { eq, sql } from "drizzle-orm";
+
+import { createLogger } from "@sofa/logger";
import { db } from "./client";
-import { platforms } from "./schema";
+import platformData from "./platforms.json";
+import { platformTmdbIds, platforms, titleAvailability, userPlatforms } from "./schema";
-interface SeedPlatform {
- tmdbProviderId: number;
+const log = createLogger("seed-platforms");
+
+export interface SeedPlatform {
+ tmdbProviderIds: number[];
name: string;
+ logoPath: string | null;
urlTemplate: string;
- displayOrder: number;
+ isSubscription: boolean;
}
-const SEED_DATA: SeedPlatform[] = [
- // Netflix
- {
- tmdbProviderId: 8,
- name: "Netflix",
- urlTemplate: "https://www.netflix.com/search?q={title}",
- displayOrder: 1,
- },
- {
- tmdbProviderId: 1796,
- name: "Netflix basic with Ads",
- urlTemplate: "https://www.netflix.com/search?q={title}",
- displayOrder: 2,
- },
-
- // Amazon
- {
- tmdbProviderId: 9,
- name: "Amazon Prime Video",
- urlTemplate: "https://www.amazon.com/s?i=instant-video&k={title}",
- displayOrder: 3,
- },
- {
- tmdbProviderId: 10,
- name: "Amazon Video",
- urlTemplate: "https://www.amazon.com/s?i=instant-video&k={title}",
- displayOrder: 4,
- },
- {
- tmdbProviderId: 119,
- name: "Amazon Prime Video",
- urlTemplate: "https://www.amazon.com/s?i=instant-video&k={title}",
- displayOrder: 5,
- },
-
- // Disney+
- {
- tmdbProviderId: 337,
- name: "Disney+",
- urlTemplate: "https://www.disneyplus.com/search/{title}",
- displayOrder: 6,
- },
-
- // Apple
- {
- tmdbProviderId: 2,
- name: "Apple iTunes",
- urlTemplate: "https://tv.apple.com/search?term={title}",
- displayOrder: 7,
- },
- {
- tmdbProviderId: 350,
- name: "Apple TV+",
- urlTemplate: "https://tv.apple.com/search?term={title}",
- displayOrder: 8,
- },
-
- // Hulu
- {
- tmdbProviderId: 15,
- name: "Hulu",
- urlTemplate: "https://www.hulu.com/search?q={title}",
- displayOrder: 9,
- },
-
- // Max (HBO)
- {
- tmdbProviderId: 384,
- name: "HBO Max",
- urlTemplate: "https://play.max.com/search?q={title}",
- displayOrder: 10,
- },
- {
- tmdbProviderId: 1899,
- name: "Max",
- urlTemplate: "https://play.max.com/search?q={title}",
- displayOrder: 11,
- },
-
- // Paramount+
- {
- tmdbProviderId: 531,
- name: "Paramount+",
- urlTemplate: "https://www.paramountplus.com/search/?q={title}",
- displayOrder: 12,
- },
-
- // Peacock
- {
- tmdbProviderId: 386,
- name: "Peacock",
- urlTemplate: "https://www.peacocktv.com/search?q={title}",
- displayOrder: 13,
- },
-
- // Google Play
- {
- tmdbProviderId: 3,
- name: "Google Play Movies",
- urlTemplate: "https://play.google.com/store/search?q={title}&c=movies",
- displayOrder: 14,
- },
-
- // YouTube
- {
- tmdbProviderId: 192,
- name: "YouTube",
- urlTemplate: "https://www.youtube.com/results?search_query={title}",
- displayOrder: 15,
- },
-
- // Crunchyroll
- {
- tmdbProviderId: 283,
- name: "Crunchyroll",
- urlTemplate: "https://www.crunchyroll.com/search?q={title}",
- displayOrder: 16,
- },
-
- // Free / ad-supported
- {
- tmdbProviderId: 73,
- name: "Tubi",
- urlTemplate: "https://tubitv.com/search/{title}",
- displayOrder: 17,
- },
- {
- tmdbProviderId: 300,
- name: "Pluto TV",
- urlTemplate: "https://pluto.tv/search/details?q={title}",
- displayOrder: 18,
- },
-
- // Other
- {
- tmdbProviderId: 257,
- name: "fuboTV",
- urlTemplate: "https://www.fubo.tv/search/{title}",
- displayOrder: 19,
- },
- {
- tmdbProviderId: 43,
- name: "Starz",
- urlTemplate: "https://www.starz.com/search?query={title}",
- displayOrder: 20,
- },
- {
- tmdbProviderId: 37,
- name: "Showtime",
- urlTemplate: "https://www.sho.com/search?q={title}",
- displayOrder: 21,
- },
-];
+export const SEED_DATA: SeedPlatform[] = platformData;
export function seedPlatforms(): void {
- const insert = db
- .insert(platforms)
- .values({
- id: sql.placeholder("id"),
- tmdbProviderId: sql.placeholder("tmdbProviderId"),
- name: sql.placeholder("name"),
- urlTemplate: sql.placeholder("urlTemplate"),
- displayOrder: sql.placeholder("displayOrder"),
- })
- .onConflictDoUpdate({
- target: platforms.tmdbProviderId,
- set: {
- name: sql`excluded.name`,
- urlTemplate: sql`excluded.urlTemplate`,
- displayOrder: sql`excluded.displayOrder`,
- },
- })
- .prepare();
+ log.debug(`Seeding ${SEED_DATA.length} platforms`);
db.transaction(() => {
- for (const p of SEED_DATA) {
- insert.execute({
- id: Bun.randomUUIDv7(),
- tmdbProviderId: p.tmdbProviderId,
- name: p.name,
- urlTemplate: p.urlTemplate,
- displayOrder: p.displayOrder,
- });
+ const existingPlatforms = new Map(
+ db
+ .select()
+ .from(platforms)
+ .all()
+ .map((p) => [p.id, p]),
+ );
+ const existingMappings = db.select().from(platformTmdbIds).all();
+ const tmdbToPlatform = new Map(existingMappings.map((m) => [m.tmdbProviderId, m.platformId]));
+
+ for (const seed of SEED_DATA) {
+ // Find all existing platform IDs that any of this seed's TMDB IDs map to
+ const existingPlatformIds = [
+ ...new Set(
+ seed.tmdbProviderIds
+ .map((id) => tmdbToPlatform.get(id))
+ .filter((id): id is string => id != null),
+ ),
+ ];
+
+ let canonicalId: string;
+
+ if (existingPlatformIds.length === 0) {
+ // Brand new platform
+ canonicalId = Bun.randomUUIDv7();
+ db.insert(platforms)
+ .values({
+ id: canonicalId,
+ name: seed.name,
+ logoPath: seed.logoPath,
+ urlTemplate: seed.urlTemplate,
+ isSubscription: seed.isSubscription,
+ })
+ .run();
+ log.debug(`Created platform "${seed.name}"`);
+ } else {
+ // Use the first existing platform as canonical
+ canonicalId = existingPlatformIds[0]!;
+
+ // Only update if metadata actually changed
+ const existing = existingPlatforms.get(canonicalId);
+ if (
+ !existing ||
+ existing.name !== seed.name ||
+ existing.logoPath !== seed.logoPath ||
+ existing.urlTemplate !== seed.urlTemplate ||
+ existing.isSubscription !== seed.isSubscription
+ ) {
+ db.update(platforms)
+ .set({
+ name: seed.name,
+ logoPath: seed.logoPath,
+ urlTemplate: seed.urlTemplate,
+ isSubscription: seed.isSubscription,
+ })
+ .where(eq(platforms.id, canonicalId))
+ .run();
+ log.debug(`Updated platform "${seed.name}"`);
+ }
+
+ // Merge duplicates into the canonical platform
+ for (const oldId of existingPlatformIds.slice(1)) {
+ // Re-point titleAvailability rows, ignoring conflicts from duplicates
+ db.run(
+ sql`UPDATE OR IGNORE ${titleAvailability} SET "platformId" = ${canonicalId} WHERE "platformId" = ${oldId}`,
+ );
+ // Delete any titleAvailability rows that couldn't be moved due to unique constraint
+ db.delete(titleAvailability).where(eq(titleAvailability.platformId, oldId)).run();
+
+ // Re-point userPlatforms rows, ignoring conflicts
+ db.run(
+ sql`UPDATE OR IGNORE ${userPlatforms} SET "platformId" = ${canonicalId} WHERE "platformId" = ${oldId}`,
+ );
+ db.delete(userPlatforms).where(eq(userPlatforms.platformId, oldId)).run();
+
+ // Remove old TMDB ID mappings (cascade won't fire since we delete platform next)
+ db.delete(platformTmdbIds).where(eq(platformTmdbIds.platformId, oldId)).run();
+
+ // Delete the duplicate platform
+ db.delete(platforms).where(eq(platforms.id, oldId)).run();
+ log.debug(`Merged duplicate platform ${oldId} into "${seed.name}" (${canonicalId})`);
+ }
+ }
+
+ // Batch insert missing TMDB ID mappings
+ const missingTmdbIds = seed.tmdbProviderIds.filter(
+ (id) => tmdbToPlatform.get(id) !== canonicalId,
+ );
+ if (missingTmdbIds.length > 0) {
+ db.insert(platformTmdbIds)
+ .values(
+ missingTmdbIds.map((tmdbProviderId) => ({ platformId: canonicalId, tmdbProviderId })),
+ )
+ .onConflictDoNothing()
+ .run();
+ log.debug(`Added ${missingTmdbIds.length} TMDB mapping(s) for "${seed.name}"`);
+ }
}
});
+
+ log.debug(`Seeded ${SEED_DATA.length} platforms`);
}
diff --git a/packages/i18n/src/po/en.po b/packages/i18n/src/po/en.po
index d7b4f43..e272a39 100644
--- a/packages/i18n/src/po/en.po
+++ b/packages/i18n/src/po/en.po
@@ -275,6 +275,10 @@ msgstr "4★+"
msgid "5★"
msgstr "5★"
+#: apps/web/src/components/library/library-filters.tsx
+msgid "70s and earlier"
+msgstr "70s and earlier"
+
#: apps/native/src/components/library/filter-sheet.tsx
msgid "80s"
msgstr "80s"
@@ -284,7 +288,7 @@ msgid "90s"
msgstr "90s"
#: apps/native/src/app/(tabs)/(settings)/index.tsx
-#: apps/web/src/components/settings/account-section.tsx
+#: apps/web/src/routes/_app/settings.tsx
msgid "Account"
msgstr ""
@@ -451,8 +455,8 @@ msgid "Any year"
msgstr "Any year"
#: apps/web/src/routes/_app/settings.tsx
-msgid "App Settings"
-msgstr ""
+#~ msgid "App Settings"
+#~ msgstr ""
#: apps/native/src/app/(tabs)/(settings)/index.tsx
msgid "Application"
@@ -540,8 +544,13 @@ msgid "backups."
msgstr "backups."
#: apps/web/src/components/titles/title-availability.tsx
-msgid "Buy"
-msgstr ""
+#~ msgid "Buy"
+#~ msgstr ""
+
+#: apps/native/src/app/title/[id].tsx
+#: apps/web/src/components/titles/title-availability.tsx
+msgid "Buy or Rent"
+msgstr "Buy or Rent"
#: apps/web/src/components/settings/danger-section.tsx
msgid "Cache management"
@@ -661,8 +670,12 @@ msgstr "Choose how to import your {sourceLabel} data."
#~ msgstr "Choose your preferred display language"
#: apps/web/src/components/settings/streaming-services-section.tsx
-msgid "Choose your subscriptions"
-msgstr "Choose your subscriptions"
+msgid "Choose your preferred streaming platforms"
+msgstr "Choose your preferred streaming platforms"
+
+#: apps/web/src/components/settings/streaming-services-section.tsx
+#~ msgid "Choose your subscriptions"
+#~ msgstr "Choose your subscriptions"
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/app/(tabs)/(library)/index.tsx
@@ -1427,8 +1440,8 @@ msgid "Finished importing your Sofa export."
msgstr "Finished importing your Sofa export."
#: apps/web/src/components/titles/title-availability.tsx
-msgid "Free"
-msgstr ""
+#~ msgid "Free"
+#~ msgstr ""
#: apps/web/src/components/settings/danger-section.tsx
msgid "Free up disk space by clearing cached metadata and images"
@@ -1573,7 +1586,7 @@ msgid "Image cache and backup disk usage"
msgstr ""
#: apps/web/src/components/settings/account-section.tsx
-#: apps/web/src/components/settings/imports-section.tsx
+#: apps/web/src/routes/_app/settings.tsx
msgid "Import"
msgstr ""
@@ -1673,7 +1686,7 @@ msgid "Install the Webhook plugin from Jellyfin's plugin catalog"
msgstr ""
#: apps/native/src/components/settings/integrations-section.tsx
-#: apps/web/src/components/settings/integrations-section.tsx
+#: apps/web/src/routes/_app/settings.tsx
msgid "Integrations"
msgstr ""
@@ -2346,7 +2359,6 @@ msgstr "Pre-1970"
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/native/src/app/(tabs)/(library)/index.tsx
#: apps/web/src/components/library/library-filters.tsx
-#: apps/web/src/components/library/library-filters.tsx
msgid "Pre-1980"
msgstr "Pre-1980"
@@ -2653,8 +2665,8 @@ msgid "Removed from library"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
-msgid "Rent"
-msgstr ""
+#~ msgid "Rent"
+#~ msgstr ""
#: apps/web/src/routes/setup.tsx
msgid "Request an API key"
@@ -3114,6 +3126,7 @@ msgstr ""
msgid "Storage"
msgstr ""
+#: apps/native/src/app/title/[id].tsx
#: apps/web/src/components/titles/title-availability.tsx
msgid "Stream"
msgstr ""
@@ -3123,8 +3136,16 @@ msgid "Streaming"
msgstr "Streaming"
#: apps/web/src/components/settings/streaming-services-section.tsx
-msgid "Streaming Services"
-msgstr "Streaming Services"
+msgid "Streaming data provided by <0/><1>JustWatch1>"
+msgstr "Streaming data provided by <0/><1>JustWatch1>"
+
+#: apps/web/src/components/settings/streaming-services-section.tsx
+#~ msgid "Streaming Services"
+#~ msgstr "Streaming Services"
+
+#: apps/web/src/components/settings/streaming-services-section.tsx
+msgid "Subscriptions"
+msgstr "Subscriptions"
#: apps/web/src/components/settings/backup-schedule-section.tsx
msgid "Sunday"
@@ -3567,8 +3588,8 @@ msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
#: apps/web/src/components/titles/title-availability.tsx
-msgid "Watch on {name}"
-msgstr ""
+#~ msgid "Watch on {name}"
+#~ msgstr ""
#: apps/native/src/components/titles/season-accordion.tsx
#: apps/native/src/lib/title-actions.ts
@@ -3651,8 +3672,8 @@ msgid "Where to Watch"
msgstr ""
#: apps/web/src/components/titles/title-availability.tsx
-msgid "With Ads"
-msgstr ""
+#~ msgid "With Ads"
+#~ msgstr ""
#: apps/native/src/app/person/[id].tsx
#: apps/native/src/components/search/recently-viewed-row-content.tsx
diff --git a/packages/test/src/db.ts b/packages/test/src/db.ts
index 804d2f7..7cf37d8 100644
--- a/packages/test/src/db.ts
+++ b/packages/test/src/db.ts
@@ -16,6 +16,7 @@ const {
userEpisodeWatches,
userTitleStatus,
userRatings,
+ platformTmdbIds,
platforms,
titleAvailability,
userPlatforms,
@@ -178,9 +179,9 @@ export function insertPlatform(
id?: string;
name?: string;
tmdbProviderId?: number;
+ tmdbProviderIds?: number[];
logoPath?: string;
urlTemplate?: string;
- displayOrder?: number;
} = {},
) {
const id = overrides.id ?? "platform-1";
@@ -189,12 +190,15 @@ export function insertPlatform(
.values({
id,
name: overrides.name ?? "Netflix",
- tmdbProviderId: overrides.tmdbProviderId ?? 8,
logoPath: overrides.logoPath ?? "/logo.png",
urlTemplate: overrides.urlTemplate ?? "https://www.netflix.com/search?q={title}",
- displayOrder: overrides.displayOrder ?? 1,
})
.run();
+
+ const tmdbIds = overrides.tmdbProviderIds ?? [overrides.tmdbProviderId ?? 8];
+ for (const tmdbId of tmdbIds) {
+ testDb.insert(platformTmdbIds).values({ platformId: id, tmdbProviderId: tmdbId }).run();
+ }
return id;
}
diff --git a/scripts/sync-tmdb-providers.ts b/scripts/sync-tmdb-providers.ts
new file mode 100644
index 0000000..e12c566
--- /dev/null
+++ b/scripts/sync-tmdb-providers.ts
@@ -0,0 +1,95 @@
+/**
+ * Pulls all watch providers from the TMDB API, updates logo paths for existing
+ * providers, and auto-appends any missing ones to platforms.json.
+ *
+ * Usage: bun scripts/sync-tmdb-providers.ts [--region US]
+ */
+
+import type { SeedPlatform } from "../packages/db/src/seed-platforms";
+import { getWatchProviderList } from "../packages/tmdb/src/client";
+
+const region = process.argv.includes("--region")
+ ? (process.argv[process.argv.indexOf("--region") + 1] ?? "US")
+ : "US";
+
+console.log(`Fetching TMDB watch providers for region: ${region}\n`);
+
+const [movieProviders, tvProviders] = await Promise.all([
+ getWatchProviderList("movie", region),
+ getWatchProviderList("tv", region),
+]);
+
+// Union by provider_id, preferring movie entry for metadata
+const allProviders = new Map<
+ number,
+ {
+ provider_id: number;
+ provider_name: string;
+ logo_path: string | null;
+ display_priorities: Record;
+ }
+>();
+for (const p of [...movieProviders, ...tvProviders]) {
+ if (!allProviders.has(p.provider_id)) {
+ allProviders.set(p.provider_id, p);
+ }
+}
+
+// Read current platforms JSON
+const jsonPath = new URL("../packages/db/src/platforms.json", import.meta.url).pathname;
+const seedData: SeedPlatform[] = await Bun.file(jsonPath).json();
+
+// Build set of known TMDB IDs
+const knownIds = new Set(seedData.flatMap((p) => p.tmdbProviderIds));
+
+// Update logo paths for existing entries
+let logosUpdated = 0;
+for (const entry of seedData) {
+ const firstId = entry.tmdbProviderIds[0];
+ if (firstId == null) continue;
+ const tmdbProvider = allProviders.get(firstId);
+ if (!tmdbProvider?.logo_path) continue;
+ if (entry.logoPath !== tmdbProvider.logo_path) {
+ entry.logoPath = tmdbProvider.logo_path;
+ logosUpdated++;
+ }
+}
+console.log(`Updated ${logosUpdated} logo paths for existing entries\n`);
+
+// Find missing providers
+const missing = [...allProviders.values()]
+ .filter((p) => !knownIds.has(p.provider_id))
+ .sort((a, b) => {
+ const aPriority = a.display_priorities?.[region] ?? 999;
+ const bPriority = b.display_priorities?.[region] ?? 999;
+ return aPriority - bPriority;
+ });
+
+console.log(`Total TMDB providers for ${region}: ${allProviders.size}`);
+console.log(`Already in seed data: ${knownIds.size}`);
+console.log(`Missing: ${missing.length}\n`);
+
+if (missing.length > 0) {
+ for (const p of missing) {
+ seedData.push({
+ tmdbProviderIds: [p.provider_id],
+ name: p.provider_name,
+ logoPath: p.logo_path,
+ urlTemplate: "",
+ isSubscription: true,
+ });
+ }
+
+ console.log(`Added ${missing.length} new providers`);
+ console.log("\nTop 20 added:");
+ for (const p of missing.slice(0, 20)) {
+ const priority = p.display_priorities?.[region] ?? "?";
+ console.log(` [${p.provider_id}] ${p.provider_name} (priority: ${priority})`);
+ }
+ if (missing.length > 20) {
+ console.log(` ... and ${missing.length - 20} more`);
+ }
+}
+
+await Bun.write(jsonPath, JSON.stringify(seedData, null, 2) + "\n");
+console.log(`\nWrote ${seedData.length} providers to platforms.json`);